Migrate string extensions to appropriate namespaces (#7)

Introduces more tests
This commit is contained in:
Oliver Booth 2022-04-30 13:08:39 +01:00
parent c13cc934b6
commit a6139a5720
No known key found for this signature in database
GPG Key ID: 32A00B35503AF634
10 changed files with 1016 additions and 460 deletions

View File

@ -1,57 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace X10D.Tests.Core;
/// <summary>
/// Tests for <see cref="StringExtensions" />.
/// </summary>
[TestClass]
public class StringTests
{
/// <summary>
/// Tests <see cref="StringExtensions.Base64Decode" />.
/// </summary>
[TestMethod]
public void Base64Decode()
{
const string input = "SGVsbG8gV29ybGQ=";
const string expected = "Hello World";
string result = input.Base64Decode();
Assert.AreEqual(expected, result);
}
/// <summary>
/// Tests <see cref="StringExtensions.Base64Encode" />.
/// </summary>
[TestMethod]
public void Base64Encode()
{
const string input = "Hello World";
const string expected = "SGVsbG8gV29ybGQ=";
string result = input.Base64Encode();
Assert.AreEqual(expected, result);
}
/// <summary>
/// Tests <see cref="StringExtensions.Randomize" />.
/// </summary>
[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<ArgumentNullException>(() => ((string?)null)!.Randomize(1));
Assert.ThrowsException<ArgumentOutOfRangeException>(() => input.Randomize(-1));
Assert.AreEqual(string.Empty, string.Empty.Randomize(0));
Assert.AreEqual(expected, result);
}
}

View File

@ -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<ArgumentNullException>(() =>
{
using var reader = new StringBuilderReader(new StringBuilder("Hello\nWorld"));
reader.Read(null!, 0, 5);
reader.Close();
});
}
[TestMethod]
public void Read_ShouldThrow_GivenNegativeIndex()
{
Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
{
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<ArgumentOutOfRangeException>(() =>
{
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<ArgumentException>(() =>
{
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<char> 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<char> 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<char> 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<char> 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();
}
}

View File

@ -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<ArgumentNullException>(() => 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<ArgumentNullException>(() => 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<ArgumentNullException>(() => value!.ChangeEncoding(Encoding.UTF8, Encoding.ASCII));
}
[TestMethod]
public void ChangeEncoding_ShouldThrow_GivenNullSourceEncoding()
{
Assert.ThrowsException<ArgumentNullException>(() => "Hello World".ChangeEncoding(null!, Encoding.ASCII));
}
[TestMethod]
public void ChangeEncoding_ShouldThrow_GivenNullDestinationEncoding()
{
Assert.ThrowsException<ArgumentNullException>(() => "Hello World".ChangeEncoding(Encoding.UTF8, null!));
}
[TestMethod]
public void EnumParse_ShouldReturnCorrectValue_GivenString()
{
Assert.AreEqual(DayOfWeek.Monday, "Monday".EnumParse<DayOfWeek>(false));
Assert.AreEqual(DayOfWeek.Tuesday, "Tuesday".EnumParse<DayOfWeek>(false));
Assert.AreEqual(DayOfWeek.Wednesday, "Wednesday".EnumParse<DayOfWeek>(false));
Assert.AreEqual(DayOfWeek.Thursday, "Thursday".EnumParse<DayOfWeek>(false));
Assert.AreEqual(DayOfWeek.Friday, "Friday".EnumParse<DayOfWeek>(false));
Assert.AreEqual(DayOfWeek.Saturday, "Saturday".EnumParse<DayOfWeek>(false));
Assert.AreEqual(DayOfWeek.Sunday, "Sunday".EnumParse<DayOfWeek>(false));
}
[TestMethod]
public void EnumParse_ShouldTrim()
{
Assert.AreEqual(DayOfWeek.Monday, " Monday ".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Tuesday, " Tuesday ".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Wednesday, " Wednesday ".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Thursday, " Thursday ".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Friday, " Friday ".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Saturday, " Saturday ".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Sunday, " Sunday ".EnumParse<DayOfWeek>());
}
[TestMethod]
public void EnumParse_ShouldReturnCorrectValue_GivenString_Generic()
{
Assert.AreEqual(DayOfWeek.Monday, "Monday".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Tuesday, "Tuesday".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Wednesday, "Wednesday".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Thursday, "Thursday".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Friday, "Friday".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Saturday, "Saturday".EnumParse<DayOfWeek>());
Assert.AreEqual(DayOfWeek.Sunday, "Sunday".EnumParse<DayOfWeek>());
}
[TestMethod]
public void EnumParse_ShouldThrow_GivenNullString()
{
string? value = null;
Assert.ThrowsException<ArgumentNullException>(() => value!.EnumParse<DayOfWeek>());
}
[TestMethod]
public void EnumParse_ShouldThrow_GivenEmptyOrWhiteSpaceString()
{
Assert.ThrowsException<ArgumentException>(() => string.Empty.EnumParse<DayOfWeek>());
Assert.ThrowsException<ArgumentException>(() => " ".EnumParse<DayOfWeek>());
}
[TestMethod]
public void FromJson_ShouldDeserializeCorrectly_GivenJsonString()
{
const string json = "{\"values\": [1, 2, 3]}";
var target = json.FromJson<SampleStructure>();
@ -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<ArgumentNullException>(() => value!.GetBytes());
Assert.ThrowsException<ArgumentNullException>(() => value!.GetBytes(Encoding.ASCII));
}
[TestMethod]
public void GetBytes_ShouldThrow_GivenNullEncoding()
{
Assert.ThrowsException<ArgumentNullException>(() => "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<ArgumentNullException>(() => ((string?)null)!.IsLower());
Assert.IsFalse("HELLO WORLD".IsLower());
}
[TestMethod]
public void IsPalindromeShouldBeCorrect()
public void IsLower_ShouldThrow_GivenNull()
{
string? value = null;
Assert.ThrowsException<ArgumentNullException>(() => 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<ArgumentNullException>(() => ((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<ArgumentNullException>(() => ((string?)null)!.IsUpper());
Assert.IsTrue("HELLO WORLD".IsUpper());
}
[TestMethod]
public void RepeatShouldBeCorrect()
public void IsUpper_ShouldThrow_GivenNull()
{
string? value = null;
Assert.ThrowsException<ArgumentNullException>(() => 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<ArgumentNullException>(() => value!.Randomize(1));
}
[TestMethod]
public void Randomize_ShouldThrow_GivenNegativeLength()
{
Assert.ThrowsException<ArgumentOutOfRangeException>(() => 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<ArgumentOutOfRangeException>(() => "a".Repeat(-1));
}
[TestMethod]
public void RepeatNullShouldThrow()
public void Repeat_ShouldThrow_GivenNull()
{
Assert.ThrowsException<ArgumentNullException>(() => ((string?)null)!.Repeat(0));
string? value = null;
Assert.ThrowsException<ArgumentNullException>(() => 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<ArgumentNullException>(() => ((string?)null)!.Reverse());
string? value = null;
Assert.ThrowsException<ArgumentNullException>(() => 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<ArgumentNullException>(() => ((string?)null)!.Shuffled());
string? value = null;
Assert.ThrowsException<ArgumentNullException>(() => 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<ArgumentNullException>(() => 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";

View File

@ -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<ArgumentNullException>(() => value!.ToTimeSpan());
}
}

View File

@ -1,266 +0,0 @@
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Text;
using X10D.Core;
namespace X10D;
/// <summary>
/// Extension methods for <see cref="string" />.
/// </summary>
public static class StringExtensions
{
/// <summary>
/// Converts the specified string, which encodes binary data as base-64 digits, to an equivalent plain text string.
/// </summary>
/// <param name="value">The base-64 string to convert.</param>
/// <returns>The plain text string representation of <paramref name="value" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
[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);
}
/// <summary>
/// Converts the current string to its equivalent string representation that is encoded with base-64 digits.
/// </summary>
/// <param name="value">The plain text string to convert.</param>
/// <returns>The string representation, in base 64, of <paramref name="value" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
[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));
}
/// <summary>
/// Converts this string from one encoding to another.
/// </summary>
/// <param name="value">The input string.</param>
/// <param name="sourceEncoding">The input encoding.</param>
/// <param name="destinationEncoding">The output encoding.</param>
/// <returns>
/// Returns a new <see cref="string" /> with its data converted to
/// <paramref name="destinationEncoding" />.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="value" /> is <see langword="null" />
/// - or -
/// <paramref name="sourceEncoding" /> is <see langword="null" />
/// -or
/// <paramref name="destinationEncoding" /> is <see langword="null" />.
/// </exception>
[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);
}
/// <summary>
/// Parses a <see cref="string" /> into an <see cref="Enum" />.
/// </summary>
/// <typeparam name="T">The type of the <see cref="Enum" />.</typeparam>
/// <param name="value">The <see cref="string" /> value to parse.</param>
/// <returns>The <see cref="Enum" /> value corresponding to the <see cref="string" />.</returns>
/// <remarks>
/// Credit for this method goes to Scott Dorman:
/// (http://geekswithblogs.net/sdorman/Default.aspx).
/// </remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static T EnumParse<T>(this string value)
where T : struct, Enum
{
return value.EnumParse<T>(false);
}
/// <summary>
/// Parses a <see cref="string" /> into an <see cref="Enum" />.
/// </summary>
/// <typeparam name="T">The type of the <see cref="Enum" />.</typeparam>
/// <param name="value">The <see cref="string" /> value to parse.</param>
/// <param name="ignoreCase">Whether or not to ignore casing.</param>
/// <returns>The <see cref="Enum" /> value corresponding to the <see cref="string" />.</returns>
/// <remarks>
/// Credit for this method goes to Scott Dorman:
/// (http://geekswithblogs.net/sdorman/Default.aspx).
/// </remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static T EnumParse<T>(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<T>(value, ignoreCase);
}
/// <summary>
/// Gets a <see cref="byte" />[] representing the value the <see cref="string" /> with
/// <see cref="Encoding.UTF8" /> encoding.
/// </summary>
/// <param name="value">The string to convert.</param>
/// <returns>Returns a <see cref="byte" />[].</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static byte[] GetBytes(this string value)
{
return value.GetBytes(Encoding.UTF8);
}
/// <summary>
/// Gets a <see cref="byte" />[] representing the value the <see cref="string" /> with the provided encoding.
/// </summary>
/// <param name="value">The string to convert.</param>
/// <param name="encoding">The encoding to use.</param>
/// <returns>Returns a <see cref="byte" />[].</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="value" /> or <paramref name="encoding" /> or both are
/// <see langword="null" />.
/// </exception>
[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);
}
/// <summary>
/// Returns a new string of a specified length by randomly selecting characters from the current string.
/// </summary>
/// <param name="source">The pool of characters to use.</param>
/// <param name="length">The length of the new string returned.</param>
/// <param name="random">The <see cref="System.Random" /> supplier.</param>
/// <returns>
/// A new string whose length is equal to <paramref name="length" /> which contains randomly selected characters from
/// <paramref name="source" />.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="length" /> is less than 0.</exception>
[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();
}
/// <summary>
/// Splits the <see cref="string" /> into chunks that are no greater than <paramref name="chunkSize" /> in length.
/// </summary>
/// <param name="value">The string to split.</param>
/// <param name="chunkSize">The maximum length of each string in the returned result.</param>
/// <returns>
/// Returns an <see cref="IEnumerable{T}" /> containing <see cref="string" /> instances which are no
/// greater than <paramref name="chunkSize" /> in length.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static IEnumerable<string> 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)];
}
}
/// <summary>
/// Parses a shorthand time span string (e.g. 3w 2d 1.5h) and converts it to an instance of <see cref="TimeSpan" />.
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>Returns an instance of <see cref="TimeSpan" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null" />.</exception>
[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;
}
}

View File

@ -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;
}
/// <inheritdoc />

View File

@ -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);
}
/// <summary>
/// Converts the specified string, which encodes binary data as base-64 digits, to an equivalent plain text string.
/// </summary>
/// <param name="value">The base-64 string to convert.</param>
/// <returns>The plain text string representation of <paramref name="value" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
[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);
}
/// <summary>
/// Converts the current string to its equivalent string representation that is encoded with base-64 digits.
/// </summary>
/// <param name="value">The plain text string to convert.</param>
/// <returns>The string representation, in base 64, of <paramref name="value" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
[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));
}
/// <summary>
/// Converts this string from one encoding to another.
/// </summary>
/// <param name="value">The input string.</param>
/// <param name="sourceEncoding">The input encoding.</param>
/// <param name="destinationEncoding">The output encoding.</param>
/// <returns>
/// Returns a new <see cref="string" /> with its data converted to
/// <paramref name="destinationEncoding" />.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="value" /> is <see langword="null" />
/// - or -
/// <paramref name="sourceEncoding" /> is <see langword="null" />
/// -or
/// <paramref name="destinationEncoding" /> is <see langword="null" />.
/// </exception>
[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);
}
/// <summary>
/// Parses a <see cref="string" /> into an <see cref="Enum" />.
/// </summary>
/// <typeparam name="T">The type of the <see cref="Enum" />.</typeparam>
/// <param name="value">The <see cref="string" /> value to parse.</param>
/// <returns>The <see cref="Enum" /> value corresponding to the <see cref="string" />.</returns>
/// <remarks>
/// Credit for this method goes to Scott Dorman:
/// (http://geekswithblogs.net/sdorman/Default.aspx).
/// </remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static T EnumParse<T>(this string value)
where T : struct, Enum
{
return value.EnumParse<T>(false);
}
/// <summary>
/// Parses a <see cref="string" /> into an <see cref="Enum" />.
/// </summary>
/// <typeparam name="T">The type of the <see cref="Enum" />.</typeparam>
/// <param name="value">The <see cref="string" /> value to parse.</param>
/// <param name="ignoreCase">Whether or not to ignore casing.</param>
/// <returns>The <see cref="Enum" /> value corresponding to the <see cref="string" />.</returns>
/// <remarks>
/// Credit for this method goes to Scott Dorman:
/// (http://geekswithblogs.net/sdorman/Default.aspx).
/// </remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static T EnumParse<T>(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<T>(value, ignoreCase);
}
/// <summary>
/// Returns an object from the specified JSON string.
/// </summary>
@ -61,6 +187,46 @@ public static class StringExtensions
return JsonSerializer.Deserialize<T>(value, options);
}
/// <summary>
/// Gets a <see cref="byte" />[] representing the value the <see cref="string" /> with
/// <see cref="Encoding.UTF8" /> encoding.
/// </summary>
/// <param name="value">The string to convert.</param>
/// <returns>Returns a <see cref="byte" />[].</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static byte[] GetBytes(this string value)
{
return value.GetBytes(Encoding.UTF8);
}
/// <summary>
/// Gets a <see cref="byte" />[] representing the value the <see cref="string" /> with the provided encoding.
/// </summary>
/// <param name="value">The string to convert.</param>
/// <param name="encoding">The encoding to use.</param>
/// <returns>Returns a <see cref="byte" />[].</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="value" /> or <paramref name="encoding" /> or both are
/// <see langword="null" />.
/// </exception>
[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);
}
/// <summary>
/// Determines if all alpha characters in this string are considered lowercase.
/// </summary>
@ -217,6 +383,51 @@ public static class StringExtensions
return builder.ToString();
}
/// <summary>
/// Returns a new string of a specified length by randomly selecting characters from the current string.
/// </summary>
/// <param name="source">The pool of characters to use.</param>
/// <param name="length">The length of the new string returned.</param>
/// <param name="random">The <see cref="System.Random" /> supplier.</param>
/// <returns>
/// A new string whose length is equal to <paramref name="length" /> which contains randomly selected characters from
/// <paramref name="source" />.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="length" /> is less than 0.</exception>
[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();
}
/// <summary>
/// Reverses the current string.
/// </summary>
@ -272,6 +483,37 @@ public static class StringExtensions
return new string(array);
}
/// <summary>
/// Splits the <see cref="string" /> into chunks that are no greater than <paramref name="chunkSize" /> in length.
/// </summary>
/// <param name="value">The string to split.</param>
/// <param name="chunkSize">The maximum length of each string in the returned result.</param>
/// <returns>
/// Returns an <see cref="IEnumerable{T}" /> containing <see cref="string" /> instances which are no
/// greater than <paramref name="chunkSize" /> in length.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static IEnumerable<string> 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)];
}
}
/// <summary>
/// Normalizes a string which may be either <see langword="null" /> or empty to a specified alternative.
/// </summary>

View File

@ -0,0 +1,72 @@
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
namespace X10D.Time;
/// <summary>
/// Extension methods for <see cref="string" />.
/// </summary>
public static class StringExtensions
{
/// <summary>
/// Parses a shorthand time span string (e.g. 3w 2d 1h) and converts it to an instance of <see cref="TimeSpan" />.
/// </summary>
/// <param name="input">
/// The input string. Floating point is not supported, but range the following units are supported:
///
/// <list type="table">
/// <listheader>
/// <term>Suffix</term>
/// <description>Meaning</description>
/// </listheader>
///
/// <item>
/// <term>ms</term>
/// <description>Milliseconds</description>
/// </item>
/// <item>
/// <term>s</term>
/// <description>Seconds</description>
/// </item>
/// <item>
/// <term>m</term>
/// <description>Minutes</description>
/// </item>
/// <item>
/// <term>h</term>
/// <description>Hours</description>
/// </item>
/// <item>
/// <term>d</term>
/// <description>Days</description>
/// </item>
/// <item>
/// <term>w</term>
/// <description>Weeks</description>
/// </item>
/// <item>
/// <term>mo</term>
/// <description>Months</description>
/// </item>
/// <item>
/// <term>y</term>
/// <description>Years</description>
/// </item>
/// </list>
/// </param>
/// <returns>A new instance of <see cref="TimeSpan" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null" />.</exception>
[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;
}
}

View File

@ -0,0 +1,133 @@
namespace X10D.Time;
/// <summary>
/// Represents a class which contains a <see cref="string" /> parser which converts into <see cref="TimeSpan" />.
/// </summary>
public static class TimeSpanParser
{
/// <summary>
/// Attempts to parses a shorthand time span string (e.g. 3w 2d 1h), converting it to an instance of
/// <see cref="TimeSpan" /> which represents that duration of time.
/// </summary>
/// <param name="value">
/// The input string. Floating point is not supported, but range the following units are supported:
///
/// <list type="table">
/// <listheader>
/// <term>Suffix</term>
/// <description>Meaning</description>
/// </listheader>
///
/// <item>
/// <term>ms</term>
/// <description>Milliseconds</description>
/// </item>
/// <item>
/// <term>s</term>
/// <description>Seconds</description>
/// </item>
/// <item>
/// <term>m</term>
/// <description>Minutes</description>
/// </item>
/// <item>
/// <term>h</term>
/// <description>Hours</description>
/// </item>
/// <item>
/// <term>d</term>
/// <description>Days</description>
/// </item>
/// <item>
/// <term>w</term>
/// <description>Weeks</description>
/// </item>
/// <item>
/// <term>mo</term>
/// <description>Months</description>
/// </item>
/// <item>
/// <term>y</term>
/// <description>Years</description>
/// </item>
/// </list>
/// </param>
/// <param name="result">When this method returns, contains the parsed result.</param>
/// <returns><see langword="true" /> if the parse was successful, <see langword="false" /> otherwise.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value" /> is <see langword="null" />.</exception>
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;
}
}

View File

@ -1,102 +0,0 @@
using System.Globalization;
using System.Text.RegularExpressions;
namespace X10D;
/// <summary>
/// Represents a class which contains a <see cref="string" /> parser which converts into <see cref="TimeSpan" />.
/// </summary>
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);
/// <summary>
/// Attempts to parses a shorthand time span string (e.g. 3w 2d 1.5h), converting it to an instance of
/// <see cref="TimeSpan" /> which represents that duration of time.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="result">The parsed result.</param>
/// <param name="provider">The format provider.</param>
/// <returns><see langword="true" /> if the parse was successful, <see langword="false" /> otherwise.</returns>
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;
}
}