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