diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d1baf7..2c6c3cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Added a method for getting the total number of seconds in a date
- Added extensions to raise events with common event args using the data directly
- Added property to IcdEnvironment to determine whether SSL communication is enabled
+ - Added IcdTimeZoneInfo, a very light implementation of System.TimeZoneInfo for the .NET Compact Framework
### Changed
- Repeater changed to use configured callbacks instead of a dumb event
diff --git a/ICD.Common.Utils/ICD.Common.Utils_NetStandard.csproj b/ICD.Common.Utils/ICD.Common.Utils_NetStandard.csproj
index 4fda672..80fd826 100644
--- a/ICD.Common.Utils/ICD.Common.Utils_NetStandard.csproj
+++ b/ICD.Common.Utils/ICD.Common.Utils_NetStandard.csproj
@@ -49,6 +49,9 @@
PreserveNewest
+
+ PreserveNewest
+
\ No newline at end of file
diff --git a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj
index 9951fb1..ad6616e 100644
--- a/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj
+++ b/ICD.Common.Utils/ICD.Common.Utils_SimplSharp.csproj
@@ -110,6 +110,7 @@
PreserveNewest
+
@@ -222,6 +223,8 @@
+
+
@@ -238,6 +241,9 @@
+
+ PreserveNewest
+
diff --git a/ICD.Common.Utils/Sqlite/IcdSqliteDataReader.cs b/ICD.Common.Utils/Sqlite/IcdSqliteDataReader.cs
index 006d112..a271e82 100644
--- a/ICD.Common.Utils/Sqlite/IcdSqliteDataReader.cs
+++ b/ICD.Common.Utils/Sqlite/IcdSqliteDataReader.cs
@@ -40,6 +40,11 @@ namespace ICD.Common.Utils.Sqlite
return m_Reader.GetInt32(ordinal);
}
+ public long GetInt64(int ordinal)
+ {
+ return m_Reader.GetInt64(ordinal);
+ }
+
public bool GetBoolean(int ordinal)
{
return m_Reader.GetBoolean(ordinal);
diff --git a/ICD.Common.Utils/TimeZoneInfo/IcdTimeZoneInfo.cs b/ICD.Common.Utils/TimeZoneInfo/IcdTimeZoneInfo.cs
new file mode 100644
index 0000000..ae54d5f
--- /dev/null
+++ b/ICD.Common.Utils/TimeZoneInfo/IcdTimeZoneInfo.cs
@@ -0,0 +1,455 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+#if SIMPLSHARP
+using Crestron.SimplSharp.CrestronIO;
+#else
+using System.IO;
+#endif
+using ICD.Common.Utils.IO;
+using ICD.Common.Utils.Sqlite;
+
+namespace ICD.Common.Utils.TimeZoneInfo
+{
+ public sealed class IcdTimeZoneInfo
+ {
+ #region Private Members
+
+ private const string SQL_LOCAL_DATABASE_FILE = "TimeZones.sqlite";
+ private const string SQL_CONNECTION_STRING_FORMAT =
+#if SIMPLSHARP
+ "Data Source={0};Version=3;ReadOnly=True";
+#else
+ "Data Source={0}";
+#endif
+
+ private static readonly Dictionary s_Cache;
+
+ private IcdTimeZoneInfoAdjustmentRule[] m_AdjustmentRules;
+
+ #endregion
+
+ #region Properties
+
+ public string StandardName { get; private set; }
+
+ public TimeSpan BaseUtcOffset { get; private set; }
+
+ public bool SupportsDaylightSavingTime { get; private set; }
+
+ public IcdTimeZoneInfoAdjustmentRule[] GetAdjustmentRules()
+ {
+ return m_AdjustmentRules.ToArray();
+ }
+
+ #endregion
+
+ #region Constructors
+
+ static IcdTimeZoneInfo()
+ {
+ s_Cache = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ try
+ {
+ PopulateCache();
+ }
+ catch (Exception e)
+ {
+ IcdErrorLog.Exception(e, "Error populating IcdTimeZoneInfo cache - {0}", e.Message);
+ }
+ }
+
+ public static IcdTimeZoneInfo FindSystemTimeZoneById(string timeZoneId)
+ {
+ return s_Cache[timeZoneId];
+ }
+
+ #endregion
+
+ #region Cache Building
+
+ private static void PopulateCache()
+ {
+ string databasePath = IcdPath.Combine(PathUtils.ProgramPath, SQL_LOCAL_DATABASE_FILE);
+ if (!IcdFile.Exists(databasePath))
+ throw new FileNotFoundException("Failed to find database at path " + databasePath);
+
+ string sqlConnectionString = string.Format(SQL_CONNECTION_STRING_FORMAT, databasePath);
+
+ using (IcdSqliteConnection sQLiteConnection = new IcdSqliteConnection(sqlConnectionString))
+ {
+ sQLiteConnection.Open();
+
+ foreach (IcdTimeZoneInfo info in ReadTimeZoneInfos(sQLiteConnection))
+ s_Cache.Add(info.StandardName, info);
+ }
+ }
+
+ private static IEnumerable ReadTimeZoneInfos(IcdSqliteConnection sQLiteConnection)
+ {
+ const string command = "select id, name, baseOffsetTicks, hasDstRule from timeZones";
+
+ using (IcdSqliteCommand cmd = new IcdSqliteCommand(command, sQLiteConnection))
+ {
+ using (IcdSqliteDataReader reader = cmd.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ int id = reader.GetInt32(0);
+ string name = reader.GetString(1);
+ long offset = reader.GetInt64(2);
+ bool hasRule = reader.GetBoolean(3);
+
+ IcdTimeZoneInfoAdjustmentRule[] adjustmentRules = LoadAdjustmentRules(id, sQLiteConnection).ToArray();
+
+ IcdTimeZoneInfo info = new IcdTimeZoneInfo
+ {
+ StandardName = name,
+ BaseUtcOffset = TimeSpan.FromTicks(offset),
+ SupportsDaylightSavingTime = hasRule,
+ m_AdjustmentRules = adjustmentRules
+ };
+
+ yield return info;
+ }
+ }
+ }
+ }
+
+ private static IEnumerable LoadAdjustmentRules(int timeZoneId, IcdSqliteConnection sQLiteConnection)
+ {
+ string command = string.Format("select id, timeZone, deltaTicks, ruleStart, ruleEnd from rules where timeZone = {0}", timeZoneId);
+
+ using (IcdSqliteCommand cmd = new IcdSqliteCommand(command, sQLiteConnection))
+ {
+ using (IcdSqliteDataReader reader = cmd.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ int id = reader.GetInt32(0);
+ long delta = reader.GetInt64(2);
+ string ruleStart = reader.GetString(3);
+ string ruleEnd = reader.GetString(4);
+ IcdTimeZoneInfoTransitionTime transitionTimeStart = LoadTransitionTimeStart(id, sQLiteConnection);
+ IcdTimeZoneInfoTransitionTime transitionTimeEnd = LoadTransitionTimeEnd(id, sQLiteConnection);
+
+ IcdTimeZoneInfoAdjustmentRule rule = new IcdTimeZoneInfoAdjustmentRule
+ {
+ DaylightDelta = TimeSpan.FromTicks(delta),
+ DateStart = DateTime.Parse(ruleStart),
+ DateEnd = DateTime.Parse(ruleEnd),
+ DaylightTransitionStart = transitionTimeStart,
+ DaylightTransitionEnd = transitionTimeEnd
+ };
+
+ yield return rule;
+ }
+ }
+ }
+ }
+
+ private static IcdTimeZoneInfoTransitionTime LoadTransitionTimeStart(int ruleId, IcdSqliteConnection sQLiteConnection)
+ {
+ string command =
+ string.Format("select rule, isTransitionStart, isFixedRule, timeOfDay, dayOfWeek, day, week, month " +
+ "from ruleTransitions " +
+ "where rule = {0} and isTransitionStart = true",
+ ruleId);
+
+ using (IcdSqliteCommand cmd = new IcdSqliteCommand(command, sQLiteConnection))
+ {
+ using (IcdSqliteDataReader reader = cmd.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ bool isFixed = reader.GetBoolean(2);
+ string timeOfDay = reader.GetString(3);
+ int dayOfWeek = reader.GetInt32(4);
+ int day = reader.GetInt32(5);
+ int week = reader.GetInt32(6);
+ int month = reader.GetInt32(7);
+
+ return new IcdTimeZoneInfoTransitionTime
+ {
+ IsFixedDateRule = isFixed,
+ TimeOfDay = DateTime.Parse(timeOfDay),
+ DayOfWeek = (DayOfWeek)dayOfWeek,
+ Day = day,
+ Week = week,
+ Month = month
+ };
+ }
+ }
+ }
+
+ throw new ArgumentOutOfRangeException("ruleId", "There was no TransitionTime for the specified rule");
+ }
+
+ private static IcdTimeZoneInfoTransitionTime LoadTransitionTimeEnd(int ruleId, IcdSqliteConnection sQLiteConnection)
+ {
+ string command =
+ string.Format("select rule, isTransitionStart, isFixedRule, timeOfDay, dayOfWeek, day, week, month " +
+ "from ruleTransitions " +
+ "where rule = {0} and isTransitionStart = false",
+ ruleId);
+
+ using (IcdSqliteCommand cmd = new IcdSqliteCommand(command, sQLiteConnection))
+ {
+ using (IcdSqliteDataReader reader = cmd.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ bool isFixed = reader.GetBoolean(2);
+ string timeOfDay = reader.GetString(3);
+ int dayOfWeek = reader.GetInt32(4);
+ int day = reader.GetInt32(5);
+ int week = reader.GetInt32(6);
+ int month = reader.GetInt32(7);
+
+ return new IcdTimeZoneInfoTransitionTime
+ {
+ IsFixedDateRule = isFixed,
+ TimeOfDay = DateTime.Parse(timeOfDay),
+ DayOfWeek = (DayOfWeek)dayOfWeek,
+ Day = day,
+ Week = week,
+ Month = month
+ };
+ }
+ }
+ }
+
+ throw new ArgumentOutOfRangeException("ruleId", "There was no TransitionTime for the specified rule");
+ }
+
+ #endregion
+
+ #region Methods
+
+ public DateTime ConvertToUtc(DateTime time)
+ {
+ return time.Kind == DateTimeKind.Utc ? time : ConvertToUtc(time, this);
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private static DateTime ConvertToUtc(DateTime dateTime, IcdTimeZoneInfo sourceTimeZone)
+ {
+ if (sourceTimeZone == null)
+ throw new ArgumentNullException("sourceTimeZone");
+
+ IcdTimeZoneInfoAdjustmentRule sourceRule = sourceTimeZone.GetAdjustmentRuleForTime(dateTime);
+ TimeSpan sourceOffset = sourceTimeZone.BaseUtcOffset;
+
+ if (sourceRule != null)
+ {
+ if (sourceTimeZone.SupportsDaylightSavingTime)
+ {
+ DaylightTime sourceDaylightTime = GetDaylightTime(dateTime.Year, sourceRule);
+
+ bool sourceIsDaylightSavings = GetIsDaylightSavings(dateTime, sourceRule, sourceDaylightTime);
+
+ // adjust the sourceOffset according to the Adjustment Rule / Daylight Saving Rule
+ sourceOffset += (sourceIsDaylightSavings ? sourceRule.DaylightDelta : TimeSpan.Zero
+ /*FUTURE: sourceRule.StandardDelta*/);
+ }
+ }
+
+ const DateTimeKind targetKind = DateTimeKind.Utc;
+
+ long utcTicks = dateTime.Ticks - sourceOffset.Ticks;
+ return new DateTime(utcTicks, targetKind);
+ }
+
+ // assumes dateTime is in the current time zone's time
+ private IcdTimeZoneInfoAdjustmentRule GetAdjustmentRuleForTime(DateTime dateTime)
+ {
+ if (m_AdjustmentRules == null || m_AdjustmentRules.Length == 0)
+ {
+ return null;
+ }
+
+ // Only check the whole-date portion of the dateTime -
+ // This is because the AdjustmentRule DateStart & DateEnd are stored as
+ // Date-only values {4/2/2006 - 10/28/2006} but actually represent the
+ // time span {4/2/2006@00:00:00.00000 - 10/28/2006@23:59:59.99999}
+ DateTime date = dateTime.Date;
+
+ return m_AdjustmentRules.FirstOrDefault(t => t.DateStart <= date && t.DateEnd >= date);
+ }
+
+ //
+ // GetDaylightTime -
+ //
+ // Helper function that returns a DaylightTime from a year and AdjustmentRule
+ //
+ private static DaylightTime GetDaylightTime(int year, IcdTimeZoneInfoAdjustmentRule rule)
+ {
+ TimeSpan delta = rule.DaylightDelta;
+ DateTime startTime = TransitionTimeToDateTime(year, rule.DaylightTransitionStart);
+ DateTime endTime = TransitionTimeToDateTime(year, rule.DaylightTransitionEnd);
+ return new DaylightTime(startTime, endTime, delta);
+ }
+
+ //
+ // TransitionTimeToDateTime -
+ //
+ // Helper function that converts a year and TransitionTime into a DateTime
+ //
+ private static DateTime TransitionTimeToDateTime(int year, IcdTimeZoneInfoTransitionTime transitionTime)
+ {
+ DateTime value;
+ DateTime timeOfDay = transitionTime.TimeOfDay;
+
+ if (transitionTime.IsFixedDateRule)
+ {
+ // create a DateTime from the passed in year and the properties on the transitionTime
+
+ // if the day is out of range for the month then use the last day of the month
+ int day = DateTime.DaysInMonth(year, transitionTime.Month);
+
+ value = new DateTime(year, transitionTime.Month, (day < transitionTime.Day) ? day : transitionTime.Day,
+ timeOfDay.Hour, timeOfDay.Minute, timeOfDay.Second, timeOfDay.Millisecond);
+ }
+ else
+ {
+ if (transitionTime.Week <= 4)
+ {
+ //
+ // Get the (transitionTime.Week)th Sunday.
+ //
+ value = new DateTime(year, transitionTime.Month, 1,
+ timeOfDay.Hour, timeOfDay.Minute, timeOfDay.Second, timeOfDay.Millisecond);
+
+ int dayOfWeek = (int)value.DayOfWeek;
+ int delta = (int)transitionTime.DayOfWeek - dayOfWeek;
+ if (delta < 0)
+ {
+ delta += 7;
+ }
+ delta += 7 * (transitionTime.Week - 1);
+
+ if (delta > 0)
+ {
+ value = value.AddDays(delta);
+ }
+ }
+ else
+ {
+ //
+ // If TransitionWeek is greater than 4, we will get the last week.
+ //
+ Int32 daysInMonth = DateTime.DaysInMonth(year, transitionTime.Month);
+ value = new DateTime(year, transitionTime.Month, daysInMonth,
+ timeOfDay.Hour, timeOfDay.Minute, timeOfDay.Second, timeOfDay.Millisecond);
+
+ // This is the day of week for the last day of the month.
+ int dayOfWeek = (int)value.DayOfWeek;
+ int delta = dayOfWeek - (int)transitionTime.DayOfWeek;
+ if (delta < 0)
+ {
+ delta += 7;
+ }
+
+ if (delta > 0)
+ {
+ value = value.AddDays(-delta);
+ }
+ }
+ }
+ return value;
+ }
+
+ //
+ // GetIsDaylightSavings -
+ //
+ // Helper function that checks if a given dateTime is in Daylight Saving Time (DST)
+ // This function assumes the dateTime and AdjustmentRule are both in the same time zone
+ //
+ static private bool GetIsDaylightSavings(DateTime time, IcdTimeZoneInfoAdjustmentRule rule, DaylightTime daylightTime)
+ {
+ if (rule == null)
+ {
+ return false;
+ }
+
+ DateTime startTime;
+ DateTime endTime;
+
+ if (time.Kind == DateTimeKind.Local)
+ {
+ // startTime and endTime represent the period from either the start of DST to the end and ***includes*** the
+ // potentially overlapped times
+ startTime = rule.IsStartDateMarkerForBeginningOfYear() ? new DateTime(daylightTime.Start.Year, 1, 1, 0, 0, 0) : daylightTime.Start + daylightTime.Delta;
+ endTime = rule.IsEndDateMarkerForEndOfYear() ? new DateTime(daylightTime.End.Year + 1, 1, 1, 0, 0, 0).AddTicks(-1) : daylightTime.End;
+ }
+ else
+ {
+ // startTime and endTime represent the period from either the start of DST to the end and
+ // ***does not include*** the potentially overlapped times
+ //
+ // -=-=-=-=-=- Pacific Standard Time -=-=-=-=-=-=-
+ // April 2, 2006 October 29, 2006
+ // 2AM 3AM 1AM 2AM
+ // | +1 hr | | -1 hr |
+ // | | | |
+ // [========== DST ========>)
+ //
+ // -=-=-=-=-=- Some Weird Time Zone -=-=-=-=-=-=-
+ // April 2, 2006 October 29, 2006
+ // 1AM 2AM 2AM 3AM
+ // | -1 hr | | +1 hr |
+ // | | | |
+ // [======== DST ========>)
+ //
+ bool invalidAtStart = rule.DaylightDelta > TimeSpan.Zero;
+ startTime = rule.IsStartDateMarkerForBeginningOfYear() ? new DateTime(daylightTime.Start.Year, 1, 1, 0, 0, 0) : daylightTime.Start + (invalidAtStart ? rule.DaylightDelta : TimeSpan.Zero); /* FUTURE: - rule.StandardDelta; */
+ endTime = rule.IsEndDateMarkerForEndOfYear() ? new DateTime(daylightTime.End.Year + 1, 1, 1, 0, 0, 0).AddTicks(-1) : daylightTime.End + (invalidAtStart ? -rule.DaylightDelta : TimeSpan.Zero);
+ }
+
+ return CheckIsDst(startTime, time, endTime, false);
+ }
+
+ private static bool CheckIsDst(DateTime startTime, DateTime time, DateTime endTime, bool ignoreYearAdjustment)
+ {
+ bool isDst;
+
+ if (!ignoreYearAdjustment)
+ {
+ int startTimeYear = startTime.Year;
+ int endTimeYear = endTime.Year;
+
+ if (startTimeYear != endTimeYear)
+ {
+ endTime = endTime.AddYears(startTimeYear - endTimeYear);
+ }
+
+ int timeYear = time.Year;
+
+ if (startTimeYear != timeYear)
+ {
+ time = time.AddYears(startTimeYear - timeYear);
+ }
+ }
+
+ if (startTime > endTime)
+ {
+ // In southern hemisphere, the daylight saving time starts later in the year, and ends in the beginning of next year.
+ // Note, the summer in the southern hemisphere begins late in the year.
+ isDst = (time < endTime || time >= startTime);
+ }
+ else
+ {
+ // In northern hemisphere, the daylight saving time starts in the middle of the year.
+ isDst = (time >= startTime && time < endTime);
+ }
+ return isDst;
+ }
+
+ #endregion
+ }
+}
diff --git a/ICD.Common.Utils/TimeZoneInfo/IcdTimeZoneInfoAdjustmentRule.cs b/ICD.Common.Utils/TimeZoneInfo/IcdTimeZoneInfoAdjustmentRule.cs
new file mode 100644
index 0000000..53b3901
--- /dev/null
+++ b/ICD.Common.Utils/TimeZoneInfo/IcdTimeZoneInfoAdjustmentRule.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace ICD.Common.Utils.TimeZoneInfo
+{
+ public sealed class IcdTimeZoneInfoAdjustmentRule
+ {
+ public DateTime DateStart { get; set; }
+
+ public DateTime DateEnd { get; set; }
+
+ public TimeSpan DaylightDelta { get; set; }
+
+ public IcdTimeZoneInfoTransitionTime DaylightTransitionStart { get; set; }
+
+ public IcdTimeZoneInfoTransitionTime DaylightTransitionEnd { get; set; }
+
+ // ----- SECTION: internal utility methods ----------------*
+
+ //
+ // When Windows sets the daylight transition start Jan 1st at 12:00 AM, it means the year starts with the daylight saving on.
+ // We have to special case this value and not adjust it when checking if any date is in the daylight saving period.
+ //
+ public bool IsStartDateMarkerForBeginningOfYear()
+ {
+ return DaylightTransitionStart.Month == 1 && DaylightTransitionStart.Day == 1 && DaylightTransitionStart.TimeOfDay.Hour == 0 &&
+ DaylightTransitionStart.TimeOfDay.Minute == 0 && DaylightTransitionStart.TimeOfDay.Second == 0 &&
+ DateStart.Year == DateEnd.Year;
+ }
+
+ //
+ // When Windows sets the daylight transition end Jan 1st at 12:00 AM, it means the year ends with the daylight saving on.
+ // We have to special case this value and not adjust it when checking if any date is in the daylight saving period.
+ //
+ public bool IsEndDateMarkerForEndOfYear()
+ {
+ return DaylightTransitionEnd.Month == 1 && DaylightTransitionEnd.Day == 1 && DaylightTransitionEnd.TimeOfDay.Hour == 0 &&
+ DaylightTransitionEnd.TimeOfDay.Minute == 0 && DaylightTransitionEnd.TimeOfDay.Second == 0 &&
+ DateStart.Year == DateEnd.Year;
+ }
+ }
+}
diff --git a/ICD.Common.Utils/TimeZoneInfo/IcdTimeZoneInfoTransitionTime.cs b/ICD.Common.Utils/TimeZoneInfo/IcdTimeZoneInfoTransitionTime.cs
new file mode 100644
index 0000000..4c1c0a4
--- /dev/null
+++ b/ICD.Common.Utils/TimeZoneInfo/IcdTimeZoneInfoTransitionTime.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace ICD.Common.Utils.TimeZoneInfo
+{
+ public sealed class IcdTimeZoneInfoTransitionTime
+ {
+ public bool IsFixedDateRule { get; set; }
+
+ public DateTime TimeOfDay { get; set; }
+
+ public DayOfWeek DayOfWeek { get; set; }
+
+ public int Day { get; set; }
+
+ public int Week { get; set; }
+
+ public int Month { get; set; }
+ }
+}
diff --git a/ICD.Common.Utils/TimeZones.sqlite b/ICD.Common.Utils/TimeZones.sqlite
new file mode 100644
index 0000000..a972092
Binary files /dev/null and b/ICD.Common.Utils/TimeZones.sqlite differ