Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/devdocs/modules/launcher/plugins/timedate.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The following formats are currently available:
| Now | 3/5/2022 5:10 PM | x | x | |
| Time UTC | 4:10 PM | x | x | |
| Now UTC | 3/5/2022 4:10 PM | x | x | |
| Friendly | Yesterday | x | | |
| Unix Timestamp | 1646496622 | x | x | |
| Unix Timestamp in milliseconds | 1646496622500 | x | x | |
| Hour | 10 | x | | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public void Setup()
[DataRow("iso zone", "ISO 8601 with time zone - ", "Images\\timeDate.dark.png")]
[DataRow("iso utc zone", "ISO 8601 UTC with time zone - ", "Images\\timeDate.dark.png")]
[DataRow("rfc", "RFC1123 -", "Images\\timeDate.dark.png")]
[DataRow("friendly", "Friendly -", "Images\\timeDate.dark.png")]
[DataRow("compatible", "Date and time in filename-compatible format", "Images\\timeDate.dark.png")]
public void IconThemeDarkTest(string typedString, string subTitleMatch, string expectedResult)
{
Expand Down Expand Up @@ -109,6 +110,7 @@ public void IconThemeDarkTest(string typedString, string subTitleMatch, string e
[DataRow("iso zone", "ISO 8601 with time zone - ", "Images\\timeDate.light.png")]
[DataRow("iso utc zone", "ISO 8601 UTC with time zone - ", "Images\\timeDate.light.png")]
[DataRow("rfc", "RFC1123 -", "Images\\timeDate.light.png")]
[DataRow("friendly", "Friendly -", "Images\\timeDate.light.png")]
[DataRow("compatible", "Date and time in filename-compatible format", "Images\\timeDate.light.png")]
public void IconThemeLightTest(string typedString, string subTitleMatch, string expectedResult)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ public void CountWithoutPluginKeyword(string typedString, int expectedResultCoun
[DataRow("(time", 18)]
[DataRow("(date", 28)]
[DataRow("(year", 8)]
[DataRow("(now", 34)]
[DataRow("(current", 34)]
[DataRow("(", 34)]
[DataRow("(now", 35)]
[DataRow("(current", 35)]
[DataRow("(", 35)]
[DataRow("(now::10:10:10", 1)] // Windows file time
[DataRow("(current::10:10:10", 0)]
public void CountWithPluginKeyword(string typedString, int expectedResultCount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,22 @@ public void GetWeekOfMonth(string date, int week)
Assert.AreEqual(result, week);
}

[DataTestMethod]
[DataRow("2026-04-07T12:00:00", "2026-04-07T08:00:00", "4 hours ago")]
[DataRow("2026-04-07T12:00:00", "2026-04-07T15:00:00", "in 3 hours")]
[DataRow("2026-04-07T12:00:00", "2026-04-07T12:00:05", "Today")]
[DataRow("2026-04-07T12:00:00", "2026-04-06T12:00:00", "Yesterday")]
[DataRow("2026-04-07T12:00:00", "2026-04-08T12:00:00", "Tomorrow")]
public void FriendlyDateTimeFormats(string now, string target, string expected)
{
DateTime referenceNow = DateTime.Parse(now, CultureInfo.InvariantCulture);
DateTime targetTime = DateTime.Parse(target, CultureInfo.InvariantCulture);

var result = TimeAndDateHelper.GetFriendlyDateTime(targetTime, referenceNow);

Assert.AreEqual(expected, result);
}

[TestCleanup]
public void CleanUp()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ private DateTime GetDateTimeForTest(bool embedUtc = false)
public void LocalFormatsWithShortTimeAndShortDate(string formatLabel, string expectedResult)
{
// Setup
var helperResults = AvailableResultsList.GetList(true, false, false, GetDateTimeForTest(), CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, false, false, GetDateTimeForTest(), now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);

// Act
var result = helperResults.FirstOrDefault(x => x.Label.Equals(formatLabel, StringComparison.OrdinalIgnoreCase));
Expand Down Expand Up @@ -101,7 +101,7 @@ public void LocalFormatsWithShortTimeAndShortDate(string formatLabel, string exp
public void LocalFormatsWithShortTimeAndLongDate(string formatLabel, string expectedResult)
{
// Setup
var helperResults = AvailableResultsList.GetList(true, false, true, GetDateTimeForTest(), CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, false, true, GetDateTimeForTest(), now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);

// Act
var result = helperResults.FirstOrDefault(x => x.Label.Equals(formatLabel, StringComparison.OrdinalIgnoreCase));
Expand Down Expand Up @@ -136,7 +136,7 @@ public void LocalFormatsWithShortTimeAndLongDate(string formatLabel, string expe
public void LocalFormatsWithLongTimeAndShortDate(string formatLabel, string expectedResult)
{
// Setup
var helperResults = AvailableResultsList.GetList(true, true, false, GetDateTimeForTest(), CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, true, false, GetDateTimeForTest(), now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);

// Act
var result = helperResults.FirstOrDefault(x => x.Label.Equals(formatLabel, StringComparison.OrdinalIgnoreCase));
Expand Down Expand Up @@ -171,7 +171,7 @@ public void LocalFormatsWithLongTimeAndShortDate(string formatLabel, string expe
public void LocalFormatsWithLongTimeAndLongDate(string formatLabel, string expectedResult)
{
// Setup
var helperResults = AvailableResultsList.GetList(true, true, true, GetDateTimeForTest(), CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, true, true, GetDateTimeForTest(), now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);

// Act
var result = helperResults.FirstOrDefault(x => x.Label.Equals(formatLabel, StringComparison.OrdinalIgnoreCase));
Expand All @@ -190,7 +190,7 @@ public void LocalFormatsWithLongTimeAndLongDate(string formatLabel, string expec
public void UtcFormatsWithShortTimeAndShortDate(string formatLabel, string expectedFormat)
{
// Setup
var helperResults = AvailableResultsList.GetList(true, false, false, GetDateTimeForTest(true), CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, false, false, GetDateTimeForTest(true), now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var expectedResult = GetDateTimeForTest().ToString(expectedFormat, CultureInfo.CurrentCulture);

// Act
Expand All @@ -210,7 +210,7 @@ public void UtcFormatsWithShortTimeAndShortDate(string formatLabel, string expec
public void UtcFormatsWithShortTimeAndLongDate(string formatLabel, string expectedFormat)
{
// Setup
var helperResults = AvailableResultsList.GetList(true, false, true, GetDateTimeForTest(true), CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, false, true, GetDateTimeForTest(true), now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var expectedResult = GetDateTimeForTest().ToString(expectedFormat, CultureInfo.CurrentCulture);

// Act
Expand All @@ -230,7 +230,7 @@ public void UtcFormatsWithShortTimeAndLongDate(string formatLabel, string expect
public void UtcFormatsWithLongTimeAndShortDate(string formatLabel, string expectedFormat)
{
// Setup
var helperResults = AvailableResultsList.GetList(true, true, false, GetDateTimeForTest(true), CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, true, false, GetDateTimeForTest(true), now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var expectedResult = GetDateTimeForTest().ToString(expectedFormat, CultureInfo.CurrentCulture);

// Act
Expand All @@ -250,7 +250,7 @@ public void UtcFormatsWithLongTimeAndShortDate(string formatLabel, string expect
public void UtcFormatsWithLongTimeAndLongDate(string formatLabel, string expectedFormat)
{
// Setup
var helperResults = AvailableResultsList.GetList(true, true, true, GetDateTimeForTest(true), CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, true, true, GetDateTimeForTest(true), now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var expectedResult = GetDateTimeForTest().ToString(expectedFormat, CultureInfo.CurrentCulture);

// Act
Expand All @@ -266,7 +266,7 @@ public void UnixTimestampSecondsFormat()
// Setup
string formatLabel = "Unix epoch time";
DateTime timeValue = DateTime.Now.ToUniversalTime();
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var expectedResult = (long)timeValue.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;

// Act
Expand All @@ -282,7 +282,7 @@ public void UnixTimestampMillisecondsFormat()
// Setup
string formatLabel = "Unix epoch time in milliseconds";
DateTime timeValue = DateTime.Now.ToUniversalTime();
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var expectedResult = (long)timeValue.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds;

// Act
Expand All @@ -298,7 +298,7 @@ public void WindowsFileTimeFormat()
// Setup
string formatLabel = "Windows file time (Int64 number)";
DateTime timeValue = DateTime.Now;
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var expectedResult = timeValue.ToFileTime().ToString(CultureInfo.CurrentCulture);

// Act
Expand All @@ -314,7 +314,7 @@ public void ValidateEraResult()
// Setup
string formatLabel = "Era";
DateTime timeValue = DateTime.Now;
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var expectedResult = DateTimeFormatInfo.CurrentInfo.GetEraName(CultureInfo.CurrentCulture.Calendar.GetEra(timeValue));

// Act
Expand All @@ -330,7 +330,7 @@ public void ValidateEraAbbreviationResult()
// Setup
string formatLabel = "Era abbreviation";
DateTime timeValue = DateTime.Now;
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, now: null, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
var expectedResult = DateTimeFormatInfo.CurrentInfo.GetAbbreviatedEraName(CultureInfo.CurrentCulture.Calendar.GetEra(timeValue));

// Act
Expand All @@ -348,7 +348,7 @@ public void DifferentFirstWeekSettingConfigurations(CalendarWeekRule weekRule, s
{
// Setup
DateTime timeValue = new DateTime(2021, 1, 12);
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, weekRule, DayOfWeek.Sunday);
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, now: null, weekRule, DayOfWeek.Sunday);

// Act
var resultWeekOfYear = helperResults.FirstOrDefault(x => x.Label.Equals("week of the year (calendar week, week number)", StringComparison.OrdinalIgnoreCase));
Expand All @@ -369,7 +369,7 @@ public void DifferentFirstDayOfWeekSettingConfigurations(DayOfWeek dayOfWeek, st
{
// Setup
DateTime timeValue = new DateTime(2024, 1, 12); // Friday
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, CalendarWeekRule.FirstDay, dayOfWeek);
var helperResults = AvailableResultsList.GetList(true, false, false, timeValue, now: null, CalendarWeekRule.FirstDay, dayOfWeek);

// Act
var resultWeekOfYear = helperResults.FirstOrDefault(x => x.Label.Equals("week of the year (calendar week, week number)", StringComparison.OrdinalIgnoreCase));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ internal static class AvailableResultsList
/// <param name="timeLongFormat">Required for UnitTest: Show time in long format</param>
/// <param name="dateLongFormat">Required for UnitTest: Show date in long format</param>
/// <param name="timestamp">Use custom <see cref="DateTime"/> object to calculate results instead of the system date/time</param>
/// <param name="now">Required for UnitTest: Reference "now" time used for relative/friendly formatting</param>
/// <param name="firstWeekOfYear">Required for UnitTest: Use custom first week of the year instead of the plugin setting.</param>
/// <param name="firstDayOfWeek">Required for UnitTest: Use custom first day of the week instead the plugin setting.</param>
/// <returns>List of results</returns>
internal static List<AvailableResult> GetList(bool isKeywordSearch, bool? timeLongFormat = null, bool? dateLongFormat = null, DateTime? timestamp = null, CalendarWeekRule? firstWeekOfYear = null, DayOfWeek? firstDayOfWeek = null)
internal static List<AvailableResult> GetList(bool isKeywordSearch, bool? timeLongFormat = null, bool? dateLongFormat = null, DateTime? timestamp = null, DateTime? now = null, CalendarWeekRule? firstWeekOfYear = null, DayOfWeek? firstDayOfWeek = null)
{
List<AvailableResult> results = new List<AvailableResult>();
Calendar calendar = CultureInfo.CurrentCulture.Calendar;
Expand All @@ -32,6 +33,7 @@ internal static List<AvailableResult> GetList(bool isKeywordSearch, bool? timeLo
bool dateExtended = dateLongFormat ?? TimeDateSettings.Instance.DateWithWeekday;
bool isSystemDateTime = timestamp == null;
DateTime dateTimeNow = timestamp ?? DateTime.Now;
DateTime dateTimeReferenceNow = now ?? DateTime.Now;
DateTime dateTimeNowUtc = dateTimeNow.ToUniversalTime();
CalendarWeekRule firstWeekRule = firstWeekOfYear ?? TimeAndDateHelper.GetCalendarWeekRule(TimeDateSettings.Instance.CalendarFirstWeekRule);
DayOfWeek firstDayOfTheWeek = firstDayOfWeek ?? TimeAndDateHelper.GetFirstDayOfWeek(TimeDateSettings.Instance.FirstDayOfWeek);
Expand Down Expand Up @@ -72,6 +74,14 @@ internal static List<AvailableResult> GetList(bool isKeywordSearch, bool? timeLo
string era = DateTimeFormatInfo.CurrentInfo.GetEraName(calendar.GetEra(dateTimeNow));
string eraShort = DateTimeFormatInfo.CurrentInfo.GetAbbreviatedEraName(calendar.GetEra(dateTimeNow));

results.Add(new AvailableResult()
{
Value = TimeAndDateHelper.GetFriendlyDateTime(dateTimeNow, dateTimeReferenceNow),
Label = Resources.Microsoft_plugin_timedate_Friendly,
AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFriendly"),
IconType = ResultIconType.DateTime,
});

// Custom formats
foreach (string f in TimeDateSettings.Instance.CustomFormats)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,72 @@ internal static DayOfWeek GetFirstDayOfWeek(int pluginSetting)
return DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek;
}
}

/// <summary>
/// Returns a friendly human-readable representation of a timestamp relative to a reference "now".
/// Uses local date boundaries for Today / Yesterday / Tomorrow.
/// </summary>
internal static string GetFriendlyDateTime(DateTime target, DateTime referenceNow)
{
// Use local date boundaries. If the input carries UTC kind, convert for comparison/display purposes.
DateTime localTarget = target.Kind == DateTimeKind.Utc ? target.ToLocalTime() : target;
DateTime localNow = referenceNow.Kind == DateTimeKind.Utc ? referenceNow.ToLocalTime() : referenceNow;

int dayDiff = (localTarget.Date - localNow.Date).Days;
if (dayDiff == 0)
{
return Resources.Microsoft_plugin_timedate_Friendly_Today;
}

if (dayDiff == -1)
{
return Resources.Microsoft_plugin_timedate_Friendly_Yesterday;
}

if (dayDiff == 1)
{
return Resources.Microsoft_plugin_timedate_Friendly_Tomorrow;
}

TimeSpan span = localTarget - localNow;
bool isFuture = span.Ticks > 0;
TimeSpan abs = span.Duration();

if (abs.TotalSeconds < 10)
{
return Resources.Microsoft_plugin_timedate_Friendly_JustNow;
}

if (abs.TotalMinutes < 60)
{
int minutes = (int)Math.Round(abs.TotalMinutes, MidpointRounding.AwayFromZero);
if (minutes <= 1)
{
return isFuture ? Resources.Microsoft_plugin_timedate_Friendly_InMinute : Resources.Microsoft_plugin_timedate_Friendly_MinuteAgo;
}

return string.Format(CultureInfo.CurrentCulture, isFuture ? Resources.Microsoft_plugin_timedate_Friendly_InMinutes : Resources.Microsoft_plugin_timedate_Friendly_MinutesAgo, minutes);
}

if (abs.TotalHours < 24)
{
int hours = (int)Math.Round(abs.TotalHours, MidpointRounding.AwayFromZero);
if (hours <= 1)
{
return isFuture ? Resources.Microsoft_plugin_timedate_Friendly_InHour : Resources.Microsoft_plugin_timedate_Friendly_HourAgo;
}

return string.Format(CultureInfo.CurrentCulture, isFuture ? Resources.Microsoft_plugin_timedate_Friendly_InHours : Resources.Microsoft_plugin_timedate_Friendly_HoursAgo, hours);
}

int days = (int)Math.Round(abs.TotalDays, MidpointRounding.AwayFromZero);
if (days <= 1)
{
return isFuture ? Resources.Microsoft_plugin_timedate_Friendly_InDay : Resources.Microsoft_plugin_timedate_Friendly_DayAgo;
}

return string.Format(CultureInfo.CurrentCulture, isFuture ? Resources.Microsoft_plugin_timedate_Friendly_InDays : Resources.Microsoft_plugin_timedate_Friendly_DaysAgo, days);
}
}

/// <summary>
Expand Down
Loading
Loading