Last active
September 5, 2022 11:22
-
-
Save StudioLE/2dd394e3f792e79adc927ede274df56e to your computer and use it in GitHub Desktop.
Convert TimeSpan and DateTime to a natural language representations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// Convert a <see cref="TimeSpan"/> to a natural language representation. | |
/// </summary> | |
/// <example> | |
/// <code> | |
/// TimeSpan.FromSeconds(10).ToNaturalLanguage(); | |
/// // 10 seconds | |
/// </code> | |
/// </example> | |
public static string ToNaturalLanguage(this TimeSpan @this) | |
{ | |
const int daysInWeek = 7; | |
const int daysInMonth = 30; | |
const int daysInYear = 365; | |
const long threshold = 100 * TimeSpan.TicksPerMillisecond; | |
@this = @this.TotalSeconds < 0 | |
? TimeSpan.FromSeconds(@this.TotalSeconds * -1) | |
: @this; | |
return (@this.Ticks + threshold) switch | |
{ | |
< 2 * TimeSpan.TicksPerSecond => "a second", | |
< 1 * TimeSpan.TicksPerMinute => @this.Seconds + " seconds", | |
< 2 * TimeSpan.TicksPerMinute => "a minute", | |
< 1 * TimeSpan.TicksPerHour => @this.Minutes + " minutes", | |
< 2 * TimeSpan.TicksPerHour => "an hour", | |
< 1 * TimeSpan.TicksPerDay => @this.Hours + " hours", | |
< 2 * TimeSpan.TicksPerDay => "a day", | |
< 1 * daysInWeek * TimeSpan.TicksPerDay => @this.Days + " days", | |
< 2 * daysInWeek * TimeSpan.TicksPerDay => "a week", | |
< 1 * daysInMonth * TimeSpan.TicksPerDay => (@this.Days / daysInWeek).ToString("F0") + " weeks", | |
< 2 * daysInMonth * TimeSpan.TicksPerDay => "a month", | |
< 1 * daysInYear * TimeSpan.TicksPerDay => (@this.Days / daysInMonth).ToString("F0") + " months", | |
< 2 * daysInYear * TimeSpan.TicksPerDay => "a year", | |
_ => (@this.Days / daysInYear).ToString("F0") + " years" | |
}; | |
} | |
/// <summary> | |
/// Convert a <see cref="DateTime"/> to a natural language representation. | |
/// </summary> | |
/// <example> | |
/// <code> | |
/// (DateTime.Now - TimeSpan.FromSeconds(10)).ToNaturalLanguage() | |
/// // 10 seconds ago | |
/// </code> | |
/// </example> | |
public static string ToNaturalLanguage(this DateTime @this) | |
{ | |
TimeSpan timeSpan = @this - DateTime.Now; | |
return timeSpan.TotalSeconds switch | |
{ | |
>= 1 => timeSpan.ToNaturalLanguage() + " until", | |
<= -1 => timeSpan.ToNaturalLanguage() + " ago", | |
_ => "now", | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[TestCase("a second", 0)] | |
[TestCase("a second", 1)] | |
[TestCase("2 seconds", 2)] | |
[TestCase("a minute", 0, 1)] | |
[TestCase("5 minutes", 0, 5)] | |
[TestCase("an hour", 0, 0, 1)] | |
[TestCase("2 hours", 0, 0, 2)] | |
[TestCase("a day", 0, 0, 24)] | |
[TestCase("a day", 0, 0, 0, 1)] | |
[TestCase("6 days", 0, 0, 0, 6)] | |
[TestCase("a week", 0, 0, 0, 7)] | |
[TestCase("4 weeks", 0, 0, 0, 29)] | |
[TestCase("a month", 0, 0, 0, 30)] | |
[TestCase("6 months", 0, 0, 0, 6 * 30)] | |
[TestCase("a year", 0, 0, 0, 365)] | |
[TestCase("68 years", int.MaxValue)] | |
public void NaturalLanguageHelpers_TimeSpan( | |
string expected, | |
int seconds, | |
int minutes = 0, | |
int hours = 0, | |
int days = 0 | |
) | |
{ | |
// Arrange | |
TimeSpan timeSpan = new(days, hours, minutes, seconds); | |
// Act | |
string result = timeSpan.ToNaturalLanguage(); | |
// Assert | |
Assert.That(result, Is.EqualTo(expected)); | |
} | |
[TestCase("now", 0)] | |
[TestCase("10 minutes ago", 0, -10)] | |
[TestCase("10 minutes until", 10, 10)] | |
[TestCase("68 years until", int.MaxValue)] | |
[TestCase("68 years ago", int.MinValue)] | |
public void NaturalLanguageHelpers_DateTime( | |
string expected, | |
int seconds, | |
int minutes = 0, | |
int hours = 0, | |
int days = 0 | |
) | |
{ | |
// Arrange | |
TimeSpan timeSpan = new(days, hours, minutes, seconds); | |
DateTime now = DateTime.Now; | |
DateTime dateTime = now + timeSpan; | |
// Act | |
string result = dateTime.ToNaturalLanguage(); | |
// Assert | |
Assert.That(result, Is.EqualTo(expected)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The accepted answer by Vincent makes quite a few arbitrary decisions.
Why is 45 minutes rounded up to an hour while 45 seconds is not rounded up to a minute?
It also has an increased level of cyclomatic complexity within the years and month calculations that makes it more complex to follow the logic.
It makes the assumption that the TimeSpan is relative to the past (2 days ago) when it could very well be in the future (2 days until).
This implementation resolves the above and updates the syntax to use switch expressions and relational patterns
...
You can test it with NUnit as follows:
...
Or as a gist: https://gist.github.com/StudioLE/2dd394e3f792e79adc927ede274df56e