Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion benchmarks/DurationMancer.Benchmarks/Configuration.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Jobs;

namespace DurationMancer.Benchmarks;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,28 @@ public class Parse
public TimeSpan Standard_HHmmss()
=> DurationTimeParser.Parse("12:30:45");

[Benchmark(Description = "Standard negative: -HH:mm:ss")]
public TimeSpan Standard_NegativeHHmmss()
=> DurationTimeParser.Parse("-12:30:45");

[Benchmark(Description = "Standard: d.HH:mm:ss.fff")]
public TimeSpan Standard_Full()
=> DurationTimeParser.Parse("3.08:15:30.250");

[Benchmark(Description = "Standard negative: -d.HH:mm:ss.fff")]
public TimeSpan Standard_NegativeFull()
=> DurationTimeParser.Parse("-3.08:15:30.250");

// --- Human-readable format inputs ---

[Benchmark(Description = "Human: single unit (5m)")]
public TimeSpan Human_SingleUnit()
=> DurationTimeParser.Parse("5m");

[Benchmark(Description = "Human: single unit negative (-5m)")]
public TimeSpan Human_NegativeSingleUnit()
=> DurationTimeParser.Parse("-5m");

[Benchmark(Description = "Human: two units (1h 30m)")]
public TimeSpan Human_TwoUnits()
=> DurationTimeParser.Parse("1h 30m");
Expand All @@ -33,6 +45,10 @@ public TimeSpan Human_TwoUnits()
public TimeSpan Human_FullCombo()
=> DurationTimeParser.Parse("2 days 4 hours 15 minutes 30 seconds 500 milliseconds");

[Benchmark(Description = "Human: full combo negative")]
public TimeSpan Human_NegativeFullCombo()
=> DurationTimeParser.Parse("-2 days 4 hours 15 minutes 30 seconds 500 milliseconds");

[Benchmark(Description = "Human: decimals (1.5h 100ms)")]
public TimeSpan Human_Decimals()
=> DurationTimeParser.Parse("1.5h 100ms");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,24 @@ public bool Standard_HHmmss()
return DurationTimeParser.TryParse("12:30:45", out _);
}

[Benchmark(Description = "Standard negative: -HH:mm:ss")]
public bool Standard_NegativeHHmmss()
{
return DurationTimeParser.TryParse("-12:30:45", out _);
}

[Benchmark(Description = "Standard: d.HH:mm:ss.fff")]
public bool Standard_Full()
{
return DurationTimeParser.TryParse("3.08:15:30.250", out _);
}

[Benchmark(Description = "Standard negative: -d.HH:mm:ss.fff")]
public bool Standard_NegativeFull()
{
return DurationTimeParser.TryParse("-3.08:15:30.250", out _);
}

// --- Human-readable format inputs ---

[Benchmark(Description = "Human: single unit (5m)")]
Expand All @@ -31,6 +43,12 @@ public bool Human_SingleUnit()
return DurationTimeParser.TryParse("5m", out _);
}

[Benchmark(Description = "Human: single unit negative (-5m)")]
public bool Human_NegativeSingleUnit()
{
return DurationTimeParser.TryParse("-5m", out _);
}

[Benchmark(Description = "Human: two units (1h 30m)")]
public bool Human_TwoUnits()
{
Expand All @@ -43,6 +61,12 @@ public bool Human_FullCombo()
return DurationTimeParser.TryParse("2 days 4 hours 15 minutes 30 seconds 500 milliseconds", out _);
}

[Benchmark(Description = "Human: full combo negative")]
public bool Human_NegativeFullCombo()
{
return DurationTimeParser.TryParse("-2 days 4 hours 15 minutes 30 seconds 500 milliseconds", out _);
}

[Benchmark(Description = "Human: decimals (1.5h 100ms)")]
public bool Human_Decimals()
{
Expand Down
53 changes: 39 additions & 14 deletions src/DurationMancer/DurationTimeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,28 @@ namespace DurationMancer;
/// </summary>
public static partial class DurationTimeParser
{
private const string MinusPattern = @"(?<minus>-)?\s*";
private const string DaysPattern = @"(?<days>\d+(?:\.\d+)?)\s*(d|day|days)\b";
private const string HoursPattern = @"(?<hours>\d+(?:\.\d+)?)\s*(h|hour|hours)\b";
private const string MillisecondsPattern = @"(?<milliseconds>\d+)\s*(ms|millisecond|milliseconds)\b";
private const string MinutesPattern = @"(?<minutes>\d+(?:\.\d+)?)\s*(m|min|minute|minutes)\b";
private const string SecondsPattern = @"(?<seconds>\d+(?:\.\d+)?)\s*(s|sec|second|seconds)\b";

private const string StandardDurationTimeFormat =
@"^(?:(?<d>\d+)\.)?(?<hh>[01]\d|2[0-3]):(?<mm>[0-5]\d):(?<ss>[0-5]\d)(?:\.(?<ms>\d{0,3}))?$";
@$"^{MinusPattern}(?:(?<d>\d+)\.)?(?<hh>[01]\d|2[0-3]):(?<mm>[0-5]\d):(?<ss>[0-5]\d)(?:\.(?<ms>\d{{0,3}}))?$";

private const string TimePattern = $"{StandardDurationTimeFormat}|" +
@$"^(?:{DaysPattern})?\s*" +
@$"(?:{HoursPattern})?\s*" +
@$"(?:{MinutesPattern})?\s*" +
@$"(?:{SecondsPattern})?\s*" +
$"(?:{MillisecondsPattern})?$";
private const string AnyHumanReadableUnitPattern =
@"\d+(?:\.\d+)?\s*(?:d|day|days|h|hour|hours|m|min|minute|minutes|s|sec|second|seconds)\b|\d+\s*(?:ms|millisecond|milliseconds)\b";

private const string HumanReadableDurationTimeFormat =
@$"^{MinusPattern}(?=.*(?:{AnyHumanReadableUnitPattern}))" +
@$"(?:{DaysPattern})?\s*" +
@$"(?:{HoursPattern})?\s*" +
@$"(?:{MinutesPattern})?\s*" +
@$"(?:{SecondsPattern})?\s*" +
@$"(?:{MillisecondsPattern})?$";

private const string TimePattern = $"{StandardDurationTimeFormat}|{HumanReadableDurationTimeFormat}";

private static readonly Regex TimeRegex = CreateTimeFormatRegex();

Expand All @@ -40,8 +47,8 @@ public static partial class DurationTimeParser
/// Supports two format types:
/// <para>
/// 1. Standard duration format:<br />
/// - [d.]HH:mm:ss[.fff]<br />
/// 2. Human-readable format with combinations of:<br />
/// - [-][d.]HH:mm:ss[.fff]<br />
/// 2. Human-readable format with combinations of the following elements, optionally preceded by "-":<br />
/// - days: "1d", "2 days"<br />
/// - hours: "1h", "2 hours"<br />
/// - minutes: "1m", "5 minutes"<br />
Expand All @@ -51,6 +58,7 @@ public static partial class DurationTimeParser
/// Examples of valid human-readable formats:<br />
/// - "1s"<br />
/// - "5 minutes"<br />
/// - "-7 minutes"<br />
/// - "3m 10s"<br />
/// - "1 hour 30 minutes"<br />
/// - "2 days 4 hours 15m 30s"<br />
Expand All @@ -73,12 +81,24 @@ public static bool TryParse(string? input, out TimeSpan timeSpan)
return false;
}

return TryParseDurationFormat(match, out timeSpan) ||
TryParseHumanReadableDurationFormat(match, out timeSpan);
if (!TryParseDurationFormat(match, out timeSpan) &&
!TryParseHumanReadableDurationFormat(match, out timeSpan))
{
return false;
}

var isNegative = match.Groups["minus"].Success;
if (isNegative)
{
timeSpan = -timeSpan;
}

return true;
}

/// <summary>
/// Parses a given input string into a <see cref="TimeSpan" /> representation.
/// Supports standard duration format and human-readable duration format.
/// Throws a <see cref="FormatException" /> if the input string is not in a valid duration format.
/// </summary>
/// <param name="input">The input string that represents a duration in a valid format.</param>
Expand All @@ -91,7 +111,8 @@ public static TimeSpan Parse(string? input)

return TryParse(input, out var result)
? result
: throw new FormatException($"Input string '{input}' was not in a correct duration format.");
: throw new FormatException(
$"The input string '{input}' was not recognized as a valid duration. Expected either standard format '[-][d.]HH:mm:ss[.fff]' or human-readable format (e.g. '[-]1d 2h 30m 15s 100ms').");
}

/// <summary>
Expand Down Expand Up @@ -193,8 +214,12 @@ private static bool TryParseHumanReadableDurationFormat(Match match, out TimeSpa
/// <returns>The new rounded <see cref="TimeSpan" /> instance.</returns>
private static TimeSpan RoundOnMilliseconds(TimeSpan timeSpan)
{
var roundedTicks =
(long)(Math.Round((double)timeSpan.Ticks / TimeSpan.TicksPerMillisecond) * TimeSpan.TicksPerMillisecond);
var milliseconds = (double)timeSpan.Ticks / TimeSpan.TicksPerMillisecond;

// Round to the nearest millisecond
var roundedMilliseconds = Math.Round(milliseconds);

var roundedTicks = (long)roundedMilliseconds * TimeSpan.TicksPerMillisecond;

return new TimeSpan(roundedTicks);
}
Expand Down
Loading