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();
+ }
+ }
+ }
+}