diff --git a/readme.md b/readme.md
index eb4c35b3c..e96a64159 100644
--- a/readme.md
+++ b/readme.md
@@ -370,6 +370,12 @@ Culture to use can be specified explicitly. If it is not, current thread's curre
TimeSpan.FromDays(1).Humanize(culture: "ru-RU") => "один день"
```
+In addition, a maximum unit of time may be specified to avoid rolling up to the next largest unit. For example:
+```C#
+TimeSpan.FromDays(7).Humanize(maxUnit: TimeUnit.Day) => "7 days" // instead of 1 week
+TimeSpan.FromMilliseconds(2000).Humanize(maxUnit: TimeUnit.Millisecond) => "2000 milliseconds" // instead of 2 seconds
+```
+
###Humanize Collections
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`.
diff --git a/release_notes.md b/release_notes.md
index afa84d524..311731fbc 100644
--- a/release_notes.md
+++ b/release_notes.md
@@ -1,6 +1,7 @@
###In Development
- [#381](https://github.com/MehdiK/Humanizer/pull/381): Fixes trailing question mark reported in #378.
- [#382](https://github.com/MehdiK/Humanizer/pull/382): Fix 90000th and -thousandth in RussianNumberToWordsConverter.
+ - [#384](https://github.com/MehdiK/Humanizer/pull/384): Added maximum TimeUnit to TimeSpanHumanizeExtensions.Humanize to enable prevention of rolling up the next largest unit of time.
[Commits](https://github.com/MehdiK/Humanizer/compare/v1.33.7...master)
diff --git a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt
index 955bf1c34..17307cfd5 100644
--- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt
+++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt
@@ -468,8 +468,8 @@ public class StringHumanizeExtensions
public class TimeSpanHumanizeExtensions
{
- public string Humanize(System.TimeSpan timeSpan, int precision, System.Globalization.CultureInfo culture) { }
- public string Humanize(System.TimeSpan timeSpan, int precision, bool countEmptyUnits, System.Globalization.CultureInfo culture) { }
+ public string Humanize(System.TimeSpan timeSpan, int precision, System.Globalization.CultureInfo culture, Humanizer.Localisation.TimeUnit maxUnit) { }
+ public string Humanize(System.TimeSpan timeSpan, int precision, bool countEmptyUnits, System.Globalization.CultureInfo culture, Humanizer.Localisation.TimeUnit maxUnit) { }
}
public class To
diff --git a/src/Humanizer.Tests/TimeSpanHumanizeTests.cs b/src/Humanizer.Tests/TimeSpanHumanizeTests.cs
index 345dbd5c2..9108b5a4a 100644
--- a/src/Humanizer.Tests/TimeSpanHumanizeTests.cs
+++ b/src/Humanizer.Tests/TimeSpanHumanizeTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using Humanizer.Localisation;
using Xunit;
using Xunit.Extensions;
@@ -68,6 +69,18 @@ public void Milliseconds(int ms, string expected)
Assert.Equal(expected, actual);
}
+ [Theory]
+ [InlineData(7 * 24 * 60 * 60 * 1000, "7 days", TimeUnit.Day)]
+ [InlineData(24 * 60 * 60 * 1000, "24 hours", TimeUnit.Hour)]
+ [InlineData(60 * 60 * 1000, "60 minutes", TimeUnit.Minute)]
+ [InlineData(60 * 1000, "60 seconds", TimeUnit.Second)]
+ [InlineData(1000, "1000 milliseconds", TimeUnit.Millisecond)]
+ public void TimeSpanWithMaxTimeUnit(int ms, string expected, TimeUnit maxUnit)
+ {
+ var actual = TimeSpan.FromMilliseconds(ms).Humanize(maxUnit: maxUnit);
+ Assert.Equal(expected, actual);
+ }
+
[Theory]
[InlineData(0, 3, "no time")]
[InlineData(0, 2, "no time")]
diff --git a/src/Humanizer/TimeSpanHumanizeExtensions.cs b/src/Humanizer/TimeSpanHumanizeExtensions.cs
index 6914b9cef..d9fb7d41c 100644
--- a/src/Humanizer/TimeSpanHumanizeExtensions.cs
+++ b/src/Humanizer/TimeSpanHumanizeExtensions.cs
@@ -19,10 +19,11 @@ public static class TimeSpanHumanizeExtensions
///
/// The maximum number of time units to return. Defaulted is 1 which means the largest unit is returned
/// Culture to use. If null, current thread's UI culture is used.
+ /// The maximum unit of time to output.
///
- public static string Humanize(this TimeSpan timeSpan, int precision = 1, CultureInfo culture = null)
+ public static string Humanize(this TimeSpan timeSpan, int precision = 1, CultureInfo culture = null, TimeUnit maxUnit = TimeUnit.Week)
{
- return Humanize(timeSpan, precision, false, culture);
+ return Humanize(timeSpan, precision, false, culture, maxUnit);
}
///
@@ -32,10 +33,11 @@ public static string Humanize(this TimeSpan timeSpan, int precision = 1, Culture
/// The maximum number of time units to return.
/// Controls whether empty time units should be counted towards maximum number of time units. Leading empty time units never count.
/// Culture to use. If null, current thread's UI culture is used.
+ /// The maximum unit of time to output.
///
- public static string Humanize(this TimeSpan timeSpan, int precision, bool countEmptyUnits, CultureInfo culture = null)
+ public static string Humanize(this TimeSpan timeSpan, int precision, bool countEmptyUnits, CultureInfo culture = null, TimeUnit maxUnit = TimeUnit.Week)
{
- var timeParts = GetTimeParts(timeSpan, culture);
+ var timeParts = GetTimeParts(timeSpan, culture, maxUnit);
if (!countEmptyUnits)
timeParts = timeParts.Where(x => x != null);
timeParts = timeParts.Take(precision);
@@ -44,27 +46,27 @@ public static string Humanize(this TimeSpan timeSpan, int precision, bool countE
return string.Join(", ", timeParts);
}
- private static IEnumerable GetTimeParts(TimeSpan timespan, CultureInfo culture)
+ private static IEnumerable GetTimeParts(TimeSpan timespan, CultureInfo culture, TimeUnit maxUnit)
{
- var weeks = timespan.Days / 7;
- var daysInWeek = timespan.Days % 7;
- var hours = timespan.Hours;
- var minutes = timespan.Minutes;
- var seconds = timespan.Seconds;
- var milliseconds = timespan.Milliseconds;
+ var weeks = maxUnit > TimeUnit.Day ? timespan.Days / 7 : 0;
+ var days = maxUnit > TimeUnit.Day ? timespan.Days % 7 : (int)timespan.TotalDays;
+ var hours = maxUnit > TimeUnit.Hour ? timespan.Hours : (int)timespan.TotalHours;
+ var minutes = maxUnit > TimeUnit.Minute ? timespan.Minutes : (int)timespan.TotalMinutes;
+ var seconds = maxUnit > TimeUnit.Second ? timespan.Seconds : (int)timespan.TotalSeconds;
+ var milliseconds = maxUnit > TimeUnit.Millisecond ? timespan.Milliseconds : (int)timespan.TotalMilliseconds;
- var outputWeeks = weeks > 0;
- var outputDays = outputWeeks || daysInWeek > 0;
- var outputHours = outputDays || hours > 0;
- var outputMinutes = outputHours || minutes > 0;
- var outputSeconds = outputMinutes || seconds > 0;
- var outputMilliseconds = outputSeconds || milliseconds > 0;
+ var outputWeeks = weeks > 0 && maxUnit == TimeUnit.Week;
+ var outputDays = (outputWeeks || days > 0) && maxUnit >= TimeUnit.Day;
+ var outputHours = (outputDays || hours > 0) && maxUnit >= TimeUnit.Hour;
+ var outputMinutes = (outputHours || minutes > 0) && maxUnit >= TimeUnit.Minute;
+ var outputSeconds = (outputMinutes || seconds > 0) && maxUnit >= TimeUnit.Second;
+ var outputMilliseconds = (outputSeconds || milliseconds > 0) && maxUnit >= TimeUnit.Millisecond;
var formatter = Configurator.GetFormatter(culture);
if (outputWeeks)
yield return GetTimePart(formatter, TimeUnit.Week, weeks);
if (outputDays)
- yield return GetTimePart(formatter, TimeUnit.Day, daysInWeek);
+ yield return GetTimePart(formatter, TimeUnit.Day, days);
if (outputHours)
yield return GetTimePart(formatter, TimeUnit.Hour, hours);
if (outputMinutes)