diff --git a/readme.md b/readme.md
index 79ab0e674..c594e2e61 100644
--- a/readme.md
+++ b/readme.md
@@ -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)
@@ -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.
+### TimeOnly to Clock Notation
+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"
+```
+
### Roman numerals
Humanizer can change numbers to Roman numerals using the `ToRoman` extension. The numbers 1 to 10 can be expressed in Roman numerals as follows:
diff --git a/src/Humanizer.Tests.Shared/Humanizer.Tests.Shared.projitems b/src/Humanizer.Tests.Shared/Humanizer.Tests.Shared.projitems
index d2a2bcb77..f67af31f6 100644
--- a/src/Humanizer.Tests.Shared/Humanizer.Tests.Shared.projitems
+++ b/src/Humanizer.Tests.Shared/Humanizer.Tests.Shared.projitems
@@ -25,6 +25,7 @@
+
@@ -36,6 +37,7 @@
+
diff --git a/src/Humanizer.Tests.Shared/Localisation/en/TimeToClockNotationTests.cs b/src/Humanizer.Tests.Shared/Localisation/en/TimeToClockNotationTests.cs
new file mode 100644
index 000000000..9b19d0669
--- /dev/null
+++ b/src/Humanizer.Tests.Shared/Localisation/en/TimeToClockNotationTests.cs
@@ -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
diff --git a/src/Humanizer.Tests.Shared/Localisation/pt-BR/TimeToClockNotationTests.cs b/src/Humanizer.Tests.Shared/Localisation/pt-BR/TimeToClockNotationTests.cs
new file mode 100644
index 000000000..d232338bc
--- /dev/null
+++ b/src/Humanizer.Tests.Shared/Localisation/pt-BR/TimeToClockNotationTests.cs
@@ -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
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 cfc0bca08..5d2374c2a 100644
--- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt
+++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt
@@ -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(this System.Collections.Generic.IEnumerable collection) { }
@@ -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) { }
@@ -1736,6 +1745,7 @@ namespace Humanizer.Configuration
public static Humanizer.Configuration.LocaliserRegistry NumberToWordsConverters { get; }
public static Humanizer.Configuration.LocaliserRegistry Ordinalizers { get; }
public static Humanizer.DateTimeHumanizeStrategy.ITimeOnlyHumanizeStrategy TimeOnlyHumanizeStrategy { get; set; }
+ public static Humanizer.Configuration.LocaliserRegistry TimeOnlyToClockNotationConverters { get; }
}
public class LocaliserRegistry
where TLocaliser : class
@@ -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);
+ }
}
\ No newline at end of file
diff --git a/src/Humanizer/ClockNotationRounding.cs b/src/Humanizer/ClockNotationRounding.cs
new file mode 100644
index 000000000..7d9fa8f71
--- /dev/null
+++ b/src/Humanizer/ClockNotationRounding.cs
@@ -0,0 +1,8 @@
+namespace Humanizer
+{
+ public enum ClockNotationRounding
+ {
+ None,
+ NearestFiveMinutes
+ }
+}
diff --git a/src/Humanizer/Configuration/Configurator.cs b/src/Humanizer/Configuration/Configurator.cs
index f9e5dcbae..157d76865 100644
--- a/src/Humanizer/Configuration/Configurator.cs
+++ b/src/Humanizer/Configuration/Configurator.cs
@@ -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
{
@@ -70,6 +73,15 @@ public static LocaliserRegistry DateOnlyToOrdin
{
get { return _dateOnlyToOrdinalWordConverters; }
}
+
+ private static readonly LocaliserRegistry _timeOnlyToClockNotationConverters = new TimeOnlyToClockNotationConvertersRegistry();
+ ///
+ /// A registry of time to clock notation converters used to localise ToClockNotation methods
+ ///
+ public static LocaliserRegistry TimeOnlyToClockNotationConverters
+ {
+ get { return _timeOnlyToClockNotationConverters; }
+ }
#endif
internal static ICollectionFormatter CollectionFormatter
@@ -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();
diff --git a/src/Humanizer/Configuration/TimeOnlyToClockNotationConvertersRegistry.cs b/src/Humanizer/Configuration/TimeOnlyToClockNotationConvertersRegistry.cs
new file mode 100644
index 000000000..07d435e29
--- /dev/null
+++ b/src/Humanizer/Configuration/TimeOnlyToClockNotationConvertersRegistry.cs
@@ -0,0 +1,16 @@
+#if NET6_0_OR_GREATER
+
+using Humanizer.Localisation.TimeToClockNotation;
+
+namespace Humanizer.Configuration
+{
+ internal class TimeOnlyToClockNotationConvertersRegistry : LocaliserRegistry
+ {
+ public TimeOnlyToClockNotationConvertersRegistry() : base(new DefaultTimeOnlyToClockNotationConverter())
+ {
+ Register("pt-BR", new BrazilianPortugueseTimeOnlyToClockNotationConverter());
+ }
+ }
+}
+
+#endif
diff --git a/src/Humanizer/Localisation/TimeToClockNotation/BrazilianPortugueseTimeOnlyToClockNotationConverter.cs b/src/Humanizer/Localisation/TimeToClockNotation/BrazilianPortugueseTimeOnlyToClockNotationConverter.cs
new file mode 100644
index 000000000..d295d808b
--- /dev/null
+++ b/src/Humanizer/Localisation/TimeToClockNotation/BrazilianPortugueseTimeOnlyToClockNotationConverter.cs
@@ -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
diff --git a/src/Humanizer/Localisation/TimeToClockNotation/DefaultTimeOnlyToClockNotationConverter.cs b/src/Humanizer/Localisation/TimeToClockNotation/DefaultTimeOnlyToClockNotationConverter.cs
new file mode 100644
index 000000000..b01954dca
--- /dev/null
+++ b/src/Humanizer/Localisation/TimeToClockNotation/DefaultTimeOnlyToClockNotationConverter.cs
@@ -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
diff --git a/src/Humanizer/Localisation/TimeToClockNotation/ITimeOnlyToClockNotationConverter.cs b/src/Humanizer/Localisation/TimeToClockNotation/ITimeOnlyToClockNotationConverter.cs
new file mode 100644
index 000000000..1920ebb72
--- /dev/null
+++ b/src/Humanizer/Localisation/TimeToClockNotation/ITimeOnlyToClockNotationConverter.cs
@@ -0,0 +1,24 @@
+#if NET6_0_OR_GREATER
+
+using System;
+
+using Humanizer;
+
+namespace Humanizer.Localisation.TimeToClockNotation
+{
+ ///
+ /// The interface used to localise the ToClockNotation method.
+ ///
+ public interface ITimeOnlyToClockNotationConverter
+ {
+ ///
+ /// Converts the time to Clock Notation
+ ///
+ ///
+ ///
+ ///
+ string Convert(TimeOnly time, ClockNotationRounding roundToNearestFive);
+ }
+}
+
+#endif
diff --git a/src/Humanizer/TimeOnlyToClockNotationExtensions.cs b/src/Humanizer/TimeOnlyToClockNotationExtensions.cs
new file mode 100644
index 000000000..aaa501b0a
--- /dev/null
+++ b/src/Humanizer/TimeOnlyToClockNotationExtensions.cs
@@ -0,0 +1,29 @@
+#if NET6_0_OR_GREATER
+
+using System;
+
+using Humanizer;
+using Humanizer.Configuration;
+using Humanizer.Localisation.TimeToClockNotation;
+
+namespace Humanizer
+{
+ ///
+ /// Humanizes TimeOnly into human readable sentence
+ ///
+ public static class TimeOnlyToClockNotationExtensions
+ {
+ ///
+ /// Turns the provided time into clock notation
+ ///
+ /// The time to be made into clock notation
+ /// Whether to round the minutes to the nearest five or not
+ /// The time in clock notation
+ public static string ToClockNotation(this TimeOnly input, ClockNotationRounding roundToNearestFive = ClockNotationRounding.None)
+ {
+ return Configurator.TimeOnlyToClockNotationConverter.Convert(input, roundToNearestFive);
+ }
+ }
+}
+
+#endif