Skip to content

Commit

Permalink
Merge pull request #399 from tompazourek/datetimeoffset
Browse files Browse the repository at this point in the history
Support for humanizing DateTimeOffset
  • Loading branch information
MehdiK committed Mar 28, 2015
2 parents cc06fe9 + 134d1cd commit 3b3288a
Show file tree
Hide file tree
Showing 14 changed files with 317 additions and 107 deletions.
12 changes: 9 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,22 +263,25 @@ By default both methods throw a `NoMatchFoundException` when they cannot match t
In the non-generic method you can also ask the method to return null by setting the second optional parameter to `NoMatch.ReturnsNull`.

###<a id="humanize-datetime">Humanize DateTime</a>
You can `Humanize` an instance of `DateTime` and get back a string telling how far back or forward in time that is:
You can `Humanize` an instance of `DateTime` or `DateTimeOffset` and get back a string telling how far back or forward in time that is:

```C#
DateTime.UtcNow.AddHours(-30).Humanize() => "yesterday"
DateTime.UtcNow.AddHours(-2).Humanize() => "2 hours ago"

DateTime.UtcNow.AddHours(30).Humanize() => "tomorrow"
DateTime.UtcNow.AddHours(2).Humanize() => "2 hours from now"

DateTimeOffset.AddHours(1).Humanize() => "an hour 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.
Humanizer supports both local and UTC dates as well as dates with offset (`DateTimeOffset`). 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, CultureInfo culture = null)
public static string Humanize(this DateTimeOffset input, DateTimeOffset? dateToCompareAgainst = null, CultureInfo culture = null)
```

Many localizations are available for this method. Here is a few examples:
Expand All @@ -302,7 +305,10 @@ DateTime.UtcNow.AddMinutes(-40).Humanize() => "40 минут назад"
There are two strategies for `DateTime.Humanize`: the default one as seen above and a precision based one.
To use the precision based strategy you need to configure it:

`Configurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(precision = .75)`.
```C#
Configurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(precision = .75);
Configurator.DateTimeOffsetHumanizeStrategy = new PrecisionDateTimeOffsetHumanizeStrategy(precision = .75); // configure when humanizing DateTimeOffset
```

The default precision is set to .75 but you can pass your desired precision too. With precision set to 0.75:

Expand Down
1 change: 1 addition & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
###In Development
- [#399](https://github.com/MehdiK/Humanizer/pull/399): Support for humanizing DateTimeOffset

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public class Configurator
{
public Humanizer.Configuration.LocaliserRegistry<Humanizer.Localisation.CollectionFormatters.ICollectionFormatter> CollectionFormatters { get; }
public Humanizer.DateTimeHumanizeStrategy.IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; }
public Humanizer.DateTimeHumanizeStrategy.IDateTimeOffsetHumanizeStrategy DateTimeOffsetHumanizeStrategy { get; set; }
public System.Func<System.Reflection.PropertyInfo, bool> EnumDescriptionPropertyLocator { get; set; }
public Humanizer.Configuration.LocaliserRegistry<Humanizer.Localisation.Formatters.IFormatter> Formatters { get; }
public Humanizer.Configuration.LocaliserRegistry<Humanizer.Localisation.NumberToWords.INumberToWordsConverter> NumberToWordsConverters { get; }
Expand All @@ -144,6 +145,7 @@ public class LocaliserRegistry`1
public class DateHumanizeExtensions
{
public string Humanize(System.DateTime input, bool utcDate, System.Nullable<System.DateTime> dateToCompareAgainst, System.Globalization.CultureInfo culture) { }
public string Humanize(System.DateTimeOffset input, System.Nullable<System.DateTimeOffset> dateToCompareAgainst, System.Globalization.CultureInfo culture) { }
}

public class DefaultDateTimeHumanizeStrategy
Expand All @@ -152,17 +154,34 @@ public class DefaultDateTimeHumanizeStrategy
public string Humanize(System.DateTime input, System.DateTime comparisonBase, System.Globalization.CultureInfo culture) { }
}

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

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

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

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

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

public class EnumDehumanizeExtensions
{
public TTargetEnum DehumanizeTo(string input) { }
Expand Down
73 changes: 73 additions & 0 deletions src/Humanizer.Tests/DateTimeOffsetHumanizeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Humanizer.Configuration;
using Humanizer.DateTimeHumanizeStrategy;
using Xunit;

namespace Humanizer.Tests
{
public class DateTimeOffsetHumanizeTests : AmbientCulture
{
public DateTimeOffsetHumanizeTests() : base("en-US")
{
}

[Fact]
public void DefaultStrategy_SameOffset()
{
Configurator.DateTimeOffsetHumanizeStrategy = new DefaultDateTimeOffsetHumanizeStrategy();

var inputTime = new DateTimeOffset(2015, 07, 05, 04, 0, 0, TimeSpan.Zero);
var baseTime = new DateTimeOffset(2015, 07, 05, 03, 0, 0, TimeSpan.Zero);

const string expectedResult = "an hour from now";
var actualResult = inputTime.Humanize(baseTime);

Assert.Equal(expectedResult, actualResult);
}

[Fact]
public void DefaultStrategy_DifferentOffsets()
{
Configurator.DateTimeOffsetHumanizeStrategy = new DefaultDateTimeOffsetHumanizeStrategy();

var inputTime = new DateTimeOffset(2015, 07, 05, 03, 0, 0, new TimeSpan(2, 0, 0));
var baseTime = new DateTimeOffset(2015, 07, 05, 02, 30, 0, new TimeSpan(1, 0, 0));

const string expectedResult = "30 minutes ago";
var actualResult = inputTime.Humanize(baseTime);

Assert.Equal(expectedResult, actualResult);
}

[Fact]
public void PrecisionStrategy_SameOffset()
{
Configurator.DateTimeOffsetHumanizeStrategy = new PrecisionDateTimeOffsetHumanizeStrategy(0.75);

var inputTime = new DateTimeOffset(2015, 07, 05, 04, 0, 0, TimeSpan.Zero);
var baseTime = new DateTimeOffset(2015, 07, 04, 05, 0, 0, TimeSpan.Zero);

const string expectedResult = "tomorrow";
var actualResult = inputTime.Humanize(baseTime);

Assert.Equal(expectedResult, actualResult);
}

[Fact]
public void PrecisionStrategy_DifferentOffsets()
{
Configurator.DateTimeOffsetHumanizeStrategy = new PrecisionDateTimeOffsetHumanizeStrategy(0.75);

var inputTime = new DateTimeOffset(2015, 07, 05, 03, 45, 0, new TimeSpan(2, 0, 0));
var baseTime = new DateTimeOffset(2015, 07, 05, 02, 30, 0, new TimeSpan(-5, 0, 0));

const string expectedResult = "6 hours ago";
var actualResult = inputTime.Humanize(baseTime);

Assert.Equal(expectedResult, actualResult);
}
}
}
1 change: 1 addition & 0 deletions src/Humanizer.Tests/Humanizer.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<Compile Include="CasingTests.cs" />
<Compile Include="DateHumanize.cs" />
<Compile Include="CollectionHumanizeTests.cs" />
<Compile Include="DateTimeOffsetHumanizeTests.cs" />
<Compile Include="EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs" />
<Compile Include="Localisation\bg\DateHumanizeTests.cs" />
<Compile Include="Localisation\bg\TimeSpanHumanizeTests.cs" />
Expand Down
10 changes: 10 additions & 0 deletions src/Humanizer/Configuration/Configurator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy
set { _dateTimeHumanizeStrategy = value; }
}

private static IDateTimeOffsetHumanizeStrategy _dateTimeOffsetHumanizeStrategy = new DefaultDateTimeOffsetHumanizeStrategy();
/// <summary>
/// The strategy to be used for DateTimeOffset.Humanize
/// </summary>
public static IDateTimeOffsetHumanizeStrategy DateTimeOffsetHumanizeStrategy
{
get { return _dateTimeOffsetHumanizeStrategy; }
set { _dateTimeOffsetHumanizeStrategy = value; }
}

private static readonly Func<PropertyInfo, bool> DefaultEnumDescriptionPropertyLocator = p => p.Name == "Description";
private static Func<PropertyInfo, bool> _enumDescriptionPropertyLocator = DefaultEnumDescriptionPropertyLocator;
/// <summary>
Expand Down
14 changes: 14 additions & 0 deletions src/Humanizer/DateHumanizeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,19 @@ public static string Humanize(this DateTime input, bool utcDate = true, DateTime

return Configurator.DateTimeHumanizeStrategy.Humanize(input, comparisonBase, culture);
}

/// <summary>
/// Turns the current or provided date into a human readable sentence
/// </summary>
/// <param name="input">The date to be humanized</param>
/// <param name="dateToCompareAgainst">Date to compare the input against. If null, current date is used as base</param>
/// <param name="culture">Culture to use. If null, current thread's UI culture is used.</param>
/// <returns>distance of time in words</returns>
public static string Humanize(this DateTimeOffset input, DateTimeOffset? dateToCompareAgainst = null, CultureInfo culture = null)
{
var comparisonBase = dateToCompareAgainst ?? DateTimeOffset.UtcNow;

return Configurator.DateTimeOffsetHumanizeStrategy.Humanize(input, comparisonBase, culture);
}
}
}
116 changes: 116 additions & 0 deletions src/Humanizer/DateTimeHumanizeStrategy/DateTimeHumanizeAlgorithms.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System;
using System.Globalization;
using Humanizer.Configuration;
using Humanizer.Localisation;

namespace Humanizer.DateTimeHumanizeStrategy
{
/// <summary>
/// Algorithms used to convert distance between two dates into words.
/// </summary>
internal static class DateTimeHumanizeAlgorithms
{
/// <summary>
/// Returns localized &amp; humanized distance of time between two dates; given a specific precision.
/// </summary>
public static string PrecisionHumanize(DateTime input, DateTime comparisonBase, double precision, CultureInfo culture)
{
var ts = new TimeSpan(Math.Abs(comparisonBase.Ticks - input.Ticks));
var tense = input > comparisonBase ? Tense.Future : Tense.Past;

int seconds = ts.Seconds, minutes = ts.Minutes, hours = ts.Hours, days = ts.Days;
int years = 0, months = 0;

// start approximate from smaller units towards bigger ones
if (ts.Milliseconds >= 999 * precision) seconds += 1;
if (seconds >= 59 * precision) minutes += 1;
if (minutes >= 59 * precision) hours += 1;
if (hours >= 23 * precision) days += 1;

// month calculation
if (days >= 30 * precision & days <= 31) months = 1;
if (days > 31 && days < 365 * precision)
{
int factor = Convert.ToInt32(Math.Floor((double)days / 30));
int maxMonths = Convert.ToInt32(Math.Ceiling((double)days / 30));
months = (days >= 30 * (factor + precision)) ? maxMonths : maxMonths - 1;
}

// year calculation
if (days >= 365 * precision && days <= 366) years = 1;
if (days > 365)
{
int factor = Convert.ToInt32(Math.Floor((double)days / 365));
int maxMonths = Convert.ToInt32(Math.Ceiling((double)days / 365));
years = (days >= 365 * (factor + precision)) ? maxMonths : maxMonths - 1;
}

// start computing result from larger units to smaller ones
var formatter = Configurator.GetFormatter(culture);
if (years > 0) return formatter.DateHumanize(TimeUnit.Year, tense, years);
if (months > 0) return formatter.DateHumanize(TimeUnit.Month, tense, months);
if (days > 0) return formatter.DateHumanize(TimeUnit.Day, tense, days);
if (hours > 0) return formatter.DateHumanize(TimeUnit.Hour, tense, hours);
if (minutes > 0) return formatter.DateHumanize(TimeUnit.Minute, tense, minutes);
if (seconds > 0) return formatter.DateHumanize(TimeUnit.Second, tense, seconds);
return formatter.DateHumanize(TimeUnit.Millisecond, tense, 0);
}

// http://stackoverflow.com/questions/11/how-do-i-calculate-relative-time
/// <summary>
/// Calculates the distance of time in words between two provided dates
/// </summary>
public static string DefaultHumanize(DateTime input, DateTime comparisonBase, CultureInfo culture)
{
var tense = input > comparisonBase ? Tense.Future : Tense.Past;
var ts = new TimeSpan(Math.Abs(comparisonBase.Ticks - input.Ticks));

var formatter = Configurator.GetFormatter(culture);

if (ts.TotalMilliseconds < 500)
return formatter.DateHumanize(TimeUnit.Millisecond, tense, 0);

if (ts.TotalSeconds < 60)
return formatter.DateHumanize(TimeUnit.Second, tense, ts.Seconds);

if (ts.TotalSeconds < 120)
return formatter.DateHumanize(TimeUnit.Minute, tense, 1);

if (ts.TotalMinutes < 60)
return formatter.DateHumanize(TimeUnit.Minute, tense, ts.Minutes);

if (ts.TotalMinutes < 90)
return formatter.DateHumanize(TimeUnit.Hour, tense, 1);

if (ts.TotalHours < 24)
return formatter.DateHumanize(TimeUnit.Hour, tense, ts.Hours);

if (ts.TotalHours < 48)
{
var days = Math.Abs((input.Date - comparisonBase.Date).Days);
return formatter.DateHumanize(TimeUnit.Day, tense, days);
}

if (ts.TotalDays < 28)
return formatter.DateHumanize(TimeUnit.Day, tense, ts.Days);

if (ts.TotalDays >= 28 && ts.TotalDays < 30)
{
if (comparisonBase.Date.AddMonths(tense == Tense.Future ? 1 : -1) == input.Date)
return formatter.DateHumanize(TimeUnit.Month, tense, 1);
return formatter.DateHumanize(TimeUnit.Day, tense, ts.Days);
}

if (ts.TotalDays < 345)
{
int months = Convert.ToInt32(Math.Floor(ts.TotalDays / 29.5));
return formatter.DateHumanize(TimeUnit.Month, tense, months);
}

int years = Convert.ToInt32(Math.Floor(ts.TotalDays / 365));
if (years == 0) years = 1;

return formatter.DateHumanize(TimeUnit.Year, tense, years);
}
}
}
Loading

0 comments on commit 3b3288a

Please sign in to comment.