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