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

Added optional Culture parameter to DateTime.Humanize #286

Closed
wants to merge 11 commits into from
14 changes: 12 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,12 @@ DateTime.UtcNow.AddHours(30).Humanize() => "tomorrow"
DateTime.UtcNow.AddHours(2).Humanize() => "2 hours from now"
```

Humanizer supports local as well as UTC dates. You could also provide the date you want the input date to be compared against. If null, it will use the current date as comparison base. Here is the API signature:
Humanizer supports local as well as UTC dates. You could also provide the date you want the input date to be compared against. If null, it will use the current date as comparison base.
Also, culture to use can be specified explicitly. If it is not, current thread's current UI culture is used.
Here is the API signature:

```C#
public static string Humanize(this DateTime input, bool utcDate = true, DateTime? dateToCompareAgainst = null)
public static string Humanize(this DateTime input, bool utcDate = true, DateTime? dateToCompareAgainst = null, CultureInfo culture = null)
```

Many localizations are available for this method. Here is a few examples:
Expand Down Expand Up @@ -313,6 +315,14 @@ TimeSpan.FromMilliseconds(2).Humanize() => "2 milisekundy"
TimeSpan.FromMilliseconds(5).Humanize() => "5 milisekúnd"
```

Culture to use can be specified explicitly. If it is not, current thread's current UI culture is used. Example:

```C#

TimeSpan.FromDays(1).Humanize(culture: "ru-RU") => "один день"

```

###<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
25 changes: 13 additions & 12 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
- [#281](https://github.com/Mehdik/Humanizer/pull/281): Changed the logic for handling hyphenation and large numbers ending in twelve for English ordinal words; e.g. before "twenty first" now "twenty-first"
- [#278](https://github.com/MehdiK/Humanizer/pull/278): Changed DefaultDateTimeHumanizeStrategy to turn 60 min to one hour not 45
- [#283](https://github.com/MehdiK/Humanizer/pull/283): Added Neutral nb support for DateTime and TimeSpan Humanize

- [#286](https://github.com/MehdiK/Humanizer/pull/286): Added optional Culture parameter to DateTime.Humanize & TimeSpan.Humanize

[Commits](https://github.com/MehdiK/Humanizer/compare/v1.26.1...master)

###v1.26.1 - 2014-05-20
Expand All @@ -26,7 +27,7 @@
[Commits](https://github.com/MehdiK/Humanizer/compare/v1.24.1...v1.25.4)

###v1.24.1 - 2014-04-21
- [#232](https://github.com/Mehdik/Humanizer/pull/232): Adding code & tests to handle Arabic numbers to ordinal
- [#232](https://github.com/Mehdik/Humanizer/pull/232): Adding code & tests to handle Arabic numbers to ordinal
- [#235](https://github.com/Mehdik/Humanizer/pull/235): Fixed the conversion for "1 millon" in SpanishNumberToWordsConverter
- [#233](https://github.com/Mehdik/Humanizer/pull/233): Added build.cmd and Verify build configuration for strict project build and analysis

Expand All @@ -49,8 +50,8 @@
- [#199](https://github.com/MehdiK/Humanizer/pull/199): Added Hebrew Number to words (both genders)
- [#202](https://github.com/MehdiK/Humanizer/pull/202): Fixed typo sekunttia -> sekuntia (Finnish translation)
- [#203](https://github.com/MehdiK/Humanizer/pull/203): Added feminine gender for french ordinal words
- [#208](https://github.com/MehdiK/Humanizer/pull/208): Added Hebrew implementation of future DateTime
- [#208](https://github.com/MehdiK/Humanizer/pull/208): Added Hebrew implementation of future DateTime

[Commits](https://github.com/MehdiK/Humanizer/compare/v1.21.1...v1.22.1)

###v1.21.1 - 2014-04-12
Expand All @@ -60,7 +61,7 @@
- [#190](https://github.com/MehdiK/Humanizer/pull/190): Added French translation for ToWords and ToOrdinalWords
- [#179](https://github.com/MehdiK/Humanizer/pull/179): Added Hungarian localisation
- [#181](https://github.com/Mehdik/Humanizer/pull/181): Added Bulgarian localization, date and timespan tests
- [#141](https://github.com/MehdiK/Humanizer/pull/141): Added Indonesian localization
- [#141](https://github.com/MehdiK/Humanizer/pull/141): Added Indonesian localization
- [#148](https://github.com/Mehdik/Humanizer/pull/148): Added Hebrew localization for date and timespan

[Commits](https://github.com/MehdiK/Humanizer/compare/v1.20.15...v1.21.1)
Expand All @@ -82,12 +83,12 @@
[Commits](https://github.com/MehdiK/Humanizer/compare/v1.19.1...v1.20.2)

###v1.19.1 - 2014-04-10
- [#149](https://github.com/MehdiK/Humanizer/pull/149): Improved & refactored number to words localisation
- [#149](https://github.com/MehdiK/Humanizer/pull/149): Improved & refactored number to words localisation
- [#143](https://github.com/MehdiK/Humanizer/pull/143): Added Russian translation for future DateTime, TimeSpan and Now
- [#144](https://github.com/MehdiK/Humanizer/pull/144): Added Danish localization (strings, tests)
- [#146](https://github.com/MehdiK/Humanizer/pull/146): Added Spanish translation for future DateTime, TimeSpan and Now


[Commits](https://github.com/MehdiK/Humanizer/compare/v1.18.1...v1.19.1)

###v1.18.1 - 2014-04-09
Expand Down Expand Up @@ -120,7 +121,7 @@

###v1.14.1 - 2014-03-26
- [#108](https://github.com/MehdiK/Humanizer/pull/108): Added support for custom description attributes
- [#106](https://github.com/MehdiK/Humanizer/pull/106):
- [#106](https://github.com/MehdiK/Humanizer/pull/106):
- Refactored IFormatter and DefaultFormatter
- Refactored `DateTime.Humanize` and `TimeSpan.Humanize`
- Changed `ResourceKeys` to use a dynamic key generation
Expand Down Expand Up @@ -179,7 +180,7 @@ If you were catching `CannotMapToTargetException` on a `DehumanizeTo` call, that
####Potential breaking change
The return type of `DehumanizeTo<TTargetEnum>` was changed from `Enum` to `TTargetEnum` to make the API a lot easier to work with.
That also potentially means that your calls to the old method may be broken.
Depending on how you were using the method you might have to either drop the now redundant cast to `TTargetEnum` in your code, or
Depending on how you were using the method you might have to either drop the now redundant cast to `TTargetEnum` in your code, or
fix it based on your requirements.

[Commits](https://github.com/MehdiK/Humanizer/compare/v1.5.1...v1.6.1)
Expand All @@ -205,7 +206,7 @@ fix it based on your requirements.

###v1.1.0 - 2014-01-01
- [#37](https://github.com/MehdiK/Humanizer/pull/37): added `ToQuantity` method
- [#43](https://github.com/MehdiK/Humanizer/pull/43):
- [#43](https://github.com/MehdiK/Humanizer/pull/43):
- added `Plurality` enum
- can call `Singularize` on singular and `Pluralize` on plural words
- `ToQuantity` can be called on words with unknown plurality
Expand All @@ -214,7 +215,7 @@ fix it based on your requirements.

###v1.0.29 - 2013-12-25
- [#26](https://github.com/MehdiK/Humanizer/pull/26): added Norwegian (nb-NO) localization for `DateTime.Humanize()`
- [#33](https://github.com/MehdiK/Humanizer/pull/33):
- [#33](https://github.com/MehdiK/Humanizer/pull/33):
- changed to Portable Class Library with support for .Net 4+, SilverLight 5, Windows Phone 8 and Win Store applications
- symbols nuget package is published so you can step through Humanizer code while debugging your code

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,29 +87,30 @@ public class LocaliserRegistry`1
public void Register(string localeCode) { }
public void Register(System.Func<> localiserFactory, string localeCode) { }
public void RegisterDefault(TLocaliser defaultLocaliser) { }
public TLocaliser ResolveForCulture(System.Globalization.CultureInfo culture) { }
public TLocaliser ResolveForUiCulture() { }
}

public class DateHumanizeExtensions
{
public string Humanize(System.DateTime input, bool utcDate, System.Nullable<System.DateTime> dateToCompareAgainst) { }
public string Humanize(System.DateTime input, bool utcDate, System.Nullable<System.DateTime> dateToCompareAgainst, System.Globalization.CultureInfo culture) { }
}

public class DefaultDateTimeHumanizeStrategy
{
public DefaultDateTimeHumanizeStrategy() { }
public string Humanize(System.DateTime input, System.DateTime comparisonBase) { }
public string Humanize(System.DateTime input, System.DateTime comparisonBase, System.Globalization.CultureInfo culture) { }
}

public interface IDateTimeHumanizeStrategy
{
string Humanize(System.DateTime input, System.DateTime comparisonBase);
string Humanize(System.DateTime input, System.DateTime comparisonBase, System.Globalization.CultureInfo culture);
}

public class PrecisionDateTimeHumanizeStrategy
{
public PrecisionDateTimeHumanizeStrategy(double precision) { }
public string Humanize(System.DateTime input, System.DateTime comparisonBase) { }
public string Humanize(System.DateTime input, System.DateTime comparisonBase, System.Globalization.CultureInfo culture) { }
}

public class EnumDehumanizeExtensions
Expand Down Expand Up @@ -214,7 +215,7 @@ public interface ICollectionFormatter

public class DefaultFormatter
{
public DefaultFormatter() { }
public DefaultFormatter(string localeCode) { }
public string DateHumanize(Humanizer.Localisation.TimeUnit timeUnit, Humanizer.Localisation.Tense timeUnitTense, int unit) { }
public string DateHumanize_Now() { }
public string TimeSpanHumanize(Humanizer.Localisation.TimeUnit timeUnit, int unit) { }
Expand Down Expand Up @@ -250,7 +251,7 @@ public class ResourceKeys

public class Resources
{
public string GetResource(string resourceKey) { }
public string GetResource(string resourceKey, System.Globalization.CultureInfo culture) { }
}

public enum Tense
Expand Down Expand Up @@ -366,7 +367,7 @@ public class StringHumanizeExtensions

public class TimeSpanHumanizeExtensions
{
public string Humanize(System.TimeSpan timeSpan, int precision) { }
public string Humanize(System.TimeSpan timeSpan, int precision, System.Globalization.CultureInfo culture) { }
}

public class To
Expand Down
19 changes: 10 additions & 9 deletions src/Humanizer.Tests/DateHumanize.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using Humanizer.Configuration;
using Humanizer.DateTimeHumanizeStrategy;
using Humanizer.Localisation;
Expand All @@ -8,26 +9,26 @@ namespace Humanizer.Tests
{
public class DateHumanize
{
static void VerifyWithCurrentDate(string expectedString, TimeSpan deltaFromNow)
static void VerifyWithCurrentDate(string expectedString, TimeSpan deltaFromNow, CultureInfo culture)
{
var utcNow = DateTime.UtcNow;
var localNow = DateTime.Now;

// feels like the only way to avoid breaking tests because CPU ticks over is to inject the base date
Assert.Equal(expectedString, utcNow.Add(deltaFromNow).Humanize(utcDate: true, dateToCompareAgainst: utcNow));
Assert.Equal(expectedString, localNow.Add(deltaFromNow).Humanize(utcDate: false, dateToCompareAgainst: localNow));
Assert.Equal(expectedString, utcNow.Add(deltaFromNow).Humanize(utcDate: true, dateToCompareAgainst: utcNow, culture: culture));
Assert.Equal(expectedString, localNow.Add(deltaFromNow).Humanize(utcDate: false, dateToCompareAgainst: localNow, culture: culture));
}

static void VerifyWithDateInjection(string expectedString, TimeSpan deltaFromNow)
static void VerifyWithDateInjection(string expectedString, TimeSpan deltaFromNow, CultureInfo culture)
{
var utcNow = new DateTime(2013, 6, 20, 9, 58, 22, DateTimeKind.Utc);
var now = new DateTime(2013, 6, 20, 11, 58, 22, DateTimeKind.Local);

Assert.Equal(expectedString, utcNow.Add(deltaFromNow).Humanize(utcDate: true, dateToCompareAgainst: utcNow));
Assert.Equal(expectedString, now.Add(deltaFromNow).Humanize(false, now));
Assert.Equal(expectedString, utcNow.Add(deltaFromNow).Humanize(utcDate: true, dateToCompareAgainst: utcNow, culture: culture));
Assert.Equal(expectedString, now.Add(deltaFromNow).Humanize(false, now, culture: culture));
}

public static void Verify(string expectedString, int unit, TimeUnit timeUnit, Tense tense, double? precision = null)
public static void Verify(string expectedString, int unit, TimeUnit timeUnit, Tense tense, double? precision = null, CultureInfo culture = null)
{
if (precision.HasValue)
Configurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(precision.Value);
Expand Down Expand Up @@ -65,8 +66,8 @@ public static void Verify(string expectedString, int unit, TimeUnit timeUnit, Te
break;
}

VerifyWithCurrentDate(expectedString, deltaFromNow);
VerifyWithDateInjection(expectedString, deltaFromNow);
VerifyWithCurrentDate(expectedString, deltaFromNow, culture);
VerifyWithDateInjection(expectedString, deltaFromNow, culture);
}
}
}
12 changes: 11 additions & 1 deletion src/Humanizer.Tests/DateHumanizeDefaultStrategyTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Humanizer.Localisation;
using System.Globalization;
using Humanizer.Localisation;
using Xunit;
using Xunit.Extensions;

Expand Down Expand Up @@ -138,5 +139,14 @@ public void Now()
{
DateHumanize.Verify("now", 0, TimeUnit.Year, Tense.Future);
}

[Theory]
[InlineData(1, TimeUnit.Year, Tense.Future, "en-US", "one year from now")]
[InlineData(40, TimeUnit.Second, Tense.Past, "ru-RU", "40 секунд назад")]
[InlineData(2, TimeUnit.Day, Tense.Past, "sv-SE", "för 2 dagar sedan")]
public void ExplicitCultureIsUsed(int unit, TimeUnit timeUnit, Tense tense, string culture, string expected)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly what I meant. Thanks.

{
DateHumanize.Verify(expected, unit, timeUnit, tense, culture: new CultureInfo(culture));
}
}
}
12 changes: 10 additions & 2 deletions src/Humanizer.Tests/Localisation/ResourcesTests.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
using Humanizer.Localisation;
using System.Globalization;
using Humanizer.Localisation;
using Xunit;

namespace Humanizer.Tests.Localisation
{
public class ResourcesTests
{
[Fact]
public void CanGetCultureSpecificTranslations()
public void CanGetCultureSpecificTranslationsWithImplicitCulture()
{
using (new AmbientCulture("ro"))
{
var format = Resources.GetResource("DateHumanize_MultipleYearsAgo_Above20");
Assert.Equal("acum {0} de ani", format);
}
}

[Fact]
public void CanGetCultureSpecificTranslationsWithExplicitCulture()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

{
var format = Resources.GetResource("DateHumanize_MultipleYearsAgo_Above20", new CultureInfo("ro"));
Assert.Equal("acum {0} de ani", format);
}
}
}
10 changes: 5 additions & 5 deletions src/Humanizer.Tests/Localisation/nl/DateHumanizeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ public DateHumanizeTests() : base("nl-NL") { }
[InlineData(-1, "gisteren")]
public void DaysAgo(int days, string expected)
{
Assert.Equal(expected, DateTime.UtcNow.AddDays(days).Humanize());
DateHumanize.Verify(expected, days, TimeUnit.Day, Tense.Past);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing these.

}

[Theory]
[InlineData(-2, "2 uur geleden")]
[InlineData(-1, "één uur geleden")]
public void HoursAgo(int hours, string expected)
{
Assert.Equal(expected, DateTime.UtcNow.AddHours(hours).Humanize());
DateHumanize.Verify(expected, hours, TimeUnit.Hour, Tense.Past);
}

[Theory]
Expand All @@ -39,23 +39,23 @@ public void MinutesAgo(int minutes, string expected)
[InlineData(-1, "één maand geleden")]
public void MonthsAgo(int months, string expected)
{
Assert.Equal(expected, DateTime.UtcNow.AddMonths(months).Humanize());
DateHumanize.Verify(expected, months, TimeUnit.Month, Tense.Past);
}

[Theory]
[InlineData(-2, "2 seconden geleden")]
[InlineData(-1, "één seconde geleden")]
public void SecondsAgo(int seconds, string expected)
{
Assert.Equal(expected, DateTime.UtcNow.AddSeconds(seconds).Humanize());
DateHumanize.Verify(expected, seconds, TimeUnit.Second, Tense.Past);
}

[Theory]
[InlineData(-2, "2 jaar geleden")]
[InlineData(-1, "één jaar geleden")]
public void YearsAgo(int years, string expected)
{
Assert.Equal(expected, DateTime.UtcNow.AddYears(years).Humanize());
DateHumanize.Verify(expected, years, TimeUnit.Year, Tense.Past);
}
}
}
11 changes: 11 additions & 0 deletions src/Humanizer.Tests/TimeSpanHumanizeTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using Xunit;
using Xunit.Extensions;

Expand Down Expand Up @@ -110,5 +111,15 @@ public void NoTime()
var actual = noTime.Humanize();
Assert.Equal("no time", actual);
}

[Theory]
[InlineData(1, "en-US", "1 millisecond")]
[InlineData(6 * 24 * 60 * 60 * 1000, "ru-RU", "6 дней")]
[InlineData(11 * 60 * 60 * 1000, "ar", "11 ساعة")]
public void ExplicitCultureIsUsed(int ms, string culture, string expected)
{
var actual = TimeSpan.FromMilliseconds(ms).Humanize(culture: new CultureInfo(culture));
Assert.Equal(expected, actual);
}
}
}
11 changes: 5 additions & 6 deletions src/Humanizer/Configuration/Configurator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using Humanizer.DateTimeHumanizeStrategy;
using Humanizer.Localisation.Formatters;
Expand Down Expand Up @@ -58,16 +59,14 @@ internal static ICollectionFormatter CollectionFormatter
return CollectionFormatters.ResolveForUiCulture();
}
}

/// <summary>
/// The formatter to be used
/// </summary>
internal static IFormatter Formatter
/// <param name="culture">The culture to retrieve formatter for. Null means that current thread's UI culture should be used.</param>
internal static IFormatter GetFormatter(CultureInfo culture)
{
get
{
return Formatters.ResolveForUiCulture();
}
return Formatters.ResolveForCulture(culture);
}

/// <summary>
Expand Down
Loading