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 { /// /// Turns a TimeSpan into a human-readable text. /// Uses the specified timeSpanFormatOptions. /// For example: "31.23:59:00.555" = "31 days 23 hours 59 minutes 0 seconds 555 milliseconds" /// /// /// /// A combination of flags that determine the formatting options. /// These will be combined with the default timeSpanFormatOptions. /// /// An object that supplies the text to use for output 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)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; } /// /// These are the default options that will be used when no option is specified. /// public static TimeSpanFormatOptions DefaultFormatOptions { get; set; } /// /// These are the absolute default options that will be used as /// a safeguard, just in case DefaultFormatOptions is missing a value. /// public static TimeSpanFormatOptions AbsoluteDefaults { get; } /// /// Returns the TimeSpan closest to the specified interval. /// For example: Round("00:57:00", TimeSpan.TicksPerMinute * 5) => "00:55:00" /// /// A TimeSpan to be rounded. /// Specifies the interval for rounding. Use TimeSpan.TicksPer____. 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); } } /// /// Determines all options for time formatting. /// This one value actually contains 4 settings: /// Abbreviate / AbbreviateOff /// LessThan / LessThanOff /// Truncate   Auto / Shortest / Fill / Full /// /// Range   MilliSeconds / Seconds / Minutes / Hours / Days / /// Weeks (Min / Max) /// /// [Flags] public enum TimeSpanFormatOptions { /// /// Specifies that all timeSpanFormatOptions should be inherited from /// TimeSpanUtility.DefaultTimeFormatOptions. /// InheritDefaults = 0x0, /// /// Abbreviates units. /// Example: "1d 2h 3m 4s 5ms" /// Abbreviate = 0x1, /// /// Does not abbreviate units. /// Example: "1 day 2 hours 3 minutes 4 seconds 5 milliseconds" /// AbbreviateOff = 0x2, /// /// Displays "less than 1 (unit)" when the TimeSpan is smaller than the minimum range. /// LessThan = 0x4, /// /// Displays "0 (units)" when the TimeSpan is smaller than the minimum range. /// LessThanOff = 0x8, /// /// Displays the highest non-zero value within the range. /// Example: "00.23:00:59.000" = "23 hours" /// TruncateShortest = 0x10, /// /// Displays all non-zero values within the range. /// Example: "00.23:00:59.000" = "23 hours 59 minutes" /// TruncateAuto = 0x20, /// /// Displays the highest non-zero value and all lesser values within the range. /// Example: "00.23:00:59.000" = "23 hours 0 minutes 59 seconds 0 milliseconds" /// TruncateFill = 0x40, /// /// Displays all values within the range. /// Example: "00.23:00:59.000" = "0 days 23 hours 0 minutes 59 seconds 0 milliseconds" /// TruncateFull = 0x80, /// /// Determines the range of units to display. /// You may combine two values to form the minimum and maximum for the range. /// /// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours /// to Seconds. /// /// RangeMilliSeconds = 0x100, /// /// Determines the range of units to display. /// You may combine two values to form the minimum and maximum for the range. /// /// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours /// to Seconds. /// /// RangeSeconds = 0x200, /// /// Determines the range of units to display. /// You may combine two values to form the minimum and maximum for the range. /// /// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours /// to Seconds. /// /// RangeMinutes = 0x400, /// /// Determines the range of units to display. /// You may combine two values to form the minimum and maximum for the range. /// /// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours /// to Seconds. /// /// RangeHours = 0x800, /// /// Determines the range of units to display. /// You may combine two values to form the minimum and maximum for the range. /// /// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours /// to Seconds. /// /// RangeDays = 0x1000, /// /// Determines the range of units to display. /// You may combine two values to form the minimum and maximum for the range. /// /// Example: (RangeMinutes) defines a range of Minutes only; (RangeHours | RangeSeconds) defines a range of Hours /// to Seconds. /// /// 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 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; } } /// /// Supplies the localized text used for TimeSpan formatting. /// 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; } } } }