From a6139a5720c482e6b896239870da3e8cd73f9ffa Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 30 Apr 2022 13:08:39 +0100 Subject: [PATCH] Migrate string extensions to appropriate namespaces (#7) Introduces more tests --- X10D.Tests/src/Core/StringTests.cs | 57 ---- .../src/Text/StringBuilderReaderTests.cs | 271 +++++++++++++++++ X10D.Tests/src/Text/StringTests.cs | 286 +++++++++++++++--- X10D.Tests/src/Time/StringTests.cs | 38 +++ X10D/src/StringExtensions/StringExtensions.cs | 266 ---------------- X10D/src/Text/StringBuilderReader.cs | 9 +- X10D/src/Text/StringExtensions.cs | 242 +++++++++++++++ X10D/src/Time/StringExtensions.cs | 72 +++++ X10D/src/Time/TimeSpanParser.cs | 133 ++++++++ X10D/src/TimeSpanParser.cs | 102 ------- 10 files changed, 1016 insertions(+), 460 deletions(-) delete mode 100644 X10D.Tests/src/Core/StringTests.cs create mode 100644 X10D.Tests/src/Text/StringBuilderReaderTests.cs create mode 100644 X10D.Tests/src/Time/StringTests.cs delete mode 100644 X10D/src/StringExtensions/StringExtensions.cs create mode 100644 X10D/src/Time/StringExtensions.cs create mode 100644 X10D/src/Time/TimeSpanParser.cs delete mode 100644 X10D/src/TimeSpanParser.cs diff --git a/X10D.Tests/src/Core/StringTests.cs b/X10D.Tests/src/Core/StringTests.cs deleted file mode 100644 index ee6d169..0000000 --- a/X10D.Tests/src/Core/StringTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace X10D.Tests.Core; - -/// -/// Tests for . -/// -[TestClass] -public class StringTests -{ - /// - /// Tests . - /// - [TestMethod] - public void Base64Decode() - { - const string input = "SGVsbG8gV29ybGQ="; - const string expected = "Hello World"; - - string result = input.Base64Decode(); - - Assert.AreEqual(expected, result); - } - - - /// - /// Tests . - /// - [TestMethod] - public void Base64Encode() - { - const string input = "Hello World"; - const string expected = "SGVsbG8gV29ybGQ="; - - string result = input.Base64Encode(); - - Assert.AreEqual(expected, result); - } - - /// - /// Tests . - /// - [TestMethod] - public void Randomize() - { - const string input = "Hello World"; - const string expected = "le rooldeoH"; - var random = new Random(1); - - string result = input.Randomize(input.Length, random); - - Assert.ThrowsException(() => ((string?)null)!.Randomize(1)); - Assert.ThrowsException(() => input.Randomize(-1)); - Assert.AreEqual(string.Empty, string.Empty.Randomize(0)); - Assert.AreEqual(expected, result); - } -} diff --git a/X10D.Tests/src/Text/StringBuilderReaderTests.cs b/X10D.Tests/src/Text/StringBuilderReaderTests.cs new file mode 100644 index 0000000..bdd0848 --- /dev/null +++ b/X10D.Tests/src/Text/StringBuilderReaderTests.cs @@ -0,0 +1,271 @@ +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Text; + +namespace X10D.Tests.Text; + +[TestClass] +public class StringBuilderReaderTests +{ + [TestMethod] + public void Peek_ShouldReturnNextChar_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + Assert.AreEqual('H', reader.Peek()); + + reader.Close(); + } + + [TestMethod] + public void Read_ShouldReturnNextChar_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + Assert.AreEqual('H', reader.Read()); + Assert.AreEqual('e', reader.Read()); + Assert.AreEqual('l', reader.Read()); + Assert.AreEqual('l', reader.Read()); + Assert.AreEqual('o', reader.Read()); + Assert.AreEqual('\n', reader.Read()); + Assert.AreEqual('W', reader.Read()); + Assert.AreEqual('o', reader.Read()); + Assert.AreEqual('r', reader.Read()); + Assert.AreEqual('l', reader.Read()); + Assert.AreEqual('d', reader.Read()); + Assert.AreEqual(-1, reader.Read()); + + reader.Close(); + } + + [TestMethod] + public void Read_ShouldPopulateArray_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + var array = new char[5]; + int read = reader.Read(array, 0, 5); + Assert.AreEqual(5, read); + + CollectionAssert.AreEqual("Hello".ToCharArray(), array); + + reader.Close(); + } + + [TestMethod] + public void Read_ShouldReturnNegative1_GivenEndOfReader() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + var array = new char[11]; + reader.Read(array); + Assert.AreEqual(-1, reader.Read(array, 0, 1)); + reader.Close(); + } + + [TestMethod] + public void Read_ShouldThrow_GivenNullArray() + { + Assert.ThrowsException(() => + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + reader.Read(null!, 0, 5); + reader.Close(); + }); + } + + [TestMethod] + public void Read_ShouldThrow_GivenNegativeIndex() + { + Assert.ThrowsException(() => + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + var array = new char[5]; + reader.Read(array, -1, 5); + reader.Close(); + }); + } + + [TestMethod] + public void Read_ShouldThrow_GivenNegativeCount() + { + Assert.ThrowsException(() => + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + var array = new char[5]; + reader.Read(array, 0, -1); + reader.Close(); + }); + } + + [TestMethod] + public void Read_ShouldThrow_GivenSmallBuffer() + { + Assert.ThrowsException(() => + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + var array = new char[1]; + reader.Read(array, 0, 5); + reader.Close(); + }); + } + + [TestMethod] + public void Read_ShouldPopulateSpan_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + Span span = stackalloc char[5]; + int read = reader.Read(span); + Assert.AreEqual(5, read); + + CollectionAssert.AreEqual("Hello".ToCharArray(), span.ToArray()); + + reader.Close(); + } + + [TestMethod] + public void ReadAsync_ShouldPopulateArray_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + var array = new char[5]; + int read = reader.ReadAsync(array, 0, 5).GetAwaiter().GetResult(); + Assert.AreEqual(5, read); + + CollectionAssert.AreEqual("Hello".ToCharArray(), array); + + reader.Close(); + } + + [TestMethod] + public void ReadAsync_ShouldPopulateMemory_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + Memory memory = new char[5]; + int read = reader.ReadAsync(memory).GetAwaiter().GetResult(); + Assert.AreEqual(5, read); + + CollectionAssert.AreEqual("Hello".ToCharArray(), memory.ToArray()); + + reader.Close(); + } + + [TestMethod] + public void ReadBlock_ShouldPopulateArray_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + var array = new char[5]; + int read = reader.ReadBlock(array, 0, 5); + Assert.AreEqual(5, read); + + CollectionAssert.AreEqual("Hello".ToCharArray(), array); + + reader.Close(); + } + + [TestMethod] + public void ReadBlock_ShouldPopulateSpan_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + Span span = stackalloc char[5]; + int read = reader.ReadBlock(span); + Assert.AreEqual(5, read); + + CollectionAssert.AreEqual("Hello".ToCharArray(), span.ToArray()); + + reader.Close(); + } + + [TestMethod] + public void ReadBlock_ShouldReturnNegative1_GivenEndOfReader() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + var array = new char[11]; + reader.Read(array); + + int read = reader.ReadBlock(array, 0, 5); + Assert.AreEqual(-1, read); + + reader.Close(); + } + + [TestMethod] + public void ReadBlockAsync_ShouldPopulateArray_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + var array = new char[5]; + int read = reader.ReadBlockAsync(array, 0, 5).GetAwaiter().GetResult(); + Assert.AreEqual(5, read); + + CollectionAssert.AreEqual("Hello".ToCharArray(), array); + + reader.Close(); + } + + [TestMethod] + public void ReadBlockAsync_ShouldPopulateMemory_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + Memory memory = new char[5]; + int read = reader.ReadBlockAsync(memory).GetAwaiter().GetResult(); + Assert.AreEqual(5, read); + + CollectionAssert.AreEqual("Hello".ToCharArray(), memory.ToArray()); + + reader.Close(); + } + + [TestMethod] + public void ReadToEnd_ShouldReturnSourceString_GivenBuilder() + { + const string value = "Hello World"; + using var reader = new StringBuilderReader(new StringBuilder(value)); + Assert.AreEqual(value, reader.ReadToEnd()); + + reader.Close(); + } + + [TestMethod] + public void ReadToEndAsync_ShouldReturnSourceString_GivenBuilder() + { + const string value = "Hello World"; + using var reader = new StringBuilderReader(new StringBuilder(value)); + Assert.AreEqual(value, reader.ReadToEndAsync().GetAwaiter().GetResult()); + + reader.Close(); + } + + [TestMethod] + public void ReadLine_ShouldReturnSourceString_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + Assert.AreEqual("Hello", reader.ReadLine()); + Assert.AreEqual("World", reader.ReadLine()); + Assert.AreEqual(null, reader.ReadLine()); + + Assert.AreEqual(-1, reader.Peek()); + + reader.Close(); + } + + [TestMethod] + public void ReadLineAsync_ShouldReturnSourceString_GivenBuilder() + { + using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld")); + + Assert.AreEqual("Hello", reader.ReadLineAsync().GetAwaiter().GetResult()); + Assert.AreEqual("World", reader.ReadLineAsync().GetAwaiter().GetResult()); + Assert.AreEqual(null, reader.ReadLineAsync().GetAwaiter().GetResult()); + + Assert.AreEqual(-1, reader.Peek()); + + reader.Close(); + } +} diff --git a/X10D.Tests/src/Text/StringTests.cs b/X10D.Tests/src/Text/StringTests.cs index 6e13ab5..5d509f0 100644 --- a/X10D.Tests/src/Text/StringTests.cs +++ b/X10D.Tests/src/Text/StringTests.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Text; +using System.Text.Json.Serialization; using Microsoft.VisualStudio.TestTools.UnitTesting; using X10D.Text; @@ -8,7 +9,7 @@ namespace X10D.Tests.Text; public class StringTests { [TestMethod] - public void AsNullIfEmptyShouldBeCorrect() + public void AsNullIfEmpty_ShouldBeCorrect() { const string sampleString = "Hello World"; const string whitespaceString = " "; @@ -27,7 +28,7 @@ public class StringTests } [TestMethod] - public void AsNullIfWhiteSpaceShouldBeCorrect() + public void AsNullIfWhiteSpace_ShouldBeCorrect() { const string sampleString = "Hello World"; const string whitespaceString = " "; @@ -46,7 +47,110 @@ public class StringTests } [TestMethod] - public void FromJsonShouldDeserializeCorrectly() + public void Base64Decode_ShouldReturnHelloWorld_GivenBase64String() + { + Assert.AreEqual("Hello World", "SGVsbG8gV29ybGQ=".Base64Decode()); + } + + + [TestMethod] + public void Base64Decode_ShouldThrow_GivenNull() + { + string? value = null; + Assert.ThrowsException(() => value!.Base64Decode()); + } + + + [TestMethod] + public void Base64Encode_ShouldReturnBase64String_GivenHelloWorld() + { + Assert.AreEqual("SGVsbG8gV29ybGQ=", "Hello World".Base64Encode()); + } + + [TestMethod] + public void Base64Encode_ShouldThrow_GivenNull() + { + string? value = null; + Assert.ThrowsException(() => value!.Base64Encode()); + } + + [TestMethod] + public void ChangeEncoding_ShouldReturnAsciiString_GivenUtf8() + { + Assert.AreEqual("Hello World", "Hello World".ChangeEncoding(Encoding.UTF8, Encoding.ASCII)); + } + + [TestMethod] + public void ChangeEncoding_ShouldThrow_GivenNullString() + { + string? value = null; + Assert.ThrowsException(() => value!.ChangeEncoding(Encoding.UTF8, Encoding.ASCII)); + } + + [TestMethod] + public void ChangeEncoding_ShouldThrow_GivenNullSourceEncoding() + { + Assert.ThrowsException(() => "Hello World".ChangeEncoding(null!, Encoding.ASCII)); + } + + [TestMethod] + public void ChangeEncoding_ShouldThrow_GivenNullDestinationEncoding() + { + Assert.ThrowsException(() => "Hello World".ChangeEncoding(Encoding.UTF8, null!)); + } + + [TestMethod] + public void EnumParse_ShouldReturnCorrectValue_GivenString() + { + Assert.AreEqual(DayOfWeek.Monday, "Monday".EnumParse(false)); + Assert.AreEqual(DayOfWeek.Tuesday, "Tuesday".EnumParse(false)); + Assert.AreEqual(DayOfWeek.Wednesday, "Wednesday".EnumParse(false)); + Assert.AreEqual(DayOfWeek.Thursday, "Thursday".EnumParse(false)); + Assert.AreEqual(DayOfWeek.Friday, "Friday".EnumParse(false)); + Assert.AreEqual(DayOfWeek.Saturday, "Saturday".EnumParse(false)); + Assert.AreEqual(DayOfWeek.Sunday, "Sunday".EnumParse(false)); + } + + [TestMethod] + public void EnumParse_ShouldTrim() + { + Assert.AreEqual(DayOfWeek.Monday, " Monday ".EnumParse()); + Assert.AreEqual(DayOfWeek.Tuesday, " Tuesday ".EnumParse()); + Assert.AreEqual(DayOfWeek.Wednesday, " Wednesday ".EnumParse()); + Assert.AreEqual(DayOfWeek.Thursday, " Thursday ".EnumParse()); + Assert.AreEqual(DayOfWeek.Friday, " Friday ".EnumParse()); + Assert.AreEqual(DayOfWeek.Saturday, " Saturday ".EnumParse()); + Assert.AreEqual(DayOfWeek.Sunday, " Sunday ".EnumParse()); + } + + [TestMethod] + public void EnumParse_ShouldReturnCorrectValue_GivenString_Generic() + { + Assert.AreEqual(DayOfWeek.Monday, "Monday".EnumParse()); + Assert.AreEqual(DayOfWeek.Tuesday, "Tuesday".EnumParse()); + Assert.AreEqual(DayOfWeek.Wednesday, "Wednesday".EnumParse()); + Assert.AreEqual(DayOfWeek.Thursday, "Thursday".EnumParse()); + Assert.AreEqual(DayOfWeek.Friday, "Friday".EnumParse()); + Assert.AreEqual(DayOfWeek.Saturday, "Saturday".EnumParse()); + Assert.AreEqual(DayOfWeek.Sunday, "Sunday".EnumParse()); + } + + [TestMethod] + public void EnumParse_ShouldThrow_GivenNullString() + { + string? value = null; + Assert.ThrowsException(() => value!.EnumParse()); + } + + [TestMethod] + public void EnumParse_ShouldThrow_GivenEmptyOrWhiteSpaceString() + { + Assert.ThrowsException(() => string.Empty.EnumParse()); + Assert.ThrowsException(() => " ".EnumParse()); + } + + [TestMethod] + public void FromJson_ShouldDeserializeCorrectly_GivenJsonString() { const string json = "{\"values\": [1, 2, 3]}"; var target = json.FromJson(); @@ -60,21 +164,64 @@ public class StringTests } [TestMethod] - public void IsLowerShouldBeCorrect() + public void GetBytes_ShouldReturnUtf8Bytes_GivenHelloWorld() + { + var expected = new byte[] {0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64}; + byte[] actual = "Hello World".GetBytes(); + + CollectionAssert.AreEqual(expected, actual); + } + + [TestMethod] + public void GetBytes_ShouldReturnAsciiBytes_GivenHelloWorld() + { + var expected = new byte[] {0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64}; + byte[] actual = "Hello World".GetBytes(Encoding.ASCII); + + CollectionAssert.AreEqual(expected, actual); + } + + [TestMethod] + public void GetBytes_ShouldThrow_GivenNullString() + { + string? value = null; + Assert.ThrowsException(() => value!.GetBytes()); + Assert.ThrowsException(() => value!.GetBytes(Encoding.ASCII)); + } + + [TestMethod] + public void GetBytes_ShouldThrow_GivenNullEncoding() + { + Assert.ThrowsException(() => "Hello World".GetBytes(null!)); + } + + [TestMethod] + public void IsLower_ShouldReturnTrue_GivenLowercaseString() { Assert.IsTrue("hello world".IsLower()); - Assert.IsFalse("HELLO WORLD".IsLower()); + } + + [TestMethod] + public void IsLower_ShouldReturnFalse_GivenMixedCaseString() + { Assert.IsFalse("Hello World".IsLower()); } [TestMethod] - public void IsLowerNullShouldThrow() + public void IsLower_ShouldReturnFalse_GivenUppercaseString() { - Assert.ThrowsException(() => ((string?)null)!.IsLower()); + Assert.IsFalse("HELLO WORLD".IsLower()); } [TestMethod] - public void IsPalindromeShouldBeCorrect() + public void IsLower_ShouldThrow_GivenNull() + { + string? value = null; + Assert.ThrowsException(() => value!.IsLower()); + } + + [TestMethod] + public void IsPalindrome_ShouldBeCorrect_GivenString() { const string inputA = "Race car"; const string inputB = "Racecar"; @@ -92,33 +239,71 @@ public class StringTests } [TestMethod] - public void IsPalindromeEmptyShouldBeFalse() + public void IsPalindrome_ShouldReturnFalse_GivenEmptyString() { Assert.IsFalse(string.Empty.IsPalindrome()); } [TestMethod] - public void IsPalindromeNullShouldThrow() + public void IsPalindrome_ShouldThrow_GivenNull() { Assert.ThrowsException(() => ((string?)null)!.IsPalindrome()); } [TestMethod] - public void IsUpperShouldBeCorrect() + public void IsUpper_ShouldReturnFalse_GivenLowercaseString() { - Assert.IsTrue("HELLO WORLD".IsUpper()); Assert.IsFalse("hello world".IsUpper()); + } + + [TestMethod] + public void IsUpper_ShouldReturnFalse_GivenMixedCaseString() + { Assert.IsFalse("Hello World".IsUpper()); } [TestMethod] - public void IsUpperNullShouldThrow() + public void IsUpper_ShouldReturnTrue_GivenUppercaseString() { - Assert.ThrowsException(() => ((string?)null)!.IsUpper()); + Assert.IsTrue("HELLO WORLD".IsUpper()); } [TestMethod] - public void RepeatShouldBeCorrect() + public void IsUpper_ShouldThrow_GivenNull() + { + string? value = null; + Assert.ThrowsException(() => value!.IsUpper()); + } + + [TestMethod] + public void Randomize_ShouldReorder_GivenString() + { + const string input = "Hello World"; + var random = new Random(1); + Assert.AreEqual("le rooldeoH", input.Randomize(input.Length, random)); + } + + [TestMethod] + public void Randomize_ShouldReturnEmptyString_GivenLength1() + { + Assert.AreEqual(string.Empty, "Hello World".Randomize(0)); + } + + [TestMethod] + public void Randomize_ShouldThrow_GivenNull() + { + string? value = null; + Assert.ThrowsException(() => value!.Randomize(1)); + } + + [TestMethod] + public void Randomize_ShouldThrow_GivenNegativeLength() + { + Assert.ThrowsException(() => string.Empty.Randomize(-1)); + } + + [TestMethod] + public void Repeat_ShouldReturnRepeatedString_GivenString() { const string expected = "aaaaaaaaaa"; string actual = "a".Repeat(10); @@ -127,7 +312,13 @@ public class StringTests } [TestMethod] - public void RepeatOneCountShouldBeLength1String() + public void Repeat_ShouldReturnEmptyString_GivenCount0() + { + Assert.AreEqual(string.Empty, "a".Repeat(0)); + } + + [TestMethod] + public void Repeat_ShouldReturnItself_GivenCount1() { string repeated = "a".Repeat(1); Assert.AreEqual(1, repeated.Length); @@ -135,25 +326,20 @@ public class StringTests } [TestMethod] - public void RepeatZeroCountShouldBeEmpty() - { - Assert.AreEqual(string.Empty, "a".Repeat(0)); - } - - [TestMethod] - public void RepeatNegativeCountShouldThrow() + public void Repeat_ShouldThrow_GivenNegativeCount() { Assert.ThrowsException(() => "a".Repeat(-1)); } [TestMethod] - public void RepeatNullShouldThrow() + public void Repeat_ShouldThrow_GivenNull() { - Assert.ThrowsException(() => ((string?)null)!.Repeat(0)); + string? value = null; + Assert.ThrowsException(() => value!.Repeat(0)); } [TestMethod] - public void ReverseShouldBeCorrect() + public void Reverse_ShouldBeCorrect() { const string input = "Hello World"; const string expected = "dlroW olleH"; @@ -166,13 +352,14 @@ public class StringTests } [TestMethod] - public void ReverseNullShouldThrow() + public void Reverse_ShouldThrow_GivenNull() { - Assert.ThrowsException(() => ((string?)null)!.Reverse()); + string? value = null; + Assert.ThrowsException(() => value!.Reverse()); } [TestMethod] - public void ShuffleShouldReorder() + public void Shuffled_ShouldReorder_GivenString() { const string alphabet = "abcdefghijklmnopqrstuvwxyz"; string shuffled = alphabet; @@ -185,13 +372,44 @@ public class StringTests } [TestMethod] - public void NullShuffleShouldThrow() + public void Shuffled_ShouldThrow_GivenNull() { - Assert.ThrowsException(() => ((string?)null)!.Shuffled()); + string? value = null; + Assert.ThrowsException(() => value!.Shuffled()); } [TestMethod] - public void WithEmptyAlternativeShouldBeCorrect() + public void Split_ShouldYieldCorrectStrings_GivenString() + { + string[] chunks = "Hello World".Split(2).ToArray(); + Assert.AreEqual(6, chunks.Length); + Assert.AreEqual("He", chunks[0]); + Assert.AreEqual("ll", chunks[1]); + Assert.AreEqual("o ", chunks[2]); + Assert.AreEqual("Wo", chunks[3]); + Assert.AreEqual("rl", chunks[4]); + Assert.AreEqual("d", chunks[5]); + } + + [TestMethod] + public void Split_ShouldYieldEmptyString_GivenChunkSize0() + { + string[] chunks = "Hello World".Split(0).ToArray(); + Assert.AreEqual(1, chunks.Length); + Assert.AreEqual(string.Empty, chunks[0]); + } + + [TestMethod] + public void Split_ShouldThrow_GivenNullString() + { + string? value = null; + + // forcing enumeration with ToArray is required for the exception to be thrown + Assert.ThrowsException(() => value!.Split(0).ToArray()); + } + + [TestMethod] + public void WithEmptyAlternative_ShouldBeCorrect() { const string inputA = "Hello World"; const string inputB = " "; @@ -212,7 +430,7 @@ public class StringTests } [TestMethod] - public void WithWhiteSpaceAlternativeShouldBeCorrect() + public void WithWhiteSpaceAlternative_ShouldBeCorrect() { const string input = " "; const string alternative = "ALTERNATIVE"; diff --git a/X10D.Tests/src/Time/StringTests.cs b/X10D.Tests/src/Time/StringTests.cs new file mode 100644 index 0000000..9b5e68c --- /dev/null +++ b/X10D.Tests/src/Time/StringTests.cs @@ -0,0 +1,38 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Time; + +namespace X10D.Tests.Time; + +[TestClass] +public class StringTests +{ + [TestMethod] + public void ToTimeSpan_ShouldReturnCorrectTimeSpan_GivenString() + { + const string value = "1y 1mo 1w 1d 1h 1m 1s 1ms"; + + TimeSpan expected = TimeSpan.FromMilliseconds(1); + expected += TimeSpan.FromSeconds(1); + expected += TimeSpan.FromMinutes(1); + expected += TimeSpan.FromHours(1); + expected += TimeSpan.FromDays(1); + expected += TimeSpan.FromDays(7); + expected += TimeSpan.FromDays(30); + expected += TimeSpan.FromDays(365); + + Assert.AreEqual(expected, value.ToTimeSpan()); + } + + [TestMethod] + public void ToTimeSpan_ShouldReturnZero_GivenInvalidString() + { + Assert.AreEqual(TimeSpan.Zero, "Hello World".ToTimeSpan()); + } + + [TestMethod] + public void ToTimeSpan_ShouldThrow_GivenNullString() + { + string? value = null; + Assert.ThrowsException(() => value!.ToTimeSpan()); + } +} diff --git a/X10D/src/StringExtensions/StringExtensions.cs b/X10D/src/StringExtensions/StringExtensions.cs deleted file mode 100644 index c51a2e8..0000000 --- a/X10D/src/StringExtensions/StringExtensions.cs +++ /dev/null @@ -1,266 +0,0 @@ -using System.Diagnostics.Contracts; -using System.Runtime.CompilerServices; -using System.Text; -using X10D.Core; - -namespace X10D; - -/// -/// Extension methods for . -/// -public static class StringExtensions -{ - /// - /// Converts the specified string, which encodes binary data as base-64 digits, to an equivalent plain text string. - /// - /// The base-64 string to convert. - /// The plain text string representation of . - /// is . - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static string Base64Decode(this string value) - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - - return Convert.FromBase64String(value).ToString(Encoding.ASCII); - } - - /// - /// Converts the current string to its equivalent string representation that is encoded with base-64 digits. - /// - /// The plain text string to convert. - /// The string representation, in base 64, of . - /// is . - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static string Base64Encode(this string value) - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - - return Convert.ToBase64String(value.GetBytes(Encoding.ASCII)); - } - - /// - /// Converts this string from one encoding to another. - /// - /// The input string. - /// The input encoding. - /// The output encoding. - /// - /// Returns a new with its data converted to - /// . - /// - /// - /// is - /// - or - - /// is - /// -or - /// is . - /// - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static string ChangeEncoding(this string value, Encoding sourceEncoding, Encoding destinationEncoding) - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - - if (sourceEncoding is null) - { - throw new ArgumentNullException(nameof(sourceEncoding)); - } - - if (destinationEncoding is null) - { - throw new ArgumentNullException(nameof(destinationEncoding)); - } - - return value.GetBytes(sourceEncoding).ToString(destinationEncoding); - } - - /// - /// Parses a into an . - /// - /// The type of the . - /// The value to parse. - /// The value corresponding to the . - /// - /// Credit for this method goes to Scott Dorman: - /// (http://geekswithblogs.net/sdorman/Default.aspx). - /// - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static T EnumParse(this string value) - where T : struct, Enum - { - return value.EnumParse(false); - } - - /// - /// Parses a into an . - /// - /// The type of the . - /// The value to parse. - /// Whether or not to ignore casing. - /// The value corresponding to the . - /// - /// Credit for this method goes to Scott Dorman: - /// (http://geekswithblogs.net/sdorman/Default.aspx). - /// - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static T EnumParse(this string value, bool ignoreCase) - where T : struct, Enum - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - - value = value.Trim(); - - if (string.IsNullOrWhiteSpace(value)) - { - throw new ArgumentException(Resource.EnumParseEmptyStringException, nameof(value)); - } - - return Enum.Parse(value, ignoreCase); - } - - /// - /// Gets a [] representing the value the with - /// encoding. - /// - /// The string to convert. - /// Returns a []. - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static byte[] GetBytes(this string value) - { - return value.GetBytes(Encoding.UTF8); - } - - /// - /// Gets a [] representing the value the with the provided encoding. - /// - /// The string to convert. - /// The encoding to use. - /// Returns a []. - /// - /// or or both are - /// . - /// - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static byte[] GetBytes(this string value, Encoding encoding) - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - - if (encoding is null) - { - throw new ArgumentNullException(nameof(encoding)); - } - - return encoding.GetBytes(value); - } - - /// - /// Returns a new string of a specified length by randomly selecting characters from the current string. - /// - /// The pool of characters to use. - /// The length of the new string returned. - /// The supplier. - /// - /// A new string whose length is equal to which contains randomly selected characters from - /// . - /// - /// is . - /// is less than 0. - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static string Randomize(this string source, int length, Random? random = null) - { - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (length < 0) - { - throw new ArgumentOutOfRangeException(nameof(length), ExceptionMessages.LengthGreaterThanOrEqualTo0); - } - - if (length == 0) - { - return string.Empty; - } - - random ??= Random.Shared; - - char[] array = source.ToCharArray(); - var builder = new StringBuilder(length); - - while (builder.Length < length) - { - char next = random.NextFrom(array); - builder.Append(next); - } - - return builder.ToString(); - } - - /// - /// Splits the into chunks that are no greater than in length. - /// - /// The string to split. - /// The maximum length of each string in the returned result. - /// - /// Returns an containing instances which are no - /// greater than in length. - /// - /// is . - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static IEnumerable Split(this string value, int chunkSize) - { - if (value is null) - { - throw new ArgumentNullException(nameof(value)); - } - - for (var i = 0; i < value.Length; i += chunkSize) - { - yield return value[i..System.Math.Min(i + chunkSize, value.Length - 1)]; - } - } - - /// - /// Parses a shorthand time span string (e.g. 3w 2d 1.5h) and converts it to an instance of . - /// - /// The input string. - /// Returns an instance of . - /// is . - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static TimeSpan ToTimeSpan(this string input) - { - if (input is null) - { - throw new ArgumentNullException(nameof(input)); - } - - return TimeSpanParser.TryParse(input, out TimeSpan result) - ? result - : default; - } -} diff --git a/X10D/src/Text/StringBuilderReader.cs b/X10D/src/Text/StringBuilderReader.cs index 13f2737..fe6e969 100644 --- a/X10D/src/Text/StringBuilderReader.cs +++ b/X10D/src/Text/StringBuilderReader.cs @@ -189,7 +189,14 @@ public class StringBuilderReader : TextReader _index++; } - return _stringBuilder.ToString(start, _index - start - 1); + var result = _stringBuilder.ToString(start, _index - start); + + if (result.Length > 0 && (result[^1] == '\n' || result[^1] == '\r')) + { + result = result[..^1]; + } + + return result; } /// diff --git a/X10D/src/Text/StringExtensions.cs b/X10D/src/Text/StringExtensions.cs index 803cba8..2f83e3c 100644 --- a/X10D/src/Text/StringExtensions.cs +++ b/X10D/src/Text/StringExtensions.cs @@ -4,6 +4,8 @@ using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using X10D.Collections; +using X10D.Core; +using X10D.IO; namespace X10D.Text; @@ -45,6 +47,130 @@ public static class StringExtensions return value.WithWhiteSpaceAlternative(null); } + /// + /// Converts the specified string, which encodes binary data as base-64 digits, to an equivalent plain text string. + /// + /// The base-64 string to convert. + /// The plain text string representation of . + /// is . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static string Base64Decode(this string value) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + return Convert.FromBase64String(value).ToString(Encoding.ASCII); + } + + /// + /// Converts the current string to its equivalent string representation that is encoded with base-64 digits. + /// + /// The plain text string to convert. + /// The string representation, in base 64, of . + /// is . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static string Base64Encode(this string value) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + return Convert.ToBase64String(value.GetBytes(Encoding.ASCII)); + } + + /// + /// Converts this string from one encoding to another. + /// + /// The input string. + /// The input encoding. + /// The output encoding. + /// + /// Returns a new with its data converted to + /// . + /// + /// + /// is + /// - or - + /// is + /// -or + /// is . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static string ChangeEncoding(this string value, Encoding sourceEncoding, Encoding destinationEncoding) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (sourceEncoding is null) + { + throw new ArgumentNullException(nameof(sourceEncoding)); + } + + if (destinationEncoding is null) + { + throw new ArgumentNullException(nameof(destinationEncoding)); + } + + return value.GetBytes(sourceEncoding).ToString(destinationEncoding); + } + + /// + /// Parses a into an . + /// + /// The type of the . + /// The value to parse. + /// The value corresponding to the . + /// + /// Credit for this method goes to Scott Dorman: + /// (http://geekswithblogs.net/sdorman/Default.aspx). + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static T EnumParse(this string value) + where T : struct, Enum + { + return value.EnumParse(false); + } + + /// + /// Parses a into an . + /// + /// The type of the . + /// The value to parse. + /// Whether or not to ignore casing. + /// The value corresponding to the . + /// + /// Credit for this method goes to Scott Dorman: + /// (http://geekswithblogs.net/sdorman/Default.aspx). + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static T EnumParse(this string value, bool ignoreCase) + where T : struct, Enum + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + value = value.Trim(); + + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException(Resource.EnumParseEmptyStringException, nameof(value)); + } + + return Enum.Parse(value, ignoreCase); + } + /// /// Returns an object from the specified JSON string. /// @@ -61,6 +187,46 @@ public static class StringExtensions return JsonSerializer.Deserialize(value, options); } + /// + /// Gets a [] representing the value the with + /// encoding. + /// + /// The string to convert. + /// Returns a []. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static byte[] GetBytes(this string value) + { + return value.GetBytes(Encoding.UTF8); + } + + /// + /// Gets a [] representing the value the with the provided encoding. + /// + /// The string to convert. + /// The encoding to use. + /// Returns a []. + /// + /// or or both are + /// . + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static byte[] GetBytes(this string value, Encoding encoding) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (encoding is null) + { + throw new ArgumentNullException(nameof(encoding)); + } + + return encoding.GetBytes(value); + } + /// /// Determines if all alpha characters in this string are considered lowercase. /// @@ -217,6 +383,51 @@ public static class StringExtensions return builder.ToString(); } + /// + /// Returns a new string of a specified length by randomly selecting characters from the current string. + /// + /// The pool of characters to use. + /// The length of the new string returned. + /// The supplier. + /// + /// A new string whose length is equal to which contains randomly selected characters from + /// . + /// + /// is . + /// is less than 0. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static string Randomize(this string source, int length, Random? random = null) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length), ExceptionMessages.LengthGreaterThanOrEqualTo0); + } + + if (length == 0) + { + return string.Empty; + } + + random ??= Random.Shared; + + char[] array = source.ToCharArray(); + var builder = new StringBuilder(length); + + while (builder.Length < length) + { + char next = random.NextFrom(array); + builder.Append(next); + } + + return builder.ToString(); + } + /// /// Reverses the current string. /// @@ -272,6 +483,37 @@ public static class StringExtensions return new string(array); } + /// + /// Splits the into chunks that are no greater than in length. + /// + /// The string to split. + /// The maximum length of each string in the returned result. + /// + /// Returns an containing instances which are no + /// greater than in length. + /// + /// is . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static IEnumerable Split(this string value, int chunkSize) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (chunkSize == 0) + { + yield return string.Empty; + yield break; + } + + for (var i = 0; i < value.Length; i += chunkSize) + { + yield return value[i..System.Math.Min(i + chunkSize, value.Length)]; + } + } + /// /// Normalizes a string which may be either or empty to a specified alternative. /// diff --git a/X10D/src/Time/StringExtensions.cs b/X10D/src/Time/StringExtensions.cs new file mode 100644 index 0000000..6ee6afd --- /dev/null +++ b/X10D/src/Time/StringExtensions.cs @@ -0,0 +1,72 @@ +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; + +namespace X10D.Time; + +/// +/// Extension methods for . +/// +public static class StringExtensions +{ + /// + /// Parses a shorthand time span string (e.g. 3w 2d 1h) and converts it to an instance of . + /// + /// + /// The input string. Floating point is not supported, but range the following units are supported: + /// + /// + /// + /// Suffix + /// Meaning + /// + /// + /// + /// ms + /// Milliseconds + /// + /// + /// s + /// Seconds + /// + /// + /// m + /// Minutes + /// + /// + /// h + /// Hours + /// + /// + /// d + /// Days + /// + /// + /// w + /// Weeks + /// + /// + /// mo + /// Months + /// + /// + /// y + /// Years + /// + /// + /// + /// A new instance of . + /// is . + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static TimeSpan ToTimeSpan(this string input) + { + if (input is null) + { + throw new ArgumentNullException(nameof(input)); + } + + return TimeSpanParser.TryParse(input, out TimeSpan result) + ? result + : default; + } +} diff --git a/X10D/src/Time/TimeSpanParser.cs b/X10D/src/Time/TimeSpanParser.cs new file mode 100644 index 0000000..87994c3 --- /dev/null +++ b/X10D/src/Time/TimeSpanParser.cs @@ -0,0 +1,133 @@ +namespace X10D.Time; + +/// +/// Represents a class which contains a parser which converts into . +/// +public static class TimeSpanParser +{ + /// + /// Attempts to parses a shorthand time span string (e.g. 3w 2d 1h), converting it to an instance of + /// which represents that duration of time. + /// + /// + /// The input string. Floating point is not supported, but range the following units are supported: + /// + /// + /// + /// Suffix + /// Meaning + /// + /// + /// + /// ms + /// Milliseconds + /// + /// + /// s + /// Seconds + /// + /// + /// m + /// Minutes + /// + /// + /// h + /// Hours + /// + /// + /// d + /// Days + /// + /// + /// w + /// Weeks + /// + /// + /// mo + /// Months + /// + /// + /// y + /// Years + /// + /// + /// + /// When this method returns, contains the parsed result. + /// if the parse was successful, otherwise. + /// is . + public static bool TryParse(string value, out TimeSpan result) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + result = TimeSpan.Zero; + var unitValue = 0; + + for (var index = 0; index < value.Length; index++) + { + char current = value[index]; + switch (current) + { + case var digitChar when char.IsDigit(digitChar): + var digit = (int)char.GetNumericValue(digitChar); + unitValue = unitValue * 10 + digit; + break; + + case 'y': + result += TimeSpan.FromDays(unitValue * 365); + unitValue = 0; + break; + + case 'm': + if (index < value.Length - 1 && value[index + 1] == 'o') + { + index++; + result += TimeSpan.FromDays(unitValue * 30); + } + else if (index < value.Length - 1 && value[index + 1] == 's') + { + index++; + result += TimeSpan.FromMilliseconds(unitValue); + } + else + { + result += TimeSpan.FromMinutes(unitValue); + } + + unitValue = 0; + break; + + case 'w': + result += TimeSpan.FromDays(unitValue * 7); + unitValue = 0; + break; + + case 'd': + result += TimeSpan.FromDays(unitValue); + unitValue = 0; + break; + + case 'h': + result += TimeSpan.FromHours(unitValue); + unitValue = 0; + break; + + case 's': + result += TimeSpan.FromSeconds(unitValue); + unitValue = 0; + break; + + case var space when char.IsWhiteSpace(space): + break; + + default: + result = TimeSpan.Zero; + return false; + } + } + + return true; + } +} diff --git a/X10D/src/TimeSpanParser.cs b/X10D/src/TimeSpanParser.cs deleted file mode 100644 index 9f53f0d..0000000 --- a/X10D/src/TimeSpanParser.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Globalization; -using System.Text.RegularExpressions; - -namespace X10D; - -/// -/// Represents a class which contains a parser which converts into . -/// -public static class TimeSpanParser -{ - private const string RealNumberPattern = @"(\d*\.\d+|\d+)"; - - private static readonly string Pattern = $"^(?:{RealNumberPattern} *y)? *" + - $"^(?:{RealNumberPattern} *mo)? *" + - $"^(?:{RealNumberPattern} *w)? *" + - $"(?:{RealNumberPattern} *d)? *" + - $"(?:{RealNumberPattern} *h)? *" + - $"(?:{RealNumberPattern} *m)? *" + - $"(?:{RealNumberPattern} *s)? *" + - $"(?:{RealNumberPattern} *ms)?$"; - - private static readonly Regex Regex = new(Pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); - - /// - /// Attempts to parses a shorthand time span string (e.g. 3w 2d 1.5h), converting it to an instance of - /// which represents that duration of time. - /// - /// The input string. - /// The parsed result. - /// The format provider. - /// if the parse was successful, otherwise. - public static bool TryParse(string input, out TimeSpan result, IFormatProvider? provider = null) - { - result = default; - - Match? match = Regex.Match(input); - - if (!match.Success) - { - return false; - } - - bool TryParseAt(int group, out double parsedResult) - { - parsedResult = 0; - - return match.Groups[group].Success - && double.TryParse(match.Groups[group].Value, NumberStyles.Number, provider, out parsedResult); - } - - if (!TryParseAt(1, out double years)) - { - return false; - } - - if (!TryParseAt(2, out double months)) - { - return false; - } - - if (!TryParseAt(3, out double weeks)) - { - return false; - } - - if (!TryParseAt(4, out double days)) - { - return false; - } - - if (!TryParseAt(5, out double hours)) - { - return false; - } - - if (!TryParseAt(6, out double minutes)) - { - return false; - } - - if (!TryParseAt(7, out double seconds)) - { - return false; - } - - if (!TryParseAt(8, out double milliseconds)) - { - return false; - } - - result += TimeSpan.FromDays(years * 365); - result += TimeSpan.FromDays(months * 30); - result += TimeSpan.FromDays(weeks * 7); - result += TimeSpan.FromDays(days); - result += TimeSpan.FromHours(hours); - result += TimeSpan.FromMinutes(minutes); - result += TimeSpan.FromSeconds(seconds); - result += TimeSpan.FromMilliseconds(milliseconds); - - return true; - } -} \ No newline at end of file