/ src / modules / cmdpal / ext / Microsoft.CmdPal.Ext.TimeDate / Helpers / TimeAndDateHelper.cs
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  }