aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs
blob: b1597a7cc2cfb687d74c28494287c0ab198b680a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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
        };

        /// <summary>
        /// Used by <see cref="FormatFileSize"/>.
        /// </summary>
        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

        /// <summary>
        /// Creates a human-readable string from a <see cref="TimeSpan"/>.
        /// </summary>
        /// <param name="timeSpan">The <see cref="TimeSpan"/> to be formatted.</param>
        /// <returns>A formatted string that can be displayed in the UI.</returns>
        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}";
        }

        /// <summary>
        /// Creates a human-readable string from a <see cref="DateTime"/>.
        /// </summary>
        /// <param name="utcDateTime">The <see cref="DateTime"/> to be formatted. This is expected to be UTC-based.</param>
        /// <param name="culture">The <see cref="CultureInfo"/> that's used in formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.</param>
        /// <returns>A formatted string that can be displayed in the UI.</returns>
        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);
        }

        /// <summary>
        /// Creates a human-readable file size string.
        /// </summary>
        /// <param name="size">The file size in bytes.</param>
        /// <param name="forceUnit">Formats the passed size value as this unit, bypassing the automatic unit choice.</param>
        /// <returns>A human-readable file size string.</returns>
        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

        /// <summary>
        /// Parses a string generated by <see cref="FormatTimeSpan"/> and returns the original <see cref="TimeSpan"/>.
        /// </summary>
        /// <param name="timeSpanString">A string representing a <see cref="TimeSpan"/>.</param>
        /// <returns>A <see cref="TimeSpan"/> object. If the input string couldn't been parsed, <see cref="TimeSpan.Zero"/> is returned.</returns>
        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;
        }

        /// <summary>
        /// Parses a string generated by <see cref="FormatDateTime"/> and returns the original <see cref="DateTime"/>.
        /// </summary>
        /// <param name="dateTimeString">The string representing a <see cref="DateTime"/>.</param>
        /// <returns>A <see cref="DateTime"/> object. If the input string couldn't be parsed, <see cref="DateTime.UnixEpoch"/> is returned.</returns>
        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;
        }

        /// <summary>
        /// Parses a string generated by <see cref="FormatFileSize"/> and returns a <see cref="long"/> representing a number of bytes.
        /// </summary>
        /// <param name="sizeString">A string representing a file size formatted with <see cref="FormatFileSize"/>.</param>
        /// <returns>A <see cref="long"/> representing a number of bytes.</returns>
        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
    }
}