2025-05-01 01:48:08 -07:00

561 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace UnityEngine.Localization.SmartFormat.Utilities
{
public static class TimeSpanUtility
{
/// <summary>
/// <para>Turns a TimeSpan into a human-readable text.</para>
/// <para>Uses the specified timeSpanFormatOptions.</para>
/// <para>For example: "31.23:59:00.555" = "31 days 23 hours 59 minutes 0 seconds 555 milliseconds"</para>
/// </summary>
/// <param name="FromTime"></param>
/// <param name="options">
/// <para>A combination of flags that determine the formatting options.</para>
/// <para>These will be combined with the default timeSpanFormatOptions.</para>
/// </param>
/// <param name="timeTextInfo">An object that supplies the text to use for output</param>
public static string ToTimeString(this TimeSpan FromTime, TimeSpanFormatOptions options,
TimeTextInfo timeTextInfo)
{
// If there are any missing options, merge with the defaults:
// Also, as a safeguard against missing DefaultFormatOptions, let's also merge with the AbsoluteDefaults:
options = options.Merge(DefaultFormatOptions).Merge(AbsoluteDefaults);
// Extract the individual options:
var rangeMax = options.Mask(RangeAll).AllFlags().Last();
var rangeMin = options.Mask(RangeAll).AllFlags().First();
var truncate = options.Mask(TruncateAll).AllFlags().First();
var lessThan = options.Mask(LessThanAll) != TimeSpanFormatOptions.LessThanOff;
var abbreviate = options.Mask(AbbreviateAll) != TimeSpanFormatOptions.AbbreviateOff;
var round = lessThan ? (Func<double, double>)Math.Floor : Math.Ceiling;
switch (rangeMin)
{
case TimeSpanFormatOptions.RangeWeeks:
FromTime = TimeSpan.FromDays(round(FromTime.TotalDays / 7) * 7);
break;
case TimeSpanFormatOptions.RangeDays:
FromTime = TimeSpan.FromDays(round(FromTime.TotalDays));
break;
case TimeSpanFormatOptions.RangeHours:
FromTime = TimeSpan.FromHours(round(FromTime.TotalHours));
break;
case TimeSpanFormatOptions.RangeMinutes:
FromTime = TimeSpan.FromMinutes(round(FromTime.TotalMinutes));
break;
case TimeSpanFormatOptions.RangeSeconds:
FromTime = TimeSpan.FromSeconds(round(FromTime.TotalSeconds));
break;
case TimeSpanFormatOptions.RangeMilliSeconds:
FromTime = TimeSpan.FromMilliseconds(round(FromTime.TotalMilliseconds));
break;
}
// Create our result:
var textStarted = false;
var result = StringBuilderPool.Get();
for (var i = rangeMax; i >= rangeMin; i = (TimeSpanFormatOptions)((int)i >> 1))
{
// Determine the value and title:
int value;
switch (i)
{
case TimeSpanFormatOptions.RangeWeeks:
value = (int)Math.Floor(FromTime.TotalDays / 7);
FromTime -= TimeSpan.FromDays(value * 7);
break;
case TimeSpanFormatOptions.RangeDays:
value = (int)Math.Floor(FromTime.TotalDays);
FromTime -= TimeSpan.FromDays(value);
break;
case TimeSpanFormatOptions.RangeHours:
value = (int)Math.Floor(FromTime.TotalHours);
FromTime -= TimeSpan.FromHours(value);
break;
case TimeSpanFormatOptions.RangeMinutes:
value = (int)Math.Floor(FromTime.TotalMinutes);
FromTime -= TimeSpan.FromMinutes(value);
break;
case TimeSpanFormatOptions.RangeSeconds:
value = (int)Math.Floor(FromTime.TotalSeconds);
FromTime -= TimeSpan.FromSeconds(value);
break;
case TimeSpanFormatOptions.RangeMilliSeconds:
value = (int)Math.Floor(FromTime.TotalMilliseconds);
FromTime -= TimeSpan.FromMilliseconds(value);
break;
default:
// This code is unreachable, but it prevents compile-errors.
throw new ArgumentException("TimeSpanUtility");
}
//Determine whether to display this value
var displayThisValue = false;
var breakFor =
false; // I wish C# supported "break for;" (like how VB supports "Exit For" from within a "Select Case" statement)
switch (truncate)
{
case TimeSpanFormatOptions.TruncateShortest:
if (textStarted)
{
breakFor = true;
break;
}
if (value > 0) displayThisValue = true;
break;
case TimeSpanFormatOptions.TruncateAuto:
if (value > 0) displayThisValue = true;
break;
case TimeSpanFormatOptions.TruncateFill:
if (textStarted || value > 0) displayThisValue = true;
break;
case TimeSpanFormatOptions.TruncateFull:
displayThisValue = true;
break;
}
if (breakFor) break;
//we need to display SOMETHING (even if it's zero)
if (i == rangeMin && textStarted == false)
{
displayThisValue = true;
if (lessThan && value < 1)
{
// Output the "less than 1 unit" text:
var unitTitle = timeTextInfo.GetUnitText(rangeMin, 1, abbreviate);
result.Append(timeTextInfo.GetLessThanText(unitTitle));
displayThisValue = false;
}
}
// Output the value:
if (displayThisValue)
{
if (textStarted) result.Append(" ");
var unitTitle = timeTextInfo.GetUnitText(i, value, abbreviate);
result.Append(unitTitle);
textStarted = true;
}
}
var ret = result.ToString();
StringBuilderPool.Release(result);
return ret;
}
internal const TimeSpanFormatOptions AbbreviateAll = TimeSpanFormatOptions.Abbreviate | TimeSpanFormatOptions.AbbreviateOff;
internal const TimeSpanFormatOptions LessThanAll = TimeSpanFormatOptions.LessThan | TimeSpanFormatOptions.LessThanOff;
internal const TimeSpanFormatOptions RangeAll = TimeSpanFormatOptions.RangeMilliSeconds | TimeSpanFormatOptions.RangeSeconds | TimeSpanFormatOptions.RangeMinutes | TimeSpanFormatOptions.RangeHours | TimeSpanFormatOptions.RangeDays | TimeSpanFormatOptions.RangeWeeks;
internal const TimeSpanFormatOptions TruncateAll = TimeSpanFormatOptions.TruncateShortest | TimeSpanFormatOptions.TruncateAuto | TimeSpanFormatOptions.TruncateFill | TimeSpanFormatOptions.TruncateFull;
static TimeSpanUtility()
{
// Create our defaults:
DefaultFormatOptions =
TimeSpanFormatOptions.AbbreviateOff
| TimeSpanFormatOptions.LessThan
| TimeSpanFormatOptions.TruncateAuto
| TimeSpanFormatOptions.RangeSeconds
| TimeSpanFormatOptions.RangeDays;
AbsoluteDefaults = DefaultFormatOptions;
}
/// <summary>
/// These are the default options that will be used when no option is specified.
/// </summary>
public static TimeSpanFormatOptions DefaultFormatOptions { get; set; }
/// <summary>
/// These are the absolute default options that will be used as
/// a safeguard, just in case DefaultFormatOptions is missing a value.
/// </summary>
public static TimeSpanFormatOptions AbsoluteDefaults { get; }
/// <summary>
/// <para>Returns the <c>TimeSpan</c> closest to the specified interval.</para>
/// <para>For example: <c>Round("00:57:00", TimeSpan.TicksPerMinute * 5) =&gt; "00:55:00"</c></para>
/// </summary>
/// <param name="fromTime">A <c>TimeSpan</c> to be rounded.</param>
/// <param name="intervalTicks">Specifies the interval for rounding. Use <c>TimeSpan.TicksPer____</c>.</param>
public static TimeSpan Round(this TimeSpan fromTime, long intervalTicks)
{
var extra = fromTime.Ticks % intervalTicks;
if (extra >= intervalTicks >> 1) extra -= intervalTicks;
return TimeSpan.FromTicks(fromTime.Ticks - extra);
}
}
/// <summary>
/// <para>Determines all options for time formatting.</para>
/// <para>This one value actually contains 4 settings:</para>
/// <para><c>Abbreviate</c> / <c>AbbreviateOff</c></para>
/// <para><c>LessThan</c> / <c>LessThanOff</c></para>
/// <para><c>Truncate</c> &#160; <c>Auto</c> / <c>Shortest</c> / <c>Fill</c> / <c>Full</c></para>
/// <para>
/// <c>Range</c> &#160; <c>MilliSeconds</c> / <c>Seconds</c> / <c>Minutes</c> / <c>Hours</c> / <c>Days</c> /
/// <c>Weeks</c> (Min / Max)
/// </para>
/// </summary>
[Flags]
public enum TimeSpanFormatOptions
{
/// <summary>
/// Specifies that all <c>timeSpanFormatOptions</c> should be inherited from
/// <c>TimeSpanUtility.DefaultTimeFormatOptions</c>.
/// </summary>
InheritDefaults = 0x0,
/// <summary>
/// Abbreviates units.
/// Example: "1d 2h 3m 4s 5ms"
/// </summary>
Abbreviate = 0x1,
/// <summary>
/// Does not abbreviate units.
/// Example: "1 day 2 hours 3 minutes 4 seconds 5 milliseconds"
/// </summary>
AbbreviateOff = 0x2,
/// <summary>
/// Displays "less than 1 (unit)" when the TimeSpan is smaller than the minimum range.
/// </summary>
LessThan = 0x4,
/// <summary>
/// Displays "0 (units)" when the TimeSpan is smaller than the minimum range.
/// </summary>
LessThanOff = 0x8,
/// <summary>
/// <para>Displays the highest non-zero value within the range.</para>
/// <para>Example: "00.23:00:59.000" = "23 hours"</para>
/// </summary>
TruncateShortest = 0x10,
/// <summary>
/// <para>Displays all non-zero values within the range.</para>
/// <para>Example: "00.23:00:59.000" = "23 hours 59 minutes"</para>
/// </summary>
TruncateAuto = 0x20,
/// <summary>
/// <para>Displays the highest non-zero value and all lesser values within the range.</para>
/// <para>Example: "00.23:00:59.000" = "23 hours 0 minutes 59 seconds 0 milliseconds"</para>
/// </summary>
TruncateFill = 0x40,
/// <summary>
/// <para>Displays all values within the range.</para>
/// <para>Example: "00.23:00:59.000" = "0 days 23 hours 0 minutes 59 seconds 0 milliseconds"</para>
/// </summary>
TruncateFull = 0x80,
/// <summary>
/// <para>Determines the range of units to display.</para>
/// <para>You may combine two values to form the minimum and maximum for the range.</para>
/// <para>
/// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours
/// to Seconds.
/// </para>
/// </summary>
RangeMilliSeconds = 0x100,
/// <summary>
/// <para>Determines the range of units to display.</para>
/// <para>You may combine two values to form the minimum and maximum for the range.</para>
/// <para>
/// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours
/// to Seconds.
/// </para>
/// </summary>
RangeSeconds = 0x200,
/// <summary>
/// <para>Determines the range of units to display.</para>
/// <para>You may combine two values to form the minimum and maximum for the range.</para>
/// <para>
/// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours
/// to Seconds.
/// </para>
/// </summary>
RangeMinutes = 0x400,
/// <summary>
/// <para>Determines the range of units to display.</para>
/// <para>You may combine two values to form the minimum and maximum for the range.</para>
/// <para>
/// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours
/// to Seconds.
/// </para>
/// </summary>
RangeHours = 0x800,
/// <summary>
/// <para>Determines the range of units to display.</para>
/// <para>You may combine two values to form the minimum and maximum for the range.</para>
/// <para>
/// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours
/// to Seconds.
/// </para>
/// </summary>
RangeDays = 0x1000,
/// <summary>
/// <para>Determines the range of units to display.</para>
/// <para>You may combine two values to form the minimum and maximum for the range.</para>
/// <para>
/// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours
/// to Seconds.
/// </para>
/// </summary>
RangeWeeks = 0x2000,
}
internal static class TimeSpanFormatOptionsConverter
{
private static readonly Regex parser =
new Regex(
@"\b(w|week|weeks|d|day|days|h|hour|hours|m|minute|minutes|s|second|seconds|ms|millisecond|milliseconds|auto|short|fill|full|abbr|noabbr|less|noless)\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static TimeSpanFormatOptions Merge(this TimeSpanFormatOptions left, TimeSpanFormatOptions right)
{
var masks = new[]
{
TimeSpanUtility.AbbreviateAll,
TimeSpanUtility.LessThanAll,
TimeSpanUtility.RangeAll,
TimeSpanUtility.TruncateAll
};
foreach (var mask in masks)
if ((left & mask) == 0)
left |= right & mask;
return left;
}
public static TimeSpanFormatOptions Mask(this TimeSpanFormatOptions timeSpanFormatOptions,
TimeSpanFormatOptions mask)
{
return timeSpanFormatOptions & mask;
}
public static IEnumerable<TimeSpanFormatOptions> AllFlags(this TimeSpanFormatOptions timeSpanFormatOptions)
{
uint value = 0x1;
while (value <= (uint)timeSpanFormatOptions)
{
if ((value & (uint)timeSpanFormatOptions) != 0) yield return (TimeSpanFormatOptions)value;
value <<= 1;
}
}
public static TimeSpanFormatOptions Parse(string formatOptionsString)
{
formatOptionsString = formatOptionsString.ToLower();
var t = TimeSpanFormatOptions.InheritDefaults;
foreach (Match m in parser.Matches(formatOptionsString))
switch (m.Value)
{
case "w":
case "week":
case "weeks":
t |= TimeSpanFormatOptions.RangeWeeks;
break;
case "d":
case "day":
case "days":
t |= TimeSpanFormatOptions.RangeDays;
break;
case "h":
case "hour":
case "hours":
t |= TimeSpanFormatOptions.RangeHours;
break;
case "m":
case "minute":
case "minutes":
t |= TimeSpanFormatOptions.RangeMinutes;
break;
case "s":
case "second":
case "seconds":
t |= TimeSpanFormatOptions.RangeSeconds;
break;
case "ms":
case "millisecond":
case "milliseconds":
t |= TimeSpanFormatOptions.RangeMilliSeconds;
break;
case "short":
t |= TimeSpanFormatOptions.TruncateShortest;
break;
case "auto":
t |= TimeSpanFormatOptions.TruncateAuto;
break;
case "fill":
t |= TimeSpanFormatOptions.TruncateFill;
break;
case "full":
t |= TimeSpanFormatOptions.TruncateFull;
break;
case "abbr":
t |= TimeSpanFormatOptions.Abbreviate;
break;
case "noabbr":
t |= TimeSpanFormatOptions.AbbreviateOff;
break;
case "less":
t |= TimeSpanFormatOptions.LessThan;
break;
case "noless":
t |= TimeSpanFormatOptions.LessThanOff;
break;
}
return t;
}
}
/// <summary>
/// Supplies the localized text used for TimeSpan formatting.
/// </summary>
public class TimeTextInfo
{
private readonly string[] d;
private readonly string[] day;
private readonly string[] h;
private readonly string[] hour;
private readonly string lessThan;
private readonly string[] m;
private readonly string[] millisecond;
private readonly string[] minute;
private readonly string[] ms;
private readonly PluralRules.PluralRuleDelegate PluralRule;
private readonly string[] s;
private readonly string[] second;
private readonly string[] w;
private readonly string[] week;
public TimeTextInfo(PluralRules.PluralRuleDelegate pluralRule, string[] week, string[] day, string[] hour,
string[] minute, string[] second, string[] millisecond, string[] w, string[] d, string[] h, string[] m,
string[] s, string[] ms, string lessThan)
{
PluralRule = pluralRule;
this.week = week;
this.day = day;
this.hour = hour;
this.minute = minute;
this.second = second;
this.millisecond = millisecond;
this.w = w;
this.d = d;
this.h = h;
this.m = m;
this.s = s;
this.ms = ms;
this.lessThan = lessThan;
}
public TimeTextInfo(string week, string day, string hour, string minute, string second, string millisecond,
string lessThan)
{
// must not be null here
d = h = m = ms = s = w = new string[] {};
// Always use singular:
PluralRule = (d, c) => 0;
this.week = new[] {week};
this.day = new[] {day};
this.hour = new[] {hour};
this.minute = new[] {minute};
this.second = new[] {second};
this.millisecond = new[] {millisecond};
this.lessThan = lessThan;
}
private static string GetValue(PluralRules.PluralRuleDelegate pluralRule, int value, string[] units)
{
// Get the plural index from the plural rule,
// unless there's only 1 unit in the first place:
var pluralIndex = units.Length == 1 ? 0 : pluralRule(value, units.Length);
return string.Format(units[pluralIndex], value);
}
public string GetLessThanText(string minimumValue)
{
return string.Format(lessThan, minimumValue);
}
public virtual string GetUnitText(TimeSpanFormatOptions unit, int value, bool abbr)
{
switch (unit)
{
case TimeSpanFormatOptions.RangeWeeks:
return GetValue(PluralRule, value, abbr ? w : week);
case TimeSpanFormatOptions.RangeDays:
return GetValue(PluralRule, value, abbr ? d : day);
case TimeSpanFormatOptions.RangeHours:
return GetValue(PluralRule, value, abbr ? h : hour);
case TimeSpanFormatOptions.RangeMinutes:
return GetValue(PluralRule, value, abbr ? m : minute);
case TimeSpanFormatOptions.RangeSeconds:
return GetValue(PluralRule, value, abbr ? s : second);
case TimeSpanFormatOptions.RangeMilliSeconds:
return GetValue(PluralRule, value, abbr ? ms : millisecond);
}
// (should be unreachable)
return null;
}
}
public static class CommonLanguagesTimeTextInfo
{
public static TimeTextInfo English => new TimeTextInfo(
PluralRules.GetPluralRule("en"),
new[] {"{0} week", "{0} weeks"},
new[] {"{0} day", "{0} days"},
new[] {"{0} hour", "{0} hours"},
new[] {"{0} minute", "{0} minutes"},
new[] {"{0} second", "{0} seconds"},
new[] {"{0} millisecond", "{0} milliseconds"},
new[] {"{0}w"},
new[] {"{0}d"},
new[] {"{0}h"},
new[] {"{0}m"},
new[] {"{0}s"},
new[] {"{0}ms"},
"less than {0}"
);
public static TimeTextInfo GetTimeTextInfo(string twoLetterIsoLanguageName)
{
switch (twoLetterIsoLanguageName)
{
case "en":
return English;
default:
return null;
}
}
}
}