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

Add support to humanizing a TimeOnly as a readable clock notation #1134

Merged
merged 12 commits into from Nov 11, 2021
15 changes: 15 additions & 0 deletions readme.md
Expand Up @@ -39,6 +39,7 @@ Humanizer meets all your .NET needs for manipulating and displaying strings, enu
- [Number to words](#number-to-words)
- [Number to ordinal words](#number-to-ordinal-words)
- [DateTime to ordinal words](#date-time-to-ordinal-words)
- [TimeOnly to Clock Notation](#time-only-to-clock-notation)
- [Roman numerals](#roman-numerals)
- [Metric numerals](#metric-numerals)
- [ByteSize](#bytesize)
Expand Down Expand Up @@ -864,6 +865,20 @@ The possible values are `GrammaticalCase.Nominative`, `GrammaticalCase.Genitive`

Obviously this only applies to some cultures. For others passing case in doesn't make any difference in the result.

### <a id="time-only-to-clock-notation">TimeOnly to Clock Notation</a>
Extends TimeOnly to allow humanizing it to a clock notation
```C#
// for English US locale
new TimeOnly(3, 0).ToClockNotation() => "three o'clock"
new TimeOnly(12, 0).ToClockNotation() => "noon"
new TimeOnly(14, 30).ToClockNotation() => "half past two"

// for Brazilian Portuguese locale
new TimeOnly(3, 0).ToClockNotation() => "três em ponto"
new TimeOnly(12, 0).ToClockNotation() => "meio-dia"
new TimeOnly(14, 30).ToClockNotation() => "duas e meia"
```

### <a id="roman-numerals">Roman numerals</a>
Humanizer can change numbers to Roman numerals using the `ToRoman` extension. The numbers 1 to 10 can be expressed in Roman numerals as follows:

Expand Down
2 changes: 2 additions & 0 deletions src/Humanizer.Tests.Shared/Humanizer.Tests.Shared.projitems
Expand Up @@ -25,6 +25,7 @@
<Compile Include="$(MSBuildThisFileDirectory)DateHumanizeDefaultStrategyTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)FluentDate\InDateTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)FluentDate\OnDateTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Localisation\en\TimeToClockNotationTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Localisation\is\Bytes\ByteSizeExtensionsTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Localisation\is\Bytes\ToFullWordsTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Localisation\is\Bytes\ToStringTests.cs" />
Expand All @@ -36,6 +37,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Localisation\is\ResourcesTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Localisation\is\TimeOnlyHumanizeTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Localisation\is\TimeSpanHumanizeTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Localisation\pt-BR\TimeToClockNotationTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TimeOnlyHumanizeTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DateTimeHumanizePrecisionStrategyTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DateOnlyHumanizeTests.cs" />
Expand Down
@@ -0,0 +1,63 @@
#if NET6_0_OR_GREATER

using System;
using Xunit;
using Humanizer.Localisation.TimeToClockNotation;
using Humanizer;

namespace Humanizer.Tests.Localisation.en
{
[UseCulture("en")]
public class TimeToClockNotationTests
{
[Theory]
[InlineData(00, 00, "midnight")]
[InlineData(04, 00, "four o'clock")]
[InlineData(05, 01, "five one")]
[InlineData(06, 05, "five past six")]
[InlineData(07, 10, "ten past seven")]
[InlineData(08, 15, "a quarter past eight")]
[InlineData(09, 20, "twenty past nine")]
[InlineData(10, 25, "twenty-five past ten")]
[InlineData(11, 30, "half past eleven")]
[InlineData(12, 00, "noon")]
[InlineData(15, 35, "three thirty-five")]
[InlineData(16, 40, "twenty to five")]
[InlineData(17, 45, "a quarter to six")]
[InlineData(18, 50, "ten to seven")]
[InlineData(19, 55, "five to eight")]
[InlineData(20, 59, "eight fifty-nine")]
public void ConvertToClockNotationTimeOnlyStringEnUs(int hours, int minutes, string expectedResult)
{
var actualResult = new TimeOnly(hours, minutes).ToClockNotation();
Assert.Equal(expectedResult, actualResult);
}

[Theory]
[InlineData(00, 00, "midnight")]
[InlineData(04, 00, "four o'clock")]
[InlineData(05, 01, "five o'clock")]
[InlineData(06, 05, "five past six")]
[InlineData(07, 10, "ten past seven")]
[InlineData(08, 15, "a quarter past eight")]
[InlineData(09, 20, "twenty past nine")]
[InlineData(10, 25, "twenty-five past ten")]
[InlineData(11, 30, "half past eleven")]
[InlineData(12, 00, "noon")]
[InlineData(13, 23, "twenty-five past one")]
[InlineData(14, 32, "half past two")]
[InlineData(15, 35, "three thirty-five")]
[InlineData(16, 40, "twenty to five")]
[InlineData(17, 45, "a quarter to six")]
[InlineData(18, 50, "ten to seven")]
[InlineData(19, 55, "five to eight")]
[InlineData(20, 59, "nine o'clock")]
public void ConvertToRoundedClockNotationTimeOnlyStringEnUs(int hours, int minutes, string expectedResult)
{
var actualResult = new TimeOnly(hours, minutes).ToClockNotation(ClockNotationRounding.NearestFiveMinutes);
Assert.Equal(expectedResult, actualResult);
}
}
}

#endif
@@ -0,0 +1,63 @@
#if NET6_0_OR_GREATER

using System;
using Xunit;
using Humanizer.Localisation.TimeToClockNotation;
using Humanizer;

namespace Humanizer.Tests.Localisation.ptBR
{
[UseCulture("pt-BR")]
public class TimeToClockNotationTests
{
[Theory]
[InlineData(00, 00, "meia-noite")]
[InlineData(04, 00, "quatro em ponto")]
[InlineData(05, 01, "cinco e um")]
[InlineData(06, 05, "seis e cinco")]
[InlineData(07, 10, "sete e dez")]
[InlineData(08, 15, "oito e quinze")]
[InlineData(09, 20, "nove e vinte")]
[InlineData(10, 25, "dez e vinte e cinco")]
[InlineData(11, 30, "onze e meia")]
[InlineData(12, 00, "meio-dia")]
[InlineData(15, 35, "três e trinta e cinco")]
[InlineData(16, 40, "vinte para as cinco")]
[InlineData(17, 45, "quinze para as seis")]
[InlineData(18, 50, "dez para as sete")]
[InlineData(19, 55, "cinco para as oito")]
[InlineData(20, 59, "oito e cinquenta e nove")]
public void ConvertToClockNotationTimeOnlyStringPtBr(int hours, int minutes, string expectedResult)
{
var actualResult = new TimeOnly(hours, minutes).ToClockNotation();
Assert.Equal(expectedResult, actualResult);
}

[Theory]
[InlineData(00, 00, "meia-noite")]
[InlineData(04, 00, "quatro em ponto")]
[InlineData(05, 01, "cinco em ponto")]
[InlineData(06, 05, "seis e cinco")]
[InlineData(07, 10, "sete e dez")]
[InlineData(08, 15, "oito e quinze")]
[InlineData(09, 20, "nove e vinte")]
[InlineData(10, 25, "dez e vinte e cinco")]
[InlineData(11, 30, "onze e meia")]
[InlineData(12, 00, "meio-dia")]
[InlineData(13, 23, "uma e vinte e cinco")]
[InlineData(14, 32, "duas e meia")]
[InlineData(15, 35, "três e trinta e cinco")]
[InlineData(16, 40, "vinte para as cinco")]
[InlineData(17, 45, "quinze para as seis")]
[InlineData(18, 50, "dez para as sete")]
[InlineData(19, 55, "cinco para as oito")]
[InlineData(20, 59, "nove em ponto")]
public void ConvertToRoundedClockNotationTimeOnlyStringPtBr(int hours, int minutes, string expectedResult)
{
var actualResult = new TimeOnly(hours, minutes).ToClockNotation(ClockNotationRounding.NearestFiveMinutes);
Assert.Equal(expectedResult, actualResult);
}
}
}

#endif
Expand Up @@ -60,6 +60,11 @@ namespace Humanizer
{
public static string ApplyCase(this string input, Humanizer.LetterCasing casing) { }
}
public enum ClockNotationRounding
{
None = 0,
NearestFiveMinutes = 1,
}
public class static CollectionHumanizeExtensions
{
public static string Humanize<T>(this System.Collections.Generic.IEnumerable<T> collection) { }
Expand Down Expand Up @@ -1581,6 +1586,10 @@ namespace Humanizer
public static string Humanize(this string input) { }
public static string Humanize(this string input, Humanizer.LetterCasing casing) { }
}
public class static TimeOnlyToClockNotationExtensions
{
public static string ToClockNotation(this System.TimeOnly input, Humanizer.ClockNotationRounding roundToNearestFive = 0) { }
}
public class static TimeSpanHumanizeExtensions
{
public static string Humanize(this System.TimeSpan timeSpan, int precision = 1, System.Globalization.CultureInfo culture = null, Humanizer.Localisation.TimeUnit maxUnit = 5, Humanizer.Localisation.TimeUnit minUnit = 0, string collectionSeparator = ", ", bool toWords = False) { }
Expand Down Expand Up @@ -1736,6 +1745,7 @@ namespace Humanizer.Configuration
public static Humanizer.Configuration.LocaliserRegistry<Humanizer.Localisation.NumberToWords.INumberToWordsConverter> NumberToWordsConverters { get; }
public static Humanizer.Configuration.LocaliserRegistry<Humanizer.Localisation.Ordinalizers.IOrdinalizer> Ordinalizers { get; }
public static Humanizer.DateTimeHumanizeStrategy.ITimeOnlyHumanizeStrategy TimeOnlyHumanizeStrategy { get; set; }
public static Humanizer.Configuration.LocaliserRegistry<Humanizer.Localisation.TimeToClockNotation.ITimeOnlyToClockNotationConverter> TimeOnlyToClockNotationConverters { get; }
}
public class LocaliserRegistry<TLocaliser>
where TLocaliser : class
Expand Down Expand Up @@ -1945,4 +1955,11 @@ namespace Humanizer.Localisation.Ordinalizers
string Convert(int number, string numberString);
string Convert(int number, string numberString, Humanizer.GrammaticalGender gender);
}
}
namespace Humanizer.Localisation.TimeToClockNotation
{
public interface ITimeOnlyToClockNotationConverter
{
string Convert(System.TimeOnly time, Humanizer.ClockNotationRounding roundToNearestFive);
}
}
8 changes: 8 additions & 0 deletions src/Humanizer/ClockNotationRounding.cs
@@ -0,0 +1,8 @@
namespace Humanizer
{
public enum ClockNotationRounding
{
None,
NearestFiveMinutes
}
}
20 changes: 20 additions & 0 deletions src/Humanizer/Configuration/Configurator.cs
Expand Up @@ -7,6 +7,9 @@
using Humanizer.Localisation.Formatters;
using Humanizer.Localisation.NumberToWords;
using Humanizer.Localisation.Ordinalizers;
#if NET6_0_OR_GREATER
using Humanizer.Localisation.TimeToClockNotation;
#endif

namespace Humanizer.Configuration
{
Expand Down Expand Up @@ -70,6 +73,15 @@ public static LocaliserRegistry<IDateOnlyToOrdinalWordConverter> DateOnlyToOrdin
{
get { return _dateOnlyToOrdinalWordConverters; }
}

private static readonly LocaliserRegistry<ITimeOnlyToClockNotationConverter> _timeOnlyToClockNotationConverters = new TimeOnlyToClockNotationConvertersRegistry();
/// <summary>
/// A registry of time to clock notation converters used to localise ToClockNotation methods
/// </summary>
public static LocaliserRegistry<ITimeOnlyToClockNotationConverter> TimeOnlyToClockNotationConverters
{
get { return _timeOnlyToClockNotationConverters; }
}
#endif

internal static ICollectionFormatter CollectionFormatter
Expand Down Expand Up @@ -131,6 +143,14 @@ internal static IDateOnlyToOrdinalWordConverter DateOnlyToOrdinalWordsConverter
return DateOnlyToOrdinalWordsConverters.ResolveForUiCulture();
}
}

internal static ITimeOnlyToClockNotationConverter TimeOnlyToClockNotationConverter
{
get
{
return TimeOnlyToClockNotationConverters.ResolveForUiCulture();
}
}
#endif

private static IDateTimeHumanizeStrategy _dateTimeHumanizeStrategy = new DefaultDateTimeHumanizeStrategy();
Expand Down
@@ -0,0 +1,16 @@
#if NET6_0_OR_GREATER

using Humanizer.Localisation.TimeToClockNotation;

namespace Humanizer.Configuration
{
internal class TimeOnlyToClockNotationConvertersRegistry : LocaliserRegistry<ITimeOnlyToClockNotationConverter>
{
public TimeOnlyToClockNotationConvertersRegistry() : base(new DefaultTimeOnlyToClockNotationConverter())
{
Register("pt-BR", new BrazilianPortugueseTimeOnlyToClockNotationConverter());
}
}
}

#endif
@@ -0,0 +1,41 @@
#if NET6_0_OR_GREATER

using System;

using Humanizer;

namespace Humanizer.Localisation.TimeToClockNotation
{
internal class BrazilianPortugueseTimeOnlyToClockNotationConverter : ITimeOnlyToClockNotationConverter
{
public virtual string Convert(TimeOnly time, ClockNotationRounding roundToNearestFive)
{
switch (time)
{
case { Hour: 0, Minute: 0 }:
return "meia-noite";
case { Hour: 12, Minute: 0 }:
return "meio-dia";
}

var normalizedHour = time.Hour % 12;
var normalizedMinutes = (int)(roundToNearestFive == ClockNotationRounding.NearestFiveMinutes
? 5 * Math.Round(time.Minute / 5.0)
: time.Minute);

return normalizedMinutes switch
{
00 => $"{normalizedHour.ToWords(GrammaticalGender.Feminine)} em ponto",
30 => $"{normalizedHour.ToWords(GrammaticalGender.Feminine)} e meia",
40 => $"vinte para as {(normalizedHour + 1).ToWords(GrammaticalGender.Feminine)}",
45 => $"quinze para as {(normalizedHour + 1).ToWords(GrammaticalGender.Feminine)}",
50 => $"dez para as {(normalizedHour + 1).ToWords(GrammaticalGender.Feminine)}",
55 => $"cinco para as {(normalizedHour + 1).ToWords(GrammaticalGender.Feminine)}",
60 => $"{(normalizedHour + 1).ToWords(GrammaticalGender.Feminine)} em ponto",
_ => $"{normalizedHour.ToWords(GrammaticalGender.Feminine)} e {normalizedMinutes.ToWords()}"
};
}
}
}

#endif
@@ -0,0 +1,46 @@
#if NET6_0_OR_GREATER

using System;

using Humanizer;

namespace Humanizer.Localisation.TimeToClockNotation
{
internal class DefaultTimeOnlyToClockNotationConverter : ITimeOnlyToClockNotationConverter
{
public virtual string Convert(TimeOnly time, ClockNotationRounding roundToNearestFive)
{
switch (time)
{
case { Hour: 0, Minute: 0 }:
return "midnight";
case { Hour: 12, Minute: 0 }:
return "noon";
}

var normalizedHour = time.Hour % 12;
var normalizedMinutes = (int)(roundToNearestFive == ClockNotationRounding.NearestFiveMinutes
? 5 * Math.Round(time.Minute / 5.0)
: time.Minute);

return normalizedMinutes switch
{
00 => $"{normalizedHour.ToWords()} o'clock",
05 => $"five past {normalizedHour.ToWords()}",
10 => $"ten past {normalizedHour.ToWords()}",
15 => $"a quarter past {normalizedHour.ToWords()}",
20 => $"twenty past {normalizedHour.ToWords()}",
25 => $"twenty-five past {normalizedHour.ToWords()}",
30 => $"half past {normalizedHour.ToWords()}",
40 => $"twenty to {(normalizedHour + 1).ToWords()}",
45 => $"a quarter to {(normalizedHour + 1).ToWords()}",
50 => $"ten to {(normalizedHour + 1).ToWords()}",
55 => $"five to {(normalizedHour + 1).ToWords()}",
60 => $"{(normalizedHour + 1).ToWords()} o'clock",
_ => $"{normalizedHour.ToWords()} {normalizedMinutes.ToWords()}"
};
}
}
}

#endif
@@ -0,0 +1,24 @@
#if NET6_0_OR_GREATER

using System;

using Humanizer;

namespace Humanizer.Localisation.TimeToClockNotation
{
/// <summary>
/// The interface used to localise the ToClockNotation method.
/// </summary>
public interface ITimeOnlyToClockNotationConverter
{
/// <summary>
/// Converts the time to Clock Notation
/// </summary>
/// <param name="time"></param>
/// <param name="roundToNearestFive"></param>
/// <returns></returns>
string Convert(TimeOnly time, ClockNotationRounding roundToNearestFive);
}
}

#endif