Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
43 changes: 27 additions & 16 deletions dotnet/src/Plugins/Plugins.Core/TimePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ namespace Microsoft.SemanticKernel.Plugins.Core;
/// </remark>
public sealed class TimePlugin
{
private readonly TimeProvider _timeProvider;

/// <summary>
/// Initializes a new instance of the <see cref="TimePlugin"/> class.
/// </summary>
/// <param name="timeProvider">The time provider to use. Defaults to <see cref="TimeProvider.System"/>.</param>
public TimePlugin(TimeProvider? timeProvider = null)
{
this._timeProvider = timeProvider ?? TimeProvider.System;
}
Comment thread
grvnmttl marked this conversation as resolved.
Comment thread
grvnmttl marked this conversation as resolved.

/// <summary>
/// Get the current date
/// </summary>
Expand All @@ -24,7 +35,7 @@ public sealed class TimePlugin
[KernelFunction, Description("Get the current date")]
public string Date(IFormatProvider? formatProvider = null) =>
// Example: Sunday, 12 January, 2025
DateTimeOffset.Now.ToString("D", formatProvider);
this._timeProvider.GetLocalNow().ToString("D", formatProvider);

/// <summary>
/// Get the current date
Expand All @@ -48,7 +59,7 @@ public string Today(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current date and time in the local time zone")]
public string Now(IFormatProvider? formatProvider = null) =>
// Sunday, January 12, 2025 9:15 PM
DateTimeOffset.Now.ToString("f", formatProvider);
this._timeProvider.GetLocalNow().ToString("f", formatProvider);

/// <summary>
/// Get the current UTC date and time
Expand All @@ -60,7 +71,7 @@ public string Now(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current UTC date and time")]
public string UtcNow(IFormatProvider? formatProvider = null) =>
// Sunday, January 13, 2025 5:15 AM
DateTimeOffset.UtcNow.ToString("f", formatProvider);
this._timeProvider.GetUtcNow().ToString("f", formatProvider);

/// <summary>
/// Get the current time
Expand All @@ -72,7 +83,7 @@ public string UtcNow(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current time")]
public string Time(IFormatProvider? formatProvider = null) =>
// Example: 09:15:07 PM
DateTimeOffset.Now.ToString("hh:mm:ss tt", formatProvider);
this._timeProvider.GetLocalNow().ToString("hh:mm:ss tt", formatProvider);

/// <summary>
/// Get the current year
Expand All @@ -84,7 +95,7 @@ public string Time(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current year")]
public string Year(IFormatProvider? formatProvider = null) =>
// Example: 2025
DateTimeOffset.Now.ToString("yyyy", formatProvider);
this._timeProvider.GetLocalNow().ToString("yyyy", formatProvider);

/// <summary>
/// Get the current month name
Expand All @@ -96,7 +107,7 @@ public string Year(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current month name")]
public string Month(IFormatProvider? formatProvider = null) =>
// Example: January
DateTimeOffset.Now.ToString("MMMM", formatProvider);
this._timeProvider.GetLocalNow().ToString("MMMM", formatProvider);

/// <summary>
/// Get the current month number
Expand All @@ -108,7 +119,7 @@ public string Month(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current month number")]
public string MonthNumber(IFormatProvider? formatProvider = null) =>
// Example: 01
DateTimeOffset.Now.ToString("MM", formatProvider);
this._timeProvider.GetLocalNow().ToString("MM", formatProvider);

/// <summary>
/// Get the current day of the month
Expand All @@ -120,7 +131,7 @@ public string MonthNumber(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current day of the month")]
public string Day(IFormatProvider? formatProvider = null) =>
// Example: 12
DateTimeOffset.Now.ToString("dd", formatProvider);
this._timeProvider.GetLocalNow().ToString("dd", formatProvider);

/// <summary>
/// Get the date a provided number of days in the past
Expand All @@ -129,7 +140,7 @@ public string Day(IFormatProvider? formatProvider = null) =>
[KernelFunction]
[Description("Get the date offset by a provided number of days from today")]
public string DaysAgo([Description("The number of days to offset from today")] double input, IFormatProvider? formatProvider = null) =>
DateTimeOffset.Now.AddDays(-input).ToString("D", formatProvider);
this._timeProvider.GetLocalNow().AddDays(-input).ToString("D", formatProvider);

/// <summary>
/// Get the current day of the week
Expand All @@ -141,7 +152,7 @@ public string DaysAgo([Description("The number of days to offset from today")] d
[KernelFunction, Description("Get the current day of the week")]
public string DayOfWeek(IFormatProvider? formatProvider = null) =>
// Example: Sunday
DateTimeOffset.Now.ToString("dddd", formatProvider);
this._timeProvider.GetLocalNow().ToString("dddd", formatProvider);

/// <summary>
/// Get the current clock hour
Expand All @@ -153,7 +164,7 @@ public string DayOfWeek(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current clock hour")]
public string Hour(IFormatProvider? formatProvider = null) =>
// Example: 9 PM
DateTimeOffset.Now.ToString("h tt", formatProvider);
this._timeProvider.GetLocalNow().ToString("h tt", formatProvider);

/// <summary>
/// Get the current clock 24-hour number
Expand All @@ -165,7 +176,7 @@ public string Hour(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the current clock 24-hour number")]
public string HourNumber(IFormatProvider? formatProvider = null) =>
// Example: 21
DateTimeOffset.Now.ToString("HH", formatProvider);
this._timeProvider.GetLocalNow().ToString("HH", formatProvider);

/// <summary>
/// Get the date of the previous day matching the supplied day name
Expand All @@ -181,7 +192,7 @@ public string DateMatchingLastDayName(
[Description("The day name to match")] DayOfWeek input,
IFormatProvider? formatProvider = null)
{
DateTimeOffset dateTime = DateTimeOffset.Now;
DateTimeOffset dateTime = this._timeProvider.GetLocalNow();

// Walk backwards from the previous day for up to a week to find the matching day
for (int i = 1; i <= 7; ++i)
Expand All @@ -206,7 +217,7 @@ public string DateMatchingLastDayName(
[KernelFunction, Description("Get the minutes on the current hour")]
public string Minute(IFormatProvider? formatProvider = null) =>
// Example: 15
DateTimeOffset.Now.ToString("mm", formatProvider);
this._timeProvider.GetLocalNow().ToString("mm", formatProvider);

/// <summary>
/// Get the seconds on the current minute
Expand All @@ -218,7 +229,7 @@ public string Minute(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the seconds on the current minute")]
public string Second(IFormatProvider? formatProvider = null) =>
// Example: 07
DateTimeOffset.Now.ToString("ss", formatProvider);
this._timeProvider.GetLocalNow().ToString("ss", formatProvider);

/// <summary>
/// Get the local time zone offset from UTC
Expand All @@ -230,7 +241,7 @@ public string Second(IFormatProvider? formatProvider = null) =>
[KernelFunction, Description("Get the local time zone offset from UTC")]
public string TimeZoneOffset(IFormatProvider? formatProvider = null) =>
// Example: -08:00
DateTimeOffset.Now.ToString("%K", formatProvider);
this._timeProvider.GetLocalNow().ToString("%K", formatProvider);

Comment thread
grvnmttl marked this conversation as resolved.
/// <summary>
/// Get the local time zone name
Expand Down
170 changes: 123 additions & 47 deletions dotnet/src/Plugins/Plugins.UnitTests/Core/TimePluginTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,150 @@

namespace SemanticKernel.Plugins.UnitTests.Core;

// TODO: allow clock injection and test all functions
public class TimePluginTests
{
// Sunday, 15 June 2025 21:15:07 UTC — local timezone pinned to UTC so tests are machine-independent
private static readonly DateTimeOffset s_fixedTime = new(2025, 6, 15, 21, 15, 7, TimeSpan.Zero);

private static TimePlugin CreatePlugin() => new(new FixedUtcTimeProvider(s_fixedTime));

/// <summary>Minimal TimeProvider that returns a fixed UTC instant with UTC as local timezone.</summary>
private sealed class FixedUtcTimeProvider(DateTimeOffset fixedUtc) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => fixedUtc.ToUniversalTime();
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
}

[Fact]
public void ItCanBeInstantiated()
{
// Act - Assert no exception occurs
var _ = new TimePlugin();
}

[Fact]
public void ItCanBeImported()
{
// Act - Assert no exception occurs e.g. due to reflection
Assert.NotNull(KernelPluginFactory.CreateFromType<TimePlugin>("time"));
}

[Fact]
public void DaysAgo()
public void Date()
{
// InvariantCulture "D" format: dddd, dd MMMM yyyy
Assert.Equal("Sunday, 15 June 2025", CreatePlugin().Date(CultureInfo.InvariantCulture));
}

[Fact]
public void Today()
{
double interval = 2;
DateTime expected = DateTime.Now.AddDays(-interval);
var plugin = new TimePlugin();
string result = plugin.DaysAgo(interval, CultureInfo.CurrentCulture);
DateTime returned = DateTime.Parse(result, CultureInfo.CurrentCulture);
Assert.Equal(expected.Day, returned.Day);
Assert.Equal(expected.Month, returned.Month);
Assert.Equal(expected.Year, returned.Year);
Assert.Equal("Sunday, 15 June 2025", CreatePlugin().Today(CultureInfo.InvariantCulture));
}

[Fact]
public void Now()
{
// InvariantCulture "f" format: dddd, dd MMMM yyyy HH:mm
Assert.Equal("Sunday, 15 June 2025 21:15", CreatePlugin().Now(CultureInfo.InvariantCulture));
}

[Fact]
public void UtcNow()
{
Assert.Equal("Sunday, 15 June 2025 21:15", CreatePlugin().UtcNow(CultureInfo.InvariantCulture));
}

[Fact]
public void Time()
{
Assert.Equal("09:15:07 PM", CreatePlugin().Time(CultureInfo.InvariantCulture));
}

[Fact]
public void Year()
{
Assert.Equal("2025", CreatePlugin().Year(CultureInfo.InvariantCulture));
}

[Fact]
public void Month()
{
Assert.Equal("June", CreatePlugin().Month(CultureInfo.InvariantCulture));
}

[Fact]
public void MonthNumber()
{
Assert.Equal("06", CreatePlugin().MonthNumber(CultureInfo.InvariantCulture));
}

[Fact]
public void Day()
{
string expected = DateTime.Now.ToString("dd", CultureInfo.CurrentCulture);
var plugin = new TimePlugin();
string result = plugin.Day(CultureInfo.CurrentCulture);
Assert.Equal(expected, result);
Assert.True(int.TryParse(result, out _));
Assert.Equal("15", CreatePlugin().Day(CultureInfo.InvariantCulture));
}

[Fact]
public void DayOfWeek()
{
Assert.Equal("Sunday", CreatePlugin().DayOfWeek(CultureInfo.InvariantCulture));
}

[Fact]
public void Hour()
{
Assert.Equal("9 PM", CreatePlugin().Hour(CultureInfo.InvariantCulture));
}

[Fact]
public void HourNumber()
{
Assert.Equal("21", CreatePlugin().HourNumber(CultureInfo.InvariantCulture));
}

[Fact]
public void Minute()
{
Assert.Equal("15", CreatePlugin().Minute(CultureInfo.InvariantCulture));
}

[Fact]
public void Second()
{
Assert.Equal("07", CreatePlugin().Second(CultureInfo.InvariantCulture));
}

[Fact]
public void TimeZoneOffset()
{
Assert.Equal("+00:00", CreatePlugin().TimeZoneOffset(CultureInfo.InvariantCulture));
}

Comment thread
grvnmttl marked this conversation as resolved.
[Fact]
public void DaysAgo()
{
// 2 days before 2025-06-15 is 2025-06-13 (Friday)
Assert.Equal("Friday, 13 June 2025", CreatePlugin().DaysAgo(2, CultureInfo.InvariantCulture));
}

[Theory]
[MemberData(nameof(DayOfWeekCases))]
public void DateMatchingLastDayName(DayOfWeek dayName, string expectedDate)
{
// Fixed time is Sunday 2025-06-15; walk back to find each day
Assert.Equal(expectedDate, CreatePlugin().DateMatchingLastDayName(dayName, CultureInfo.InvariantCulture));
}

public static IEnumerable<object[]> DayOfWeekCases()
{
// From Sunday 2025-06-15, the previous occurrence of each day (never same day).
// InvariantCulture "D" format: dddd, dd MMMM yyyy
yield return [System.DayOfWeek.Saturday, "Saturday, 14 June 2025"];
yield return [System.DayOfWeek.Friday, "Friday, 13 June 2025"];
yield return [System.DayOfWeek.Thursday, "Thursday, 12 June 2025"];
yield return [System.DayOfWeek.Wednesday, "Wednesday, 11 June 2025"];
yield return [System.DayOfWeek.Tuesday, "Tuesday, 10 June 2025"];
yield return [System.DayOfWeek.Monday, "Monday, 09 June 2025"];
yield return [System.DayOfWeek.Sunday, "Sunday, 08 June 2025"];
}

[Fact]
Expand All @@ -60,34 +166,4 @@ public async Task LastMatchingDayBadInputAsync()

AssertExtensions.AssertIsArgumentOutOfRange(ex, "input", "not a day name");
}

[Theory]
[MemberData(nameof(DayOfWeekEnumerator))]
public void LastMatchingDay(DayOfWeek dayName)
{
int steps = 0;
DateTime date = DateTime.Now.Date.AddDays(-1);
while (date.DayOfWeek != dayName && steps <= 7)
{
date = date.AddDays(-1);
steps++;
}
bool found = date.DayOfWeek == dayName;
Assert.True(found);

var plugin = new TimePlugin();
string result = plugin.DateMatchingLastDayName(dayName, CultureInfo.CurrentCulture);
DateTime returned = DateTime.Parse(result, CultureInfo.CurrentCulture);
Assert.Equal(date.Day, returned.Day);
Assert.Equal(date.Month, returned.Month);
Assert.Equal(date.Year, returned.Year);
}

public static IEnumerable<object[]> DayOfWeekEnumerator()
{
foreach (var day in Enum.GetValues<DayOfWeek>())
{
yield return new object[] { day };
}
}
}
Loading