using System;
using System.Globalization;
using System.Text.RegularExpressions;
namespace TwitchDownloaderCLI.Models
{
public readonly record struct TimeDuration
{
public static TimeDuration MinusOneSeconds { get; } = new(-1 * TimeSpan.TicksPerSecond);
private readonly TimeSpan _timeSpan;
///
/// Constructor used by CommandLineParser
///
public TimeDuration(string str)
{
this = Parse(str);
}
public TimeDuration(TimeSpan timeSpan)
{
_timeSpan = timeSpan;
}
public TimeDuration(long ticks)
{
_timeSpan = TimeSpan.FromTicks(ticks);
}
public override string ToString() => _timeSpan.ToString();
public static TimeDuration Parse(string str)
{
if (string.IsNullOrWhiteSpace(str))
{
throw new FormatException();
}
str = str.Trim();
if (str.Contains(':'))
{
var timeSpan = ParseTimeSpan(str);
return new TimeDuration(timeSpan);
}
var multiplier = GetMultiplier(str, out var span);
if (decimal.TryParse(span, NumberStyles.Number, null, out var result))
{
var ticks = (long)(result * multiplier);
return new TimeDuration(ticks);
}
throw new FormatException();
}
private static TimeSpan ParseTimeSpan(string str)
{
// TimeSpan.Parse interprets '36:01:02' as 36 days, 1 hour, and 2 minutes, so we need to manually parse it ourselves
var match = Regex.Match(str, @"^(?:(\d{1,})[.:])?(\d{2,}):(\d{1,2}):(\d{1,2})(?:\.(\d{1,3})\d*)?$");
if (match.Success)
{
if (!int.TryParse(match.Groups[1].ValueSpan, out var days)) days = 0;
if (!int.TryParse(match.Groups[2].ValueSpan, out var hours)) hours = 0;
if (!int.TryParse(match.Groups[3].ValueSpan, out var minutes)) minutes = 0;
if (!int.TryParse(match.Groups[4].ValueSpan, out var seconds)) seconds = 0;
if (!int.TryParse(match.Groups[5].Value.PadRight(3, '0'), out var milliseconds)) milliseconds = 0;
return new TimeSpan(days, hours, minutes, seconds, milliseconds);
}
return TimeSpan.Parse(str); // Parse formats not covered by the regex
}
private static long GetMultiplier(string input, out ReadOnlySpan trimmedInput)
{
if (char.IsDigit(input[^1]))
{
trimmedInput = input.AsSpan();
return TimeSpan.TicksPerSecond;
}
if (Regex.IsMatch(input, @"\dms$", RegexOptions.RightToLeft))
{
trimmedInput = input.AsSpan()[..^2];
return TimeSpan.TicksPerMillisecond;
}
if (Regex.IsMatch(input, @"\ds$", RegexOptions.RightToLeft))
{
trimmedInput = input.AsSpan()[..^1];
return TimeSpan.TicksPerSecond;
}
if (Regex.IsMatch(input, @"\dm$", RegexOptions.RightToLeft))
{
trimmedInput = input.AsSpan()[..^1];
return TimeSpan.TicksPerMinute;
}
if (Regex.IsMatch(input, @"\dh$", RegexOptions.RightToLeft))
{
trimmedInput = input.AsSpan()[..^1];
return TimeSpan.TicksPerHour;
}
throw new FormatException();
}
public static implicit operator TimeSpan(TimeDuration timeDuration) => timeDuration._timeSpan;
}
}