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
}
}