TimeAndDateHelper.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Globalization; 7 using System.Text; 8 using System.Text.RegularExpressions; 9 using ManagedCommon; 10 11 namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; 12 13 internal static class TimeAndDateHelper 14 { 15 /* htcfreek:Currently not used. 16 * private static readonly Regex _regexSpecialInputFormats = new Regex(@"^.*(u|ums|ft|oa|exc|exf)\d"); */ 17 18 private static readonly Regex _regexCustomDateTimeFormats = new Regex(@"(?<!\\)(DOW|DIM|WOM|WOY|EAB|WFT|UXT|UMS|OAD|EXC|EXF)"); 19 private static readonly Regex _regexCustomDateTimeDim = new Regex(@"(?<!\\)DIM"); 20 private static readonly Regex _regexCustomDateTimeDow = new Regex(@"(?<!\\)DOW"); 21 private static readonly Regex _regexCustomDateTimeWom = new Regex(@"(?<!\\)WOM"); 22 private static readonly Regex _regexCustomDateTimeWoy = new Regex(@"(?<!\\)WOY"); 23 private static readonly Regex _regexCustomDateTimeEab = new Regex(@"(?<!\\)EAB"); 24 private static readonly Regex _regexCustomDateTimeWft = new Regex(@"(?<!\\)WFT"); 25 private static readonly Regex _regexCustomDateTimeUxt = new Regex(@"(?<!\\)UXT"); 26 private static readonly Regex _regexCustomDateTimeUms = new Regex(@"(?<!\\)UMS"); 27 private static readonly Regex _regexCustomDateTimeOad = new Regex(@"(?<!\\)OAD"); 28 private static readonly Regex _regexCustomDateTimeExc = new Regex(@"(?<!\\)EXC"); 29 private static readonly Regex _regexCustomDateTimeExf = new Regex(@"(?<!\\)EXF"); 30 31 private const long UnixTimeSecondsMin = -62135596800; 32 private const long UnixTimeSecondsMax = 253402300799; 33 private const long UnixTimeMillisecondsMin = -62135596800000; 34 private const long UnixTimeMillisecondsMax = 253402300799999; 35 private const long WindowsFileTimeMin = 0; 36 private const long WindowsFileTimeMax = 2650467707991000000; 37 private const double OADateMin = -657434.99999999; 38 private const double OADateMax = 2958465.99999999; 39 private const double Excel1900DateMin = 1; 40 private const double Excel1900DateMax = 2958465.99998843; 41 private const double Excel1904DateMin = 0; 42 private const double Excel1904DateMax = 2957003.99998843; 43 44 /// <summary> 45 /// Get the format for the time string 46 /// </summary> 47 /// <param name="targetFormat">Type of format</param> 48 /// <param name="timeLong">Show date with weekday and name of month (long format)</param> 49 /// <param name="dateLong">Show time with seconds (long format)</param> 50 /// <returns>String that identifies the time/date format (<see href="https://learn.microsoft.com/dotnet/api/system.datetime.tostring"/>)</returns> 51 internal static string GetStringFormat(FormatStringType targetFormat, bool timeLong, bool dateLong) 52 { 53 switch (targetFormat) 54 { 55 case FormatStringType.Time: 56 return timeLong ? "T" : "t"; 57 case FormatStringType.Date: 58 return dateLong ? "D" : "d"; 59 case FormatStringType.DateTime: 60 if (timeLong & dateLong) 61 { 62 return "F"; // Friday, October 31, 2008 5:04:32 PM 63 } 64 else if (timeLong & !dateLong) 65 { 66 return "G"; // 10/31/2008 5:04:32 PM 67 } 68 else if (!timeLong & dateLong) 69 { 70 return "f"; // Friday, October 31, 2008 5:04 PM 71 } 72 else 73 { 74 // (!timeLong & !dateLong) 75 return "g"; // 10/31/2008 5:04 PM 76 } 77 78 default: 79 return string.Empty; // Windows default based on current culture settings 80 } 81 } 82 83 /// <summary> 84 /// Returns the number week in the month (Used code from 'David Morton' from <see href="https://social.msdn.microsoft.com/Forums/vstudio/bf504bba-85cb-492d-a8f7-4ccabdf882cb/get-week-number-for-month"/>) 85 /// </summary> 86 /// <param name="date">date</param> 87 /// <param name="formatSettingFirstDayOfWeek">Setting for the first day in the week.</param> 88 /// <returns>Number of week in the month</returns> 89 internal static int GetWeekOfMonth(DateTime date, DayOfWeek formatSettingFirstDayOfWeek) 90 { 91 var weekCount = 1; 92 93 for (var i = 1; i <= date.Day; i++) 94 { 95 DateTime d = new(date.Year, date.Month, i); 96 97 // Count week number +1 if day is the first day of a week and not day 1 of the month. 98 // (If we count on day one of a month we would start the month with week number 2.) 99 if (i > 1 && d.DayOfWeek == formatSettingFirstDayOfWeek) 100 { 101 weekCount += 1; 102 } 103 } 104 105 return weekCount; 106 } 107 108 /// <summary> 109 /// Returns the number of the day in the week 110 /// </summary> 111 /// <param name="date">Date</param> 112 /// <returns>Number of the day in the week</returns> 113 internal static int GetNumberOfDayInWeek(DateTime date, DayOfWeek formatSettingFirstDayOfWeek) 114 { 115 var daysInWeek = 7; 116 var adjustment = 1; // We count from 1 to 7 and not from 0 to 6 117 118 return ((date.DayOfWeek + daysInWeek - formatSettingFirstDayOfWeek) % daysInWeek) + adjustment; 119 } 120 121 internal static double ConvertToOleAutomationFormat(DateTime date, OADateFormats type) 122 { 123 var v = date.ToOADate(); 124 125 switch (type) 126 { 127 case OADateFormats.Excel1904: 128 // Excel with base 1904: Adjust by -1462 129 v -= 1462; 130 131 // Date starts at 1/1/1904 = 0 132 if (Math.Truncate(v) < 0) 133 { 134 throw new ArgumentOutOfRangeException("Not a valid Excel date.", innerException: null); 135 } 136 137 return v; 138 case OADateFormats.Excel1900: 139 // Excel with base 1900: Adjust by -1 if v < 61 140 v = v < 61 ? v - 1 : v; 141 142 // Date starts at 1/1/1900 = 1 143 if (Math.Truncate(v) < 1) 144 { 145 throw new ArgumentOutOfRangeException("Not a valid Excel date.", innerException: null); 146 } 147 148 return v; 149 default: 150 // OLE Automation date: Return as is. 151 return v; 152 } 153 } 154 155 /// <summary> 156 /// Convert input string to a <see cref="DateTime"/> object in local time 157 /// </summary> 158 /// <param name="input">String with date/time</param> 159 /// <param name="timestamp">The new <see cref="DateTime"/> object</param> 160 /// <param name="inputParsingErrorMsg">Error message shown to the user</param> 161 /// <returns>True on success; otherwise, false</returns> 162 internal static bool ParseStringAsDateTime(in string input, out DateTime timestamp, out string inputParsingErrorMsg) 163 { 164 inputParsingErrorMsg = string.Empty; 165 CompositeFormat errorMessage = CompositeFormat.Parse(Resources.Microsoft_plugin_timedate_InvalidInput_SupportedRange); 166 167 if (DateTime.TryParse(input, out timestamp)) 168 { 169 // Known date/time format 170 Logger.LogDebug($"Successfully parsed standard date/time format: '{input}' as {timestamp}"); 171 return true; 172 } 173 else if (Regex.IsMatch(input, @"^u[\+-]?\d+$")) 174 { 175 // Unix time stamp 176 // We use long instead of int, because int is too small after 03:14:07 UTC 2038-01-19 177 var canParse = long.TryParse(input.TrimStart('u'), out var secondsU); 178 179 // Value has to be in the range from -62135596800 to 253402300799 180 if (!canParse || secondsU < UnixTimeSecondsMin || secondsU > UnixTimeSecondsMax) 181 { 182 inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_Unix, UnixTimeSecondsMin, UnixTimeSecondsMax); 183 timestamp = new DateTime(1, 1, 1, 1, 1, 1); 184 Logger.LogError($"Failed to parse unix timestamp: '{input}'. Value out of range."); 185 return false; 186 } 187 188 timestamp = DateTimeOffset.FromUnixTimeSeconds(secondsU).LocalDateTime; 189 Logger.LogDebug($"Successfully parsed unix timestamp: '{input}' as {timestamp}"); 190 return true; 191 } 192 else if (Regex.IsMatch(input, @"^ums[\+-]?\d+$")) 193 { 194 // Unix time stamp in milliseconds 195 // We use long instead of int because int is too small after 03:14:07 UTC 2038-01-19 196 var canParse = long.TryParse(input.TrimStart("ums".ToCharArray()), out var millisecondsUms); 197 198 // Value has to be in the range from -62135596800000 to 253402300799999 199 if (!canParse || millisecondsUms < UnixTimeMillisecondsMin || millisecondsUms > UnixTimeMillisecondsMax) 200 { 201 inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_Unix_Milliseconds, UnixTimeMillisecondsMin, UnixTimeMillisecondsMax); 202 timestamp = new DateTime(1, 1, 1, 1, 1, 1); 203 Logger.LogError($"Failed to parse unix millisecond timestamp: '{input}'. Value out of range."); 204 return false; 205 } 206 207 timestamp = DateTimeOffset.FromUnixTimeMilliseconds(millisecondsUms).LocalDateTime; 208 Logger.LogDebug($"Successfully parsed unix millisecond timestamp: '{input}' as {timestamp}"); 209 return true; 210 } 211 else if (Regex.IsMatch(input, @"^ft\d+$")) 212 { 213 var canParse = long.TryParse(input.TrimStart("ft".ToCharArray()), out var secondsFt); 214 215 // Windows file time 216 // Value has to be in the range from 0 to 2650467707991000000 217 if (!canParse || secondsFt < WindowsFileTimeMin || secondsFt > WindowsFileTimeMax) 218 { 219 inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_WindowsFileTime, WindowsFileTimeMin, WindowsFileTimeMax); 220 timestamp = new DateTime(1, 1, 1, 1, 1, 1); 221 Logger.LogError($"Failed to parse Windows file time: '{input}'. Value out of range."); 222 return false; 223 } 224 225 // DateTime.FromFileTime returns as local time. 226 timestamp = DateTime.FromFileTime(secondsFt); 227 Logger.LogDebug($"Successfully parsed Windows file time: '{input}' as {timestamp}"); 228 return true; 229 } 230 else if (Regex.IsMatch(input, @"^oa[+-]?\d+[,.0-9]*$")) 231 { 232 var canParse = double.TryParse(input.TrimStart("oa".ToCharArray()), out var oADate); 233 234 // OLE Automation date 235 // Input has to be in the range from -657434.99999999 to 2958465.99999999 236 // DateTime.FromOADate returns as local time. 237 if (!canParse || oADate < OADateMin || oADate > OADateMax) 238 { 239 inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_OADate, OADateMin, OADateMax); 240 timestamp = new DateTime(1, 1, 1, 1, 1, 1); 241 Logger.LogError($"Failed to parse OLE Automation date: '{input}'. Value out of range."); 242 return false; 243 } 244 245 timestamp = DateTime.FromOADate(oADate); 246 Logger.LogDebug($"Successfully parsed OLE Automation date: '{input}' as {timestamp}"); 247 return true; 248 } 249 else if (Regex.IsMatch(input, @"^exc[+-]?\d+[,.0-9]*$")) 250 { 251 var canParse = double.TryParse(input.TrimStart("exc".ToCharArray()), out var excDate); 252 253 // Excel's 1900 date value 254 // Input has to be in the range from 1 (0 = Fake date) to 2958465.99998843 and not 60 whole number 255 // Because of a bug in Excel and the way it behaves before 3/1/1900 we have to adjust all inputs lower than 61 for +1 256 // DateTime.FromOADate returns as local time. 257 if (!canParse || excDate < 0 || excDate > Excel1900DateMax) 258 { 259 // For the if itself we use 0 as min value that we can show a special message if input is 0. 260 inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_Excel1900, Excel1900DateMin, Excel1900DateMax); 261 timestamp = new DateTime(1, 1, 1, 1, 1, 1); 262 Logger.LogError($"Failed to parse Excel 1900 date value: '{input}'. Value out of range."); 263 return false; 264 } 265 266 if (Math.Truncate(excDate) == 0 || Math.Truncate(excDate) == 60) 267 { 268 inputParsingErrorMsg = Resources.Microsoft_plugin_timedate_InvalidInput_FakeExcel1900; 269 timestamp = new DateTime(1, 1, 1, 1, 1, 1); 270 Logger.LogError($"Failed to parse Excel 1900 date value: '{input}'. Invalid date (0 or 60)."); 271 return false; 272 } 273 274 excDate = excDate <= 60 ? excDate + 1 : excDate; 275 timestamp = DateTime.FromOADate(excDate); 276 Logger.LogDebug($"Successfully parsed Excel 1900 date value: '{input}' as {timestamp}"); 277 return true; 278 } 279 else if (Regex.IsMatch(input, @"^exf[+-]?\d+[,.0-9]*$")) 280 { 281 var canParse = double.TryParse(input.TrimStart("exf".ToCharArray()), out var exfDate); 282 283 // Excel's 1904 date value 284 // Input has to be in the range from 0 to 2957003.99998843 285 // Because Excel uses 01/01/1904 as base we need to adjust for +1462 286 // DateTime.FromOADate returns as local time. 287 if (!canParse || exfDate < Excel1904DateMin || exfDate > Excel1904DateMax) 288 { 289 inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_Excel1904, Excel1904DateMin, Excel1904DateMax); 290 timestamp = new DateTime(1, 1, 1, 1, 1, 1); 291 Logger.LogError($"Failed to parse Excel 1904 date value: '{input}'. Value out of range."); 292 return false; 293 } 294 295 timestamp = DateTime.FromOADate(exfDate + 1462); 296 Logger.LogDebug($"Successfully parsed Excel 1904 date value: '{input}' as {timestamp}"); 297 return true; 298 } 299 else 300 { 301 inputParsingErrorMsg = Resources.Microsoft_plugin_timedate_InvalidInput_ErrorMessageTitle; 302 timestamp = new DateTime(1, 1, 1, 1, 1, 1); 303 Logger.LogWarning($"Failed to parse input: '{input}'. Format not recognized."); 304 return false; 305 } 306 } 307 308 /* htcfreek:Currently not required 309 /// <summary> 310 /// Test if input is special parsing for Unix time, Unix time in milliseconds, file time, ... 311 /// </summary> 312 /// <param name="input">String with date/time</param> 313 /// <returns>True if yes; otherwise, false</returns> 314 internal static bool IsSpecialInputParsing(string input) 315 { 316 return _regexSpecialInputFormats.IsMatch(input); 317 }*/ 318 319 /// <summary> 320 /// Converts a DateTime object based on the format string 321 /// </summary> 322 /// <param name="date">Date/time object.</param> 323 /// <param name="unix">Value for replacing "Unix Time Stamp".</param> 324 /// <param name="unixMilliseconds">Value for replacing "Unix Time Stamp in milliseconds".</param> 325 /// <param name="calWeek">Value for relacing calendar week.</param> 326 /// <param name="eraShortFormat">Era abbreviation.</param> 327 /// <param name="format">Format definition.</param> 328 /// <returns>Formated date/time string.</returns> 329 internal static string ConvertToCustomFormat(DateTime date, long unix, long unixMilliseconds, int calWeek, string eraShortFormat, string format, CalendarWeekRule firstWeekRule, DayOfWeek firstDayOfTheWeek) 330 { 331 var result = format; 332 333 // DOW: Number of day in week 334 result = _regexCustomDateTimeDow.Replace(result, GetNumberOfDayInWeek(date, firstDayOfTheWeek).ToString(CultureInfo.CurrentCulture)); 335 336 // DIM: Days in Month 337 result = _regexCustomDateTimeDim.Replace(result, DateTime.DaysInMonth(date.Year, date.Month).ToString(CultureInfo.CurrentCulture)); 338 339 // WOM: Week of Month 340 result = _regexCustomDateTimeWom.Replace(result, GetWeekOfMonth(date, firstDayOfTheWeek).ToString(CultureInfo.CurrentCulture)); 341 342 // WOY: Week of Year 343 result = _regexCustomDateTimeWoy.Replace(result, calWeek.ToString(CultureInfo.CurrentCulture)); 344 345 // EAB: Era abbreviation 346 result = _regexCustomDateTimeEab.Replace(result, eraShortFormat); 347 348 // WFT: Week of Month 349 if (_regexCustomDateTimeWft.IsMatch(result)) 350 { 351 // Special handling as very early dates can't convert. 352 result = _regexCustomDateTimeWft.Replace(result, date.ToFileTime().ToString(CultureInfo.CurrentCulture)); 353 } 354 355 // UXT: Unix time stamp 356 result = _regexCustomDateTimeUxt.Replace(result, unix.ToString(CultureInfo.CurrentCulture)); 357 358 // UMS: Unix time stamp milli seconds 359 result = _regexCustomDateTimeUms.Replace(result, unixMilliseconds.ToString(CultureInfo.CurrentCulture)); 360 361 // OAD: OLE Automation date 362 result = _regexCustomDateTimeOad.Replace(result, ConvertToOleAutomationFormat(date, OADateFormats.OLEAutomation).ToString(CultureInfo.CurrentCulture)); 363 364 // EXC: Excel date value with base 1900 365 if (_regexCustomDateTimeExc.IsMatch(result)) 366 { 367 // Special handling as very early dates can't convert. 368 result = _regexCustomDateTimeExc.Replace(result, ConvertToOleAutomationFormat(date, OADateFormats.Excel1900).ToString(CultureInfo.CurrentCulture)); 369 } 370 371 // EXF: Excel date value with base 1904 372 if (_regexCustomDateTimeExf.IsMatch(result)) 373 { 374 // Special handling as very early dates can't convert. 375 result = _regexCustomDateTimeExf.Replace(result, ConvertToOleAutomationFormat(date, OADateFormats.Excel1904).ToString(CultureInfo.CurrentCulture)); 376 } 377 378 return result; 379 } 380 381 /// <summary> 382 /// Test a string for our custom date and time format syntax 383 /// </summary> 384 /// <param name="str">String to test.</param> 385 /// <returns>True if yes and otherwise false</returns> 386 internal static bool StringContainsCustomFormatSyntax(string str) 387 { 388 return _regexCustomDateTimeFormats.IsMatch(str); 389 } 390 391 /// <summary> 392 /// Returns a CalendarWeekRule enum value based on the plugin setting. 393 /// </summary> 394 internal static CalendarWeekRule GetCalendarWeekRule(int pluginSetting) 395 { 396 switch (pluginSetting) 397 { 398 case 0: 399 return CalendarWeekRule.FirstDay; 400 case 1: 401 return CalendarWeekRule.FirstFullWeek; 402 case 2: 403 return CalendarWeekRule.FirstFourDayWeek; 404 default: 405 // Wrong json value and system setting (-1). 406 return DateTimeFormatInfo.CurrentInfo.CalendarWeekRule; 407 } 408 } 409 410 /// <summary> 411 /// Returns a DayOfWeek enum value based on the FirstDayOfWeek plugin setting. 412 /// </summary> 413 internal static DayOfWeek GetFirstDayOfWeek(int pluginSetting) 414 { 415 switch (pluginSetting) 416 { 417 case 0: 418 return DayOfWeek.Sunday; 419 case 1: 420 return DayOfWeek.Monday; 421 case 2: 422 return DayOfWeek.Tuesday; 423 case 3: 424 return DayOfWeek.Wednesday; 425 case 4: 426 return DayOfWeek.Thursday; 427 case 5: 428 return DayOfWeek.Friday; 429 case 6: 430 return DayOfWeek.Saturday; 431 default: 432 // Wrong json value and system setting (-1). 433 return DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek; 434 } 435 } 436 } 437 438 /// <summary> 439 /// Type of time/date format 440 /// </summary> 441 internal enum FormatStringType 442 { 443 Time, 444 Date, 445 DateTime, 446 } 447 448 /// <summary> 449 /// Different versions of Date formats based on OLE Automation date 450 /// </summary> 451 internal enum OADateFormats 452 { 453 OLEAutomation, 454 Excel1900, 455 Excel1904, 456 }