Skip to content

Commit

Permalink
Merge pull request #1134 from strobelt/clocktime-timeonly-strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
clairernovotny committed Nov 11, 2021
2 parents 82f2fcf + fceb939 commit 18167e5
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 0 deletions.
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

0 comments on commit 18167e5

Please sign in to comment.