// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.PowerToys.Run.Plugin.TimeDate.Properties;
[assembly: InternalsVisibleTo("Microsoft.PowerToys.Run.Plugin.TimeDate.UnitTests")]
namespace Microsoft.PowerToys.Run.Plugin.TimeDate.Components
{
internal static class TimeAndDateHelper
{
private static readonly Regex _regexSpecialInputFormats = new Regex(@"^.*(::)?(u|ums|ft|oa|exc|exf)[+-]\d");
private static readonly Regex _regexCustomDateTimeFormats = new Regex(@"(?
/// Get the format for the time string
///
/// Type of format
/// Show date with weekday and name of month (long format)
/// Show time with seconds (long format)
/// String that identifies the time/date format ()
internal static string GetStringFormat(FormatStringType targetFormat, bool timeLong, bool dateLong)
{
switch (targetFormat)
{
case FormatStringType.Time:
return timeLong ? "T" : "t";
case FormatStringType.Date:
return dateLong ? "D" : "d";
case FormatStringType.DateTime:
if (timeLong & dateLong)
{
return "F"; // Friday, October 31, 2008 5:04:32 PM
}
else if (timeLong & !dateLong)
{
return "G"; // 10/31/2008 5:04:32 PM
}
else if (!timeLong & dateLong)
{
return "f"; // Friday, October 31, 2008 5:04 PM
}
else
{
// (!timeLong & !dateLong)
return "g"; // 10/31/2008 5:04 PM
}
default:
return string.Empty; // Windows default based on current culture settings
}
}
///
/// Returns the number week in the month (Used code from 'David Morton' from )
///
/// date
/// Setting for the first day in the week.
/// Number of week in the month
internal static int GetWeekOfMonth(DateTime date, DayOfWeek formatSettingFirstDayOfWeek)
{
int weekCount = 1;
for (int i = 1; i <= date.Day; i++)
{
DateTime d = new(date.Year, date.Month, i);
// Count week number +1 if day is the first day of a week and not day 1 of the month.
// (If we count on day one of a month we would start the month with week number 2.)
if (i > 1 && d.DayOfWeek == formatSettingFirstDayOfWeek)
{
weekCount += 1;
}
}
return weekCount;
}
///
/// Returns the number of the day in the week
///
/// Date
/// Number of the day in the week
internal static int GetNumberOfDayInWeek(DateTime date, DayOfWeek formatSettingFirstDayOfWeek)
{
int daysInWeek = 7;
int adjustment = 1; // We count from 1 to 7 and not from 0 to 6
return ((date.DayOfWeek + daysInWeek - formatSettingFirstDayOfWeek) % daysInWeek) + adjustment;
}
internal static double ConvertToOleAutomationFormat(DateTime date, OADateFormats type)
{
double v = date.ToOADate();
switch (type)
{
case OADateFormats.Excel1904:
// Excel with base 1904: Adjust by -1462
v -= 1462;
// Date starts at 1/1/1904 = 0
if (Math.Truncate(v) < 0)
{
throw new ArgumentOutOfRangeException("Not a valid Excel date.", innerException: null);
}
return v;
case OADateFormats.Excel1900:
// Excel with base 1900: Adjust by -1 if v < 61
v = v < 61 ? v - 1 : v;
// Date starts at 1/1/1900 = 1
if (Math.Truncate(v) < 1)
{
throw new ArgumentOutOfRangeException("Not a valid Excel date.", innerException: null);
}
return v;
default:
// OLE Automation date: Return as is.
return v;
}
}
///
/// Convert input string to a object in local time
///
/// String with date/time
/// The new object
/// Error message shown to the user
/// True on success; otherwise, false
internal static bool ParseStringAsDateTime(in string input, out DateTime timestamp, out string inputParsingErrorMsg)
{
inputParsingErrorMsg = string.Empty;
CompositeFormat errorMessage = CompositeFormat.Parse(Resources.Microsoft_plugin_timedate_InvalidInput_SupportedRange);
if (DateTime.TryParse(input, out timestamp))
{
// Known date/time format
return true;
}
else if (Regex.IsMatch(input, @"^u[\+-]?\d+$"))
{
// Unix time stamp
// We use long instead of int, because int is too small after 03:14:07 UTC 2038-01-19
var canParse = long.TryParse(input.TrimStart('u'), out var secondsU);
// Value has to be in the range from -62135596800 to 253402300799
if (!canParse || secondsU < UnixTimeSecondsMin || secondsU > UnixTimeSecondsMax)
{
inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_Unix, UnixTimeSecondsMin, UnixTimeSecondsMax);
timestamp = new DateTime(1, 1, 1, 1, 1, 1);
return false;
}
timestamp = DateTimeOffset.FromUnixTimeSeconds(secondsU).LocalDateTime;
return true;
}
else if (Regex.IsMatch(input, @"^ums[\+-]?\d+$"))
{
// Unix time stamp in milliseconds
// We use long instead of int because int is too small after 03:14:07 UTC 2038-01-19
var canParse = long.TryParse(input.TrimStart("ums".ToCharArray()), out var millisecondsUms);
// Value has to be in the range from -62135596800000 to 253402300799999
if (!canParse || millisecondsUms < UnixTimeMillisecondsMin || millisecondsUms > UnixTimeMillisecondsMax)
{
inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_Unix_Milliseconds, UnixTimeMillisecondsMin, UnixTimeMillisecondsMax);
timestamp = new DateTime(1, 1, 1, 1, 1, 1);
return false;
}
timestamp = DateTimeOffset.FromUnixTimeMilliseconds(millisecondsUms).LocalDateTime;
return true;
}
else if (Regex.IsMatch(input, @"^ft\d+$"))
{
var canParse = long.TryParse(input.TrimStart("ft".ToCharArray()), out var secondsFt);
// Windows file time
// Value has to be in the range from 0 to 2650467707991000000
if (!canParse || secondsFt < WindowsFileTimeMin || secondsFt > WindowsFileTimeMax)
{
inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_WindowsFileTime, WindowsFileTimeMin, WindowsFileTimeMax);
timestamp = new DateTime(1, 1, 1, 1, 1, 1);
return false;
}
// DateTime.FromFileTime returns as local time.
timestamp = DateTime.FromFileTime(secondsFt);
return true;
}
else if (Regex.IsMatch(input, @"^oa[+-]?\d+[,.0-9]*$"))
{
var canParse = double.TryParse(input.TrimStart("oa".ToCharArray()), out var oADate);
// OLE Automation date
// Input has to be in the range from -657434.99999999 to 2958465.99999999
// DateTime.FromOADate returns as local time.
if (!canParse || oADate < OADateMin || oADate > OADateMax)
{
inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_OADate, OADateMin, OADateMax);
timestamp = new DateTime(1, 1, 1, 1, 1, 1);
return false;
}
timestamp = DateTime.FromOADate(oADate);
return true;
}
else if (Regex.IsMatch(input, @"^exc[+-]?\d+[,.0-9]*$"))
{
var canParse = double.TryParse(input.TrimStart("exc".ToCharArray()), out var excDate);
// Excel's 1900 date value
// Input has to be in the range from 1 (0 = Fake date) to 2958465.99998843 and not 60 whole number
// 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
// DateTime.FromOADate returns as local time.
if (!canParse || excDate < 0 || excDate > Excel1900DateMax)
{
// For the if itself we use 0 as min value that we can show a special message if input is 0.
inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_Excel1900, Excel1900DateMin, Excel1900DateMax);
timestamp = new DateTime(1, 1, 1, 1, 1, 1);
return false;
}
if (Math.Truncate(excDate) == 0 || Math.Truncate(excDate) == 60)
{
inputParsingErrorMsg = Resources.Microsoft_plugin_timedate_InvalidInput_FakeExcel1900;
timestamp = new DateTime(1, 1, 1, 1, 1, 1);
return false;
}
excDate = excDate <= 60 ? excDate + 1 : excDate;
timestamp = DateTime.FromOADate(excDate);
return true;
}
else if (Regex.IsMatch(input, @"^exf[+-]?\d+[,.0-9]*$"))
{
var canParse = double.TryParse(input.TrimStart("exf".ToCharArray()), out var exfDate);
// Excel's 1904 date value
// Input has to be in the range from 0 to 2957003.99998843
// Because Excel uses 01/01/1904 as base we need to adjust for +1462
// DateTime.FromOADate returns as local time.
if (!canParse || exfDate < Excel1904DateMin || exfDate > Excel1904DateMax)
{
inputParsingErrorMsg = string.Format(CultureInfo.CurrentCulture, errorMessage, Resources.Microsoft_plugin_timedate_Excel1904, Excel1904DateMin, Excel1904DateMax);
timestamp = new DateTime(1, 1, 1, 1, 1, 1);
return false;
}
timestamp = DateTime.FromOADate(exfDate + 1462);
return true;
}
else
{
timestamp = new DateTime(1, 1, 1, 1, 1, 1);
return false;
}
}
///
/// Test if input is special parsing for Unix time, Unix time in milliseconds, file time, ...
///
/// String with date/time
/// True if yes; otherwise, false
internal static bool IsSpecialInputParsing(string input)
{
return _regexSpecialInputFormats.IsMatch(input);
}
///
/// Converts a DateTime object based on the format string
///
/// Date/time object.
/// Value for replacing "Unix Time Stamp".
/// Value for replacing "Unix Time Stamp in milliseconds".
/// Value for relacing calendar week.
/// Era abbreviation.
/// Format definition.
/// Formated date/time string.
internal static string ConvertToCustomFormat(DateTime date, long unix, long unixMilliseconds, int calWeek, string eraShortFormat, string format, CalendarWeekRule firstWeekRule, DayOfWeek firstDayOfTheWeek)
{
string result = format;
// DOW: Number of day in week
result = _regexCustomDateTimeDow.Replace(result, GetNumberOfDayInWeek(date, firstDayOfTheWeek).ToString(CultureInfo.CurrentCulture));
// DIM: Days in Month
result = _regexCustomDateTimeDim.Replace(result, DateTime.DaysInMonth(date.Year, date.Month).ToString(CultureInfo.CurrentCulture));
// WOM: Week of Month
result = _regexCustomDateTimeWom.Replace(result, GetWeekOfMonth(date, firstDayOfTheWeek).ToString(CultureInfo.CurrentCulture));
// WOY: Week of Year
result = _regexCustomDateTimeWoy.Replace(result, calWeek.ToString(CultureInfo.CurrentCulture));
// EAB: Era abbreviation
result = _regexCustomDateTimeEab.Replace(result, eraShortFormat);
// WFT: Week of Month
if (_regexCustomDateTimeWft.IsMatch(result))
{
// Special handling as very early dates can't convert.
result = _regexCustomDateTimeWft.Replace(result, date.ToFileTime().ToString(CultureInfo.CurrentCulture));
}
// UXT: Unix time stamp
result = _regexCustomDateTimeUxt.Replace(result, unix.ToString(CultureInfo.CurrentCulture));
// UMS: Unix time stamp milli seconds
result = _regexCustomDateTimeUms.Replace(result, unixMilliseconds.ToString(CultureInfo.CurrentCulture));
// OAD: OLE Automation date
result = _regexCustomDateTimeOad.Replace(result, ConvertToOleAutomationFormat(date, OADateFormats.OLEAutomation).ToString(CultureInfo.CurrentCulture));
// EXC: Excel date value with base 1900
if (_regexCustomDateTimeExc.IsMatch(result))
{
// Special handling as very early dates can't convert.
result = _regexCustomDateTimeExc.Replace(result, ConvertToOleAutomationFormat(date, OADateFormats.Excel1900).ToString(CultureInfo.CurrentCulture));
}
// EXF: Excel date value with base 1904
if (_regexCustomDateTimeExf.IsMatch(result))
{
// Special handling as very early dates can't convert.
result = _regexCustomDateTimeExf.Replace(result, ConvertToOleAutomationFormat(date, OADateFormats.Excel1904).ToString(CultureInfo.CurrentCulture));
}
return result;
}
///
/// Test a string for our custom date and time format syntax
///
/// String to test.
/// True if yes and otherwise false
internal static bool StringContainsCustomFormatSyntax(string str)
{
return _regexCustomDateTimeFormats.IsMatch(str);
}
///
/// Returns a CalendarWeekRule enum value based on the plugin setting.
///
internal static CalendarWeekRule GetCalendarWeekRule(int pluginSetting)
{
switch (pluginSetting)
{
case 0:
return CalendarWeekRule.FirstDay;
case 1:
return CalendarWeekRule.FirstFullWeek;
case 2:
return CalendarWeekRule.FirstFourDayWeek;
default:
// Wrong json value and system setting (-1).
return DateTimeFormatInfo.CurrentInfo.CalendarWeekRule;
}
}
///
/// Returns a DayOfWeek enum value based on the FirstDayOfWeek plugin setting.
///
internal static DayOfWeek GetFirstDayOfWeek(int pluginSetting)
{
switch (pluginSetting)
{
case 0:
return DayOfWeek.Sunday;
case 1:
return DayOfWeek.Monday;
case 2:
return DayOfWeek.Tuesday;
case 3:
return DayOfWeek.Wednesday;
case 4:
return DayOfWeek.Thursday;
case 5:
return DayOfWeek.Friday;
case 6:
return DayOfWeek.Saturday;
default:
// Wrong json value and system setting (-1).
return DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek;
}
}
}
///
/// Type of time/date format
///
internal enum FormatStringType
{
Time,
Date,
DateTime,
}
///
/// Different versions of Date formats based on OLE Automation date
///
internal enum OADateFormats
{
OLEAutomation,
Excel1900,
Excel1904,
}
}