diff --git a/readme.md b/readme.md index 87946dc57..f08b49462 100644 --- a/readme.md +++ b/readme.md @@ -25,6 +25,7 @@ Also Humanizer [symbols nuget package](http://www.symbolsource.org/Public/Metada - [Number to ordinal words](#number-to-ordinal-words) - [Roman numerals](#roman-numerals) - [ByteSize](#bytesize) + - [Truncate](#truncate) - [Mix this into your framework to simplify your life](#mix-this-into-your-framework-to-simplify-your-life) - [How to contribute?](#how-to-contribute) - [Contribution guideline](#contribution-guideline) @@ -514,6 +515,34 @@ ByteSize.Parse("1.55 tB"); ByteSize.Parse("1.55 tb"); ``` +###Truncate +You can truncate a `string` using the `Truncate` method: + +```c# +"Long text to truncate".Truncate(10) => "Long text…" +``` + +By default the `'…'` character is used to truncate strings. The advantage of using the `'…'` character instead of `"..."` is that the former only takes a single character and thus allows more text to be shown before truncation. If you want, you can also provide your own truncation string: + +```c# +"Long text to truncate".Truncate(10, "---") => "Long te---" +``` + +The default truncation strategy, `Truncator.FixedLength`, is to truncate the input string to a specific length, including the truncation string length. There are two more truncator strategies available: one for a fixed number of (alpha-numerical) characters and one for a fixed number of words. To use a specific truncator when truncating, the two `Truncate` methods shown in the previous examples both have an overload that allow you to specify the `ITruncator` instance to use for the truncation. Here are examples on how to use the three provided truncators: + +```c# +"Long text to truncate".Truncate(10, Truncator.FixedLength) => "Long text…" +"Long text to truncate".Truncate(10, "---", Truncator.FixedLength) => "Long te---" + +"Long text to truncate".Truncate(6, Truncator.FixedNumberOfCharacters) => "Long t…" +"Long text to truncate".Truncate(6, "---", Truncator.FixedNumberOfCharacters) => "Lon---" + +"Long text to truncate".Truncate(2, Truncator.FixedNumberOfWords) => "Long text…" +"Long text to truncate".Truncate(2, "---", Truncator.FixedNumberOfWords) => "Long text---" +``` + +Note that you can also use create your own truncator by having a class implement the `ITruncator` interface. + ###Mix this into your framework to simplify your life This is just a baseline and you can use this to simplify your day to day job. For example, in Asp.Net MVC we keep chucking `Display` attribute on ViewModel properties so `HtmlHelper` can generate correct labels for us; but, just like enums, in vast majority of cases we just need a space between the words in property name - so why not use `"string".Humanize` for that?! diff --git a/release_notes.md b/release_notes.md index 24b19f6d7..558770944 100644 --- a/release_notes.md +++ b/release_notes.md @@ -2,6 +2,8 @@ [Commits](https://github.com/MehdiK/Humanizer/compare/v1.14.1...master) + - [#110](https://github.com/MehdiK/Humanizer/pull/110): Added `Truncate` feature + ###v1.14.1 - 2014-03-26 - [#108](https://github.com/MehdiK/Humanizer/pull/108): Added support for custom description attributes - [#106](https://github.com/MehdiK/Humanizer/pull/106): diff --git a/src/Humanizer.Tests/Humanizer.Tests.csproj b/src/Humanizer.Tests/Humanizer.Tests.csproj index 66970d710..7d6f04165 100644 --- a/src/Humanizer.Tests/Humanizer.Tests.csproj +++ b/src/Humanizer.Tests/Humanizer.Tests.csproj @@ -112,6 +112,7 @@ + diff --git a/src/Humanizer.Tests/TruncatorTests.cs b/src/Humanizer.Tests/TruncatorTests.cs new file mode 100644 index 000000000..62dec328d --- /dev/null +++ b/src/Humanizer.Tests/TruncatorTests.cs @@ -0,0 +1,116 @@ +using Xunit; +using Xunit.Extensions; + +namespace Humanizer.Tests +{ + public class TruncatorTests + { + [Theory] + [InlineData(null, 10, null)] + [InlineData("", 10, "")] + [InlineData("a", 1, "a")] + [InlineData("Text longer than truncate length", 10, "Text long…")] + [InlineData("Text with length equal to truncate length", 41, "Text with length equal to truncate length")] + [InlineData("Text smaller than truncate length", 34, "Text smaller than truncate length")] + public void Truncate(string input, int length, string expectedOutput) + { + Assert.Equal(expectedOutput, input.Truncate(length)); + } + + [Theory] + [InlineData(null, 10, null)] + [InlineData("", 10, "")] + [InlineData("a", 1, "a")] + [InlineData("Text longer than truncate length", 10, "Text long…")] + [InlineData("Text with length equal to truncate length", 41, "Text with length equal to truncate length")] + [InlineData("Text smaller than truncate length", 34, "Text smaller than truncate length")] + public void TruncateWithFixedLengthTruncator(string input, int length, string expectedOutput) + { + Assert.Equal(expectedOutput, input.Truncate(length, Truncator.FixedLength)); + } + + [Theory] + [InlineData(null, 10, null)] + [InlineData("", 10, "")] + [InlineData("a", 1, "a")] + [InlineData("Text with more characters than truncate length", 10, "Text with m…")] + [InlineData("Text with number of characters equal to truncate length", 47, "Text with number of characters equal to truncate length")] + [InlineData("Text with less characters than truncate length", 41, "Text with less characters than truncate length")] + public void TruncateWithFixedNumberOfCharactersTruncator(string input, int length, string expectedOutput) + { + Assert.Equal(expectedOutput, input.Truncate(length, Truncator.FixedNumberOfCharacters)); + } + + [Theory] + [InlineData(null, 10, null)] + [InlineData("", 10, "")] + [InlineData("a", 1, "a")] + [InlineData("Text with more words than truncate length", 4, "Text with more words…")] + [InlineData("Text with number of words equal to truncate length", 9, "Text with number of words equal to truncate length")] + [InlineData("Text with less words than truncate length", 8, "Text with less words than truncate length")] + [InlineData("Words are\nsplit\rby\twhitespace", 4, "Words are\nsplit\rby…")] + public void TruncateWithFixedNumberOfWordsTruncator(string input, int length, string expectedOutput) + { + Assert.Equal(expectedOutput, input.Truncate(length, Truncator.FixedNumberOfWords)); + } + + [Theory] + [InlineData(null, 10, "...", null)] + [InlineData("", 10, "...", "")] + [InlineData("a", 1, "...", "a")] + [InlineData("Text longer than truncate length", 10, "...", "Text lo...")] + [InlineData("Text with length equal to truncate length", 41, "...", "Text with length equal to truncate length")] + [InlineData("Text smaller than truncate length", 34, "...", "Text smaller than truncate length")] + [InlineData("Text with delimiter length greater than truncate length truncates to fixed length without truncation string", 2, "...", "Te")] + [InlineData("Null truncation string truncates to truncate length without truncation string", 4, null, "Null")] + public void TruncateWithTruncationString(string input, int length, string truncationString, string expectedOutput) + { + Assert.Equal(expectedOutput, input.Truncate(length, truncationString)); + } + + [Theory] + [InlineData(null, 10, "...", null)] + [InlineData("", 10, "...", "")] + [InlineData("a", 1, "...", "a")] + [InlineData("Text longer than truncate length", 10, "...", "Text lo...")] + [InlineData("Text with different truncation string", 10, "---", "Text wi---")] + [InlineData("Text with length equal to truncate length", 41, "...", "Text with length equal to truncate length")] + [InlineData("Text smaller than truncate length", 34, "...", "Text smaller than truncate length")] + [InlineData("Text with delimiter length greater than truncate length truncates to fixed length without truncation string", 2, "...", "Te")] + [InlineData("Null truncation string truncates to truncate length without truncation string", 4, null, "Null")] + public void TruncateWithTruncationStringAndFixedLengthTruncator(string input, int length, string truncationString, string expectedOutput) + { + Assert.Equal(expectedOutput, input.Truncate(length, truncationString, Truncator.FixedLength)); + } + + [Theory] + [InlineData(null, 10, "...", null)] + [InlineData("", 10, "...", "")] + [InlineData("a", 1, "...", "a")] + [InlineData("Text with more characters than truncate length", 10, "...", "Text wit...")] + [InlineData("Text with different truncation string", 10, "---", "Text wit---")] + [InlineData("Text with number of characters equal to truncate length", 47, "...", "Text with number of characters equal to truncate length")] + [InlineData("Text with less characters than truncate length", 41, "...", "Text with less characters than truncate length")] + [InlineData("Text with delimiter length greater than truncate length truncates to fixed length without truncation string", 2, "...", "Te")] + [InlineData("Null truncation string truncates to truncate length without truncation string", 4, null, "Null")] + public void TruncateWithTruncationStringAndFixedNumberOfCharactersTruncator(string input, int length, string truncationString, string expectedOutput) + { + Assert.Equal(expectedOutput, input.Truncate(length, truncationString, Truncator.FixedNumberOfCharacters)); + } + + [Theory] + [InlineData(null, 10, "...", null)] + [InlineData("", 10, "...", "")] + [InlineData("a", 1, "...", "a")] + [InlineData("Text with more words than truncate length", 4, "...", "Text with more words...")] + [InlineData("Text with different truncation string", 4, "---", "Text with different truncation---")] + [InlineData("Text with number of words equal to truncate length", 9, "...", "Text with number of words equal to truncate length")] + [InlineData("Text with less words than truncate length", 8, "...", "Text with less words than truncate length")] + [InlineData("Words are\nsplit\rby\twhitespace", 4, "...", "Words are\nsplit\rby...")] + [InlineData("Null truncation string truncates to truncate length without truncation string", 4, null, "Null truncation string truncates")] + public void TruncateWithTruncationStringAndFixedNumberOfWordsTruncator(string input, int length, string truncationString, string expectedOutput) + { + Assert.Equal(expectedOutput, input.Truncate(length, truncationString, Truncator.FixedNumberOfWords)); + } + } +} \ No newline at end of file diff --git a/src/Humanizer/Humanizer.csproj b/src/Humanizer/Humanizer.csproj index 2a54d7e7d..e58bc7aea 100644 --- a/src/Humanizer/Humanizer.csproj +++ b/src/Humanizer/Humanizer.csproj @@ -126,6 +126,11 @@ + + + + + diff --git a/src/Humanizer/Humanizer.csproj.DotSettings b/src/Humanizer/Humanizer.csproj.DotSettings index 7442acdd2..57b16dc71 100644 --- a/src/Humanizer/Humanizer.csproj.DotSettings +++ b/src/Humanizer/Humanizer.csproj.DotSettings @@ -1,4 +1,5 @@  True + True False True \ No newline at end of file diff --git a/src/Humanizer/Truncation/FixedLengthTruncator.cs b/src/Humanizer/Truncation/FixedLengthTruncator.cs new file mode 100644 index 000000000..475e51c52 --- /dev/null +++ b/src/Humanizer/Truncation/FixedLengthTruncator.cs @@ -0,0 +1,22 @@ +namespace Humanizer +{ + /// + /// Truncate a string to a fixed length + /// + class FixedLengthTruncator : ITruncator + { + public string Truncate(string value, int length, string truncationString) + { + if (value == null) + return null; + + if (value.Length == 0) + return value; + + if (truncationString == null || truncationString.Length > length) + return value.Substring(0, length); + + return value.Length > length ? value.Substring(0, length - truncationString.Length) + truncationString : value; + } + } +} \ No newline at end of file diff --git a/src/Humanizer/Truncation/FixedNumberOfCharactersTruncator.cs b/src/Humanizer/Truncation/FixedNumberOfCharactersTruncator.cs new file mode 100644 index 000000000..50b5ad27a --- /dev/null +++ b/src/Humanizer/Truncation/FixedNumberOfCharactersTruncator.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; + +namespace Humanizer +{ + /// + /// Truncate a string to a fixed number of characters + /// + class FixedNumberOfCharactersTruncator : ITruncator + { + public string Truncate(string value, int length, string truncationString) + { + if (value == null) + return null; + + if (value.Length == 0) + return value; + + if (truncationString == null || truncationString.Length > length) + return value.Substring(0, length); + + var alphaNumericalCharactersProcessed = 0; + + var numberOfCharactersEqualToTruncateLength = value.ToCharArray().Count(Char.IsLetterOrDigit) == length; + + for (var i = 0; i < value.Length - truncationString.Length; i++) + { + if (Char.IsLetterOrDigit(value[i])) + alphaNumericalCharactersProcessed++; + + if (numberOfCharactersEqualToTruncateLength && alphaNumericalCharactersProcessed == length) + return value; + + if (!numberOfCharactersEqualToTruncateLength && alphaNumericalCharactersProcessed + truncationString.Length == length) + return value.Substring(0, i + 1) + truncationString; + } + + return value; + } + } +} \ No newline at end of file diff --git a/src/Humanizer/Truncation/FixedNumberOfWordsTruncator.cs b/src/Humanizer/Truncation/FixedNumberOfWordsTruncator.cs new file mode 100644 index 000000000..a083f394b --- /dev/null +++ b/src/Humanizer/Truncation/FixedNumberOfWordsTruncator.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; + +namespace Humanizer +{ + /// + /// Truncate a string to a fixed number of words + /// + class FixedNumberOfWordsTruncator : ITruncator + { + public string Truncate(string value, int length, string truncationString) + { + if (value == null) + return null; + + if (value.Length == 0) + return value; + + var numberOfWordsProcessed = 0; + var numberOfWords = value.Split((char[])null, StringSplitOptions.RemoveEmptyEntries).Count(); + + if (numberOfWords <= length) + return value; + + var lastCharactersWasWhiteSpace = true; + + for (var i = 0; i < value.Length; i++) + { + if (Char.IsWhiteSpace(value[i])) + { + if (!lastCharactersWasWhiteSpace) + numberOfWordsProcessed++; + + lastCharactersWasWhiteSpace = true; + + if (numberOfWordsProcessed == length) + return value.Substring(0, i) + truncationString; + } + else + { + lastCharactersWasWhiteSpace = false; + } + } + + return value + truncationString; + } + } +} \ No newline at end of file diff --git a/src/Humanizer/Truncation/ITruncator.cs b/src/Humanizer/Truncation/ITruncator.cs new file mode 100644 index 000000000..ecd221804 --- /dev/null +++ b/src/Humanizer/Truncation/ITruncator.cs @@ -0,0 +1,17 @@ +namespace Humanizer +{ + /// + /// Can truncate a string. + /// + public interface ITruncator + { + /// + /// Truncate a string + /// + /// The string to truncate + /// The length to truncate to + /// The string used to truncate with + /// The truncated string + string Truncate(string value, int length, string truncationString); + } +} diff --git a/src/Humanizer/Truncation/Truncator.cs b/src/Humanizer/Truncation/Truncator.cs new file mode 100644 index 000000000..8522ff1b2 --- /dev/null +++ b/src/Humanizer/Truncation/Truncator.cs @@ -0,0 +1,97 @@ +using System; + +namespace Humanizer +{ + /// + /// Allow strings to be truncated + /// + public static class Truncator + { + /// + /// Truncate the string + /// + /// The string to be truncated + /// The length to truncate to + /// The truncated string + public static string Truncate(this string input, int length) + { + return input.Truncate(length, "…", FixedLength); + } + + /// + /// Truncate the string + /// + /// The string to be truncated + /// The length to truncate to + /// The truncate to use + /// The truncated string + public static string Truncate(this string input, int length, ITruncator truncator) + { + return input.Truncate(length, "…", truncator); + } + + /// + /// Truncate the string + /// + /// The string to be truncated + /// The length to truncate to + /// The string used to truncate with + /// The truncated string + public static string Truncate(this string input, int length, string truncationString) + { + return input.Truncate(length, truncationString, FixedLength); + } + + /// + /// Truncate the string + /// + /// The string to be truncated + /// The length to truncate to + /// The string used to truncate with + /// The truncator to use + /// The truncated string + public static string Truncate(this string input, int length, string truncationString, ITruncator truncator) + { + if (truncator == null) + throw new ArgumentNullException("truncator"); + + if (input == null) + return null; + + return truncator.Truncate(input, length, truncationString); + } + + /// + /// Fixed length truncator + /// + public static ITruncator FixedLength + { + get + { + return new FixedLengthTruncator(); + } + } + + /// + /// Fixed number of characters truncator + /// + public static ITruncator FixedNumberOfCharacters + { + get + { + return new FixedNumberOfCharactersTruncator(); + } + } + + /// + /// Fixed number of words truncator + /// + public static ITruncator FixedNumberOfWords + { + get + { + return new FixedNumberOfWordsTruncator(); + } + } + } +}