Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CultureInfo to SKContext #1519

Merged
merged 6 commits into from
Jun 20, 2023
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand All @@ -17,6 +18,11 @@ namespace Microsoft.SemanticKernel.Orchestration;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class SKContext
{
/// <summary>
/// The culture currently associated with this context.
/// </summary>
private CultureInfo _culture;

/// <summary>
/// Print the processed input, aka the current data after any processing occurred.
/// </summary>
Expand Down Expand Up @@ -54,6 +60,15 @@ public sealed class SKContext
/// </summary>
public CancellationToken CancellationToken { get; }

/// <summary>
/// The culture currently associated with this context.
/// </summary>
public CultureInfo Culture
{
get => this._culture;
set => this._culture = value ?? CultureInfo.CurrentCulture;
}

/// <summary>
/// Shortcut into user data, access variables by name
/// </summary>
Expand Down Expand Up @@ -138,6 +153,7 @@ public SKContext(
this.Skills = skills ?? NullReadOnlySkillCollection.Instance;
this.Log = logger ?? NullLogger.Instance;
this.CancellationToken = cancellationToken;
this._culture = CultureInfo.CurrentCulture;
}

/// <summary>
Expand Down Expand Up @@ -180,9 +196,10 @@ public SKContext Clone()
logger: this.Log,
cancellationToken: this.CancellationToken)
{
Culture = this.Culture,
ErrorOccurred = this.ErrorOccurred,
LastErrorDescription = this.LastErrorDescription,
LastException = this.LastException
LastException = this.LastException,
};
}

Expand All @@ -209,6 +226,8 @@ private string DebuggerDisplay
display += $", Memory = {memory.GetType().Name}";
}

display += $", Culture = {this.Culture.EnglishName}";

return display;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void DaysAgo()
double interval = 2;
DateTime expected = DateTime.Now.AddDays(-interval);
var skill = new TimeSkill();
string result = skill.DaysAgo(interval);
string result = skill.DaysAgo(interval, CultureInfo.CurrentCulture);
DateTime returned = DateTime.Parse(result, CultureInfo.CurrentCulture);
Assert.Equal(expected.Day, returned.Day);
Assert.Equal(expected.Month, returned.Month);
Expand All @@ -48,7 +48,7 @@ public void Day()
{
string expected = DateTime.Now.ToString("dd", CultureInfo.CurrentCulture);
var skill = new TimeSkill();
string result = skill.Day();
string result = skill.Day(CultureInfo.CurrentCulture);
Assert.Equal(expected, result);
Assert.True(int.TryParse(result, out _));
}
Expand Down Expand Up @@ -76,7 +76,7 @@ public void LastMatchingDay(DayOfWeek dayName)
Assert.True(found);

var skill = new TimeSkill();
string result = skill.DateMatchingLastDayName(dayName);
string result = skill.DateMatchingLastDayName(dayName, CultureInfo.CurrentCulture);
DateTime returned = DateTime.Parse(result, CultureInfo.CurrentCulture);
Assert.Equal(date.Day, returned.Day);
Assert.Equal(date.Month, returned.Month);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,37 @@ static async Task AssertResult(Delegate d, SKContext context, string expected)
await AssertResult((Uri input) => new Uri(input, "kernel"), context, "http://example.com/kernel");
}

[Fact]
public async Task ItUsesContextCultureForParsingFormatting()
{
// Arrange
var context = this.MockContext("");
ISKFunction func = SKFunction.FromNativeFunction((double input) => input * 2, functionName: "Test");

// Act/Assert

context.Culture = new CultureInfo("fr-FR");
context.Variables.Update("12,34"); // tries first to parse with the specified culture
context = await func.InvokeAsync(context);
Assert.Equal("24,68", context.Variables.Input);

context.Culture = new CultureInfo("fr-FR");
context.Variables.Update("12.34"); // falls back to invariant culture
context = await func.InvokeAsync(context);
Assert.Equal("24,68", context.Variables.Input);

context.Culture = new CultureInfo("en-US");
context.Variables.Update("12.34"); // works with current culture
context = await func.InvokeAsync(context);
Assert.Equal("24.68", context.Variables.Input);

context.Culture = new CultureInfo("en-US");
context.Variables.Update("12,34"); // not parsable with current or invariant culture
context = await func.InvokeAsync(context);
Assert.True(context.ErrorOccurred);
Assert.IsType<ArgumentOutOfRangeException>(context.LastException);
}

[Fact]
public async Task ItThrowsWhenItFailsToConvertAnArgument()
{
Expand Down
6 changes: 4 additions & 2 deletions dotnet/src/SemanticKernel/CoreSkills/TextSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ public sealed class TextSkill
/// {{text.uppercase $input}} => "HELLO WORLD"
/// </example>
/// <param name="input"> The string to convert. </param>
/// <param name="cultureInfo"> An object that supplies culture-specific casing rules. </param>
/// <returns> The converted string. </returns>
[SKFunction, Description("Convert a string to uppercase.")]
public string Uppercase(string input) => input.ToUpper(CultureInfo.CurrentCulture);
public string Uppercase(string input, CultureInfo? cultureInfo = null) => input.ToUpper(cultureInfo);

/// <summary>
/// Convert a string to lowercase.
Expand All @@ -80,9 +81,10 @@ public sealed class TextSkill
/// {{text.lowercase $input}} => "hello world"
/// </example>
/// <param name="input"> The string to convert. </param>
/// <param name="cultureInfo"> An object that supplies culture-specific casing rules. </param>
/// <returns> The converted string. </returns>
[SKFunction, Description("Convert a string to lowercase.")]
public string Lowercase(string input) => input.ToLower(CultureInfo.CurrentCulture);
public string Lowercase(string input, CultureInfo? cultureInfo = null) => input.ToLower(cultureInfo);

/// <summary>
/// Get the length of a string. Returns 0 if null or empty
Expand Down
71 changes: 37 additions & 34 deletions dotnet/src/SemanticKernel/CoreSkills/TimeSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

using System;
using System.ComponentModel;
using System.Globalization;
using Microsoft.SemanticKernel.SkillDefinition;

namespace Microsoft.SemanticKernel.CoreSkills;
Expand Down Expand Up @@ -49,9 +48,9 @@ public sealed class TimeSkill
/// </example>
/// <returns> The current date </returns>
[SKFunction, Description("Get the current date")]
public string Date() =>
public string Date(IFormatProvider? formatProvider = null) =>
// Example: Sunday, 12 January, 2025
DateTimeOffset.Now.ToString("D", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("D", formatProvider);

/// <summary>
/// Get the current date
Expand All @@ -61,7 +60,9 @@ public string Date() =>
/// </example>
/// <returns> The current date </returns>
[SKFunction, Description("Get the current date")]
public string Today() => this.Date();
public string Today(IFormatProvider? formatProvider = null) =>
// Example: Sunday, 12 January, 2025
this.Date(formatProvider);

/// <summary>
/// Get the current date and time in the local time zone"
Expand All @@ -71,9 +72,9 @@ public string Date() =>
/// </example>
/// <returns> The current date and time in the local time zone </returns>
[SKFunction, Description("Get the current date and time in the local time zone")]
public string Now() =>
public string Now(IFormatProvider? formatProvider = null) =>
// Sunday, January 12, 2025 9:15 PM
DateTimeOffset.Now.ToString("f", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("f", formatProvider);

/// <summary>
/// Get the current UTC date and time
Expand All @@ -83,9 +84,9 @@ public string Now() =>
/// </example>
/// <returns> The current UTC date and time </returns>
[SKFunction, Description("Get the current UTC date and time")]
public string UtcNow() =>
public string UtcNow(IFormatProvider? formatProvider = null) =>
// Sunday, January 13, 2025 5:15 AM
DateTimeOffset.UtcNow.ToString("f", CultureInfo.CurrentCulture);
DateTimeOffset.UtcNow.ToString("f", formatProvider);

/// <summary>
/// Get the current time
Expand All @@ -95,9 +96,9 @@ public string UtcNow() =>
/// </example>
/// <returns> The current time </returns>
[SKFunction, Description("Get the current time")]
public string Time() =>
public string Time(IFormatProvider? formatProvider = null) =>
// Example: 09:15:07 PM
DateTimeOffset.Now.ToString("hh:mm:ss tt", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("hh:mm:ss tt", formatProvider);

/// <summary>
/// Get the current year
Expand All @@ -107,9 +108,9 @@ public string Time() =>
/// </example>
/// <returns> The current year </returns>
[SKFunction, Description("Get the current year")]
public string Year() =>
public string Year(IFormatProvider? formatProvider = null) =>
// Example: 2025
DateTimeOffset.Now.ToString("yyyy", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("yyyy", formatProvider);

/// <summary>
/// Get the current month name
Expand All @@ -119,9 +120,9 @@ public string Year() =>
/// </example>
/// <returns> The current month name </returns>
[SKFunction, Description("Get the current month name")]
public string Month() =>
public string Month(IFormatProvider? formatProvider = null) =>
// Example: January
DateTimeOffset.Now.ToString("MMMM", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("MMMM", formatProvider);

/// <summary>
/// Get the current month number
Expand All @@ -131,9 +132,9 @@ public string Month() =>
/// </example>
/// <returns> The current month number </returns>
[SKFunction, Description("Get the current month number")]
public string MonthNumber() =>
public string MonthNumber(IFormatProvider? formatProvider = null) =>
// Example: 01
DateTimeOffset.Now.ToString("MM", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("MM", formatProvider);

/// <summary>
/// Get the current day of the month
Expand All @@ -143,9 +144,9 @@ public string MonthNumber() =>
/// </example>
/// <returns> The current day of the month </returns>
[SKFunction, Description("Get the current day of the month")]
public string Day() =>
public string Day(IFormatProvider? formatProvider = null) =>
// Example: 12
DateTimeOffset.Now.ToString("dd", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("dd", formatProvider);

/// <summary>
/// Get the date a provided number of days in the past
Expand All @@ -157,8 +158,8 @@ public string Day() =>
/// <returns> The date the provided number of days before today </returns>
[SKFunction]
[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"), SKName("input")] double daysOffset) =>
DateTimeOffset.Now.AddDays(-daysOffset).ToString("D", CultureInfo.CurrentCulture);
public string DaysAgo([Description("The number of days to offset from today"), SKName("input")] double daysOffset, IFormatProvider? formatProvider = null) =>
DateTimeOffset.Now.AddDays(-daysOffset).ToString("D", formatProvider);

/// <summary>
/// Get the current day of the week
Expand All @@ -168,9 +169,9 @@ public string DaysAgo([Description("The number of days to offset from today"), S
/// </example>
/// <returns> The current day of the week </returns>
[SKFunction, Description("Get the current day of the week")]
public string DayOfWeek() =>
public string DayOfWeek(IFormatProvider? formatProvider = null) =>
// Example: Sunday
DateTimeOffset.Now.ToString("dddd", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("dddd", formatProvider);

/// <summary>
/// Get the current clock hour
Expand All @@ -180,9 +181,9 @@ public string DayOfWeek() =>
/// </example>
/// <returns> The current clock hour </returns>
[SKFunction, Description("Get the current clock hour")]
public string Hour() =>
public string Hour(IFormatProvider? formatProvider = null) =>
// Example: 9 PM
DateTimeOffset.Now.ToString("h tt", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("h tt", formatProvider);

/// <summary>
/// Get the current clock 24-hour number
Expand All @@ -192,9 +193,9 @@ public string Hour() =>
/// </example>
/// <returns> The current clock 24-hour number </returns>
[SKFunction, Description("Get the current clock 24-hour number")]
public string HourNumber() =>
public string HourNumber(IFormatProvider? formatProvider = null) =>
// Example: 21
DateTimeOffset.Now.ToString("HH", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("HH", formatProvider);

/// <summary>
/// Get the date of the previous day matching the supplied day name
Expand All @@ -206,7 +207,9 @@ public string HourNumber() =>
/// <exception cref="ArgumentOutOfRangeException">dayName is not a recognized name of a day of the week</exception>
[SKFunction]
[Description("Get the date of the last day matching the supplied week day name in English. Example: Che giorno era 'Martedi' scorso -> dateMatchingLastDayName 'Tuesday' => Tuesday, 16 May, 2023")]
public string DateMatchingLastDayName([Description("The day name to match"), SKName("input")] DayOfWeek dayName)
public string DateMatchingLastDayName(
[Description("The day name to match"), SKName("input")] DayOfWeek dayName,
IFormatProvider? formatProvider = null)
{
DateTimeOffset dateTime = DateTimeOffset.Now;

Expand All @@ -220,7 +223,7 @@ public string DateMatchingLastDayName([Description("The day name to match"), SKN
}
}

return dateTime.ToString("D", CultureInfo.CurrentCulture);
return dateTime.ToString("D", formatProvider);
}

/// <summary>
Expand All @@ -231,9 +234,9 @@ public string DateMatchingLastDayName([Description("The day name to match"), SKN
/// </example>
/// <returns> The minutes on the current hour </returns>
[SKFunction, Description("Get the minutes on the current hour")]
public string Minute() =>
public string Minute(IFormatProvider? formatProvider = null) =>
// Example: 15
DateTimeOffset.Now.ToString("mm", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("mm", formatProvider);

/// <summary>
/// Get the seconds on the current minute
Expand All @@ -243,9 +246,9 @@ public string Minute() =>
/// </example>
/// <returns> The seconds on the current minute </returns>
[SKFunction, Description("Get the seconds on the current minute")]
public string Second() =>
public string Second(IFormatProvider? formatProvider = null) =>
// Example: 07
DateTimeOffset.Now.ToString("ss", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("ss", formatProvider);

/// <summary>
/// Get the local time zone offset from UTC
Expand All @@ -255,9 +258,9 @@ public string Second() =>
/// </example>
/// <returns> The local time zone offset from UTC </returns>
[SKFunction, Description("Get the local time zone offset from UTC")]
public string TimeZoneOffset() =>
public string TimeZoneOffset(IFormatProvider? formatProvider = null) =>
// Example: -08:00
DateTimeOffset.Now.ToString("%K", CultureInfo.CurrentCulture);
DateTimeOffset.Now.ToString("%K", formatProvider);

/// <summary>
/// Get the local time zone name
Expand Down
Loading