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 }