Just wanted to share. With less than 80 lines of code here:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using AXFSoftware.Utilities;
namespace AXFSoftware.Utilities.ActiveDirectory.LogonHours
{
public class LogonHours
{
// One bit per hour over an entire week
const int BitArraySize = 24 * 7;
public BitArray Bits { get; }
public byte[] Bitmask
{
get
{
var bytes = new byte[BitArraySize / 8];
Bits.CopyTo(bytes, 0);
return bytes;
}
}
public TimeZoneInfo TimeZoneInfo { get; }
public bool IsUtc => TimeZoneInfo.Id == TimeZoneInfo.Utc.Id;
Lazy<Week> _days;
public Week Days => _days.Value;
public bool this[int hour]
{
get => Bits[hour];
set => Bits[hour] = value;
}
public LogonHours(byte [] bitmask)
=> new LogonHours(new BitArray(bitmask), TimeZoneInfo.Utc);
public LogonHours(BitArray bits)
=> new LogonHours(new BitArray(bits), TimeZoneInfo.Utc);
LogonHours(BitArray bits, TimeZoneInfo timeZoneInfo)
{
if (bits.Length != BitArraySize)
throw new ArgumentException("Must hold exact 168 bits for 24 hours over 7 days.", nameof(bits));
Bits = bits;
TimeZoneInfo = timeZoneInfo;
_days = new Lazy<Week>(() => new Week(this));
}
public LogonHours ToTimeZone(TimeZoneInfo timeZoneInfo)
=> ToTimeZone(timeZoneInfo, DateTime.Now);
public LogonHours ToTimeZone(TimeZoneInfo timeZoneInfo, DateTime dateTime)
{
var srcOffset = TimeZoneInfo.GetUtcOffset(dateTime);
var dstOffset = timeZoneInfo.GetUtcOffset(dateTime);
var newOffset = dstOffset - srcOffset;
int dstHour = (int)Math.Round(newOffset.TotalHours);
var bits = new BitArray(Bits);
if (dstHour < 0)
bits.LeftRotate(-dstHour);
else if (dstHour > 0)
bits.RightRotate(dstHour);
return new LogonHours(bits, timeZoneInfo);
}
public LogonHours ToUtc()
=> ToUtc(DateTime.Now);
public LogonHours ToUtc(DateTime dateTime)
=> IsUtc ? new LogonHours(new BitArray(Bits), TimeZoneInfo.Utc) : ToTimeZone(TimeZoneInfo.Utc, dateTime);
}
}
and the supporting 43 lines of code here:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace AXFSoftware.Utilities
{
public static class BitArrayExtensions
{
public static BitArray RightRotate(this BitArray bits, int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count), count, "Must be greater than or equal zero");
count %= bits.Count;
if (count == 0)
return bits;
var lowBits = (BitArray) bits.Clone();
lowBits.LeftShift(bits.Count - count);
bits.RightShift(count);
return bits.Or(lowBits);
}
public static BitArray LeftRotate(this BitArray bits, int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count), count, "Must be greater than or equal zero");
count %= bits.Count;
if (count == 0)
return bits;
var highBits = (BitArray)bits.Clone();
highBits.RightShift(bits.Count - count);
bits.LeftShift(count);
return bits.Or(highBits);
}
}
}
It is already enough to be able load the AD logon hours bitmask, convert to your desired timezone, and query and set the availability hours using the indexer, and the convert back to UTC, get the appropriate bitmask to put back into AD.
For the sake convenience and just a mere 80 extra lines, these helper classes let you index into the hours by day and hour (instead of computing the hour index yourself with the above classes):
using System;
using System.Collections;
using System.Collections.Generic;
namespace AXFSoftware.Utilities.ActiveDirectory.LogonHours
{
public struct Day : IEnumerable<bool>
{
readonly int _startOfDayHour;
public LogonHours LogonHours { get; }
public TimeZoneInfo TimeZoneInfo => LogonHours.TimeZoneInfo;
public bool this[int hour]
{
get
{
ValidateHour(hour);
return LogonHours.Bits[_startOfDayHour + hour];
}
set
{
ValidateHour(hour);
LogonHours.Bits[_startOfDayHour + hour] = value;
}
}
internal Day(LogonHours logonHours, int day)
{
LogonHours = logonHours;
_startOfDayHour = day * 24;
}
void ValidateHour(int hour)
{
if (!(0 <= hour && hour <= 23))
throw new IndexOutOfRangeException("Hour value must be between 0 and 23 inclusively.");
}
public IEnumerator<bool> GetEnumerator()
{
for (int hour = 0; hour < 24; hour++)
yield return this[hour];
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
}
}
using System;
using System.Collections;
using System.Collections.Generic;
namespace AXFSoftware.Utilities.ActiveDirectory.LogonHours
{
public struct Week : IEnumerable<Day>
{
readonly Day[] _days;
public LogonHours LogonHours { get; }
public TimeZoneInfo TimeZoneInfo => LogonHours.TimeZoneInfo;
public Day this[DayOfWeek day] => _days[(int) day];
internal Week(LogonHours logonHours)
{
LogonHours = logonHours;
_days = new Day[7];
for (int i = 0; i < 7; i++)
_days[i] = new Day(LogonHours, i);
}
public IEnumerator<Day> GetEnumerator()
=> ((IEnumerable<Day>)_days).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> _days.GetEnumerator();
}
}
Compare that to the over 300+ lines used in Anlai's code.