diff --git a/readme.md b/readme.md index ecdfe5870..b974e1c5a 100644 --- a/readme.md +++ b/readme.md @@ -38,6 +38,7 @@ Humanizer meets all your .NET needs for manipulating and displaying strings, enu - [Number to Numbers](#number-to-numbers) - [Number to words](#number-to-words) - [Number to ordinal words](#number-to-ordinal-words) + - [Date to words](#date-to-words) - [DateTime to ordinal words](#date-time-to-ordinal-words) - [TimeOnly to Clock Notation](#time-only-to-clock-notation) - [Roman numerals](#roman-numerals) @@ -884,6 +885,25 @@ Passing `wordForm` argument in when it is not applicable will not make any diffe 43.ToOrdinalWords(GrammaticalGender.Masculine, WordForm.Abbreviation, new CultureInfo("en")) => "forty-third" ``` +### Date to words +This is kind of an extension of ToWords and Ordinalize +```C# +// for Spanish locale +new DateTime(2022, 1, 1).ToWords() => "uno de enero de dos mil veintidós" +new DateOnly(2020, 6, 10).ToWords() => "diez de junio de dos mil veinte" +new DateOnly(1999, 12, 31).ToWords() => "treinta y uno de diciembre de mil novecientos noventa y nueve" +// for English UK locale +new DateTime(2022, 1, 1).ToWords() => "the first of January two thousand and twenty-two" +new DateOnly(1999, 12, 31).ToWords() => "the thirty-first of December one thousand nine hundred and ninety-nine" +// for English US locale +new DateTime(2022, 1, 1).ToWords() => "January first, two thousand and twenty-two" +new DateOnly(1999, 12, 31).ToWords() => "December thirty-first, one thousand nine hundred and ninety-nine" +``` + +The ToWords method of `DateTime` or `DateOnly` also supports grammatical case. +You can pass a second argument to `ToWords` to specify the case of the output. +The possible values are `GrammaticalCase.Nominative`, `GrammaticalCase.Genitive`, `GrammaticalCase.Dative`, `GrammaticalCase.Accusative`, `GrammaticalCase.Instrumental` and `GrammaticalGender.Prepositional` + ### DateTime to ordinal words This is kind of an extension of Ordinalize ```C# diff --git a/src/Humanizer.Tests.Shared/Localisation/en/DateToWordsTests.cs b/src/Humanizer.Tests.Shared/Localisation/en/DateToWordsTests.cs new file mode 100644 index 000000000..6057ea642 --- /dev/null +++ b/src/Humanizer.Tests.Shared/Localisation/en/DateToWordsTests.cs @@ -0,0 +1,38 @@ +using System; +using Xunit; + +namespace Humanizer.Tests.Localisation.en +{ + public class DateToWordsTests + { + [UseCulture("en-GB")] + [Fact] + public void ConvertDateToWordsGbString() + { + Assert.Equal("the first of January two thousand and twenty-two", new DateTime(2022, 1, 1).ToWords()); + } + + [UseCulture("en-US")] + [Fact] + public void ConvertDateToWordsUsString() + { + Assert.Equal("January first, two thousand and twenty-two", new DateTime(2022, 1, 1).ToWords()); + } + +#if NET6_0_OR_GREATER + [UseCulture("en-GB")] + [Fact] + public void ConvertDateOnlyToWordsGbString() + { + Assert.Equal("the first of January two thousand and twenty-two", new DateOnly(2022, 1, 1).ToWords()); + } + + [UseCulture("en-US")] + [Fact] + public void ConvertDateOnlyToWordsUsString() + { + Assert.Equal("January first, two thousand and twenty-two", new DateOnly(2022, 1, 1).ToWords()); + } +#endif + } +} diff --git a/src/Humanizer.Tests.Shared/Localisation/es/DateToWordsTests.cs b/src/Humanizer.Tests.Shared/Localisation/es/DateToWordsTests.cs new file mode 100644 index 000000000..6b1054a04 --- /dev/null +++ b/src/Humanizer.Tests.Shared/Localisation/es/DateToWordsTests.cs @@ -0,0 +1,29 @@ +using System; +using Xunit; + +namespace Humanizer.Tests.Localisation.es +{ + [UseCulture("es-419")] + public class DateToWordsTests + { + [Fact] + public void ConvertDateToWordsString() + { + Assert.Equal("dos de enero de dos mil veintidós", new DateTime(2022, 1, 2).ToWords()); + Assert.Equal("diez de junio de dos mil veinte", new DateTime(2020, 6, 10).ToWords()); + Assert.Equal("veinticinco de septiembre de dos mil diecisiete", new DateTime(2017, 9, 25).ToWords()); + Assert.Equal("treinta y uno de diciembre de mil novecientos noventa y nueve", new DateTime(1999, 12, 31).ToWords()); + } + +#if NET6_0_OR_GREATER + [Fact] + public void ConvertDateOnlyToWordsString() + { + Assert.Equal("dos de enero de dos mil veintidós", new DateOnly(2022, 1, 2).ToWords()); + Assert.Equal("diez de junio de dos mil veinte", new DateOnly(2020, 6, 10).ToWords()); + Assert.Equal("veinticinco de septiembre de dos mil diecisiete", new DateOnly(2017, 9, 25).ToWords()); + Assert.Equal("treinta y uno de diciembre de mil novecientos noventa y nueve", new DateOnly(1999, 12, 31).ToWords()); + } +#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 746484da8..959693a23 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt @@ -92,6 +92,13 @@ namespace Humanizer public static string ToOrdinalWords(this System.DateOnly input) { } public static string ToOrdinalWords(this System.DateOnly input, Humanizer.GrammaticalCase grammaticalCase) { } } + public class static DateToWordsExtensions + { + public static string ToWords(this System.DateTime input) { } + public static string ToWords(this System.DateTime input, Humanizer.GrammaticalCase grammaticalCase) { } + public static string ToWords(this System.DateOnly input) { } + public static string ToWords(this System.DateOnly input, Humanizer.GrammaticalCase grammaticalCase) { } + } public class static EnglishArticle { public static string[] AppendArticlePrefix(string[] items) { } @@ -1757,9 +1764,11 @@ namespace Humanizer.Configuration public static Humanizer.Configuration.LocaliserRegistry CollectionFormatters { get; } public static Humanizer.DateTimeHumanizeStrategy.IDateOnlyHumanizeStrategy DateOnlyHumanizeStrategy { get; set; } public static Humanizer.Configuration.LocaliserRegistry DateOnlyToOrdinalWordsConverters { get; } + public static Humanizer.Configuration.LocaliserRegistry DateOnlyToWordsConverters { get; } public static Humanizer.DateTimeHumanizeStrategy.IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } public static Humanizer.DateTimeHumanizeStrategy.IDateTimeOffsetHumanizeStrategy DateTimeOffsetHumanizeStrategy { get; set; } public static Humanizer.Configuration.LocaliserRegistry DateToOrdinalWordsConverters { get; } + public static Humanizer.Configuration.LocaliserRegistry DateToWordsConverters { get; } public static System.Func EnumDescriptionPropertyLocator { get; set; } public static Humanizer.Configuration.LocaliserRegistry Formatters { get; } public static Humanizer.Configuration.LocaliserRegistry NumberToWordsConverters { get; } @@ -1928,6 +1937,19 @@ namespace Humanizer.Localisation.DateToOrdinalWords string Convert(System.DateTime date, Humanizer.GrammaticalCase grammaticalCase); } } +namespace Humanizer.Localisation.DateToWords +{ + public interface IDateOnlyToWordConverter + { + string Convert(System.DateOnly date); + string Convert(System.DateOnly date, Humanizer.GrammaticalCase grammaticalCase); + } + public interface IDateToWordConverter + { + string Convert(System.DateTime date); + string Convert(System.DateTime date, Humanizer.GrammaticalCase grammaticalCase); + } +} namespace Humanizer.Localisation.Formatters { public class DefaultFormatter : Humanizer.Localisation.Formatters.IFormatter diff --git a/src/Humanizer/Configuration/Configurator.cs b/src/Humanizer/Configuration/Configurator.cs index 157d76865..f0d214db5 100644 --- a/src/Humanizer/Configuration/Configurator.cs +++ b/src/Humanizer/Configuration/Configurator.cs @@ -4,6 +4,7 @@ using Humanizer.DateTimeHumanizeStrategy; using Humanizer.Localisation.CollectionFormatters; using Humanizer.Localisation.DateToOrdinalWords; +using Humanizer.Localisation.DateToWords; using Humanizer.Localisation.Formatters; using Humanizer.Localisation.NumberToWords; using Humanizer.Localisation.Ordinalizers; @@ -64,6 +65,15 @@ public static LocaliserRegistry DateToOrdinalWordsC get { return _dateToOrdinalWordConverters; } } + private static readonly LocaliserRegistry _dateToWordConverters = new DateToWordsConverterRegistry(); + /// + /// A registry of date to words converters used to localise ToWords method + /// + public static LocaliserRegistry DateToWordsConverters + { + get { return _dateToWordConverters; } + } + #if NET6_0_OR_GREATER private static readonly LocaliserRegistry _dateOnlyToOrdinalWordConverters = new DateOnlyToOrdinalWordsConverterRegistry(); /// @@ -74,6 +84,15 @@ public static LocaliserRegistry DateOnlyToOrdin get { return _dateOnlyToOrdinalWordConverters; } } + private static readonly LocaliserRegistry _dateOnlyToWordConverters = new DateOnlyToWordsConverterRegistry(); + /// + /// A registry of date to words converters used to localise ToWords method + /// + public static LocaliserRegistry DateOnlyToWordsConverters + { + get { return _dateOnlyToWordConverters; } + } + private static readonly LocaliserRegistry _timeOnlyToClockNotationConverters = new TimeOnlyToClockNotationConvertersRegistry(); /// /// A registry of time to clock notation converters used to localise ToClockNotation methods @@ -132,6 +151,17 @@ internal static IDateToOrdinalWordConverter DateToOrdinalWordsConverter } } + /// + /// The converter of date to Words to be used + /// + internal static IDateToWordConverter DateToWordsConverter + { + get + { + return DateToWordsConverters.ResolveForUiCulture(); + } + } + #if NET6_0_OR_GREATER /// /// The ordinalizer to be used @@ -151,6 +181,25 @@ internal static ITimeOnlyToClockNotationConverter TimeOnlyToClockNotationConvert return TimeOnlyToClockNotationConverters.ResolveForUiCulture(); } } + + /// + /// The converter of date to Words to be used + /// + internal static IDateOnlyToWordConverter DateOnlyToWordsConverter + { + get + { + return DateOnlyToWordsConverters.ResolveForUiCulture(); + } + } + + //internal static ITimeOnlyToClockNotationConverter TimeOnlyToClockNotationConverter + //{ + // get + // { + // return TimeOnlyToClockNotationConverters.ResolveForUiCulture(); + // } + //} #endif private static IDateTimeHumanizeStrategy _dateTimeHumanizeStrategy = new DefaultDateTimeHumanizeStrategy(); diff --git a/src/Humanizer/Configuration/DateOnlyToWordsConverterRegistry.cs b/src/Humanizer/Configuration/DateOnlyToWordsConverterRegistry.cs new file mode 100644 index 000000000..b5f7f82c0 --- /dev/null +++ b/src/Humanizer/Configuration/DateOnlyToWordsConverterRegistry.cs @@ -0,0 +1,15 @@ +#if NET6_0_OR_GREATER +using Humanizer.Localisation.DateToWords; + +namespace Humanizer.Configuration +{ + internal class DateOnlyToWordsConverterRegistry : LocaliserRegistry + { + public DateOnlyToWordsConverterRegistry() : base(new DefaultDateOnlyToWordConverter()) + { + Register("en-US", new UsDateOnlyToWordsConverter()); + Register("es", new EsDateOnlyToWordsConverter()); + } + } +} +#endif diff --git a/src/Humanizer/Configuration/DateToWordsConverterRegistry.cs b/src/Humanizer/Configuration/DateToWordsConverterRegistry.cs new file mode 100644 index 000000000..9a2cb2fd0 --- /dev/null +++ b/src/Humanizer/Configuration/DateToWordsConverterRegistry.cs @@ -0,0 +1,13 @@ +using Humanizer.Localisation.DateToWords; + +namespace Humanizer.Configuration +{ + internal class DateToWordsConverterRegistry : LocaliserRegistry + { + public DateToWordsConverterRegistry() : base(new DefaultDateToWordConverter()) + { + Register("en-US", new UsDateToWordsConverter()); + Register("es", new EsDateToWordsConverter()); + } + } +} diff --git a/src/Humanizer/DateToWordsExtensions.cs b/src/Humanizer/DateToWordsExtensions.cs new file mode 100644 index 000000000..c0a727e07 --- /dev/null +++ b/src/Humanizer/DateToWordsExtensions.cs @@ -0,0 +1,53 @@ +using System; +using Humanizer.Configuration; + +namespace Humanizer +{ + /// + /// Humanizes DateTime into human readable sentence + /// + public static class DateToWordsExtensions + { + /// + /// Turns the provided date into words + /// + /// The date to be made into words + /// The date in words + public static string ToWords(this DateTime input) + { + return Configurator.DateToWordsConverter.Convert(input); + } + /// + /// Turns the provided date into words + /// + /// The date to be made into words + /// The grammatical case to use for output words + /// The date in words + public static string ToWords(this DateTime input, GrammaticalCase grammaticalCase) + { + return Configurator.DateToWordsConverter.Convert(input, grammaticalCase); + } + +#if NET6_0_OR_GREATER + /// + /// Turns the provided date into words + /// + /// The date to be made into words + /// The date in words + public static string ToWords(this DateOnly input) + { + return Configurator.DateOnlyToWordsConverter.Convert(input); + } + /// + /// Turns the provided date into words + /// + /// The date to be made into words + /// The grammatical case to use for output words + /// The date in words + public static string ToWords(this DateOnly input, GrammaticalCase grammaticalCase) + { + return Configurator.DateOnlyToWordsConverter.Convert(input, grammaticalCase); + } +#endif + } +} diff --git a/src/Humanizer/Localisation/DateToWords/DefaultDateOnlyToWordConverter.cs b/src/Humanizer/Localisation/DateToWords/DefaultDateOnlyToWordConverter.cs new file mode 100644 index 000000000..aac84b64b --- /dev/null +++ b/src/Humanizer/Localisation/DateToWords/DefaultDateOnlyToWordConverter.cs @@ -0,0 +1,23 @@ +#if NET6_0_OR_GREATER + +using System; + +namespace Humanizer.Localisation.DateToWords +{ + internal class DefaultDateOnlyToWordConverter : IDateOnlyToWordConverter + { + + public virtual string Convert(DateOnly date) + { + return "the " + date.Day.ToOrdinalWords() + date.ToString(" 'of' MMMM ") + date.Year.ToWords(); + } + + public virtual string Convert(DateOnly date, GrammaticalCase grammaticalCase) + { + return Convert(date); + } + + } +} + +#endif diff --git a/src/Humanizer/Localisation/DateToWords/DefaultDateToWordConverter.cs b/src/Humanizer/Localisation/DateToWords/DefaultDateToWordConverter.cs new file mode 100644 index 000000000..b2cbec01f --- /dev/null +++ b/src/Humanizer/Localisation/DateToWords/DefaultDateToWordConverter.cs @@ -0,0 +1,19 @@ +using System; + +namespace Humanizer.Localisation.DateToWords +{ + internal class DefaultDateToWordConverter : IDateToWordConverter + { + + public virtual string Convert(DateTime date) + { + return "the " + date.Day.ToOrdinalWords() + date.ToString(" 'of' MMMM ") + date.Year.ToWords(); + } + + public virtual string Convert(DateTime date, GrammaticalCase grammaticalCase) + { + return Convert(date); + } + + } +} diff --git a/src/Humanizer/Localisation/DateToWords/EsDateOnlyToWordsConverter.cs b/src/Humanizer/Localisation/DateToWords/EsDateOnlyToWordsConverter.cs new file mode 100644 index 000000000..0c3a92cd2 --- /dev/null +++ b/src/Humanizer/Localisation/DateToWords/EsDateOnlyToWordsConverter.cs @@ -0,0 +1,18 @@ +#if NET6_0_OR_GREATER + +using System; +using Humanizer.Configuration; + +namespace Humanizer.Localisation.DateToWords +{ + internal class EsDateOnlyToWordsConverter : DefaultDateOnlyToWordConverter + { + public override string Convert(DateOnly date) + { + var equivalentDateTime = date.ToDateTime(TimeOnly.MinValue); + return Configurator.DateToWordsConverter.Convert(equivalentDateTime); + } + } +} + +#endif diff --git a/src/Humanizer/Localisation/DateToWords/EsDateToWordsConverter.cs b/src/Humanizer/Localisation/DateToWords/EsDateToWordsConverter.cs new file mode 100644 index 000000000..12e17fd51 --- /dev/null +++ b/src/Humanizer/Localisation/DateToWords/EsDateToWordsConverter.cs @@ -0,0 +1,12 @@ +using System; + +namespace Humanizer.Localisation.DateToWords +{ + internal class EsDateToWordsConverter : DefaultDateToWordConverter + { + public override string Convert(DateTime date) + { + return date.Day.ToWords() + date.ToString(" 'de' MMMM 'de' ") + date.Year.ToWords(); + } + } +} diff --git a/src/Humanizer/Localisation/DateToWords/IDateOnlyToWordConverter.cs b/src/Humanizer/Localisation/DateToWords/IDateOnlyToWordConverter.cs new file mode 100644 index 000000000..724e676d4 --- /dev/null +++ b/src/Humanizer/Localisation/DateToWords/IDateOnlyToWordConverter.cs @@ -0,0 +1,28 @@ +#if NET6_0_OR_GREATER + +using System; + +namespace Humanizer.Localisation.DateToWords +{ + /// + /// The interface used to localise the ToWords method. + /// + public interface IDateOnlyToWordConverter + { + /// + /// Converts the date to Words + /// + /// + /// + string Convert(DateOnly date); + + /// + /// Converts the date to Words using the provided grammatical case + /// + /// + /// + /// + string Convert(DateOnly date, GrammaticalCase grammaticalCase); + } +} +#endif diff --git a/src/Humanizer/Localisation/DateToWords/IDateToWordConverter.cs b/src/Humanizer/Localisation/DateToWords/IDateToWordConverter.cs new file mode 100644 index 000000000..1ae806dc9 --- /dev/null +++ b/src/Humanizer/Localisation/DateToWords/IDateToWordConverter.cs @@ -0,0 +1,25 @@ +using System; + +namespace Humanizer.Localisation.DateToWords +{ + /// + /// The interface used to localise the ToWords method. + /// + public interface IDateToWordConverter + { + /// + /// Converts the date to Words + /// + /// + /// + string Convert(DateTime date); + + /// + /// Converts the date to Words using the provided grammatical case + /// + /// + /// + /// + string Convert(DateTime date, GrammaticalCase grammaticalCase); + } +} diff --git a/src/Humanizer/Localisation/DateToWords/UsDateOnlyToWordsConverter.cs b/src/Humanizer/Localisation/DateToWords/UsDateOnlyToWordsConverter.cs new file mode 100644 index 000000000..a3cafd742 --- /dev/null +++ b/src/Humanizer/Localisation/DateToWords/UsDateOnlyToWordsConverter.cs @@ -0,0 +1,18 @@ +#if NET6_0_OR_GREATER + +using System; +using Humanizer.Configuration; + +namespace Humanizer.Localisation.DateToWords +{ + internal class UsDateOnlyToWordsConverter : DefaultDateOnlyToWordConverter + { + public override string Convert(DateOnly date) + { + var equivalentDateTime = date.ToDateTime(TimeOnly.MinValue); + return Configurator.DateToWordsConverter.Convert(equivalentDateTime); + } + } +} + +#endif diff --git a/src/Humanizer/Localisation/DateToWords/UsDateToWordsConverter.cs b/src/Humanizer/Localisation/DateToWords/UsDateToWordsConverter.cs new file mode 100644 index 000000000..51f4a5bea --- /dev/null +++ b/src/Humanizer/Localisation/DateToWords/UsDateToWordsConverter.cs @@ -0,0 +1,14 @@ +using System; + +using Humanizer.Localisation.DateToWords; + +namespace Humanizer.Localisation.DateToWords +{ + internal class UsDateToWordsConverter : DefaultDateToWordConverter + { + public override string Convert(DateTime date) + { + return date.ToString("MMMM ") + date.Day.ToOrdinalWords() + ", " + date.Year.ToWords(); + } + } +}