/ src / Ryujinx.UI.Common / Helper / ValueFormatUtils.cs
ValueFormatUtils.cs
  1  using System;
  2  using System.Globalization;
  3  using System.Linq;
  4  
  5  namespace Ryujinx.UI.Common.Helper
  6  {
  7      public static class ValueFormatUtils
  8      {
  9          private static readonly string[] _fileSizeUnitStrings =
 10          {
 11              "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB",  // Base 10 units, used for formatting and parsing
 12              "KB", "MB", "GB", "TB", "PB", "EB",             // Base 2 units, used for parsing legacy values
 13          };
 14  
 15          /// <summary>
 16          /// Used by <see cref="FormatFileSize"/>.
 17          /// </summary>
 18          public enum FileSizeUnits
 19          {
 20              Auto = -1,
 21              Bytes = 0,
 22              Kibibytes = 1,
 23              Mebibytes = 2,
 24              Gibibytes = 3,
 25              Tebibytes = 4,
 26              Pebibytes = 5,
 27              Exbibytes = 6,
 28              Kilobytes = 7,
 29              Megabytes = 8,
 30              Gigabytes = 9,
 31              Terabytes = 10,
 32              Petabytes = 11,
 33              Exabytes = 12,
 34          }
 35  
 36          private const double SizeBase10 = 1000;
 37          private const double SizeBase2 = 1024;
 38          private const int UnitEBIndex = 6;
 39  
 40          #region Value formatters
 41  
 42          /// <summary>
 43          /// Creates a human-readable string from a <see cref="TimeSpan"/>.
 44          /// </summary>
 45          /// <param name="timeSpan">The <see cref="TimeSpan"/> to be formatted.</param>
 46          /// <returns>A formatted string that can be displayed in the UI.</returns>
 47          public static string FormatTimeSpan(TimeSpan? timeSpan)
 48          {
 49              if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1)
 50              {
 51                  // Game was never played
 52                  return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture);
 53              }
 54  
 55              if (timeSpan.Value.TotalDays < 1)
 56              {
 57                  // Game was played for less than a day
 58                  return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture);
 59              }
 60  
 61              // Game was played for more than a day
 62              TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days));
 63              string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture);
 64  
 65              return $"{timeSpan.Value.Days}d, {onlyTimeString}";
 66          }
 67  
 68          /// <summary>
 69          /// Creates a human-readable string from a <see cref="DateTime"/>.
 70          /// </summary>
 71          /// <param name="utcDateTime">The <see cref="DateTime"/> to be formatted. This is expected to be UTC-based.</param>
 72          /// <param name="culture">The <see cref="CultureInfo"/> that's used in formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.</param>
 73          /// <returns>A formatted string that can be displayed in the UI.</returns>
 74          public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null)
 75          {
 76              culture ??= CultureInfo.CurrentCulture;
 77  
 78              if (!utcDateTime.HasValue)
 79              {
 80                  // In the Avalonia UI, this is turned into a localized version of "Never" by LocalizedNeverConverter.
 81                  return "Never";
 82              }
 83  
 84              return utcDateTime.Value.ToLocalTime().ToString(culture);
 85          }
 86  
 87          /// <summary>
 88          /// Creates a human-readable file size string.
 89          /// </summary>
 90          /// <param name="size">The file size in bytes.</param>
 91          /// <param name="forceUnit">Formats the passed size value as this unit, bypassing the automatic unit choice.</param>
 92          /// <returns>A human-readable file size string.</returns>
 93          public static string FormatFileSize(long size, FileSizeUnits forceUnit = FileSizeUnits.Auto)
 94          {
 95              if (size <= 0)
 96              {
 97                  return $"0 {_fileSizeUnitStrings[0]}";
 98              }
 99  
100              int unitIndex = (int)forceUnit;
101              if (forceUnit == FileSizeUnits.Auto)
102              {
103                  unitIndex = Convert.ToInt32(Math.Floor(Math.Log(size, SizeBase10)));
104  
105                  // Apply an upper bound so that exabytes are the biggest unit used when formatting.
106                  if (unitIndex > UnitEBIndex)
107                  {
108                      unitIndex = UnitEBIndex;
109                  }
110              }
111  
112              double sizeRounded;
113  
114              if (unitIndex > UnitEBIndex)
115              {
116                  sizeRounded = Math.Round(size / Math.Pow(SizeBase10, unitIndex - UnitEBIndex), 1);
117              }
118              else
119              {
120                  sizeRounded = Math.Round(size / Math.Pow(SizeBase2, unitIndex), 1);
121              }
122  
123              string sizeFormatted = sizeRounded.ToString(CultureInfo.InvariantCulture);
124  
125              return $"{sizeFormatted} {_fileSizeUnitStrings[unitIndex]}";
126          }
127  
128          #endregion
129  
130          #region Value parsers
131  
132          /// <summary>
133          /// Parses a string generated by <see cref="FormatTimeSpan"/> and returns the original <see cref="TimeSpan"/>.
134          /// </summary>
135          /// <param name="timeSpanString">A string representing a <see cref="TimeSpan"/>.</param>
136          /// <returns>A <see cref="TimeSpan"/> object. If the input string couldn't been parsed, <see cref="TimeSpan.Zero"/> is returned.</returns>
137          public static TimeSpan ParseTimeSpan(string timeSpanString)
138          {
139              TimeSpan returnTimeSpan = TimeSpan.Zero;
140  
141              // 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.
142              // Here, we split the input string to check if it's the former or the latter.
143              var valueSplit = timeSpanString.Split(", ");
144              if (valueSplit.Length > 1)
145              {
146                  var dayPart = valueSplit[0].Split("d")[0];
147                  if (int.TryParse(dayPart, out int days))
148                  {
149                      returnTimeSpan = returnTimeSpan.Add(TimeSpan.FromDays(days));
150                  }
151              }
152  
153              if (TimeSpan.TryParse(valueSplit.Last(), out TimeSpan parsedTimeSpan))
154              {
155                  returnTimeSpan = returnTimeSpan.Add(parsedTimeSpan);
156              }
157  
158              return returnTimeSpan;
159          }
160  
161          /// <summary>
162          /// Parses a string generated by <see cref="FormatDateTime"/> and returns the original <see cref="DateTime"/>.
163          /// </summary>
164          /// <param name="dateTimeString">The string representing a <see cref="DateTime"/>.</param>
165          /// <returns>A <see cref="DateTime"/> object. If the input string couldn't be parsed, <see cref="DateTime.UnixEpoch"/> is returned.</returns>
166          public static DateTime ParseDateTime(string dateTimeString)
167          {
168              if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime))
169              {
170                  // Games that were never played are supposed to appear before the oldest played games in the list,
171                  // so returning DateTime.UnixEpoch here makes sense.
172                  return DateTime.UnixEpoch;
173              }
174  
175              return parsedDateTime;
176          }
177  
178          /// <summary>
179          /// Parses a string generated by <see cref="FormatFileSize"/> and returns a <see cref="long"/> representing a number of bytes.
180          /// </summary>
181          /// <param name="sizeString">A string representing a file size formatted with <see cref="FormatFileSize"/>.</param>
182          /// <returns>A <see cref="long"/> representing a number of bytes.</returns>
183          public static long ParseFileSize(string sizeString)
184          {
185              // Enumerating over the units backwards because otherwise, sizeString.EndsWith("B") would exit the loop in the first iteration.
186              for (int i = _fileSizeUnitStrings.Length - 1; i >= 0; i--)
187              {
188                  string unit = _fileSizeUnitStrings[i];
189                  if (!sizeString.EndsWith(unit))
190                  {
191                      continue;
192                  }
193  
194                  string numberString = sizeString.Split(" ")[0];
195                  if (!double.TryParse(numberString, CultureInfo.InvariantCulture, out double number))
196                  {
197                      break;
198                  }
199  
200                  double sizeBase = SizeBase2;
201  
202                  // 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.
203                  if (i > UnitEBIndex)
204                  {
205                      i -= UnitEBIndex;
206                      sizeBase = SizeBase10;
207                  }
208  
209                  number *= Math.Pow(sizeBase, i);
210  
211                  return Convert.ToInt64(number);
212              }
213  
214              return 0;
215          }
216  
217          #endregion
218      }
219  }