mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-03-30 12:53:45 -04:00
No made up format for minutes as .NET already knows how to format them.
User documentation needs to follow.
This commit is contained in:
@@ -71,7 +71,7 @@ public static class UtilityExtensions
|
||||
IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
|
||||
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
|
||||
|
||||
LengthInMinutes = libraryBook.Book.LengthInMinutes,
|
||||
LengthInMinutes = TimeSpan.FromMinutes(libraryBook.Book.LengthInMinutes),
|
||||
Language = libraryBook.Book.Language?.Trim(),
|
||||
Codec = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.CodecString,
|
||||
BitRate = libraryBook.Book.UserDefinedItem.LastDownloadedFormat?.BitRate,
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace FileManager.NamingTemplate;
|
||||
public static partial class CommonFormatters
|
||||
{
|
||||
public const string DefaultDateFormat = "yyyy-MM-dd";
|
||||
public const string DefaultTimeSpanFormat = "MMM";
|
||||
|
||||
public delegate TFormatted? PropertyFormatter<in TProperty, out TFormatted>(ITemplateTag templateTag, TProperty? value, string? formatString, CultureInfo? culture);
|
||||
|
||||
@@ -107,38 +108,39 @@ public static partial class CommonFormatters
|
||||
return new string('0', zeroPad) + strValue;
|
||||
}
|
||||
|
||||
public static string MinutesFormatter(ITemplateTag templateTag, int value, string? formatString, CultureInfo? culture)
|
||||
public static string MinutesFormatter(ITemplateTag templateTag, TimeSpan value, string? formatString, CultureInfo? culture)
|
||||
{
|
||||
culture ??= CultureInfo.CurrentCulture;
|
||||
if (string.IsNullOrWhiteSpace(formatString))
|
||||
return value.ToString(culture);
|
||||
formatString ??= DefaultTimeSpanFormat;
|
||||
|
||||
var timeSpan = TimeSpan.FromMinutes(value);
|
||||
var result = formatString;
|
||||
// the format string is build as a custom format for TimeSpans. Time portion like 'h' and 'm' are used to format the minutes and hours part of the TimeSpan.
|
||||
// They are limited by the next greater domain. So 'h' will be between 0 and 23, 'm' between 0 and 59. 'd' will be the total number of days.
|
||||
// To get the total timespan display in terms of total hours or total minutes, we allow the format string to include number formats with uppercase D, H or M.
|
||||
// As there might be up to three numbers shown in the format string, we distinguish between total days, hours and minutes with uppercase D, H and M instead of zeros.
|
||||
// A format "#,##0'minutes'" would for example become "#,##M'minutes'". If you combine them the lower units will be reduced by the higher units.
|
||||
// "D'days and'#,##0'minutes'" will show 1,439 minutes at maximum.
|
||||
// In the first step we search for number formats with uppercase D, H or M, format their values as number and replace them as quoted strings in the format string.
|
||||
var timeSpanForTotal = value;
|
||||
formatString = FormatAsNumberIntoTemplate(templateTag, formatString, culture, ref timeSpanForTotal, RegexMinutesTotalD(), TimeSpan.TicksPerDay);
|
||||
formatString = FormatAsNumberIntoTemplate(templateTag, formatString, culture, ref timeSpanForTotal, RegexMinutesTotalH(), TimeSpan.TicksPerHour);
|
||||
formatString = FormatAsNumberIntoTemplate(templateTag, formatString, culture, ref timeSpanForTotal, RegexMinutesTotalM(), TimeSpan.TicksPerMinute);
|
||||
|
||||
// replace all placeholders with formatted values
|
||||
result = RegexMinutesD().Replace(result, m =>
|
||||
// The formatString should now be a valid TimeSpan format string.
|
||||
return value.ToString(formatString, culture);
|
||||
}
|
||||
|
||||
private static string FormatAsNumberIntoTemplate(ITemplateTag templateTag, string formatString, CultureInfo culture, ref TimeSpan timeSpanForTotal, Regex regex, long ticks)
|
||||
{
|
||||
var total = timeSpanForTotal.Ticks / ticks;
|
||||
var matched = false;
|
||||
var result = regex.Replace(formatString, m =>
|
||||
{
|
||||
var val = (int)timeSpan.TotalDays;
|
||||
timeSpan = timeSpan.Subtract(TimeSpan.FromDays(val));
|
||||
|
||||
return FloatFormatter(templateTag, val, m.Groups["format"].Value, culture);
|
||||
matched = true;
|
||||
var numPattern = RegexTimeStampToNumberPattern().Replace(m.Groups["format"].Value, "0");
|
||||
var formatted = FloatFormatter(templateTag, total, numPattern, culture);
|
||||
return $"'{formatted}'";
|
||||
});
|
||||
result = RegexMinutesH().Replace(result, m =>
|
||||
{
|
||||
var val = (int)timeSpan.TotalHours;
|
||||
timeSpan = timeSpan.Subtract(TimeSpan.FromHours(val));
|
||||
|
||||
return FloatFormatter(templateTag, val, m.Groups["format"].Value, culture);
|
||||
});
|
||||
result = RegexMinutesM().Replace(result, m =>
|
||||
{
|
||||
var val = (int)timeSpan.TotalMinutes;
|
||||
timeSpan = timeSpan.Subtract(TimeSpan.FromMinutes(val));
|
||||
|
||||
return FloatFormatter(templateTag, val, m.Groups["format"].Value, culture);
|
||||
});
|
||||
|
||||
if (matched) timeSpanForTotal = TimeSpan.FromTicks(timeSpanForTotal.Ticks % ticks);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -155,13 +157,38 @@ public static partial class CommonFormatters
|
||||
return StringFormatter(templateTag, language, "3u", culture);
|
||||
}
|
||||
|
||||
// Regex to find patterns like {D:3}, {h:4}, {m}
|
||||
[GeneratedRegex("""\{D(?::(?<format>(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex RegexMinutesD();
|
||||
// These search for number formats with all notions of escaping and quoting, but all zeros replaced with D, H, or M to indicate that they should be replaced with the total number of
|
||||
// days hours or minutes in the timespan (not just the minutes part). Only one of them is written commented. The others are identical except for the letter D, H or M.
|
||||
// I most cases this regex will only find a straight bunch of D's, H's or M's, but it also allows for more complex formats.
|
||||
[GeneratedRegex("""
|
||||
(?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with #
|
||||
(?<=\G(?: # We lookbehind up to the start or the end of the last match for a number format.
|
||||
\\. # - '\' escapes allways the next character. Especially further '\' and the closing ']'
|
||||
| '(?:[^']|'')*' # - allow 'string' to be included in the format, with '' being an escaped ' character
|
||||
| "(?:[^"]|"")*" # - allow "string" to be included in the format, with "" being an escaped " character
|
||||
| . # - match any character. This will not catch the number format at first. Because ...
|
||||
) *? ) # With *? the pattern above tries not to consume the number format.
|
||||
(?<format> # We capture the whole number format in a group called '<format>'.
|
||||
(?:\#[\#,.]*)? # - For grouping a number format may start with `#` and grouping hints `,` or even a decimal point `.`.
|
||||
D # - At least one unescaped, unquoted uppercase D must be included in the format to indicate that this is a total days format.
|
||||
(?:(?: # - Before further D's, there may be any combination of escaped characters and quoted strings.
|
||||
\\. # - '\' escapes allways the next character. Especially further '\' and the closing ']'
|
||||
| '(?:[^']|'')*' # - allow 'string' to be included in the format, with '' being an escaped ' character
|
||||
| "(?:[^"]|"")*" # - allow "string" to be included in the format, with "" being an escaped " character
|
||||
)* [\#,.%‰D]+ # After escaped characters and quoted strings, there needs to be at least one more real number format character (which may be D as well).
|
||||
)* # This may extend the format several times, for example in `D\:DD` or `D' days 'D\-D`.
|
||||
(?:[Ee][+-]?0+)? # The original number format may end with an optional scientific notation part. This is also optional.
|
||||
) # end of capture group '<format>'
|
||||
""")]
|
||||
private static partial Regex RegexMinutesTotalD();
|
||||
|
||||
[GeneratedRegex("""\{H(?::(?<format>(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex RegexMinutesH();
|
||||
[GeneratedRegex("""(?<=\G(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?)(?<format>(?:#[#,.]*)?H(?:(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*")*[H%‰#,.]+)*(?:[Ee][+-]?0+)?)""")]
|
||||
private static partial Regex RegexMinutesTotalH();
|
||||
|
||||
[GeneratedRegex("""\{M(?::(?<format>(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?))?\}""", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex RegexMinutesM();
|
||||
[GeneratedRegex("""(?<=\G(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?)(?<format>(?:#[#,.]*)?M(?:(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*")*[M%‰#,.]+)*(?:[Ee][+-]?0+)?)""")]
|
||||
private static partial Regex RegexMinutesTotalM();
|
||||
|
||||
// Capture all D H or M characters in the number format, so that they can be replaced with zeros.
|
||||
[GeneratedRegex("""(?<=\G(?:\\.|'(?:[^']|'')*'|"(?:[^"]|"")*"|.)*?)[DHM]""")]
|
||||
private static partial Regex RegexTimeStampToNumberPattern();
|
||||
}
|
||||
@@ -188,6 +188,7 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
|
||||
null => false,
|
||||
IEnumerable<object> e => checkItem(e.Count(), culture),
|
||||
string s => checkItem(s.Length, culture),
|
||||
TimeSpan ts => checkItem(ts.TotalMinutes, culture),
|
||||
_ => checkItem(v, culture)
|
||||
}
|
||||
: (v, culture) => v switch
|
||||
|
||||
@@ -27,7 +27,7 @@ public class BookDto
|
||||
public bool IsPodcastParent { get; set; }
|
||||
public bool IsPodcast { get; set; }
|
||||
|
||||
public int LengthInMinutes { get; set; }
|
||||
public TimeSpan LengthInMinutes { get; set; }
|
||||
public int? BitRate { get; set; }
|
||||
public int? SampleRate { get; set; }
|
||||
public int? Channels { get; set; }
|
||||
|
||||
@@ -34,8 +34,8 @@ public class CommonFormattersTests
|
||||
{
|
||||
// GIVEN
|
||||
var templateTag = new TemplateTag { TagName = "MINUTES" };
|
||||
var value = 0;
|
||||
var format = "{H}:{M}";
|
||||
var value = TimeSpan.FromMinutes(0);
|
||||
var format = @"h\:m";
|
||||
|
||||
// WHEN
|
||||
var result = CommonFormatters.MinutesFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
@@ -49,8 +49,8 @@ public class CommonFormattersTests
|
||||
{
|
||||
// GIVEN
|
||||
var templateTag = new TemplateTag { TagName = "MINUTES" };
|
||||
var value = 1440; // 24 hours
|
||||
var format = "{D}d {H}h {M}m";
|
||||
var value = TimeSpan.FromHours(24);
|
||||
var format = @"d'd 'h'h 'm\m";
|
||||
|
||||
// WHEN
|
||||
var result = CommonFormatters.MinutesFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
@@ -64,8 +64,8 @@ public class CommonFormattersTests
|
||||
{
|
||||
// GIVEN
|
||||
var templateTag = new TemplateTag { TagName = "MINUTES" };
|
||||
var value = 3000; // 50 hours
|
||||
var format = "{D}d {H}h {M}m";
|
||||
var value = TimeSpan.FromHours(50); // 50 hours
|
||||
var format = @"d'd 'h'h 'm\m";
|
||||
|
||||
// WHEN
|
||||
var result = CommonFormatters.MinutesFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace TemplatesTests
|
||||
Codec = @"AAC[LC]\MP3", // special chars added
|
||||
FileVersion = null, // explicitly null
|
||||
LibationVersion = "", // explicitly empty string
|
||||
LengthInMinutes = 100,
|
||||
LengthInMinutes = TimeSpan.FromMinutes(100),
|
||||
IsAbridged = true,
|
||||
Tags = [new StringDto("Tag1"), new StringDto("Tag2"), new StringDto("Tag3")],
|
||||
};
|
||||
@@ -194,26 +194,29 @@ namespace TemplatesTests
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<minutes>", 100, "100")]
|
||||
[DataRow("<minutes[{m}]>", 100, "100")]
|
||||
[DataRow("<minutes[{m:2}]>", 100, "100")]
|
||||
[DataRow("<minutes[{h}-{m}]>", 100, "1-40")]
|
||||
[DataRow("<minutes[{h:2}-{m:2}]>", 100, "01-40")]
|
||||
[DataRow("<minutes[{d}.{h:2}-{m:2}]>", 100, "0.01-40")]
|
||||
[DataRow("<minutes[{d:2}d{h:2}h{m:2}m]>", 100, "00d01h40m")]
|
||||
[DataRow("<minutes[{d} days, {h} hours, {m} minutes]>", 100, "0 days, 1 hours, 40 minutes")]
|
||||
[DataRow("<minutes[{h}-{m}]>", 2000, "33-20")]
|
||||
[DataRow("<minutes[{d:3}-{h:3}-{m:3}]>", 2000, "001-009-020")]
|
||||
[DataRow("<minutes[{m}-{h}-{d}]>", 2000, "20-9-1")]
|
||||
[DataRow("<minutes[{d}-{m}]>", 100, "0-100")]
|
||||
[DataRow("<minutes[{d}-{m}]>", 1500, "1-60")]
|
||||
[DataRow("<minutes[{d}-{m}]>", 2000, "1-560")]
|
||||
[DataRow("<minutes[{d}-{m}]>", 2880, "2-0")]
|
||||
[DataRow("<minutes[{d:2}-{m:2}]>", 1500, "01-60")]
|
||||
[DataRow(@"<minutes[{d:0}-{m:000'{'00\}}]>", 2000, "1-005{60}")]
|
||||
[DataRow("<minutes[M]>", 100, "100")]
|
||||
[DataRow("<minutes[MM]>", 100, "100")]
|
||||
[DataRow(@"<minutes[H\-m]>", 100, "1-40")]
|
||||
[DataRow(@"<minutes[hh\-MM]>", 100, "01-100")]
|
||||
[DataRow(@"<minutes[%m\ m\ mm]>", 100, "40 40 40")]
|
||||
[DataRow(@"<minutes[\%M\ M\ MM]>", 100, "%0 1 00")]
|
||||
[DataRow(@"<minutes[D\.hh\-MM]>", 100, "0.01-100")]
|
||||
[DataRow(@"<minutes[dd\dhh\hmm\m]>", 100, "00d01h40m")]
|
||||
[DataRow("""<minutes[d'[days], 'h"(hours), "m'{minutes}']>""", 100, "0[days], 1(hours), 40{minutes}")]
|
||||
[DataRow(@"<minutes[H\-M]>", 2000, "33-20")]
|
||||
[DataRow(@"<minutes[DDD\-HHH\-MMM]>", 2000, "001-009-020")]
|
||||
[DataRow(@"<minutes[M\-H\-D]>", 2000, "20-9-1")]
|
||||
[DataRow(@"<minutes[D\-M]>", 100, "0-100")]
|
||||
[DataRow(@"<minutes[D\-M]>", 1500, "1-60")]
|
||||
[DataRow(@"<minutes[D\-M]>", 2000, "1-560")]
|
||||
[DataRow(@"<minutes[D\-M]>", 2880, "2-0")]
|
||||
[DataRow(@"<minutes[DD\-MM]>", 1500, "01-60")]
|
||||
[DataRow(@"<minutes[D\-MMM'{'MM\}]>", 2000, "1-005{60}")]
|
||||
[DataRow(@"<minutes[D,DDD.DDE-0\-H,HHH.HH\-#,##M.##]>", 123456789, "8.573,30E1-0.021,00-9")]
|
||||
public void MinutesFormat(string template, int minutes, string expected)
|
||||
{
|
||||
var bookDto = GetLibraryBook();
|
||||
bookDto.LengthInMinutes = minutes;
|
||||
bookDto.LengthInMinutes = TimeSpan.FromMinutes(minutes);
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
fileTemplate.GetFilename(bookDto, "", "", Replacements).PathWithoutPrefix.Should().Be(expected);
|
||||
|
||||
Reference in New Issue
Block a user