using System; using System.Globalization; using System.Linq; namespace Ryujinx.UI.Common.Helper { public static class ValueFormatUtils { private static readonly string[] _fileSizeUnitStrings = { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", // Base 10 units, used for formatting and parsing "KB", "MB", "GB", "TB", "PB", "EB", // Base 2 units, used for parsing legacy values }; /// /// Used by . /// public enum FileSizeUnits { Auto = -1, Bytes = 0, Kibibytes = 1, Mebibytes = 2, Gibibytes = 3, Tebibytes = 4, Pebibytes = 5, Exbibytes = 6, Kilobytes = 7, Megabytes = 8, Gigabytes = 9, Terabytes = 10, Petabytes = 11, Exabytes = 12, } private const double SizeBase10 = 1000; private const double SizeBase2 = 1024; private const int UnitEBIndex = 6; #region Value formatters /// /// Creates a human-readable string from a . /// /// The to be formatted. /// A formatted string that can be displayed in the UI. public static string FormatTimeSpan(TimeSpan? timeSpan) { if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1) { // Game was never played return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture); } if (timeSpan.Value.TotalDays < 1) { // Game was played for less than a day return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture); } // Game was played for more than a day TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days)); string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture); return $"{timeSpan.Value.Days}d, {onlyTimeString}"; } /// /// Creates a human-readable string from a . /// /// The to be formatted. This is expected to be UTC-based. /// The that's used in formatting. Defaults to . /// A formatted string that can be displayed in the UI. public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null) { culture ??= CultureInfo.CurrentCulture; if (!utcDateTime.HasValue) { // In the Avalonia UI, this is turned into a localized version of "Never" by LocalizedNeverConverter. return "Never"; } return utcDateTime.Value.ToLocalTime().ToString(culture); } /// /// Creates a human-readable file size string. /// /// The file size in bytes. /// Formats the passed size value as this unit, bypassing the automatic unit choice. /// A human-readable file size string. public static string FormatFileSize(long size, FileSizeUnits forceUnit = FileSizeUnits.Auto) { if (size <= 0) { return $"0 {_fileSizeUnitStrings[0]}"; } int unitIndex = (int)forceUnit; if (forceUnit == FileSizeUnits.Auto) { unitIndex = Convert.ToInt32(Math.Floor(Math.Log(size, SizeBase10))); // Apply an upper bound so that exabytes are the biggest unit used when formatting. if (unitIndex > UnitEBIndex) { unitIndex = UnitEBIndex; } } double sizeRounded; if (unitIndex > UnitEBIndex) { sizeRounded = Math.Round(size / Math.Pow(SizeBase10, unitIndex - UnitEBIndex), 1); } else { sizeRounded = Math.Round(size / Math.Pow(SizeBase2, unitIndex), 1); } string sizeFormatted = sizeRounded.ToString(CultureInfo.InvariantCulture); return $"{sizeFormatted} {_fileSizeUnitStrings[unitIndex]}"; } #endregion #region Value parsers /// /// Parses a string generated by and returns the original . /// /// A string representing a . /// A object. If the input string couldn't been parsed, is returned. public static TimeSpan ParseTimeSpan(string timeSpanString) { TimeSpan returnTimeSpan = TimeSpan.Zero; // An input string can either look like "01:23:45" or "1d, 01:23:45" if the timespan represents a duration of more than a day. // Here, we split the input string to check if it's the former or the latter. var valueSplit = timeSpanString.Split(", "); if (valueSplit.Length > 1) { var dayPart = valueSplit[0].Split("d")[0]; if (int.TryParse(dayPart, out int days)) { returnTimeSpan = returnTimeSpan.Add(TimeSpan.FromDays(days)); } } if (TimeSpan.TryParse(valueSplit.Last(), out TimeSpan parsedTimeSpan)) { returnTimeSpan = returnTimeSpan.Add(parsedTimeSpan); } return returnTimeSpan; } /// /// Parses a string generated by and returns the original . /// /// The string representing a . /// A object. If the input string couldn't be parsed, is returned. public static DateTime ParseDateTime(string dateTimeString) { if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime)) { // Games that were never played are supposed to appear before the oldest played games in the list, // so returning DateTime.UnixEpoch here makes sense. return DateTime.UnixEpoch; } return parsedDateTime; } /// /// Parses a string generated by and returns a representing a number of bytes. /// /// A string representing a file size formatted with . /// A representing a number of bytes. public static long ParseFileSize(string sizeString) { // Enumerating over the units backwards because otherwise, sizeString.EndsWith("B") would exit the loop in the first iteration. for (int i = _fileSizeUnitStrings.Length - 1; i >= 0; i--) { string unit = _fileSizeUnitStrings[i]; if (!sizeString.EndsWith(unit)) { continue; } string numberString = sizeString.Split(" ")[0]; if (!double.TryParse(numberString, CultureInfo.InvariantCulture, out double number)) { break; } double sizeBase = SizeBase2; // If the unit index is one that points to a base 10 unit in the FileSizeUnitStrings array, subtract 6 to arrive at a usable power value. if (i > UnitEBIndex) { i -= UnitEBIndex; sizeBase = SizeBase10; } number *= Math.Pow(sizeBase, i); return Convert.ToInt64(number); } return 0; } #endregion } }