diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 46432105d052..ea8ffa19ada8 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -273,6 +273,7 @@ CURSORINFO cursorpos customaction CUSTOMACTIONTEST +CUSTOMFORMATPLACEHOLDER CVal cvd CVirtual @@ -1648,6 +1649,7 @@ telephon templatenamespace testprocess TEXCOORD +TEXTBOXNEWLINE TEXTEXTRACTOR TEXTINCLUDE tfopen diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResult.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResult.cs index 9393d82b6f13..7665de85df23 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResult.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResult.cs @@ -4,11 +4,9 @@ using System.Runtime.CompilerServices; using Microsoft.CommandPalette.Extensions.Toolkit; -[assembly: InternalsVisibleTo("Microsoft.PowerToys.Run.Plugin.TimeDate.UnitTests")] - namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; -internal class AvailableResult +internal sealed class AvailableResult { /// /// Gets or sets the time/date value @@ -30,6 +28,11 @@ internal class AvailableResult /// internal ResultIconType IconType { get; set; } + /// + /// Gets or sets a value to show additional error details + /// + internal string ErrorDetails { get; set; } = string.Empty; + /// /// Returns the path to the icon /// @@ -42,6 +45,7 @@ public IconInfo GetIconInfo() ResultIconType.Time => ResultHelper.TimeIcon, ResultIconType.Date => ResultHelper.CalendarIcon, ResultIconType.DateTime => ResultHelper.TimeDateIcon, + ResultIconType.Error => ResultHelper.ErrorIcon, _ => null, }; } @@ -53,6 +57,7 @@ public ListItem ToListItem() Title = this.Value, Subtitle = this.Label, Icon = this.GetIconInfo(), + Details = string.IsNullOrEmpty(this.ErrorDetails) ? null : new Details() { Body = this.ErrorDetails }, }; } @@ -81,4 +86,5 @@ public enum ResultIconType Time, Date, DateTime, + Error, } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs index f01e6b8b0706..bc6a3b972c61 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.RegularExpressions; namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; @@ -69,6 +70,86 @@ internal static List GetList(bool isKeywordSearch, SettingsMana var era = DateTimeFormatInfo.CurrentInfo.GetEraName(calendar.GetEra(dateTimeNow)); var eraShort = DateTimeFormatInfo.CurrentInfo.GetAbbreviatedEraName(calendar.GetEra(dateTimeNow)); + // Custom formats + foreach (var f in settings.CustomFormats) + { + var formatParts = f.Split("=", 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var formatSyntax = formatParts.Length == 2 ? formatParts[1] : string.Empty; + var searchTags = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagCustom"); + var dtObject = dateTimeNow; + + // If Length = 0 then empty string. + if (formatParts.Length >= 1) + { + try + { + // Verify and check input and update search tags + if (formatParts.Length == 1) + { + throw new FormatException("Format syntax part after equal sign is missing."); + } + + var containsCustomSyntax = TimeAndDateHelper.StringContainsCustomFormatSyntax(formatSyntax); + if (formatSyntax.StartsWith("UTC:", StringComparison.InvariantCulture)) + { + searchTags = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagCustomUtc"); + dtObject = dateTimeNowUtc; + } + + // Get formated date + var value = TimeAndDateHelper.ConvertToCustomFormat(dtObject, unixTimestamp, unixTimestampMilliseconds, weekOfYear, eraShort, Regex.Replace(formatSyntax, "^UTC:", string.Empty), firstWeekRule, firstDayOfTheWeek); + try + { + value = dtObject.ToString(value, CultureInfo.CurrentCulture); + } + catch + { + if (!containsCustomSyntax) + { + throw; + } + else + { + // Do not fail as we have custom format syntax. Instead fix backslashes. + value = Regex.Replace(value, @"(? GetList(bool isKeywordSearch, SettingsMana IconType = ResultIconType.Date, }, new AvailableResult() + { + Value = DateTime.DaysInMonth(dateTimeNow.Year, dateTimeNow.Month).ToString(CultureInfo.CurrentCulture), + Label = Resources.Microsoft_plugin_timedate_DaysInMonth, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() { Value = dateTimeNow.DayOfYear.ToString(CultureInfo.CurrentCulture), Label = Resources.Microsoft_plugin_timedate_DayOfYear, @@ -198,6 +286,13 @@ internal static List GetList(bool isKeywordSearch, SettingsMana IconType = ResultIconType.Date, }, new AvailableResult() + { + Value = DateTime.IsLeapYear(dateTimeNow.Year) ? Resources.Microsoft_plugin_timedate_LeapYear : Resources.Microsoft_plugin_timedate_NoLeapYear, + Label = Resources.Microsoft_plugin_timedate_LeapYear, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), + IconType = ResultIconType.Date, + }, + new AvailableResult() { Value = era, Label = Resources.Microsoft_plugin_timedate_Era, @@ -218,13 +313,31 @@ internal static List GetList(bool isKeywordSearch, SettingsMana AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"), IconType = ResultIconType.Date, }, - new AvailableResult() + }); + + try + { + results.Add(new AvailableResult() { Value = dateTimeNow.ToFileTime().ToString(CultureInfo.CurrentCulture), Label = Resources.Microsoft_plugin_timedate_WindowsFileTime, AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), IconType = ResultIconType.DateTime, - }, + }); + } + catch + { + results.Add(new AvailableResult() + { + Value = Resources.Microsoft_plugin_timedate_ErrorConvertWft, + Label = Resources.Microsoft_plugin_timedate_WindowsFileTime, + AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"), + IconType = ResultIconType.Error, + }); + } + + results.AddRange(new[] + { new AvailableResult() { Value = dateTimeNowUtc.ToString("u"), diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/ResultHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/ResultHelper.cs index 7051894223ef..e2ba93274340 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/ResultHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/ResultHelper.cs @@ -2,9 +2,8 @@ // 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.IO; +using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; @@ -33,21 +32,24 @@ internal static string SelectStringFromResources(bool isSystemTimeDate, string s public static IconInfo TimeDateIcon { get; } = new IconInfo("\uEC92"); + public static IconInfo ErrorIcon { get; } = IconHelpers.FromRelativePaths("Microsoft.CmdPal.Ext.TimeDate\\Assets\\Warning.light.png", "Microsoft.CmdPal.Ext.TimeDate\\Assets\\Warning.dark.png"); + /// - /// Gets a result with an error message that only numbers can't be parsed + /// Gets a result with an error message that input can't be parsed /// /// Element of type . - internal static ListItem CreateNumberErrorResult() => new ListItem(new NoOpCommand()) - { - Title = Resources.Microsoft_plugin_timedate_ErrorResultTitle, - Subtitle = Resources.Microsoft_plugin_timedate_ErrorResultSubTitle, - Icon = IconHelpers.FromRelativePaths("Microsoft.CmdPal.Ext.TimeDate\\Assets\\Warning.light.png", "Microsoft.CmdPal.Ext.TimeDate\\Assets\\Warning.dark.png"), - }; - +#pragma warning disable CA1863 // Use 'CompositeFormat' internal static ListItem CreateInvalidInputErrorResult() => new ListItem(new NoOpCommand()) { Title = Resources.Microsoft_plugin_timedate_InvalidInput_ErrorMessageTitle, - Subtitle = Resources.Microsoft_plugin_timedate_InvalidInput_ErrorMessageSubTitle, - Icon = IconHelpers.FromRelativePaths("Microsoft.CmdPal.Ext.TimeDate\\Assets\\Warning.light.png", "Microsoft.CmdPal.Ext.TimeDate\\Assets\\Warning.dark.png"), + Icon = ErrorIcon, + Details = new Details() + { + Title = Resources.Microsoft_plugin_timedate_InvalidInput_DetailsHeader, + + // Because of translation we can't use 'CompositeFormat'. + Body = string.Format(CultureInfo.CurrentCulture, Resources.Microsoft_plugin_timedate_InvalidInput_SupportedInput, "**", "\n\n", "\n\n* "), + }, }; +#pragma warning restore CA1863 // Use 'CompositeFormat' } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs index d0de0017b82c..5b30a4816f83 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs @@ -13,6 +13,11 @@ namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; public class SettingsManager : JsonSettingsManager { + // Line break character used in WinUI3 TextBox and TextBlock. + private const char TEXTBOXNEWLINE = '\r'; + + private const string CUSTOMFORMATPLACEHOLDER = "MyFormat=dd-MMM-yyyy\rMySecondFormat=dddd (Da\\y nu\\mber: DOW)\rMyUtcFormat=UTC:hh:mm:ss"; + private static readonly string _namespace = "timeDate"; private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; @@ -94,6 +99,12 @@ public class SettingsManager : JsonSettingsManager Resources.Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery, true); // TODO -- double check default value + private readonly TextSetting _customFormats = new( + Namespaced(nameof(CustomFormats)), + Resources.Microsoft_plugin_timedate_Setting_CustomFormats, + Resources.Microsoft_plugin_timedate_Setting_CustomFormats + TEXTBOXNEWLINE + string.Format(CultureInfo.CurrentCulture, Resources.Microsoft_plugin_timedate_Setting_CustomFormatsDescription.ToString(), "DOW", "DIM", "WOM", "WOY", "EAB", "WFT", "UXT", "UMS", "OAD", "EXC", "EXF", "UTC:"), + string.Empty); + public int FirstWeekOfYear { get @@ -142,6 +153,8 @@ public int FirstDayOfWeek public bool HideNumberMessageOnGlobalQuery => _hideNumberMessageOnGlobalQuery.Value; + public List CustomFormats => _customFormats.Value.Split(TEXTBOXNEWLINE).ToList(); + internal static string SettingsJsonPath() { var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); @@ -155,12 +168,18 @@ public SettingsManager() { FilePath = SettingsJsonPath(); - Settings.Add(_firstWeekOfYear); - Settings.Add(_firstDayOfWeek); + /* The following two settings make no sense with current CmdPal behavior. Settings.Add(_onlyDateTimeNowGlobal); + Settings.Add(_hideNumberMessageOnGlobalQuery); */ + Settings.Add(_timeWithSeconds); Settings.Add(_dateWithWeekday); - Settings.Add(_hideNumberMessageOnGlobalQuery); + Settings.Add(_firstWeekOfYear); + Settings.Add(_firstDayOfWeek); + + _customFormats.Multiline = true; + _customFormats.Placeholder = CUSTOMFORMATPLACEHOLDER; + Settings.Add(_customFormats); // Load settings from file upon initialization LoadSettings(); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeAndDateHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeAndDateHelper.cs index 3450eda62757..368e20a48ec3 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeAndDateHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeAndDateHelper.cs @@ -4,12 +4,42 @@ using System; using System.Globalization; +using System.Text; using System.Text.RegularExpressions; namespace Microsoft.CmdPal.Ext.TimeDate.Helpers; internal static class TimeAndDateHelper { + /* htcfreek:Currently not used. + * 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 /// @@ -53,18 +83,25 @@ internal static string GetStringFormat(FormatStringType targetFormat, bool timeL /// 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) { - var beginningOfMonth = new DateTime(date.Year, date.Month, 1); - var adjustment = 1; // We count from 1 to 7 and not from 0 to 6 + var weekCount = 1; - while (date.Date.AddDays(1).DayOfWeek != formatSettingFirstDayOfWeek) + for (var i = 1; i <= date.Day; i++) { - date = date.AddDays(1); + 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 (int)Math.Truncate((double)date.Subtract(beginningOfMonth).TotalDays / 7f) + adjustment; + return weekCount; } /// @@ -80,40 +117,170 @@ internal static int GetNumberOfDayInWeek(DateTime date, DayOfWeek formatSettingF return ((date.DayOfWeek + daysInWeek - formatSettingFirstDayOfWeek) % daysInWeek) + adjustment; } + internal static double ConvertToOleAutomationFormat(DateTime date, OADateFormats type) + { + var 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) + 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{1,10}$") && long.TryParse(input.TrimStart('u'), out var secondsU)) + 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{1,13}$") && long.TryParse(input.TrimStart("ums".ToCharArray()), out var millisecondsUms)) + 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+$") && long.TryParse(input.TrimStart("ft".ToCharArray()), out var secondsFt)) + 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); @@ -121,14 +288,87 @@ internal static bool ParseStringAsDateTime(in string input, out DateTime timesta } } + /* htcfreek:Currently not required /// - /// Test if input is special parsing for Unix time, Unix time in milliseconds or File time. + /// 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 Regex.IsMatch(input, @"^.*(u|ums|ft)\d"); + 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) + { + var 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); } /// @@ -187,3 +427,13 @@ internal enum FormatStringType Date, DateTime, } + +/// +/// Different versions of Date formats based on OLE Automation date +/// +internal enum OADateFormats +{ + OLEAutomation, + Excel1900, + Excel1904, +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs index 50222366780a..eb4eed18cd33 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs @@ -29,13 +29,16 @@ public sealed partial class TimeDateCalculator /// List of Wox s. public static List ExecuteSearch(SettingsManager settings, string query) { - var isEmptySearchInput = string.IsNullOrEmpty(query); + var isEmptySearchInput = string.IsNullOrWhiteSpace(query); List availableFormats = new List(); List results = new List(); // currently, all of the search in V2 is keyword search. var isKeywordSearch = true; + // Last input parsing error + var lastInputParsingErrorMsg = string.Empty; + // Switch search type if (isEmptySearchInput || (!isKeywordSearch && settings.OnlyDateTimeNowGlobal)) { @@ -47,13 +50,13 @@ public static List ExecuteSearch(SettingsManager settings, string quer { // Search for specified format with specified time/date value var userInput = query.Split(InputDelimiter); - if (TimeAndDateHelper.ParseStringAsDateTime(userInput[1], out DateTime timestamp)) + if (TimeAndDateHelper.ParseStringAsDateTime(userInput[1], out DateTime timestamp, out lastInputParsingErrorMsg)) { availableFormats.AddRange(AvailableResultsList.GetList(isKeywordSearch, settings, null, null, timestamp)); query = userInput[0]; } } - else if (TimeAndDateHelper.ParseStringAsDateTime(query, out DateTime timestamp)) + else if (TimeAndDateHelper.ParseStringAsDateTime(query, out DateTime timestamp, out lastInputParsingErrorMsg)) { // Return all formats for specified time/date value availableFormats.AddRange(AvailableResultsList.GetList(isKeywordSearch, settings, null, null, timestamp)); @@ -88,19 +91,32 @@ public static List ExecuteSearch(SettingsManager settings, string quer } } + /*htcfreek:Code obsolete with current CmdPal behavior. // If search term is only a number that can't be parsed return an error message if (!isEmptySearchInput && results.Count == 0 && Regex.IsMatch(query, @"\w+\d+.*$") && !query.Any(char.IsWhiteSpace) && (TimeAndDateHelper.IsSpecialInputParsing(query) || !Regex.IsMatch(query, @"\d+[\.:/]\d+"))) { // Without plugin key word show only if message is not hidden by setting if (!settings.HideNumberMessageOnGlobalQuery) { - results.Add(ResultHelper.CreateNumberErrorResult()); + var er = ResultHelper.CreateInvalidInputErrorResult(); + if (!string.IsNullOrEmpty(lastInputParsingErrorMsg)) + { + er.Details = new Details() { Body = lastInputParsingErrorMsg }; + } + + results.Add(er); } - } + } */ if (results.Count == 0) { - results.Add(ResultHelper.CreateInvalidInputErrorResult()); + var er = ResultHelper.CreateInvalidInputErrorResult(); + if (!string.IsNullOrEmpty(lastInputParsingErrorMsg)) + { + er.Details = new Details() { Body = lastInputParsingErrorMsg }; + } + + results.Add(er); } return results; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Pages/TimeDateExtensionPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Pages/TimeDateExtensionPage.cs index edcdb02e4bab..024a55908772 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Pages/TimeDateExtensionPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Pages/TimeDateExtensionPage.cs @@ -30,6 +30,7 @@ public TimeDateExtensionPage(SettingsManager settingsManager) PlaceholderText = Resources.Microsoft_plugin_timedate_placeholder_text; Id = "com.microsoft.cmdpal.timedate"; _settingsManager = settingsManager; + ShowDetails = true; } public override IListItem[] GetItems() diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs index 8891e81c2b36..d62d76eefdd8 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs @@ -150,6 +150,15 @@ public static string Microsoft_plugin_timedate_DayOfYear { } } + /// + /// Looks up a localized string similar to Days in month. + /// + public static string Microsoft_plugin_timedate_DaysInMonth { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_DaysInMonth", resourceCulture); + } + } + /// /// Looks up a localized string similar to Era. /// @@ -169,20 +178,38 @@ public static string Microsoft_plugin_timedate_EraAbbreviation { } /// - /// Looks up a localized string similar to Valid prefixes: 'u' for Unix Timestamp, 'ums' for Unix Timestamp in milliseconds, 'ft' for Windows file time. + /// Looks up a localized string similar to Failed to convert into custom format. + /// + public static string Microsoft_plugin_timedate_ErrorConvertCustomFormat { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_ErrorConvertCustomFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Not a valid Windows file time. + /// + public static string Microsoft_plugin_timedate_ErrorConvertWft { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_ErrorConvertWft", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Excel's 1900 date value. /// - public static string Microsoft_plugin_timedate_ErrorResultSubTitle { + public static string Microsoft_plugin_timedate_Excel1900 { get { - return ResourceManager.GetString("Microsoft_plugin_timedate_ErrorResultSubTitle", resourceCulture); + return ResourceManager.GetString("Microsoft_plugin_timedate_Excel1900", resourceCulture); } } /// - /// Looks up a localized string similar to Error: Invalid number input. + /// Looks up a localized string similar to Excel's 1904 date value. /// - public static string Microsoft_plugin_timedate_ErrorResultTitle { + public static string Microsoft_plugin_timedate_Excel1904 { get { - return ResourceManager.GetString("Microsoft_plugin_timedate_ErrorResultTitle", resourceCulture); + return ResourceManager.GetString("Microsoft_plugin_timedate_Excel1904", resourceCulture); } } @@ -205,11 +232,20 @@ public static string Microsoft_plugin_timedate_Hour { } /// - /// Looks up a localized string similar to Valid prefixes: 'u' for Unix Timestamp, 'ums' for Unix Timestamp in milliseconds, 'ft' for Windows file time. + /// Looks up a localized string similar to Invalid custom format:. /// - public static string Microsoft_plugin_timedate_InvalidInput_ErrorMessageSubTitle { + public static string Microsoft_plugin_timedate_InvalidCustomFormat { get { - return ResourceManager.GetString("Microsoft_plugin_timedate_InvalidInput_ErrorMessageSubTitle", resourceCulture); + return ResourceManager.GetString("Microsoft_plugin_timedate_InvalidCustomFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Supported input. + /// + public static string Microsoft_plugin_timedate_InvalidInput_DetailsHeader { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_InvalidInput_DetailsHeader", resourceCulture); } } @@ -222,6 +258,33 @@ public static string Microsoft_plugin_timedate_InvalidInput_ErrorMessageTitle { } } + /// + /// Looks up a localized string similar to Cannot parse the input as Excel's 1900 date value because it is a fake date. (In Excel 0 stands for 0/1/1900 and this date doesn't exist. And 60 stands for 2/29/1900 and this date only exists in Excel for compatibility with Lotus 123.). + /// + public static string Microsoft_plugin_timedate_InvalidInput_FakeExcel1900 { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_InvalidInput_FakeExcel1900", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A {0}format name{0}, a {0}valid date or time value{0}, or a {0}prefixed number{0}. To search for a format in a specific date/time please use the syntax {0}format::date/time/number{0}.{1}Supported prefixes:{2}'{0}u{0}' for Unix Timestamp{2}'{0}ums{0}' for Unix Timestamp in milliseconds{2}'{0}ft{0}' for Windows file time{2}'{0}oa{0}' for OLE Automation Date{2}'{0}exc{0}' for Excel's 1900 date value{2}'{0}exf{0}' for Excel's 1904 date value. + /// + public static string Microsoft_plugin_timedate_InvalidInput_SupportedInput { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_InvalidInput_SupportedInput", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your input for {0} is outside the range **from {1} to {2}**.. + /// + public static string Microsoft_plugin_timedate_InvalidInput_SupportedRange { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_InvalidInput_SupportedRange", resourceCulture); + } + } + /// /// Looks up a localized string similar to ISO 8601. /// @@ -258,6 +321,15 @@ public static string Microsoft_plugin_timedate_Iso8601ZoneUtc { } } + /// + /// Looks up a localized string similar to Leap year. + /// + public static string Microsoft_plugin_timedate_LeapYear { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_LeapYear", resourceCulture); + } + } + /// /// Looks up a localized string similar to Open. /// @@ -321,6 +393,15 @@ public static string Microsoft_plugin_timedate_MonthYear { } } + /// + /// Looks up a localized string similar to Not a leap year. + /// + public static string Microsoft_plugin_timedate_NoLeapYear { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_NoLeapYear", resourceCulture); + } + } + /// /// Looks up a localized string similar to Now. /// @@ -339,6 +420,15 @@ public static string Microsoft_plugin_timedate_NowUtc { } } + /// + /// Looks up a localized string similar to OLE Automation Date. + /// + public static string Microsoft_plugin_timedate_OADate { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_OADate", resourceCulture); + } + } + /// /// Looks up a localized string similar to Search values or type a custom time stamp.... /// @@ -411,6 +501,42 @@ public static string Microsoft_plugin_timedate_Search_ConjunctionList { } } + /// + /// Looks up a localized string similar to Date and time; Time and Date; Custom format. + /// + public static string Microsoft_plugin_timedate_SearchTagCustom { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagCustom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current date and time; Current time and date; Now; Custom format. + /// + public static string Microsoft_plugin_timedate_SearchTagCustomNow { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagCustomNow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Date and time UTC; Time UTC and Date; Custom UTC format. + /// + public static string Microsoft_plugin_timedate_SearchTagCustomUtc { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagCustomUtc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current date and time UTC; Current time UTC and date; Now UTC; Custom UTC format. + /// + public static string Microsoft_plugin_timedate_SearchTagCustomUtcNow { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagCustomUtcNow", resourceCulture); + } + } + /// /// Looks up a localized string similar to Date. /// @@ -492,6 +618,24 @@ public static string Microsoft_plugin_timedate_Second { } } + /// + /// Looks up a localized string similar to Custom formats. + /// + public static string Microsoft_plugin_timedate_Setting_CustomFormats { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Setting_CustomFormats", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use date and time string format syntax and {0} (Day of Week), {1} (Days in Month), {2} (Week of Month), {3} (Week of the year), {4} (Era abbreviation), {5} (Windows File Time), {6} (Unix Time), {7} (Unix Time in milliseconds), {8} (OLE Automation date), {9} (Excel's 1900 based date value), {10} (Excel's 1904 based date value). If the format starts with {11}, then Universal Time (UTC) is used. (Use a backslash to escape format sequences and the backslash character as text.). + /// + public static string Microsoft_plugin_timedate_Setting_CustomFormatsDescription { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_Setting_CustomFormatsDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use system setting. /// @@ -681,6 +825,15 @@ public static string Microsoft_plugin_timedate_SettingTimeWithSeconds_Descriptio } } + /// + /// Looks up a localized string similar to Select for more details.. + /// + public static string Microsoft_plugin_timedate_show_details { + get { + return ResourceManager.GetString("Microsoft_plugin_timedate_show_details", resourceCulture); + } + } + /// /// Looks up a localized string similar to Select or press Ctrl+C to copy. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx index a3974911c461..f1a36e2a906a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx @@ -155,11 +155,8 @@ Era abbreviation - - Valid prefixes: 'u' for Unix Timestamp, 'ums' for Unix Timestamp in milliseconds, 'ft' for Windows file time - - - Error: Invalid number input + + Supported input Hour @@ -372,7 +369,68 @@ Time and Date - - Valid prefixes: 'u' for Unix Timestamp, 'ums' for Unix Timestamp in milliseconds, 'ft' for Windows file time + + A {0}format name{0}, a {0}valid date or time value{0}, or a {0}prefixed number{0}. To search for a format in a specific date/time please use the syntax {0}format::date/time/number{0}.{1}Supported prefixes:{2}'{0}u{0}' for Unix Timestamp{2}'{0}ums{0}' for Unix Timestamp in milliseconds{2}'{0}ft{0}' for Windows file time{2}'{0}oa{0}' for OLE Automation Date{2}'{0}exc{0}' for Excel's 1900 date value{2}'{0}exf{0}' for Excel's 1904 date value + The placed holders are replaced with formatting syntax in code. + + + Date and time; Time and Date; Custom format + Don't change order + + + Date and time UTC; Time UTC and Date; Custom UTC format + Don't change order + + + Current date and time; Current time and date; Now; Custom format + Don't change order + + + Current date and time UTC; Current time UTC and date; Now UTC; Custom UTC format + Don't change order + + + Invalid custom format: + + + Custom formats + + + Use date and time string format syntax and {0} (Day of Week), {1} (Days in Month), {2} (Week of Month), {3} (Week of the year), {4} (Era abbreviation), {5} (Windows File Time), {6} (Unix Time), {7} (Unix Time in milliseconds), {8} (OLE Automation date), {9} (Excel's 1900 based date value), {10} (Excel's 1904 based date value). If the format starts with {11}, then Universal Time (UTC) is used. (Use a backslash to escape format sequences and the backslash character as text.) + The {n} parts are place holders and get replaced in the code. + + + Select for more details. + + + Failed to convert into custom format + + + Not a valid Windows file time + + + Your input for {0} is outside the range **from {1} to {2}**. + The placeholder will be replace in code. + + + Cannot parse the input as Excel's 1900 date value because it is a fake date. (In Excel 0 stands for 0/1/1900 and this date doesn't exist. And 60 stands for 2/29/1900 and this date only exists in Excel for compatibility with Lotus 123.) + + + OLE Automation Date + + + Excel's 1900 date value + + + Excel's 1904 date value + + + Leap year + + + Not a leap year + + + Days in month \ No newline at end of file