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 TimeSpan.ToAge() extension method #1068

Merged
merged 3 commits into from
Feb 22, 2024
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
13 changes: 12 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,18 @@ TimeSpan.FromMilliseconds(1299630020).Humanize(3, collectionSeparator: null) =>
If words are preferred to numbers, a `toWords: true` parameter can be set to convert the numbers in a humanized TimeSpan to words:
```C#
TimeSpan.FromMilliseconds(1299630020).Humanize(3, toWords: true) => "two weeks, one day, one hour"
````
```

By calling `ToAge`, a `TimeSpan` can also be expressed as an age.
For cultures that do not define an age expression, the result will be the same as calling `Humanize` _(but with a default `maxUnit` equal to `TimeUnit.Year`)_.

```C#
// in en-US culture
TimeSpan.FromDays(750).ToAge() => "2 years old"

// in fr culture
TimeSpan.FromDays(750).ToAge() => "2 ans"
```

### <a id="humanize-collections">Humanize Collections</a>
You can call `Humanize` on any `IEnumerable` to get a nicely formatted string representing the objects in the collection. By default `ToString()` will be called on each item to get its representation but a formatting function may be passed to `Humanize` instead. Additionally, a default separator is provided ("and" in English), but a different separator may be passed into `Humanize`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ public void MillisecondsToWords(int ms, string expected)
Assert.Equal(expected, actual);
}

[Theory]
[InlineData(4, false, "4 jours")]
[InlineData(23, false, "3 semaines")]
[InlineData(64, false, "2 mois")]
[InlineData(367, true, "un an")]
[InlineData(750, true, "deux ans")]
public void Age(int days, bool toWords, string expected)
{
var actual = TimeSpan.FromDays(days).ToAge(toWords: toWords);
hazzik marked this conversation as resolved.
Show resolved Hide resolved
Assert.Equal(expected, actual);
}

[Theory]
[InlineData(TimeUnit.Year, "0 an")]
[InlineData(TimeUnit.Month, "0 mois")]
Expand Down
22 changes: 17 additions & 5 deletions src/Humanizer.Tests.Shared/TimeSpanHumanizeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ group t by t into g
[InlineData(365 + 365 + 365 + 365 + 1, "4 years")]
[InlineData(365 + 365 + 365 + 365 + 366, "4 years, 11 months, 30 days")]
[InlineData(365 + 365 + 365 + 365 + 366 + 1, "5 years")]
public void Year(int days, string expected)
public void Years(int days, string expected)
{
var actual = TimeSpan.FromDays(days).Humanize(precision: 7, maxUnit: TimeUnit.Year);
Assert.Equal(expected, actual);
Expand All @@ -48,7 +48,7 @@ public void Year(int days, string expected)
[InlineData(30 + 30 + 31 + 30 + 31 + 1, "5 months")]
[InlineData(365, "11 months, 30 days")]
[InlineData(366, "1 year")]
public void Month(int days, string expected)
public void Months(int days, string expected)
{
var actual = TimeSpan.FromDays(days).Humanize(precision: 7, maxUnit: TimeUnit.Year);
Assert.Equal(expected, actual);
Expand Down Expand Up @@ -131,6 +131,18 @@ public void Milliseconds(int ms, string expected)
Assert.Equal(expected, actual);
}

[Theory]
[InlineData(4, false, "4 days old")]
[InlineData(23, false, "3 weeks old")]
[InlineData(64, false, "2 months old")]
[InlineData(367, true, "one year old")]
[InlineData(750, true, "two years old")]
public void Age(int days, bool toWords, string expected)
{
var actual = TimeSpan.FromDays(days).ToAge(toWords: toWords);
Assert.Equal(expected, actual);
}

[Theory]
[InlineData((long)366 * 24 * 60 * 60 * 1000, "12 months", TimeUnit.Month)]
[InlineData((long)6 * 7 * 24 * 60 * 60 * 1000, "6 weeks", TimeUnit.Week)]
Expand Down Expand Up @@ -438,9 +450,9 @@ public void CanSpecifyCultureExplicitly(int ms, int precision, string culture, s
}
[Theory]
[InlineData(31 * 4, 1, "en-US", "four months")]
[InlineData(236,2,"ar", "سبعة أشهر, اثنان و عشرون يوم")]
[InlineData(321, 2,"es", "diez meses, dieciséis días")]
public void CanSpecifyCultureExplicitlyToWords(int days, int precision,string culture, string expected)
[InlineData(236, 2, "ar", "سبعة أشهر, اثنان و عشرون يوم")]
[InlineData(321, 2, "es", "diez meses, dieciséis días")]
public void CanSpecifyCultureExplicitlyToWords(int days, int precision, string culture, string expected)
{
var timeSpan = new TimeSpan(days, 0, 0, 0);
var actual = timeSpan.Humanize(precision: precision, culture: new(culture), maxUnit: TimeUnit.Year, toWords: true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ namespace Humanizer
protected virtual string GetResourceKey(string resourceKey) { }
protected virtual string GetResourceKey(string resourceKey, int number) { }
public virtual string TimeSpanHumanize(Humanizer.TimeUnit timeUnit, int unit, bool toWords = false) { }
public virtual string TimeSpanHumanize_Age() { }
public virtual string TimeSpanHumanize_Zero() { }
public virtual string TimeUnitHumanize(Humanizer.TimeUnit timeUnit) { }
}
Expand Down Expand Up @@ -330,6 +331,7 @@ namespace Humanizer
string DateHumanize_Never();
string DateHumanize_Now();
string TimeSpanHumanize(Humanizer.TimeUnit timeUnit, int unit, bool toWords = false);
string TimeSpanHumanize_Age();
string TimeSpanHumanize_Zero();
string TimeUnitHumanize(Humanizer.TimeUnit timeUnit);
}
Expand Down Expand Up @@ -1838,6 +1840,7 @@ namespace Humanizer
public static class Resources
{
public static string GetResource(string resourceKey, System.Globalization.CultureInfo culture = null) { }
public static bool TryGetResource(string resourceKey, System.Globalization.CultureInfo culture, out string result) { }
}
public static class RomanNumeralExtensions
{
Expand Down Expand Up @@ -1872,6 +1875,7 @@ namespace Humanizer
{
public static string Humanize(this System.TimeSpan timeSpan, int precision = 1, System.Globalization.CultureInfo culture = null, Humanizer.TimeUnit maxUnit = 5, Humanizer.TimeUnit minUnit = 0, string collectionSeparator = ", ", bool toWords = false) { }
public static string Humanize(this System.TimeSpan timeSpan, int precision, bool countEmptyUnits, System.Globalization.CultureInfo culture = null, Humanizer.TimeUnit maxUnit = 5, Humanizer.TimeUnit minUnit = 0, string collectionSeparator = ", ", bool toWords = false) { }
public static string ToAge(this System.TimeSpan timeSpan, System.Globalization.CultureInfo culture = null, Humanizer.TimeUnit maxUnit = 7, bool toWords = false) { }
}
public enum TimeUnit
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ namespace Humanizer
protected virtual string GetResourceKey(string resourceKey) { }
protected virtual string GetResourceKey(string resourceKey, int number) { }
public virtual string TimeSpanHumanize(Humanizer.TimeUnit timeUnit, int unit, bool toWords = false) { }
public virtual string TimeSpanHumanize_Age() { }
public virtual string TimeSpanHumanize_Zero() { }
public virtual string TimeUnitHumanize(Humanizer.TimeUnit timeUnit) { }
}
Expand Down Expand Up @@ -301,6 +302,7 @@ namespace Humanizer
string DateHumanize_Never();
string DateHumanize_Now();
string TimeSpanHumanize(Humanizer.TimeUnit timeUnit, int unit, bool toWords = false);
string TimeSpanHumanize_Age();
string TimeSpanHumanize_Zero();
string TimeUnitHumanize(Humanizer.TimeUnit timeUnit);
}
Expand Down Expand Up @@ -1182,6 +1184,7 @@ namespace Humanizer
public static class Resources
{
public static string GetResource(string resourceKey, System.Globalization.CultureInfo culture = null) { }
public static bool TryGetResource(string resourceKey, System.Globalization.CultureInfo culture, out string result) { }
}
public static class RomanNumeralExtensions
{
Expand Down Expand Up @@ -1212,6 +1215,7 @@ namespace Humanizer
{
public static string Humanize(this System.TimeSpan timeSpan, int precision = 1, System.Globalization.CultureInfo culture = null, Humanizer.TimeUnit maxUnit = 5, Humanizer.TimeUnit minUnit = 0, string collectionSeparator = ", ", bool toWords = false) { }
public static string Humanize(this System.TimeSpan timeSpan, int precision, bool countEmptyUnits, System.Globalization.CultureInfo culture = null, Humanizer.TimeUnit maxUnit = 5, Humanizer.TimeUnit minUnit = 0, string collectionSeparator = ", ", bool toWords = false) { }
public static string ToAge(this System.TimeSpan timeSpan, System.Globalization.CultureInfo culture = null, Humanizer.TimeUnit maxUnit = 7, bool toWords = false) { }
}
public enum TimeUnit
{
Expand Down
8 changes: 8 additions & 0 deletions src/Humanizer/Localisation/Formatters/DefaultFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ public virtual string TimeSpanHumanize_Zero() =>
public virtual string TimeSpanHumanize(TimeUnit timeUnit, int unit, bool toWords = false) =>
GetResourceForTimeSpan(timeUnit, unit, toWords);

/// <inheritdoc/>
public virtual string TimeSpanHumanize_Age()
{
if (Resources.TryGetResource("TimeSpanHumanize_Age", _culture, out var ageFormat))
return ageFormat;
return "{0}";
}

/// <inheritdoc cref="IFormatter.DataUnitHumanize(DataUnit, double, bool)"/>
public virtual string DataUnitHumanize(DataUnit dataUnit, double count, bool toSymbol = true)
{
Expand Down
7 changes: 7 additions & 0 deletions src/Humanizer/Localisation/Formatters/IFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ public interface IFormatter
/// </summary>
string TimeSpanHumanize(TimeUnit timeUnit, int unit, bool toWords = false);

/// <summary>
/// Returns the age format that converts a humanized TimeSpan string to an age expression.
/// For instance, in English that format adds the " old" suffix, so that "40 years" becomes "40 years old".
/// </summary>
/// <returns>Age format</returns>
string TimeSpanHumanize_Age();
louis-z marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Returns the string representation of the provided DataUnit, either as a symbol or full word
/// </summary>
Expand Down
15 changes: 15 additions & 0 deletions src/Humanizer/Localisation/Resources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,20 @@ public static class Resources
/// <returns>The value of the resource localized for the specified culture.</returns>
public static string GetResource(string resourceKey, CultureInfo culture = null) =>
ResourceManager.GetString(resourceKey, culture);

/// <summary>
/// Tries to get the value of the specified string resource, without fallback
/// </summary>
/// <param name="resourceKey">The name of the resource to retrieve.</param>
/// <param name="culture">The culture of the resource to retrieve. If not specified, current thread's UI culture is used.</param>
/// <param name="result">The value of the resource localized for the specified culture if found; null otherwise.</param>
/// <returns>true if the specified string resource was found for the given culture; otherwise, false.</returns>
public static bool TryGetResource(string resourceKey, CultureInfo culture, out string result)
{
culture ??= CultureInfo.CurrentUICulture;
var resourceSet = ResourceManager.GetResourceSet(culture, createIfNotExists: false, tryParents: false);
result = resourceSet?.GetString(resourceKey);
return result is not null;
}
}
}
3 changes: 3 additions & 0 deletions src/Humanizer/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -804,4 +804,7 @@
<data name="TimeSpanHumanize_MultipleYears_Paucal" xml:space="preserve">
<value>{0} years</value>
</data>
<data name="TimeSpanHumanize_Age" xml:space="preserve">
<value>{0} old</value>
</data>
</root>
18 changes: 18 additions & 0 deletions src/Humanizer/TimeSpanHumanizeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ public static string Humanize(this TimeSpan timeSpan, int precision, bool countE
return ConcatenateTimeSpanParts(timeParts, culture, collectionSeparator);
}

/// <summary>
/// Turns a TimeSpan into an age expression, e.g. "40 years old"
/// </summary>
/// <param name="timeSpan">Elapsed time</param>
/// <param name="culture">Culture to use. If null, current thread's UI culture is used.</param>
/// <param name="maxUnit">The maximum unit of time to output. The default value is <see cref="TimeUnit.Year"/>.</param>
/// <param name="toWords">Uses words instead of numbers if true. E.g. "forty years old".</param>
/// <returns>Age expression in the given culture/language</returns>
public static string ToAge(this TimeSpan timeSpan, CultureInfo culture = null, TimeUnit maxUnit = TimeUnit.Year, bool toWords = false)
{
var timeSpanExpression = timeSpan.Humanize(culture: culture, maxUnit: maxUnit, toWords: toWords);

var cultureFormatter = Configurator.GetFormatter(culture);
var ageExpression = string.Format(cultureFormatter.TimeSpanHumanize_Age(), timeSpanExpression);

return ageExpression;
}

static IEnumerable<string> CreateTheTimePartsWithUpperAndLowerLimits(TimeSpan timespan, CultureInfo culture, TimeUnit maxUnit, TimeUnit minUnit, bool toWords = false)
{
var cultureFormatter = Configurator.GetFormatter(culture);
Expand Down