From 0f96208cecd71181f5e3e339c6acc3a038a915f5 Mon Sep 17 00:00:00 2001
From: Christopher Wolf <4867233+HaGGi13@users.noreply.github.com>
Date: Sat, 14 Feb 2026 11:33:42 +0100
Subject: [PATCH 01/17] Initial `DurationTimeParser` impl. + tests
Implemented the class `DurationTimeParser`, which provides methods to
parse formatted duration strings into `TimeSpan`.
Added extensive unit tests to validate functionality.
---
Directory.Build.props | 2 +-
Directory.Packages.props | 9 +
DurationMancer.slnx | 3 +-
src/Class1.cs | 6 -
src/DurationMancer/DurationTimeParser.cs | 167 ++++
.../DurationMancer.Tests.csproj | 43 +
.../DurationTimeParserTests.cs | 799 ++++++++++++++++++
7 files changed, 1021 insertions(+), 8 deletions(-)
delete mode 100644 src/Class1.cs
create mode 100644 src/DurationMancer/DurationTimeParser.cs
create mode 100644 tests/DurationMancer.Tests/DurationMancer.Tests.csproj
create mode 100644 tests/DurationMancer.Tests/DurationTimeParserTests.cs
diff --git a/Directory.Build.props b/Directory.Build.props
index 62abc08..51d10a2 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,7 @@
- net6.0;net8.0;net9.0;net10.0
+ net8.0;net9.0;net10.0
enable
enable
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5c1d675..871dfbc 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,5 +4,14 @@
true
+
+
+
+
+
+
+
+
+
diff --git a/DurationMancer.slnx b/DurationMancer.slnx
index f2525a4..40c5c69 100644
--- a/DurationMancer.slnx
+++ b/DurationMancer.slnx
@@ -1,3 +1,4 @@
-
+
+
diff --git a/src/Class1.cs b/src/Class1.cs
deleted file mode 100644
index 09601fc..0000000
--- a/src/Class1.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace DurationMancer
-{
- public class Class1
- {
- }
-}
diff --git a/src/DurationMancer/DurationTimeParser.cs b/src/DurationMancer/DurationTimeParser.cs
new file mode 100644
index 0000000..c5a16f0
--- /dev/null
+++ b/src/DurationMancer/DurationTimeParser.cs
@@ -0,0 +1,167 @@
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+namespace DurationMancer;
+
+///
+/// Provides methods for parsing various duration string formats into a .
+///
+public static partial class DurationTimeParser
+{
+ private const string DaysPattern = @"(?\d+(?:\.\d+)?)\s*(d|day|days)\b";
+ private const string HoursPattern = @"(?\d+(?:\.\d+)?)\s*(h|hour|hours)\b";
+ private const string MillisecondsPattern = @"(?\d+)\s*(ms|millisecond|milliseconds)\b";
+ private const string MinutesPattern = @"(?\d+(?:\.\d+)?)\s*(m|min|minute|minutes)\b";
+ private const string SecondsPattern = @"(?\d+(?:\.\d+)?)\s*(s|sec|second|seconds)\b";
+
+ private const string StandardDurationTimeFormat =
+ @"^(?:(?\d+)\.)?(?[01]\d|2[0-3]):(?[0-5]\d):(?[0-5]\d)(?:\.(?\d{0,3}))?$";
+
+ private const string TimePattern = StandardDurationTimeFormat + @"|" +
+ @"^(?:" + DaysPattern + @")?\s*" +
+ @"(?:" + HoursPattern + @")?\s*" +
+ @"(?:" + MinutesPattern + @")?\s*" +
+ @"(?:" + SecondsPattern + @")?\s*" +
+ @"(?:" + MillisecondsPattern + @")?$";
+
+ private static readonly Regex TimeRegex = CreateTimeFormatRegex();
+
+ [GeneratedRegex(TimePattern, RegexOptions.IgnoreCase | RegexOptions.Compiled)]
+ private static partial Regex CreateTimeFormatRegex();
+
+ ///
+ /// Tries to parse a given input string into a representation.
+ /// Supports standard duration format and human-readable duration format.
+ ///
+ /// The input string that represents a duration in a valid format.
+ /// The parsed value if parsing is successful; otherwise .
+ /// true if the input string was successfully parsed into a valid ; otherwise false.
+ ///
+ /// Supports two format types:
+ ///
+ /// 1. Standard duration format:
+ /// - [d.]HH:mm:ss[.fff]
+ /// 2. Human-readable format with combinations of:
+ /// - days: "1d", "2 days"
+ /// - hours: "1h", "2 hours"
+ /// - minutes: "1m", "5 minutes"
+ /// - seconds: "1s", "30 seconds"
+ /// - milliseconds: "100ms", "500 milliseconds"
+ ///
+ /// Examples of valid human-readable formats:
+ /// - "1s"
+ /// - "5 minutes"
+ /// - "3m 10s"
+ /// - "1 hour 30 minutes"
+ /// - "2 days 4 hours 15m 30s"
+ ///
+ ///
+ public static bool TryParse(string? input, out TimeSpan timeSpan)
+ {
+ timeSpan = TimeSpan.Zero;
+
+ if (string.IsNullOrWhiteSpace(input))
+ {
+ return false;
+ }
+
+ input = input.Trim();
+
+ var match = TimeRegex.Match(input);
+ if (!match.Success)
+ {
+ return false;
+ }
+
+ return TryParseDurationFormat(match, out timeSpan) ||
+ TryParseHumanReadableDurationFormat(match, out timeSpan);
+ }
+
+ ///
+ /// Tries to parse a possibly matched time in duration format [d.]HH:mm:ss[.fff].
+ ///
+ /// Possibly RegEx match that contains values to process.
+ /// The parsed result object, if successfully parsed; otherwise .
+ /// true if successfully parsed; otherwise false
+ private static bool TryParseDurationFormat(Match match, out TimeSpan result)
+ {
+ result = TimeSpan.Zero;
+
+ if (!match.Groups["hh"].Success)
+ {
+ return false;
+ }
+
+ _ = int.TryParse(match.Groups["d"].Value, out var d);
+ _ = int.TryParse(match.Groups["hh"].Value, out var hh);
+ _ = int.TryParse(match.Groups["mm"].Value, out var mm);
+ _ = int.TryParse(match.Groups["ss"].Value, out var ss);
+ _ = int.TryParse(match.Groups["ms"].Value, out var ms);
+
+ result = new TimeSpan(d, hh, mm, ss, ms);
+
+ return true;
+ }
+
+ ///
+ /// Parses a possibly matched time in human-readable duration format. For instance, like "1h 13m 2s", "5 sec" or "10min 30s".
+ ///
+ /// Possibly RegEx match that contains values to process.
+ /// The parsed result object, if successfully parsed; otherwise .
+ /// The parsed result object.
+ private static bool TryParseHumanReadableDurationFormat(Match match, out TimeSpan result)
+ {
+ result = TimeSpan.Zero;
+
+ try
+ {
+ var days = match.Groups["days"].Success
+ ? double.Parse(match.Groups["days"].Value, CultureInfo.InvariantCulture)
+ : 0;
+
+ var hours = match.Groups["hours"].Success
+ ? double.Parse(match.Groups["hours"].Value, CultureInfo.InvariantCulture)
+ : 0;
+
+ var minutes = match.Groups["minutes"].Success
+ ? double.Parse(match.Groups["minutes"].Value, CultureInfo.InvariantCulture)
+ : 0;
+
+ var seconds = match.Groups["seconds"].Success
+ ? double.Parse(match.Groups["seconds"].Value, CultureInfo.InvariantCulture)
+ : 0;
+
+ var milliSeconds = match.Groups["milliseconds"].Success
+ ? int.Parse(match.Groups["milliseconds"].Value, CultureInfo.InvariantCulture)
+ : 0;
+
+ var timeSpan = TimeSpan.FromDays(days) +
+ TimeSpan.FromHours(hours) +
+ TimeSpan.FromMinutes(minutes) +
+ TimeSpan.FromSeconds(seconds) +
+ TimeSpan.FromMilliseconds(milliSeconds);
+
+ // As we do not support nanoseconds or ticks, we have to round to the nearest millisecond
+ result = RoundOnMilliseconds(timeSpan);
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Rounds a given to the nearest milliseconds with a precision of 1 millisecond.
+ ///
+ /// The instance to round.
+ /// The new rounded instance.
+ private static TimeSpan RoundOnMilliseconds(TimeSpan timeSpan)
+ {
+ var roundedTicks =
+ (long)(Math.Round((double)timeSpan.Ticks / TimeSpan.TicksPerMillisecond) * TimeSpan.TicksPerMillisecond);
+
+ return new TimeSpan(roundedTicks);
+ }
+}
diff --git a/tests/DurationMancer.Tests/DurationMancer.Tests.csproj b/tests/DurationMancer.Tests/DurationMancer.Tests.csproj
new file mode 100644
index 0000000..5eccec8
--- /dev/null
+++ b/tests/DurationMancer.Tests/DurationMancer.Tests.csproj
@@ -0,0 +1,43 @@
+
+
+
+ false
+ false
+ true
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/DurationMancer.Tests/DurationTimeParserTests.cs b/tests/DurationMancer.Tests/DurationTimeParserTests.cs
new file mode 100644
index 0000000..66815c0
--- /dev/null
+++ b/tests/DurationMancer.Tests/DurationTimeParserTests.cs
@@ -0,0 +1,799 @@
+using AwesomeAssertions;
+
+namespace DurationMancer.Tests;
+
+public sealed class DurationTimeParserTests
+{
+ [Theory]
+ [InlineData("00:00:00.0")]
+ [InlineData("00:00:00.00")]
+ [InlineData("00:00:00")]
+ [InlineData("0.00:00:00")]
+ [InlineData("00.00:00:00")]
+ [InlineData("0ms")]
+ [InlineData("0millisecond")]
+ [InlineData("0milliseconds")]
+ [InlineData("0 ms")]
+ [InlineData("0 millisecond")]
+ [InlineData("0 milliseconds")]
+ [InlineData("0s")]
+ [InlineData("0sec")]
+ [InlineData("0second")]
+ [InlineData("0seconds")]
+ [InlineData("0 s")]
+ [InlineData("0 sec")]
+ [InlineData("0 second")]
+ [InlineData("0 seconds")]
+ [InlineData("0m")]
+ [InlineData("0min")]
+ [InlineData("0minute")]
+ [InlineData("0minutes")]
+ [InlineData("0 m")]
+ [InlineData("0 min")]
+ [InlineData("0 minute")]
+ [InlineData("0 minutes")]
+ [InlineData("0h")]
+ [InlineData("0hour")]
+ [InlineData("0hours")]
+ [InlineData("0 h")]
+ [InlineData("0 hour")]
+ [InlineData("0 hours")]
+ [InlineData("0d")]
+ [InlineData("0day")]
+ [InlineData("0days")]
+ [InlineData("0 d")]
+ [InlineData("0 day")]
+ [InlineData("0 days")]
+ [InlineData("0d 0h 0m 0s 0ms")]
+ [InlineData("0day 0hour 0min 0sec 0ms")]
+ [InlineData("0day 0hour 0minute 0second 0millisecond")]
+ [InlineData("0days 0hours 0minutes 0seconds 0milliseconds")]
+ [InlineData("0 d 0 h 0 m 0 s 0 ms")]
+ [InlineData("0 day 0 hour 0 min 0 sec 0 ms")]
+ [InlineData("0 day 0 hour 0 minute 0 second 0 millisecond")]
+ [InlineData("0 days 0 hours 0 minutes 0 seconds 0 milliseconds")]
+
+ // Mixed w/ spaces and w/o spaces between units
+ [InlineData("0d 0 h 0m 0 s 0ms")]
+ [InlineData("0day 0 hour 0min 0 sec 0ms")]
+ [InlineData("0day 0 hour 0minute 0 second 0millisecond")]
+ [InlineData("0days 0hours 0minutes 0 seconds 0milliseconds")]
+ public void TryParse_ZeroInput_ReturnsTrueAndTimeSpanZero(string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(TimeSpan.Zero);
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("00:00:03")]
+ [InlineData("00:00:03.0")]
+ [InlineData("00:00:03.00")]
+ [InlineData("0.00:00:03")]
+ [InlineData("00.00:00:03")]
+ [InlineData("3s")]
+ [InlineData("3sec")]
+ [InlineData("3second")]
+ [InlineData("3seconds")]
+ [InlineData("3 s")]
+ [InlineData("3 sec")]
+ [InlineData("3 second")]
+ [InlineData("3 seconds")]
+ [InlineData("3s 0ms")]
+ [InlineData("3sec 0ms")]
+ [InlineData("3second 0millisecond")]
+ [InlineData("3seconds 0milliseconds")]
+ [InlineData("3 s 0 ms")]
+ [InlineData("3 sec 0 ms")]
+ [InlineData("3 second 0 millisecond")]
+ [InlineData("3 seconds 0 milliseconds")]
+ [InlineData("0m 3s")]
+ [InlineData("0min 3sec")]
+ [InlineData("0minute 3second")]
+ [InlineData("0minutes 3seconds")]
+ [InlineData("0 m 3 s")]
+ [InlineData("0 min 3 sec")]
+ [InlineData("0 minute 3 second")]
+ [InlineData("0 minutes 3 seconds")]
+ [InlineData("0m 3s 0ms")]
+ [InlineData("0min 3sec 0ms")]
+ [InlineData("0minute 3second 0millisecond")]
+ [InlineData("0minutes 3seconds 0milliseconds")]
+ [InlineData("0 m 3 s 0 ms")]
+ [InlineData("0 min 3 sec 0 ms")]
+ [InlineData("0 minute 3 second 0 millisecond")]
+ [InlineData("0 minutes 3 seconds 0 milliseconds")]
+ [InlineData("0h 0m 3s")]
+ [InlineData("0hour 0min 3sec")]
+ [InlineData("0hour 0minute 3second")]
+ [InlineData("0hours 0minutes 3seconds")]
+ [InlineData("0 h 0 m 3 s")]
+ [InlineData("0 hour 0 min 3 sec")]
+ [InlineData("0 hour 0 minute 3 second")]
+ [InlineData("0 hours 0 minutes 3 seconds")]
+ [InlineData("0h 0m 3s 0ms")]
+ [InlineData("0hour 0min 3sec 0ms")]
+ [InlineData("0hour 0minute 3second 0millisecond")]
+ [InlineData("0hours 0minutes 3seconds 0milliseconds")]
+ [InlineData("0 h 0 m 3 s 0 ms")]
+ [InlineData("0 hour 0 min 3 sec 0 ms")]
+ [InlineData("0 hour 0 minute 3 second 0 millisecond")]
+ [InlineData("0 hours 0 minutes 3 seconds 0 milliseconds")]
+ [InlineData("0d 0h 0m 3s")]
+ [InlineData("0day 0hour 0min 3sec")]
+ [InlineData("0day 0hour 0minute 3second")]
+ [InlineData("0days 0hours 0minutes 3seconds")]
+ [InlineData("0 d 0 h 0 m 3 s")]
+ [InlineData("0 day 0 hour 0 min 3 sec")]
+ [InlineData("0 day 0 hour 0 minute 3 second")]
+ [InlineData("0 days 0 hours 0 minutes 3 seconds")]
+ [InlineData("0d 0h 0m 3s 0ms")]
+ [InlineData("0day 0hour 0min 3sec 0ms")]
+ [InlineData("0day 0hour 0minute 3second 0millisecond")]
+ [InlineData("0days 0hours 0minutes 3seconds 0milliseconds")]
+ [InlineData("0 d 0 h 0 m 3 s 0 ms")]
+ [InlineData("0 day 0 hour 0 min 3 sec 0 ms")]
+ [InlineData("0 day 0 hour 0 minute 3 second 0 millisecond")]
+ [InlineData("0 days 0 hours 0 minutes 3 seconds 0 milliseconds")]
+
+ // Mixed w/ spaces and w/o spaces between units
+ [InlineData("0d 0 h 0m 3 s 0ms")]
+ [InlineData("0day 0hour 0min 3 sec 0ms")]
+ [InlineData("0day 0 hour 0 minute 3second 0millisecond")]
+ [InlineData("0days 0hours 0minutes 3 seconds 0milliseconds")]
+ public void TryParse_ThreeSecondsInput_ReturnsTrueAndTimeSpanWithThreeSeconds(string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(new TimeSpan(0, 0, 3));
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("00:00:00", 0, 0, 0, 0)] // Zero time
+ [InlineData("01:00:00", 0, 1, 0, 0)] // Hours only
+ [InlineData("00:30:00", 0, 0, 30, 0)] // Minutes only
+ [InlineData("00:00:45", 0, 0, 0, 45)] // Seconds only
+ [InlineData("1.00:00:00", 1, 0, 0, 0)] // Days only
+ [InlineData("00:00:00.500", 0, 0, 0, 0, 500)] // With milliseconds
+ [InlineData("1.23:45:56.789", 1, 23, 45, 56, 789)] // Complex case
+ public void TryParse_StandardDurationFormat_ShouldParseCorrectly(string input, int days, int hours, int minutes, int seconds,
+ int milliseconds = 0)
+ {
+ // Arrange
+ var expected = new TimeSpan(days, hours, minutes, seconds, milliseconds);
+
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(expected);
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("1d", "1.00:00:00")]
+ [InlineData("2 days", "2.00:00:00")]
+ [InlineData("3h", "03:00:00")]
+ [InlineData("4 hours", "04:00:00")]
+ [InlineData("5m", "00:05:00")]
+ [InlineData("6 minutes", "00:06:00")]
+ [InlineData("7s", "00:00:07")]
+ [InlineData("8 seconds", "00:00:08")]
+ [InlineData("100ms", "00:00:00.100")]
+ [InlineData("1d 2h 3m 4s 500ms", "1.02:03:04.500")]
+ public void TryParse_HumanReadableFormat_ShouldParseCorrectly(string input, string result)
+ {
+ // Arrange
+ _ = TimeSpan.TryParse(result, out var expectedResult);
+
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Arrange
+ parsedValue.Should().Be(expectedResult);
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("69.5m 0.5s", "01:09:30.500")]
+ [InlineData("0.3m 100.07s", "00:01:58.070")]
+ [InlineData("0.01h 100.007s", "00:02:16.007")]
+ [InlineData("0.0003d", "00:00:25.920")]
+ public void TryParse_MixedUnrollingAndRollingOverInput_ReturnsTrueAndTimeSpanWithCorrectValue(string input, string result)
+ {
+ // Arrange
+ _ = TimeSpan.TryParse(result, out var expectedResult);
+
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(expectedResult);
+ parsedResult.Should().BeTrue();
+ }
+
+ #region Invalid input tests
+
+ [Fact]
+ public void TryParse_NullInput_ReturnsFalseAndTimeSpanZero()
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(null, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(TimeSpan.Zero);
+ parsedResult.Should().BeFalse();
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData(" ")]
+ [InlineData("\t")]
+ [InlineData("\n")]
+ [InlineData(" \n ")]
+ public void TryParse_EmptyInput_ReturnsFalseAndTimeSpanZero(string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(TimeSpan.Zero);
+ parsedResult.Should().BeFalse();
+ }
+
+ [Theory]
+ [InlineData("Foo")]
+ [InlineData(" Bar")]
+ [InlineData("Foo 3s")]
+ [InlineData("3s Bar")]
+ [InlineData("Foo 00:00:03")]
+ [InlineData("00:00:03 Bar")]
+ [InlineData("Foo 00:00:03 Bar")]
+ [InlineData("3house")]
+ [InlineData("3 house")]
+ [InlineData("3houses")]
+ [InlineData("3 houses")]
+ [InlineData("invalid")]
+ [InlineData("0h0m3s")]
+ [InlineData("0m 3s 1m")]
+ [InlineData("3s 1h")]
+ [InlineData("+3s")]
+ [InlineData("-3s")]
+ [InlineData("3,5s")]
+ [InlineData("3,005s")]
+ [InlineData("3,005.0s")]
+ [InlineData("3,5 s")]
+ [InlineData("1y")]
+ [InlineData("1d 2x")]
+ [InlineData("00:00")]
+ [InlineData("00:00.0")]
+ [InlineData("0.00:00")]
+ [InlineData("00:00:60")]
+ [InlineData("00:00:61")]
+ [InlineData("00:60:00")]
+ [InlineData("00:61:00")]
+ [InlineData("24:00:00")]
+ [InlineData("25:00:00")]
+ [InlineData("00:00:00.0001")]
+ [InlineData("00:00:60.0")]
+ [InlineData("00:00:61.0")]
+ [InlineData("00:60:00.0")]
+ [InlineData("00:61:00.0")]
+ [InlineData("24:00:00.0")]
+ [InlineData("25:00:00.0")]
+ [InlineData("0.00:00:60")]
+ [InlineData("0.00:00:61")]
+ [InlineData("0.00:60:00")]
+ [InlineData("0.00:61:00")]
+ [InlineData("0.24:00:00")]
+ [InlineData("0.25:00:00")]
+ [InlineData("1E+999999999 days")] // The value is too large for double.Parse and will throw OverflowException
+ [InlineData("1.2.3 days")] // The double.Parse will throw FormatException due to multiple decimal points
+ [InlineData(
+ "999999999 days 23 hours 59 minutes 59 seconds")] // This will cause TimeSpan overflow when components are added together
+ [InlineData("1 day 2.5ms")] // int.Parse will throw FormatException due to an invalid integer format
+ public void TryParse_InvalidInput_ReturnsFalseAndTimeSpanZero(string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(TimeSpan.Zero);
+ parsedResult.Should().BeFalse();
+ }
+
+ #endregion
+
+ #region Value rolling over tests that result in next bigger unit
+
+ [Theory]
+ [InlineData("1000ms")]
+ [InlineData("1000millisecond")]
+ [InlineData("1000milliseconds")]
+ [InlineData("1000 ms")]
+ [InlineData("1000 millisecond")]
+ [InlineData("1000 milliseconds")]
+ [InlineData("0s 1000ms")]
+ [InlineData("0sec 1000ms")]
+ [InlineData("0second 1000millisecond")]
+ [InlineData("0seconds 1000milliseconds")]
+ [InlineData("0 s 1000 ms")]
+ [InlineData("0 sec 1000 ms")]
+ [InlineData("0 second 1000 millisecond")]
+ [InlineData("0 seconds 1000 milliseconds")]
+ [InlineData("0m 0s 1000ms")]
+ [InlineData("0min 0sec 1000ms")]
+ [InlineData("0minute 0second 1000millisecond")]
+ [InlineData("0minutes 0seconds 1000milliseconds")]
+ [InlineData("0 m 0 s 1000 ms")]
+ [InlineData("0 min 0 sec 1000 ms")]
+ [InlineData("0 minute 0 second 1000 millisecond")]
+ [InlineData("0 minutes 0 seconds 1000 milliseconds")]
+ [InlineData("0h 0m 0s 1000ms")]
+ [InlineData("0hour 0min 0sec 1000ms")]
+ [InlineData("0hour 0minute 0second 1000millisecond")]
+ [InlineData("0hours 0minutes 0seconds 1000milliseconds")]
+ [InlineData("0 h 0 m 0 s 1000 ms")]
+ [InlineData("0 hour 0 min 0 sec 1000 ms")]
+ [InlineData("0 hour 0 minute 0 second 1000 millisecond")]
+ [InlineData("0 hours 0 minutes 0 seconds 1000 milliseconds")]
+ [InlineData("0d 0h 0m 0s 1000ms")]
+ [InlineData("0day 0hour 0min 0sec 1000ms")]
+ [InlineData("0day 0hour 0minute 0second 1000millisecond")]
+ [InlineData("0days 0hours 0minutes 0seconds 1000milliseconds")]
+ [InlineData("0 d 0 h 0 m 0 s 1000 ms")]
+ [InlineData("0 day 0 hour 0 min 0 sec 1000 ms")]
+ [InlineData("0 day 0 hour 0 minute 0 second 1000 millisecond")]
+ [InlineData("0 days 0 hours 0 minutes 0 seconds 1000 milliseconds")]
+ public void TryParse_RollingOverThousandMillisecondsInNotDurationFormat_ReturnsTrueAndTimeSpanWithOneSecond(string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(new TimeSpan(0, 0, 1));
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("1001ms")]
+ [InlineData("1001millisecond")]
+ [InlineData("1001milliseconds")]
+ [InlineData("1001 ms")]
+ [InlineData("1001 millisecond")]
+ [InlineData("1001 milliseconds")]
+ [InlineData("0s 1001ms")]
+ [InlineData("0sec 1001ms")]
+ [InlineData("0second 1001millisecond")]
+ [InlineData("0seconds 1001milliseconds")]
+ [InlineData("0 s 1001 ms")]
+ [InlineData("0 sec 1001 ms")]
+ [InlineData("0 second 1001 millisecond")]
+ [InlineData("0 seconds 1001 milliseconds")]
+ [InlineData("0m 0s 1001ms")]
+ [InlineData("0min 0sec 1001ms")]
+ [InlineData("0minute 0second 1001millisecond")]
+ [InlineData("0minutes 0seconds 1001milliseconds")]
+ [InlineData("0 m 0 s 1001 ms")]
+ [InlineData("0 min 0 sec 1001 ms")]
+ [InlineData("0 minute 0 second 1001 millisecond")]
+ [InlineData("0 minutes 0 seconds 1001 milliseconds")]
+ [InlineData("0h 0m 0s 1001ms")]
+ [InlineData("0hour 0min 0sec 1001ms")]
+ [InlineData("0hour 0minute 0second 1001millisecond")]
+ [InlineData("0hours 0minutes 0seconds 1001milliseconds")]
+ [InlineData("0 h 0 m 0 s 1001 ms")]
+ [InlineData("0 hour 0 min 0 sec 1001 ms")]
+ [InlineData("0 hour 0 minute 0 second 1001 millisecond")]
+ [InlineData("0 hours 0 minutes 0 seconds 1001 milliseconds")]
+ [InlineData("0d 0h 0m 0s 1001ms")]
+ [InlineData("0day 0hour 0min 0sec 1001ms")]
+ [InlineData("0day 0hour 0minute 0second 1001millisecond")]
+ [InlineData("0days 0hours 0minutes 0seconds 1001milliseconds")]
+ [InlineData("0 d 0 h 0 m 0 s 1001 ms")]
+ [InlineData("0 day 0 hour 0 min 0 sec 1001 ms")]
+ [InlineData("0 day 0 hour 0 minute 0 second 1001 millisecond")]
+ [InlineData("0 days 0 hours 0 minutes 0 seconds 1001 milliseconds")]
+ public void
+ TryParse_RollingOverThousandAndOneMillisecondsInNotDurationFormat_ReturnsTrueAndTimeSpanWithOneSecondAndOneMillisecond(
+ string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(new TimeSpan(0, 0, 0, 1, 1));
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("60s")]
+ [InlineData("60sec")]
+ [InlineData("60second")]
+ [InlineData("60seconds")]
+ [InlineData("60 s")]
+ [InlineData("60 sec")]
+ [InlineData("60 second")]
+ [InlineData("60 seconds")]
+ [InlineData("0m 60s")]
+ [InlineData("0min 60sec")]
+ [InlineData("0minute 60second")]
+ [InlineData("0minutes 60seconds")]
+ [InlineData("0 m 60 s")]
+ [InlineData("0 min 60 sec")]
+ [InlineData("0 minute 60 second")]
+ [InlineData("0 minutes 60 seconds")]
+ [InlineData("0h 0m 60s")]
+ [InlineData("0hour 0min 60sec")]
+ [InlineData("0hour 0minute 60second")]
+ [InlineData("0hours 0minutes 60seconds")]
+ [InlineData("0 h 0 m 60 s")]
+ [InlineData("0 hour 0 min 60 sec")]
+ [InlineData("0 hour 0 minute 60 second")]
+ [InlineData("0 hours 0minutes 60seconds")]
+ [InlineData("0d 0h 0m 60s")]
+ [InlineData("0day 0hour 0min 60sec")]
+ [InlineData("0day 0hour 0minute 60second")]
+ [InlineData("0days 0hours 0minutes 60seconds")]
+ [InlineData("0 d 0 h 0 m 60 s")]
+ [InlineData("0 day 0 hour 0 min 60 sec")]
+ [InlineData("0 day 0 hour 0 minute 60 second")]
+ [InlineData("0 days 0 hours 0minutes 60 seconds")]
+
+ // Mixed w/ spaces and w/o spaces between units
+ [InlineData("0d 0 h 0m 60 s 0ms")]
+ [InlineData("0day 0hour 0min 60 sec 0ms")]
+ [InlineData("0day 0 hour 0 minute 60second 0millisecond")]
+ [InlineData("0days 0hours 0minutes 60 seconds 0milliseconds")]
+ public void TryParse_RollingOverSixtySecondsInNotDurationFormat_ReturnsTrueAndTimeSpanWithOneMinute(string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(new TimeSpan(0, 1, 0));
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("65s")]
+ [InlineData("65sec")]
+ [InlineData("65second")]
+ [InlineData("65seconds")]
+ [InlineData("65 s")]
+ [InlineData("65 sec")]
+ [InlineData("65 second")]
+ [InlineData("65 seconds")]
+ [InlineData("0m 65s")]
+ [InlineData("0min 65sec")]
+ [InlineData("0minute 65second")]
+ [InlineData("0minutes 65seconds")]
+ [InlineData("0 m 65 s")]
+ [InlineData("0 min 65 sec")]
+ [InlineData("0 minute 65 second")]
+ [InlineData("0 minutes 65 seconds")]
+ [InlineData("0h 0m 65s")]
+ [InlineData("0hour 0min 65sec")]
+ [InlineData("0hour 0minute 65second")]
+ [InlineData("0hours 0minutes 65seconds")]
+ [InlineData("0 h 0 m 65 s")]
+ [InlineData("0 hour 0 min 65 sec")]
+ [InlineData("0 hour 0 minute 65 second")]
+ [InlineData("0 hours 0minutes 65seconds")]
+ [InlineData("0d 0h 0m 65s")]
+ [InlineData("0day 0hour 0min 65sec")]
+ [InlineData("0day 0hour 0minute 65second")]
+ [InlineData("0days 0hours 0minutes 65seconds")]
+ [InlineData("0 d 0 h 0 m 65 s")]
+ [InlineData("0 day 0 hour 0 min 65 sec")]
+ [InlineData("0 day 0 hour 0 minute 65 second")]
+ [InlineData("0 days 0 hours 0minutes 65 seconds")]
+
+ // Mixed w/ spaces and w/o spaces between units
+ [InlineData("0d 0 h 0m 65 s 0ms")]
+ [InlineData("0day 0hour 0min 65 sec 0ms")]
+ [InlineData("0day 0 hour 0 minute 65second 0millisecond")]
+ [InlineData("0days 0hours 0minutes 65 seconds 0milliseconds")]
+ public void
+ TryParse_RollingOverSixtyFiveSecondsInNotDurationFormat_ReturnsTrueAndTimeSpanWithOneMinuteAndFiveSeconds(
+ string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(new TimeSpan(0, 1, 5));
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("60m")]
+ [InlineData("60min")]
+ [InlineData("60minute")]
+ [InlineData("60minutes")]
+ [InlineData("60 m")]
+ [InlineData("60 min")]
+ [InlineData("60 minute")]
+ [InlineData("60 minutes")]
+ [InlineData("60m 0s")]
+ [InlineData("60min 0sec")]
+ [InlineData("60minute 0second")]
+ [InlineData("60minutes 0seconds")]
+ [InlineData("60 m 0 s")]
+ [InlineData("60 min 0 sec")]
+ [InlineData("60 minute 0 second")]
+ [InlineData("60 minutes 0 seconds")]
+ [InlineData("0h 60m 0s")]
+ [InlineData("0hour 60min 0sec")]
+ [InlineData("0hour 60minute 0second")]
+ [InlineData("0hours 60minutes 0seconds")]
+ [InlineData("0 h 60 m 0 s")]
+ [InlineData("0 hour 60 min 0 sec")]
+ [InlineData("0 hour 60 minute 0 second")]
+ [InlineData("0 hours 60 minutes 0 seconds")]
+ [InlineData("0d 0h 60m 0s")]
+ [InlineData("0day 0hour 60min 0sec")]
+ [InlineData("0day 0hour 60minute 0second")]
+ [InlineData("0days 0hours 60minutes 0seconds")]
+ [InlineData("0 d 0 h 60 m 0 s")]
+ [InlineData("0 day 0 hour 60 min 0 sec")]
+ [InlineData("0 day 0 hour 60 minute 0 second")]
+ [InlineData("0 days 0 hours 60 minutes 0 seconds")]
+
+ // Mixed w/ spaces and w/o spaces between units
+ [InlineData("0d 0 h 60m 0 s 0ms")]
+ [InlineData("0day 0hour 60min 0 sec 0ms")]
+ [InlineData("0day 0 hour 60 minute 0second 0millisecond")]
+ [InlineData("0days 0hours 60minutes 0 seconds 0milliseconds")]
+ public void TryParse_RollingOverSixtyMinutesInNotDurationFormat_ReturnsTrueAndTimeSpanWithOneHour(string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(new TimeSpan(1, 0, 0));
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("65m")]
+ [InlineData("65min")]
+ [InlineData("65minute")]
+ [InlineData("65minutes")]
+ [InlineData("65 m")]
+ [InlineData("65 min")]
+ [InlineData("65 minute")]
+ [InlineData("65 minutes")]
+ [InlineData("65m 0s")]
+ [InlineData("65min 0sec")]
+ [InlineData("65minute 0second")]
+ [InlineData("65minutes 0seconds")]
+ [InlineData("65 m 0 s")]
+ [InlineData("65 min 0 sec")]
+ [InlineData("65 minute 0 second")]
+ [InlineData("65 minutes 0 seconds")]
+ [InlineData("0h 65m 0s")]
+ [InlineData("0hour 65min 0sec")]
+ [InlineData("0hour 65minute 0second")]
+ [InlineData("0hours 65minutes 0seconds")]
+ [InlineData("0 h 65 m 0 s")]
+ [InlineData("0 hour 65 min 0 sec")]
+ [InlineData("0 hour 65 minute 0 second")]
+ [InlineData("0 hours 65 minutes 0 seconds")]
+ [InlineData("0d 0h 65m 0s")]
+ [InlineData("0day 0hour 65min 0sec")]
+ [InlineData("0day 0hour 65minute 0second")]
+ [InlineData("0days 0hours 65minutes 0seconds")]
+ [InlineData("0 d 0 h 65 m 0 s")]
+ [InlineData("0 day 0 hour 65 min 0 sec")]
+ [InlineData("0 day 0 hour 65 minute 0 second")]
+ [InlineData("0 days 0 hours 65 minutes 0 seconds")]
+
+ // Mixed w/ spaces and w/o spaces between units
+ [InlineData("0d 0 h 65m 0 s 0ms")]
+ [InlineData("0day 0hour 65min 0 sec 0ms")]
+ [InlineData("0day 0 hour 65 minute 0second 0millisecond")]
+ [InlineData("0days 0hours 65minutes 0 seconds 0milliseconds")]
+ public void TryParse_RollingOverSixtyFiveMinutesInNotDurationFormat_ReturnsTrueAndTimeSpanWithOneHourAndFiveMinutes(
+ string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(new TimeSpan(1, 5, 0));
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("24h 0m 0s")]
+ [InlineData("24hour 0min 0sec")]
+ [InlineData("24hour 0minute 0second")]
+ [InlineData("24hours 0minutes 0seconds")]
+ [InlineData("24 h 0 m 0 s")]
+ [InlineData("24 hour 0 min 0 sec")]
+ [InlineData("24 hour 0 minute 0 second")]
+ [InlineData("24 hours 0 minutes 0 seconds")]
+ [InlineData("0d 24h 0m 0s")]
+ [InlineData("0day 24hour 0min 0sec")]
+ [InlineData("0day 24hour 0minute 0second")]
+ [InlineData("0days 24hours 0minutes 0seconds")]
+ [InlineData("0 d 24 h 0 m 0 s")]
+ [InlineData("0 day 24 hour 0 min 0 sec")]
+ [InlineData("0 day 24 hour 0 minute 0 second")]
+ [InlineData("0 days 24 hours 0 minutes 0 seconds")]
+
+ // Mixed w/ spaces and w/o spaces between units
+ [InlineData("0d 24 h 0m 0 s 0ms")]
+ [InlineData("0day 24hour 0min 0 sec 0ms")]
+ [InlineData("0day 24 hour 0 minute 0second 0millisecond")]
+ [InlineData("0days 24hours 0minutes 0 seconds 0milliseconds")]
+ public void TryParse_RollingOverTwentyFourHoursInNotDurationFormat_ReturnsTrueAndTimeSpanWithOneDay(string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(new TimeSpan(1, 0, 0, 0));
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("25h 0m 0s")]
+ [InlineData("25hour 0min 0sec")]
+ [InlineData("25hour 0minute 0second")]
+ [InlineData("25hours 0minutes 0seconds")]
+ [InlineData("25 h 0 m 0 s")]
+ [InlineData("25 hour 0 min 0 sec")]
+ [InlineData("25 hour 0 minute 0 second")]
+ [InlineData("25 hours 0 minutes 0 seconds")]
+ [InlineData("0d 25h 0m 0s")]
+ [InlineData("0day 25hour 0min 0sec")]
+ [InlineData("0day 25hour 0minute 0second")]
+ [InlineData("0days 25hours 0minutes 0seconds")]
+ [InlineData("0 d 25 h 0 m 0 s")]
+ [InlineData("0 day 25 hour 0 min 0 sec")]
+ [InlineData("0 day 25 hour 0 minute 0 second")]
+ [InlineData("0 days 25 hours 0 minutes 0 seconds")]
+
+ // Mixed w/ spaces and w/o spaces between units
+ [InlineData("0d 25 h 0m 0 s 0ms")]
+ [InlineData("0day 25hour 0min 0 sec 0ms")]
+ [InlineData("0day 25 hour 0 minute 0second 0millisecond")]
+ [InlineData("0days 25hours 0minutes 0 seconds 0milliseconds")]
+ public void TryParse_RollingOverTwentyFiveHoursInNotDurationFormat_ReturnsTrueAndTimeSpanWithOneDayAndOneHour(
+ string? input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(new TimeSpan(1, 1, 0, 0));
+ parsedResult.Should().BeTrue();
+ }
+
+ #endregion
+
+ #region Value unrolling tests that result in current and next smaller unit
+
+ [Theory]
+ [InlineData("3.5s")]
+ [InlineData("3.5sec")]
+ [InlineData("3.5second")]
+ [InlineData("3.5seconds")]
+ [InlineData("3.5 s")]
+ [InlineData("3.5 sec")]
+ [InlineData("3.5 second")]
+ [InlineData("3.5 seconds")]
+ [InlineData("3.5s 0ms")]
+ [InlineData("3.5sec 0ms")]
+ [InlineData("3.5second 0millisecond")]
+ [InlineData("3.5seconds 0milliseconds")]
+ [InlineData("3.5 s 0 ms")]
+ [InlineData("3.5 sec 0 ms")]
+ [InlineData("3.5 second 0 millisecond")]
+ [InlineData("3.5 seconds 0 milliseconds")]
+ [InlineData("0m 3.5s")]
+ [InlineData("0min 3.5sec")]
+ [InlineData("0minute 3.5second")]
+ [InlineData("0minutes 3.5seconds")]
+ [InlineData("0 m 3.5 s")]
+ [InlineData("0 min 3.5 sec")]
+ [InlineData("0 minute 3.5 second")]
+ [InlineData("0 minutes 3.5 seconds")]
+ [InlineData("0m 3.5s 0ms")]
+ [InlineData("0min 3.5sec 0ms")]
+ [InlineData("0minute 3.5second 0millisecond")]
+ [InlineData("0minutes 3.5seconds 0milliseconds")]
+ [InlineData("0 m 3.5 s 0 ms")]
+ [InlineData("0 min 3.5 sec 0 ms")]
+ [InlineData("0 minute 3.5 second 0 millisecond")]
+ [InlineData("0 minutes 3.5 seconds 0 milliseconds")]
+ [InlineData("0h 0m 3.5s")]
+ [InlineData("0hour 0min 3.5sec")]
+ [InlineData("0hour 0minute 3.5second")]
+ [InlineData("0hours 0minutes 3.5seconds")]
+ [InlineData("0 h 0 m 3.5 s")]
+ [InlineData("0 hour 0 min 3.5 sec")]
+ [InlineData("0 hour 0 minute 3.5 second")]
+ [InlineData("0 hours 0 minutes 3.5 seconds")]
+ [InlineData("0h 0m 3.5s 0ms")]
+ [InlineData("0hour 0min 3.5sec 0ms")]
+ [InlineData("0hour 0minute 3.5second 0millisecond")]
+ [InlineData("0hours 0minutes 3.5seconds 0milliseconds")]
+ [InlineData("0 h 0 m 3.5 s 0 ms")]
+ [InlineData("0 hour 0 min 3.5 sec 0 ms")]
+ [InlineData("0 hour 0 minute 3.5 second 0 millisecond")]
+ [InlineData("0 hours 0 minutes 3.5 seconds 0 milliseconds")]
+ [InlineData("0d 0h 0m 3.5s")]
+ [InlineData("0day 0hour 0min 3.5sec")]
+ [InlineData("0day 0hour 0minute 3.5second")]
+ [InlineData("0days 0hours 0minutes 3.5seconds")]
+ [InlineData("0 d 0 h 0 m 3.5 s")]
+ [InlineData("0 day 0 hour 0 min 3.5 sec")]
+ [InlineData("0 day 0 hour 0 minute 3.5 second")]
+ [InlineData("0 days 0 hours 0 minutes 3.5 seconds")]
+ [InlineData("0d 0h 0m 3.5s 0ms")]
+ [InlineData("0day 0hour 0min 3.5sec 0ms")]
+ [InlineData("0day 0hour 0minute 3.5second 0millisecond")]
+ [InlineData("0days 0hours 0minutes 3.5seconds 0milliseconds")]
+ [InlineData("0 d 0 h 0 m 3.5 s 0 ms")]
+ [InlineData("0 day 0 hour 0 min 3.5 sec 0 ms")]
+ [InlineData("0 day 0 hour 0 minute 3.5 second 0 millisecond")]
+ [InlineData("0 days 0 hours 0 minutes 3.5 seconds 0 milliseconds")]
+
+ // Mixed w/ spaces and w/o spaces between units
+ [InlineData("0d 0 h 0m 3.5 s 0ms")]
+ [InlineData("0day 0hour 0min 3.5 sec 0ms")]
+ [InlineData("0day 0 hour 0 minute 3.5second 0millisecond")]
+ [InlineData("0days 0hours 0minutes 3.5 seconds 0milliseconds")]
+ public void TryParse_UnrollFractionalValue_ReturnsTrueAndTimeSpanWithThreeSecondsAndFiveHundredMilliseconds(string input)
+ {
+ // Arrange
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(new TimeSpan(0, 0, 0, 3, 500));
+ parsedResult.Should().BeTrue();
+ }
+
+ [Theory]
+ [InlineData("1.25d 1.5h 1.5m 1.5s 7ms", "1.07:31:31.507")]
+ [InlineData("1.25d 1.5m 7ms", "1.06:01:30.007")]
+ [InlineData("1.5m 13ms", "00:01:30.013")]
+ public void TryParse_UnrollFractionalValue_ReturnsTrueAndTimeSpanWithCorrectValue(string input, string result)
+ {
+ // Arrange
+ _ = TimeSpan.TryParse(result, out var expectedResult);
+
+ // Act
+ var parsedResult = DurationTimeParser.TryParse(input, out var parsedValue);
+
+ // Assert
+ parsedValue.Should().Be(expectedResult);
+ parsedResult.Should().BeTrue();
+ }
+
+ #endregion
+}
From f7616893fbceca82d69fd3591d6866a60f189d3c Mon Sep 17 00:00:00 2001
From: Christopher Wolf <4867233+HaGGi13@users.noreply.github.com>
Date: Sat, 14 Feb 2026 11:41:02 +0100
Subject: [PATCH 02/17] Fix folder structure
---
src/{ => DurationMancer}/DurationMancer.csproj | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename src/{ => DurationMancer}/DurationMancer.csproj (100%)
diff --git a/src/DurationMancer.csproj b/src/DurationMancer/DurationMancer.csproj
similarity index 100%
rename from src/DurationMancer.csproj
rename to src/DurationMancer/DurationMancer.csproj
From ec468a903e83a38f40798adac1b46a2acfbfd9ef Mon Sep 17 00:00:00 2001
From: Christopher Wolf <4867233+HaGGi13@users.noreply.github.com>
Date: Sun, 15 Feb 2026 16:55:21 +0100
Subject: [PATCH 03/17] Add assets and documentation for NuGet packaging
---
README.md | 85 ++++++++++++++++++++
assets/images/package-icon.png | Bin 0 -> 82170 bytes
assets/images/package-icon.svg | 97 +++++++++++++++++++++++
src/DurationMancer/DurationMancer.csproj | 20 +++++
4 files changed, 202 insertions(+)
create mode 100644 README.md
create mode 100644 assets/images/package-icon.png
create mode 100644 assets/images/package-icon.svg
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b95d745
--- /dev/null
+++ b/README.md
@@ -0,0 +1,85 @@
+# DurationMancer
+
+DurationMancer is a simle library that provides a flexible time parser that understands both technical and natural language inputs. You can use these formats wherever a time duration is required and you don't want to deal with the hassle of parsing strings yourself.
+
+ℹ️ Negative values are not supported (yet)!
+
+## Usage
+
+The usage very simple, like shown in the examples below.
+
+### Humand-readable Format
+```csharp
+// Example 1: Mix of full words and shorthand
+string input1 = "2days 4h 15m 30s";
+TimeSpan result1 = DurationParser.Parse(input1);
+// result1 == TimeSpan.FromDays(2) + TimeSpan.FromHours(4) + TimeSpan.FromMinutes(15) + TimeSpan.FromSeconds(30)
+
+// Example 2: Decimal values with flexible spacing
+string input2 = "1.5 hours 100ms";
+TimeSpan result2 = DurationParser.Parse(input2);
+// result2 == TimeSpan.FromHours(1.5) + TimeSpan.FromMilliseconds(100)
+```
+
+### Standard Duration Format
+```csharp
+// Example 1: Days, hours, minutes, and seconds
+string input1 = "1.05:00:00";
+TimeSpan result1 = DurationParser.Parse(input1);
+// result1 == new TimeSpan(days: 1, hours: 5, minutes: 0, seconds: 0)
+
+// Example 2: Seconds with milliseconds
+string input2 = "00:00:45.500";
+TimeSpan result2 = DurationParser.Parse(input2);
+// result2 == TimeSpan.FromSeconds(45.5)
+```
+
+## Supported Formats
+
+### Standard Duration Format
+
+This format is best for precise, structured input. It follows the pattern: `[days.]HH:mm:ss[.milliseconds]`
+
+| Component | Format | Range / Description |
+| :--------------- | :----- | :------------------------------------------------ |
+| **Days** | `d.` | Optional. Any number of digits followed by a dot. |
+| **Hours** | `HH` | **Required.** 00 to 23. |
+| **Minutes** | `mm` | **Required.** 00 to 59. |
+| **Seconds** | `ss` | **Required.** 00 to 59. |
+| **Milliseconds** | `.fff` | Optional. Up to 3 digits following a dot. |
+
+**Examples:**
+
+- `12:30:00` → 12 hours, 30 minutes
+- `1.05:00:00` → 1 day, 5 hours
+- `00:00:45.500` → 45.5 seconds
+
+---
+
+### Human-Readable Format
+
+This format is ideal for quick CLI input. You can mix and match units using full names or shorthand.
+
+#### Supported Units
+
+| Unit | Keywords (Case-Insensitive) |
+| :--------------- | :---------------------------------- |
+| **Days** | `d`, `day`, `days` |
+| **Hours** | `h`, `hour`, `hours` |
+| **Minutes** | `m`, `min`, `minute`, `minutes` |
+| **Seconds** | `s`, `sec`, `second`, `seconds` |
+| **Milliseconds** | `ms`, `millisecond`, `milliseconds` |
+
+#### Key Features
+
+- **Decimal Support:** You can use decimals for larger units (e.g., `1.5h` for 1 hour and 30 minutes).
+- **Flexible Spacing:** `1h30m` and `1 h 30 m` are both valid.
+- **Automatic Rounding:** All inputs are rounded to the nearest millisecond.
+
+**Examples:**
+
+- `1d 12h`
+- `5m 30s`
+- `1.5 hours`
+- `100ms`
+- `2days 4h 15m 30s`
diff --git a/assets/images/package-icon.png b/assets/images/package-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..682d35a19024dda61fe3b011f3142827c0503199
GIT binary patch
literal 82170
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