diff --git a/release_notes.md b/release_notes.md index 3f1f35993..5fbbd0c62 100644 --- a/release_notes.md +++ b/release_notes.md @@ -2,6 +2,7 @@ - [#257](https://github.com/Mehdik/Humanizer/pull/257): Added German localisation for ToOrdinalWords and Ordinalize - [#261](https://github.com/Mehdik/Humanizer/pull/261): Added future dates to Portuguese - Brazil - [#269](https://github.com/MehdiK/Humanizer/pull/269): Added Vietnamese localisation + - [#268](https://github.com/Mehdik/Humanizer/pull/268): Added humanization of collections [Commits](https://github.com/MehdiK/Humanizer/compare/v1.25.4...master) 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 225c452a5..91a70936d 100644 --- a/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt +++ b/src/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.approve_public_api.approved.txt @@ -63,8 +63,17 @@ public class CasingExtensions public string ApplyCase(string input, Humanizer.LetterCasing casing) { } } +public class CollectionHumanizeExtensions +{ + public string Humanize(System.Collections.Generic.IEnumerable<> collection) { } + public string Humanize(System.Collections.Generic.IEnumerable<> collection, System.Func<, > displayFormatter) { } + public string Humanize(System.Collections.Generic.IEnumerable<> collection, string separator) { } + public string Humanize(System.Collections.Generic.IEnumerable<> collection, System.Func<, > displayFormatter, string separator) { } +} + public class Configurator { + public Humanizer.Configuration.LocaliserRegistry CollectionFormatters { get; } public Humanizer.DateTimeHumanizeStrategy.IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } public Humanizer.Configuration.LocaliserRegistry Formatters { get; } public Humanizer.Configuration.LocaliserRegistry NumberToWordsConverters { get; } @@ -194,6 +203,14 @@ public enum LetterCasing value__, } +public interface ICollectionFormatter +{ + string Humanize(System.Collections.Generic.IEnumerable<> collection); + string Humanize(System.Collections.Generic.IEnumerable<> collection, System.Func<, > objectFormatter); + string Humanize(System.Collections.Generic.IEnumerable<> collection, string separator); + string Humanize(System.Collections.Generic.IEnumerable<> collection, System.Func<, > objectFormatter, string separator); +} + public class DefaultFormatter { public DefaultFormatter() { } diff --git a/src/Humanizer.Tests/CollectionHumanizeTests.cs b/src/Humanizer.Tests/CollectionHumanizeTests.cs new file mode 100644 index 000000000..3661f0882 --- /dev/null +++ b/src/Humanizer.Tests/CollectionHumanizeTests.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using Xunit; + +namespace Humanizer.Tests +{ + public class SomeClass + { + public string SomeString; + public int SomeInt; + public override string ToString() + { + return "ToString"; + } + } + + public class CollectionHumanizeTests : AmbientCulture + { + public CollectionHumanizeTests() : base("en") { } + + [Fact] + public void HumanizeReturnsOnlyNameWhenCollectionContainsOneItem() + { + var collection = new List { "A String" }; + + Assert.Equal("A String", collection.Humanize()); + } + + [Fact] + public void HumanizeUsesSeparatorWhenMoreThanOneItemIsInCollection() + { + var collection = new List + { + "A String", + "Another String", + }; + + Assert.Equal("A String or Another String", collection.Humanize("or")); + } + + [Fact] + public void HumanizeDefaultsSeparatorToAnd() + { + var collection = new List + { + "A String", + "Another String", + }; + + Assert.Equal("A String and Another String", collection.Humanize()); + } + + [Fact] + public void HumanizeUsesOxfordComma() + { + var collection = new List + { + "A String", + "Another String", + "A Third String", + }; + + Assert.Equal("A String, Another String, or A Third String", collection.Humanize("or")); + } + + private readonly List _testCollection = new List + { + new SomeClass { SomeInt = 1, SomeString = "One" }, + new SomeClass { SomeInt = 2, SomeString = "Two" }, + new SomeClass { SomeInt = 3, SomeString = "Three" } + }; + + [Fact] + public void HumanizeDefaultsToToString() + { + Assert.Equal("ToString, ToString, or ToString", _testCollection.Humanize("or")); + } + + [Fact] + public void HumanizeUsesObjectFormatter() + { + var humanized = _testCollection.Humanize(sc => string.Format("SomeObject #{0} - {1}", sc.SomeInt, sc.SomeString)); + Assert.Equal("SomeObject #1 - One, SomeObject #2 - Two, and SomeObject #3 - Three", humanized); + } + + [Fact] + public void HumanizeUsesObjectFormatterWhenSeparatorIsProvided() + { + var humanized = _testCollection.Humanize(sc => string.Format("SomeObject #{0} - {1}", sc.SomeInt, sc.SomeString), "or"); + Assert.Equal("SomeObject #1 - One, SomeObject #2 - Two, or SomeObject #3 - Three", humanized); + } + } +} diff --git a/src/Humanizer.Tests/Humanizer.Tests.csproj b/src/Humanizer.Tests/Humanizer.Tests.csproj index 326f9e612..cd7832059 100644 --- a/src/Humanizer.Tests/Humanizer.Tests.csproj +++ b/src/Humanizer.Tests/Humanizer.Tests.csproj @@ -62,6 +62,7 @@ + diff --git a/src/Humanizer/CollectionHumanizeExtensions.cs b/src/Humanizer/CollectionHumanizeExtensions.cs new file mode 100644 index 000000000..2124213a7 --- /dev/null +++ b/src/Humanizer/CollectionHumanizeExtensions.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Humanizer.Configuration; + + +namespace Humanizer +{ + /// + /// Humanizes an IEnumerable into a human readable list + /// + public static class CollectionHumanizeExtensions + { + /// + /// Formats the collection for display, calling ToString() on each object and + /// using the default separator for the current culture. + /// + /// + public static string Humanize(this IEnumerable collection) + { + return Configurator.CollectionFormatter.Humanize(collection); + } + + /// + /// Formats the collection for display, calling `objectFormatter` on each object + /// and using the default separator for the current culture. + /// + /// + public static string Humanize(this IEnumerable collection, Func displayFormatter) + { + if (displayFormatter == null) + throw new ArgumentNullException("displayFormatter"); + + return Configurator.CollectionFormatter.Humanize(collection, displayFormatter); + } + + /// + /// Formats the collection for display, calling ToString() on each object + /// and using the provided separator. + /// + /// + public static string Humanize(this IEnumerable collection, String separator) + { + + return Configurator.CollectionFormatter.Humanize(collection, separator); + } + + /// + /// Formats the collection for display, calling `objectFormatter` on each object + /// and using the provided separator. + /// + /// + public static string Humanize(this IEnumerable collection, Func displayFormatter, String separator) + { + if (displayFormatter == null) + throw new ArgumentNullException("displayFormatter"); + + return Configurator.CollectionFormatter.Humanize(collection, displayFormatter, separator); + } + } +} diff --git a/src/Humanizer/Configuration/CollectionFormatterRegistry.cs b/src/Humanizer/Configuration/CollectionFormatterRegistry.cs new file mode 100644 index 000000000..ef486b4dc --- /dev/null +++ b/src/Humanizer/Configuration/CollectionFormatterRegistry.cs @@ -0,0 +1,13 @@ +using Humanizer.Localisation.CollectionFormatters; + +namespace Humanizer.Configuration +{ + internal class CollectionFormatterRegistry : LocaliserRegistry + { + public CollectionFormatterRegistry() + : base(new DefaultCollectionFormatter()) + { + Register("en"); + } + } +} diff --git a/src/Humanizer/Configuration/Configurator.cs b/src/Humanizer/Configuration/Configurator.cs index cbcd61179..227e37b4f 100644 --- a/src/Humanizer/Configuration/Configurator.cs +++ b/src/Humanizer/Configuration/Configurator.cs @@ -2,6 +2,7 @@ using Humanizer.Localisation.Formatters; using Humanizer.Localisation.NumberToWords; using Humanizer.Localisation.Ordinalizers; +using Humanizer.Localisation.CollectionFormatters; namespace Humanizer.Configuration { @@ -10,6 +11,16 @@ namespace Humanizer.Configuration /// public static class Configurator { + private static readonly LocaliserRegistry _collectionFormatters = new CollectionFormatterRegistry(); + + /// + /// A registry of formatters used to format collections based on the current locale + /// + public static LocaliserRegistry CollectionFormatters + { + get { return _collectionFormatters; } + } + private static readonly LocaliserRegistry _formatters = new FormatterRegistry(); /// /// A registry of formatters used to format strings based on the current locale @@ -36,6 +47,14 @@ public static LocaliserRegistry Ordinalizers { get { return _ordinalizers; } } + + internal static ICollectionFormatter CollectionFormatter + { + get + { + return CollectionFormatters.ResolveForUiCulture(); + } + } /// /// The formatter to be used diff --git a/src/Humanizer/Humanizer.csproj b/src/Humanizer/Humanizer.csproj index 9f07a6c02..33ea70e0c 100644 --- a/src/Humanizer/Humanizer.csproj +++ b/src/Humanizer/Humanizer.csproj @@ -49,6 +49,11 @@ Humanizer.snk + + + + + diff --git a/src/Humanizer/Localisation/CollectionFormatters/DefaultCollectionFormatter.cs b/src/Humanizer/Localisation/CollectionFormatters/DefaultCollectionFormatter.cs new file mode 100644 index 000000000..525bea9b5 --- /dev/null +++ b/src/Humanizer/Localisation/CollectionFormatters/DefaultCollectionFormatter.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace Humanizer.Localisation.CollectionFormatters +{ + class DefaultCollectionFormatter : ICollectionFormatter + { + protected String DefaultSeparator = ""; + + public virtual string Humanize(IEnumerable collection) + { + return Humanize(collection, o => o.ToString(), DefaultSeparator); + } + + public virtual string Humanize(IEnumerable collection, Func objectFormatter) + { + return Humanize(collection, objectFormatter, DefaultSeparator); + } + + public virtual string Humanize(IEnumerable collection, String separator) + { + return Humanize(collection, o => o.ToString(), separator); + } + + public virtual string Humanize(IEnumerable collection, Func objectFormatter, String separator) + { + throw new NotImplementedException("A collection formatter for the current culture has not been implemented yet."); + } + } +} diff --git a/src/Humanizer/Localisation/CollectionFormatters/EnglishCollectionFormatter.cs b/src/Humanizer/Localisation/CollectionFormatters/EnglishCollectionFormatter.cs new file mode 100644 index 000000000..0265fe351 --- /dev/null +++ b/src/Humanizer/Localisation/CollectionFormatters/EnglishCollectionFormatter.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Humanizer.Localisation.CollectionFormatters +{ + internal class EnglishCollectionFormatter : DefaultCollectionFormatter + { + public EnglishCollectionFormatter() + { + DefaultSeparator = "and"; + } + + public override string Humanize(IEnumerable collection, Func objectFormatter, String separator) + { + if (collection == null) + throw new ArgumentException("collection"); + + var enumerable = collection as T[] ?? collection.ToArray(); + + int count = enumerable.Count(); + + if (count == 0) + return ""; + + if (count == 1) + return objectFormatter(enumerable.First()); + + string formatString = count > 2 ? "{0}, {1} {2}" : "{0} {1} {2}"; + + return String.Format(formatString, + String.Join(", ", enumerable.Take(count - 1).Select(objectFormatter)), + separator, + objectFormatter(enumerable.Skip(count - 1).First())); + } + } +} diff --git a/src/Humanizer/Localisation/CollectionFormatters/ICollectionFormatter.cs b/src/Humanizer/Localisation/CollectionFormatters/ICollectionFormatter.cs new file mode 100644 index 000000000..1fc0bf7f7 --- /dev/null +++ b/src/Humanizer/Localisation/CollectionFormatters/ICollectionFormatter.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace Humanizer.Localisation.CollectionFormatters +{ + /// + /// An interface you should implement to localize Humanize for collections + /// + public interface ICollectionFormatter + { + /// + /// Formats the collection for display, calling ToString() on each object. + /// + /// + String Humanize(IEnumerable collection); + + /// + /// Formats the collection for display, calling `objectFormatter` on each object. + /// + /// + String Humanize(IEnumerable collection, Func objectFormatter); + + /// + /// Formats the collection for display, calling ToString() on each object + /// and using `separator` before the final item. + /// + /// + String Humanize(IEnumerable collection, String separator); + + /// + /// Formats the collection for display, calling `objectFormatter` on each object + /// and using `separator` before the final item. + /// + /// + String Humanize(IEnumerable collection, Func objectFormatter, String separator); + } +}