using System;
using System.Text;
using TwitchDownloaderCore.Extensions;
namespace TwitchDownloaderCore.Tools
{
/// Adds an 'H' parameter to string formatting. The 'H' parameter is equivalent to flooring ..
///
/// For optimal memory performance, resulting strings split about any 'H' parameters should be less than 256 chars in length.
///
public class TimeSpanHFormat : IFormatProvider, ICustomFormatter
{
public static readonly TimeSpanHFormat ReusableInstance = new();
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
return this;
else
return null;
}
public string Format(string format, object arg, IFormatProvider formatProvider = null)
{
if (arg is TimeSpan timeSpan)
{
return Format(format, timeSpan);
}
return HandleOtherFormats(format, arg, formatProvider);
}
/// Provides an identical output to but without boxing the .
/// This method is not part of the interface.
public string Format(string format, TimeSpan timeSpan, IFormatProvider formatProvider = null)
{
if (string.IsNullOrEmpty(format))
{
return timeSpan.ToString(format, formatProvider);
}
if (!format.Contains('H'))
{
return HandleOtherFormats(format, timeSpan, formatProvider);
}
// If the timespan is less than 24 hours, HandleOtherFormats can be up to 3x faster and half the allocations
if (timeSpan.Days == 0)
{
var newFormat = format.Length <= 256 ? stackalloc char[format.Length] : new char[format.Length];
if (!format.AsSpan().TryReplaceNonEscaped(newFormat, 'H', 'h'))
{
throw new FormatException($"Invalid character escaping in the format string: {format}");
}
// If the format contains more than 2 sequential unescaped h's, it will throw a format exception. If so, we can fallback to our parser.
if (newFormat.IndexOf("hhh") == -1)
{
return HandleOtherFormats(newFormat.ToString(), timeSpan, formatProvider);
}
}
return HandleBigHFormat(format.AsSpan(), timeSpan);
}
private static string HandleBigHFormat(ReadOnlySpan format, TimeSpan timeSpan)
{
var formatLength = format.Length;
var sb = new StringBuilder(formatLength);
var regularFormatCharStart = -1;
var bigHStart = -1;
for (var i = 0; i < formatLength; i++)
{
var readChar = format[i];
if (readChar == 'H')
{
if (bigHStart == -1)
bigHStart = i;
if (regularFormatCharStart != -1)
{
var formatEnd = i - regularFormatCharStart;
AppendRegularFormat(sb, timeSpan, format.Slice(regularFormatCharStart, formatEnd));
regularFormatCharStart = -1;
}
}
else
{
if (regularFormatCharStart == -1)
regularFormatCharStart = i;
if (bigHStart != -1)
{
var bigHCount = i - bigHStart;
AppendBigHFormat(sb, timeSpan, bigHCount);
bigHStart = -1;
}
switch (readChar)
{
// If the current char is an escape we can skip the next char
case '\\':
i++;
continue;
// If the current char is a quote we can skip the next quote, if it exists
case '\'':
case '\"':
{
i = FindCloseQuoteChar(format, i, formatLength, readChar);
if (i == -1)
{
throw new FormatException($"Invalid character escaping in the format string: {format}");
}
continue;
}
}
}
}
if (regularFormatCharStart != -1)
{
var formatEnd = format.Length - regularFormatCharStart;
AppendRegularFormat(sb, timeSpan, format.Slice(regularFormatCharStart, formatEnd));
}
else if (bigHStart != -1)
{
var bigHCount = format.Length - bigHStart;
AppendBigHFormat(sb, timeSpan, bigHCount);
}
return sb.ToString();
}
private static int FindCloseQuoteChar(ReadOnlySpan destination, int openQuoteIndex, int endIndex, char openQuoteChar)
{
var i = openQuoteIndex + 1;
while (i < endIndex)
{
var readChar = destination[i];
i++;
if (readChar == '\\')
{
i++;
continue;
}
if (readChar == openQuoteChar)
{
i--;
return i;
}
}
return -1;
}
private static void AppendRegularFormat(StringBuilder sb, TimeSpan timeSpan, ReadOnlySpan format)
{
Span destination = stackalloc char[256];
if (timeSpan.TryFormat(destination, out var charsWritten, format))
{
sb.Append(destination[..charsWritten]);
}
else
{
sb.Append(timeSpan.ToString(format.ToString()));
}
}
private static void AppendBigHFormat(StringBuilder sb, TimeSpan timeSpan, int count)
{
const int TIMESPAN_MAX_HOURS_LENGTH = 9; // The maximum integer hours a TimeSpan can hold is 256204778.
Span destination = stackalloc char[TIMESPAN_MAX_HOURS_LENGTH];
if (((uint)timeSpan.TotalHours).TryFormat(destination, out var charsWritten))
{
sb.Append('0', Math.Max(0, count - charsWritten));
sb.Append(destination[..charsWritten]);
}
else
{
var hourString = ((uint)timeSpan.TotalHours).ToString();
sb.Append('0', Math.Max(0, count - hourString.Length));
sb.Append(hourString);
}
}
private static string HandleOtherFormats(string format, object arg, IFormatProvider formatProvider)
{
if (arg is IFormattable formattable)
return formattable.ToString(format, formatProvider);
else if (arg != null)
return arg.ToString();
else
return "";
}
private static string HandleOtherFormats(string format, TimeSpan arg, IFormatProvider formatProvider) => arg.ToString(format, formatProvider);
}
}