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 TimeSpan minUnit option #388 #403

Merged
merged 2 commits into from
May 24, 2015
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
6 changes: 6 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,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 minimum unit of time may be specified to avoid rolling down to a smaller unit. For example:
```C#
TimeSpan.FromMilliseconds(122500).Humanize(minUnit: TimeUnit.Second) => "2 minutes, 2 seconds" // instead of 2 minutes, 2 seconds, 500 milliseconds
TimeSpan.FromHours(25).Humanize(minUnit: TimeUnit.Day) => "1 Day" //instead of 1 Day, 1 Hour
```

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,8 +487,8 @@ public class StringHumanizeExtensions

public class TimeSpanHumanizeExtensions
{
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 string Humanize(System.TimeSpan timeSpan, int precision, System.Globalization.CultureInfo culture, Humanizer.Localisation.TimeUnit maxUnit, Humanizer.Localisation.TimeUnit minUnit) { }
public string Humanize(System.TimeSpan timeSpan, int precision, bool countEmptyUnits, System.Globalization.CultureInfo culture, Humanizer.Localisation.TimeUnit maxUnit, Humanizer.Localisation.TimeUnit minUnit) { }
}

public class To
Expand Down
43 changes: 43 additions & 0 deletions src/Humanizer.Tests/TimeSpanHumanizeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,49 @@ public void TimeSpanWithMaxTimeUnit(int ms, string expected, TimeUnit maxUnit)
Assert.Equal(expected, actual);
}

[Theory]
[InlineData(10, "10 milliseconds", TimeUnit.Millisecond)]
[InlineData(10, "no time", TimeUnit.Second)]
[InlineData(10, "no time", TimeUnit.Minute)]
[InlineData(10, "no time", TimeUnit.Hour)]
[InlineData(10, "no time", TimeUnit.Day)]
[InlineData(10, "no time", TimeUnit.Week)]
[InlineData(2500, "2 seconds, 500 milliseconds", TimeUnit.Millisecond)]
[InlineData(2500, "2 seconds", TimeUnit.Second)]
[InlineData(2500, "no time", TimeUnit.Minute)]
[InlineData(2500, "no time", TimeUnit.Hour)]
[InlineData(2500, "no time", TimeUnit.Day)]
[InlineData(2500, "no time", TimeUnit.Week)]
[InlineData(122500, "2 minutes, 2 seconds, 500 milliseconds", TimeUnit.Millisecond)]
[InlineData(122500, "2 minutes, 2 seconds", TimeUnit.Second)]
[InlineData(122500, "2 minutes", TimeUnit.Minute)]
[InlineData(122500, "no time", TimeUnit.Hour)]
[InlineData(122500, "no time", TimeUnit.Day)]
[InlineData(122500, "no time", TimeUnit.Week)]
[InlineData(3722500, "1 hour, 2 minutes, 2 seconds, 500 milliseconds", TimeUnit.Millisecond)]
[InlineData(3722500, "1 hour, 2 minutes, 2 seconds", TimeUnit.Second)]
[InlineData(3722500, "1 hour, 2 minutes", TimeUnit.Minute)]
[InlineData(3722500, "1 hour", TimeUnit.Hour)]
[InlineData(3722500, "no time", TimeUnit.Day)]
[InlineData(3722500, "no time", TimeUnit.Week)]
[InlineData(90122500, "1 day, 1 hour, 2 minutes, 2 seconds, 500 milliseconds", TimeUnit.Millisecond)]
[InlineData(90122500, "1 day, 1 hour, 2 minutes, 2 seconds", TimeUnit.Second)]
[InlineData(90122500, "1 day, 1 hour, 2 minutes", TimeUnit.Minute)]
[InlineData(90122500, "1 day, 1 hour", TimeUnit.Hour)]
[InlineData(90122500, "1 day", TimeUnit.Day)]
[InlineData(90122500, "no time", TimeUnit.Week)]
[InlineData(694922500, "1 week, 1 day, 1 hour, 2 minutes, 2 seconds, 500 milliseconds", TimeUnit.Millisecond)]
[InlineData(694922500, "1 week, 1 day, 1 hour, 2 minutes, 2 seconds", TimeUnit.Second)]
[InlineData(694922500, "1 week, 1 day, 1 hour, 2 minutes", TimeUnit.Minute)]
[InlineData(694922500, "1 week, 1 day, 1 hour", TimeUnit.Hour)]
[InlineData(694922500, "1 week, 1 day", TimeUnit.Day)]
[InlineData(694922500, "1 week", TimeUnit.Week)]
public void TimeSpanWithMinTimeUnit(int ms, string expected, TimeUnit minUnit)
{
var actual = TimeSpan.FromMilliseconds(ms).Humanize(minUnit: minUnit, precision: 6);
Assert.Equal(expected, actual);
}

[Theory]
[InlineData(0, 3, "no time")]
[InlineData(0, 2, "no time")]
Expand Down
182 changes: 141 additions & 41 deletions src/Humanizer/TimeSpanHumanizeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@ namespace Humanizer
/// </summary>
public static class TimeSpanHumanizeExtensions
{
private const int _lastTimeUnitTypeIndexImplemented = (int)TimeUnit.Week;
private const int _daysInAWeek = 7;

/// <summary>
/// Turns a TimeSpan into a human readable form. E.g. 1 day.
/// </summary>
/// <param name="timeSpan"></param>
/// <param name="precision">The maximum number of time units to return. Defaulted is 1 which means the largest unit is returned</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.</param>
/// <param name="minUnit">The minimum unit of time to output.</param>
/// <returns></returns>
public static string Humanize(this TimeSpan timeSpan, int precision = 1, CultureInfo culture = null, TimeUnit maxUnit = TimeUnit.Week)
public static string Humanize(this TimeSpan timeSpan, int precision = 1, CultureInfo culture = null, TimeUnit maxUnit = TimeUnit.Week, TimeUnit minUnit = TimeUnit.Millisecond)
{
return Humanize(timeSpan, precision, false, culture, maxUnit);
return Humanize(timeSpan, precision, false, culture, maxUnit, minUnit);
}

/// <summary>
Expand All @@ -34,56 +38,152 @@ public static string Humanize(this TimeSpan timeSpan, int precision = 1, Culture
/// <param name="countEmptyUnits">Controls whether empty time units should be counted towards maximum number of time units. Leading empty time units never count.</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.</param>
/// <param name="minUnit">The minimum unit of time to output.</param>
/// <returns></returns>
public static string Humanize(this TimeSpan timeSpan, int precision, bool countEmptyUnits, CultureInfo culture = null, TimeUnit maxUnit = TimeUnit.Week)
public static string Humanize(this TimeSpan timeSpan, int precision, bool countEmptyUnits, CultureInfo culture = null, TimeUnit maxUnit = TimeUnit.Week, TimeUnit minUnit = TimeUnit.Millisecond)
{
IEnumerable<string> timeParts = CreateTheTimePartsWithUperAndLowerLimits(timeSpan, culture, maxUnit, minUnit);
timeParts = SetPrecisionOfTimeSpan(timeParts, precision, countEmptyUnits);

return ConcatenateTimeSpanParts(timeParts);
}

private static IEnumerable<string> CreateTheTimePartsWithUperAndLowerLimits(TimeSpan timespan, CultureInfo culture, TimeUnit maxUnit, TimeUnit minUnit)
{
var cultureFormatter = Configurator.GetFormatter(culture);
bool firstValueFound = false;
IEnumerable<TimeUnit> timeUnitsEnumTypes = GetEnumTypesForTimeUnit();
List<string> timeParts = new List<string>();

foreach (var timeUnitType in timeUnitsEnumTypes)
{
var timepart = GetTimeUnitPart(timeUnitType, timespan, culture, maxUnit, minUnit, cultureFormatter);

if (timepart != null || firstValueFound)
{
firstValueFound = true;
timeParts.Add(timepart);
}
}
if (IsContainingOnlyNullValue(timeParts))
{
string noTimeValueCultureFarmated = cultureFormatter.TimeSpanHumanize_Zero();
timeParts = CreateTimePartsWithNoTimeValue(noTimeValueCultureFarmated);
}
return timeParts;
}

private static IEnumerable<TimeUnit> GetEnumTypesForTimeUnit()
{
IEnumerable<TimeUnit> enumTypeEnumerator = (IEnumerable<TimeUnit>)Enum.GetValues(typeof(TimeUnit));
enumTypeEnumerator = enumTypeEnumerator.Take(_lastTimeUnitTypeIndexImplemented + 1);

return enumTypeEnumerator.Reverse();
}

private static string GetTimeUnitPart(TimeUnit timeUnitToGet, TimeSpan timespan, CultureInfo culture, TimeUnit maximumTimeUnit, TimeUnit minimumTimeUnit, IFormatter cultureFormatter)
{
if(timeUnitToGet <= maximumTimeUnit && timeUnitToGet >= minimumTimeUnit)
{
var isTimeUnitToGetTheMaximumTimeUnit = (timeUnitToGet == maximumTimeUnit);
var numberOfTimeUnits = GetTimeUnitNumericalValue(timeUnitToGet, timespan, isTimeUnitToGetTheMaximumTimeUnit);
return BuildFormatTimePart(cultureFormatter, timeUnitToGet, numberOfTimeUnits);
}
return null;
}

private static int GetTimeUnitNumericalValue(TimeUnit timeUnitToGet, TimeSpan timespan, bool isTimeUnitToGetTheMaximumTimeUnit)
{
switch (timeUnitToGet)
{
case TimeUnit.Millisecond:
return GetNormalCaseTimeAsInteger(timespan.Milliseconds, timespan.TotalMilliseconds, isTimeUnitToGetTheMaximumTimeUnit);
case TimeUnit.Second:
return GetNormalCaseTimeAsInteger(timespan.Seconds, timespan.TotalSeconds, isTimeUnitToGetTheMaximumTimeUnit);
case TimeUnit.Minute:
return GetNormalCaseTimeAsInteger(timespan.Minutes, timespan.TotalMinutes, isTimeUnitToGetTheMaximumTimeUnit);
case TimeUnit.Hour:
return GetNormalCaseTimeAsInteger(timespan.Hours, timespan.TotalHours, isTimeUnitToGetTheMaximumTimeUnit);
case TimeUnit.Day:
return GetSpecialCaseDaysAsInteger(timespan, isTimeUnitToGetTheMaximumTimeUnit);
case TimeUnit.Week:
return GetSpecialCaseWeeksAsInteger(timespan, isTimeUnitToGetTheMaximumTimeUnit);
case TimeUnit.Month:
// To be implemented
case TimeUnit.Year:
// To be implemented
default:
return 0;
}
}

private static int GetSpecialCaseWeeksAsInteger(TimeSpan timespan, bool isTimeUnitToGetTheMaximumTimeUnit)
{
if (isTimeUnitToGetTheMaximumTimeUnit)
{
return timespan.Days / _daysInAWeek;
}
// To be implemented with the implementation of Month and Year
return 0;
}

private static int GetSpecialCaseDaysAsInteger(TimeSpan timespan, bool isTimeUnitToGetTheMaximumTimeUnit)
{
if (isTimeUnitToGetTheMaximumTimeUnit)
{
return timespan.Days;
}
return timespan.Days % _daysInAWeek;
}

private static int GetNormalCaseTimeAsInteger(int timeNumberOfUnits, double totalTimeNumberOfUnits, bool isTimeUnitToGetTheMaximumTimeUnit)
{
if (isTimeUnitToGetTheMaximumTimeUnit)
{
try
{
return Convert.ToInt32(totalTimeNumberOfUnits);
}
catch
{
//To be implemented so that TimeSpanHumanize method accepts double type as unit
return 0;
}
}
return timeNumberOfUnits;
}

private static string BuildFormatTimePart(IFormatter cultureFormatter, TimeUnit timeUnitType, int amountOfTimeUnits)
{
return amountOfTimeUnits != 0
? cultureFormatter.TimeSpanHumanize(timeUnitType, amountOfTimeUnits)
: null;
}

private static List<string> CreateTimePartsWithNoTimeValue(string noTimeValue)
{
return new List<string>() { noTimeValue };
}

private static bool IsContainingOnlyNullValue(IEnumerable<string> timeParts)
{
return (timeParts.Count(x => x != null) == 0);
}

private static IEnumerable<string> SetPrecisionOfTimeSpan(IEnumerable<string> timeParts, int precision, bool countEmptyUnits)
{
var timeParts = GetTimeParts(timeSpan, culture, maxUnit);
if (!countEmptyUnits)
timeParts = timeParts.Where(x => x != null);
timeParts = timeParts.Take(precision);
if (countEmptyUnits)
timeParts = timeParts.Where(x => x != null);
return string.Join(", ", timeParts);
}

private static IEnumerable<string> GetTimeParts(TimeSpan timespan, CultureInfo culture, TimeUnit maxUnit)
{
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 && 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, days);
if (outputHours)
yield return GetTimePart(formatter, TimeUnit.Hour, hours);
if (outputMinutes)
yield return GetTimePart(formatter, TimeUnit.Minute, minutes);
if (outputSeconds)
yield return GetTimePart(formatter, TimeUnit.Second, seconds);
if (outputMilliseconds)
yield return GetTimePart(formatter, TimeUnit.Millisecond, milliseconds);
else
yield return formatter.TimeSpanHumanize_Zero();
return timeParts;
}

private static string GetTimePart(IFormatter formatter, TimeUnit timeUnit, int unit)
private static string ConcatenateTimeSpanParts(IEnumerable<string> timeSpanParts)
{
return unit != 0
? formatter.TimeSpanHumanize(timeUnit, unit)
: null;
return string.Join(", ", timeSpanParts);
}
}
}