1 | /* |
2 | This file is part of the KDE Frameworks |
3 | |
4 | SPDX-FileCopyrightText: 2013 Alex Merry <alex.merry@kdemail.net> |
5 | SPDX-FileCopyrightText: 2013 John Layt <jlayt@kde.org> |
6 | SPDX-FileCopyrightText: 2010 Michael Leupold <lemma@confuego.org> |
7 | SPDX-FileCopyrightText: 2009 Michael Pyne <mpyne@kde.org> |
8 | SPDX-FileCopyrightText: 2008 Albert Astals Cid <aacid@kde.org> |
9 | |
10 | SPDX-License-Identifier: LGPL-2.0-or-later |
11 | */ |
12 | |
13 | #include "kformatprivate_p.h" |
14 | |
15 | #include <QDateTime> |
16 | #include <QSettings> |
17 | #include <QStandardPaths> |
18 | #include <QTimeZone> |
19 | |
20 | #include <math.h> |
21 | |
22 | using namespace Qt::Literals; |
23 | |
24 | KFormatPrivate::KFormatPrivate(const QLocale &locale) |
25 | : m_locale(locale) |
26 | { |
27 | } |
28 | |
29 | KFormatPrivate::~KFormatPrivate() = default; |
30 | |
31 | constexpr double bpow(int exp) |
32 | { |
33 | return (exp > 0) ? 2.0 * bpow(exp: exp - 1) : (exp < 0) ? 0.5 * bpow(exp: exp + 1) : 1.0; |
34 | } |
35 | |
36 | QString KFormatPrivate::formatValue(double value, |
37 | KFormat::Unit unit, |
38 | QString unitString, |
39 | int precision, |
40 | KFormat::UnitPrefix prefix, |
41 | KFormat::BinaryUnitDialect dialect) const |
42 | { |
43 | if (dialect <= KFormat::DefaultBinaryDialect || dialect > KFormat::LastBinaryDialect) { |
44 | dialect = KFormat::IECBinaryDialect; |
45 | } |
46 | |
47 | if (static_cast<int>(prefix) < static_cast<int>(KFormat::UnitPrefix::Yocto) || static_cast<int>(prefix) > static_cast<int>(KFormat::UnitPrefix::Yotta)) { |
48 | prefix = KFormat::UnitPrefix::AutoAdjust; |
49 | } |
50 | |
51 | double multiplier = 1024.0; |
52 | if (dialect == KFormat::MetricBinaryDialect) { |
53 | multiplier = 1000.0; |
54 | } |
55 | |
56 | if (prefix == KFormat::UnitPrefix::AutoAdjust) { |
57 | int power = 0; |
58 | double adjustValue = qAbs(t: value); |
59 | while (adjustValue >= multiplier) { |
60 | adjustValue /= multiplier; |
61 | power += 1; |
62 | } |
63 | while (adjustValue && adjustValue < 1.0) { |
64 | adjustValue *= multiplier; |
65 | power -= 1; |
66 | } |
67 | const KFormat::UnitPrefix map[] = { |
68 | KFormat::UnitPrefix::Yocto, // -8 |
69 | KFormat::UnitPrefix::Zepto, |
70 | KFormat::UnitPrefix::Atto, |
71 | KFormat::UnitPrefix::Femto, |
72 | KFormat::UnitPrefix::Pico, |
73 | KFormat::UnitPrefix::Nano, |
74 | KFormat::UnitPrefix::Micro, |
75 | KFormat::UnitPrefix::Milli, |
76 | KFormat::UnitPrefix::Unity, // 0 |
77 | KFormat::UnitPrefix::Kilo, |
78 | KFormat::UnitPrefix::Mega, |
79 | KFormat::UnitPrefix::Giga, |
80 | KFormat::UnitPrefix::Tera, |
81 | KFormat::UnitPrefix::Peta, |
82 | KFormat::UnitPrefix::Exa, |
83 | KFormat::UnitPrefix::Zetta, |
84 | KFormat::UnitPrefix::Yotta, // 8 |
85 | }; |
86 | power = std::max(a: -8, b: std::min(a: 8, b: power)); |
87 | prefix = map[power + 8]; |
88 | } |
89 | |
90 | if (prefix == KFormat::UnitPrefix::Unity && unit == KFormat::Unit::Byte) { |
91 | precision = 0; |
92 | } |
93 | |
94 | struct PrefixMapEntry { |
95 | KFormat::UnitPrefix prefix; |
96 | double decimalFactor; |
97 | double binaryFactor; |
98 | QString prefixCharSI; |
99 | QString prefixCharIEC; |
100 | }; |
101 | |
102 | const PrefixMapEntry map[] = { |
103 | {.prefix: KFormat::UnitPrefix::Yocto, .decimalFactor: 1e-24, .binaryFactor: bpow(exp: -80), .prefixCharSI: tr(sourceText: "y" , disambiguation: "SI prefix for 10^⁻24" ), .prefixCharIEC: QString()}, |
104 | {.prefix: KFormat::UnitPrefix::Zepto, .decimalFactor: 1e-21, .binaryFactor: bpow(exp: -70), .prefixCharSI: tr(sourceText: "z" , disambiguation: "SI prefix for 10^⁻21" ), .prefixCharIEC: QString()}, |
105 | {.prefix: KFormat::UnitPrefix::Atto, .decimalFactor: 1e-18, .binaryFactor: bpow(exp: -60), .prefixCharSI: tr(sourceText: "a" , disambiguation: "SI prefix for 10^⁻18" ), .prefixCharIEC: QString()}, |
106 | {.prefix: KFormat::UnitPrefix::Femto, .decimalFactor: 1e-15, .binaryFactor: bpow(exp: -50), .prefixCharSI: tr(sourceText: "f" , disambiguation: "SI prefix for 10^⁻15" ), .prefixCharIEC: QString()}, |
107 | {.prefix: KFormat::UnitPrefix::Pico, .decimalFactor: 1e-12, .binaryFactor: bpow(exp: -40), .prefixCharSI: tr(sourceText: "p" , disambiguation: "SI prefix for 10^⁻12" ), .prefixCharIEC: QString()}, |
108 | {.prefix: KFormat::UnitPrefix::Nano, .decimalFactor: 1e-9, .binaryFactor: bpow(exp: -30), .prefixCharSI: tr(sourceText: "n" , disambiguation: "SI prefix for 10^⁻9" ), .prefixCharIEC: QString()}, |
109 | {.prefix: KFormat::UnitPrefix::Micro, .decimalFactor: 1e-6, .binaryFactor: bpow(exp: -20), .prefixCharSI: tr(sourceText: "µ" , disambiguation: "SI prefix for 10^⁻6" ), .prefixCharIEC: QString()}, |
110 | {.prefix: KFormat::UnitPrefix::Milli, .decimalFactor: 1e-3, .binaryFactor: bpow(exp: -10), .prefixCharSI: tr(sourceText: "m" , disambiguation: "SI prefix for 10^⁻3" ), .prefixCharIEC: QString()}, |
111 | {.prefix: KFormat::UnitPrefix::Unity, .decimalFactor: 1.0, .binaryFactor: 1.0, .prefixCharSI: QString(), .prefixCharIEC: QString()}, |
112 | {.prefix: KFormat::UnitPrefix::Kilo, .decimalFactor: 1e3, .binaryFactor: bpow(exp: 10), .prefixCharSI: tr(sourceText: "k" , disambiguation: "SI prefix for 10^3" ), .prefixCharIEC: tr(sourceText: "Ki" , disambiguation: "IEC binary prefix for 2^10" )}, |
113 | {.prefix: KFormat::UnitPrefix::Mega, .decimalFactor: 1e6, .binaryFactor: bpow(exp: 20), .prefixCharSI: tr(sourceText: "M" , disambiguation: "SI prefix for 10^6" ), .prefixCharIEC: tr(sourceText: "Mi" , disambiguation: "IEC binary prefix for 2^20" )}, |
114 | {.prefix: KFormat::UnitPrefix::Giga, .decimalFactor: 1e9, .binaryFactor: bpow(exp: 30), .prefixCharSI: tr(sourceText: "G" , disambiguation: "SI prefix for 10^9" ), .prefixCharIEC: tr(sourceText: "Gi" , disambiguation: "IEC binary prefix for 2^30" )}, |
115 | {.prefix: KFormat::UnitPrefix::Tera, .decimalFactor: 1e12, .binaryFactor: bpow(exp: 40), .prefixCharSI: tr(sourceText: "T" , disambiguation: "SI prefix for 10^12" ), .prefixCharIEC: tr(sourceText: "Ti" , disambiguation: "IEC binary prefix for 2^40" )}, |
116 | {.prefix: KFormat::UnitPrefix::Peta, .decimalFactor: 1e15, .binaryFactor: bpow(exp: 50), .prefixCharSI: tr(sourceText: "P" , disambiguation: "SI prefix for 10^15" ), .prefixCharIEC: tr(sourceText: "Pi" , disambiguation: "IEC binary prefix for 2^50" )}, |
117 | {.prefix: KFormat::UnitPrefix::Exa, .decimalFactor: 1e18, .binaryFactor: bpow(exp: 60), .prefixCharSI: tr(sourceText: "E" , disambiguation: "SI prefix for 10^18" ), .prefixCharIEC: tr(sourceText: "Ei" , disambiguation: "IEC binary prefix for 2^60" )}, |
118 | {.prefix: KFormat::UnitPrefix::Zetta, .decimalFactor: 1e21, .binaryFactor: bpow(exp: 70), .prefixCharSI: tr(sourceText: "Z" , disambiguation: "SI prefix for 10^21" ), .prefixCharIEC: tr(sourceText: "Zi" , disambiguation: "IEC binary prefix for 2^70" )}, |
119 | {.prefix: KFormat::UnitPrefix::Yotta, .decimalFactor: 1e24, .binaryFactor: bpow(exp: 80), .prefixCharSI: tr(sourceText: "Y" , disambiguation: "SI prefix for 10^24" ), .prefixCharIEC: tr(sourceText: "Yi" , disambiguation: "IEC binary prefix for 2^80" )}, |
120 | }; |
121 | |
122 | auto entry = std::find_if(first: std::begin(arr: map), last: std::end(arr: map), pred: [prefix](const PrefixMapEntry &e) { |
123 | return e.prefix == prefix; |
124 | }); |
125 | |
126 | switch (unit) { |
127 | case KFormat::Unit::Bit: |
128 | unitString = tr(sourceText: "bit" , disambiguation: "Symbol of binary digit" ); |
129 | break; |
130 | case KFormat::Unit::Byte: |
131 | unitString = tr(sourceText: "B" , disambiguation: "Symbol of byte" ); |
132 | break; |
133 | case KFormat::Unit::Meter: |
134 | unitString = tr(sourceText: "m" , disambiguation: "Symbol of meter" ); |
135 | break; |
136 | case KFormat::Unit::Hertz: |
137 | unitString = tr(sourceText: "Hz" , disambiguation: "Symbol of hertz" ); |
138 | break; |
139 | case KFormat::Unit::Other: |
140 | break; |
141 | } |
142 | |
143 | if (prefix == KFormat::UnitPrefix::Unity) { |
144 | QString numString = m_locale.toString(f: value, format: 'f', precision); |
145 | //: value without prefix, format "<val> <unit>" |
146 | return tr(sourceText: "%1 %2" , disambiguation: "no Prefix" ).arg(args&: numString, args&: unitString); |
147 | } |
148 | |
149 | QString prefixString; |
150 | if (dialect == KFormat::MetricBinaryDialect) { |
151 | value /= entry->decimalFactor; |
152 | prefixString = entry->prefixCharSI; |
153 | } else { |
154 | value /= entry->binaryFactor; |
155 | if (dialect == KFormat::IECBinaryDialect) { |
156 | prefixString = entry->prefixCharIEC; |
157 | } else { |
158 | prefixString = entry->prefixCharSI.toUpper(); |
159 | } |
160 | } |
161 | |
162 | QString numString = m_locale.toString(f: value, format: 'f', precision); |
163 | |
164 | //: value with prefix, format "<val> <prefix><unit>" |
165 | return tr(sourceText: "%1 %2%3" , disambiguation: "MetricBinaryDialect" ).arg(args&: numString, args&: prefixString, args&: unitString); |
166 | } |
167 | |
168 | QString KFormatPrivate::formatByteSize(double size, int precision, KFormat::BinaryUnitDialect dialect, KFormat::BinarySizeUnits units) const |
169 | { |
170 | // Current KDE default is IECBinaryDialect |
171 | const auto fallbackDialect = KFormat::IECBinaryDialect; |
172 | |
173 | if (dialect <= KFormat::DefaultBinaryDialect || dialect > KFormat::LastBinaryDialect) { |
174 | const auto kdeglobals = QStandardPaths::locate(type: QStandardPaths::GenericConfigLocation, QStringLiteral("kdeglobals" )); |
175 | QSettings settings(kdeglobals, QSettings::IniFormat); |
176 | dialect = static_cast<KFormat::BinaryUnitDialect>(settings.value(key: "Locale/BinaryUnitDialect" , defaultValue: fallbackDialect).toInt()); |
177 | } |
178 | |
179 | // Current KDE default is to auto-adjust so the size falls in the range 0 to 1000/1024 |
180 | if (units < KFormat::DefaultBinaryUnits || units > KFormat::UnitLastUnit) { |
181 | units = KFormat::DefaultBinaryUnits; |
182 | } |
183 | |
184 | int unit = 0; // Selects what unit to use |
185 | double multiplier = 1024.0; |
186 | |
187 | if (dialect == KFormat::MetricBinaryDialect) { |
188 | multiplier = 1000.0; |
189 | } |
190 | |
191 | // If a specific unit conversion is given, use it directly. Otherwise |
192 | // search until the result is in [0, multiplier] (or out of our range). |
193 | if (units == KFormat::DefaultBinaryUnits) { |
194 | while (qAbs(t: size) >= multiplier && unit < int(KFormat::UnitYottaByte)) { |
195 | size /= multiplier; |
196 | ++unit; |
197 | } |
198 | } else { |
199 | // A specific unit is in use |
200 | unit = static_cast<int>(units); |
201 | if (unit > 0) { |
202 | size /= pow(x: multiplier, y: unit); |
203 | } |
204 | } |
205 | |
206 | // Bytes, no rounding |
207 | if (unit == 0) { |
208 | precision = 0; |
209 | } |
210 | |
211 | QString numString = m_locale.toString(f: size, format: 'f', precision); |
212 | |
213 | // Do not remove "//:" comments below, they are used by the translators. |
214 | // NB: we cannot pass pluralization arguments, as the size may be negative |
215 | if (dialect == KFormat::MetricBinaryDialect) { |
216 | switch (unit) { |
217 | case KFormat::UnitByte: |
218 | //: MetricBinaryDialect size in bytes |
219 | return tr(sourceText: "%1 B" , disambiguation: "MetricBinaryDialect" ).arg(a: numString); |
220 | case KFormat::UnitKiloByte: |
221 | //: MetricBinaryDialect size in 1000 bytes |
222 | return tr(sourceText: "%1 kB" , disambiguation: "MetricBinaryDialect" ).arg(a: numString); |
223 | case KFormat::UnitMegaByte: |
224 | //: MetricBinaryDialect size in 10^6 bytes |
225 | return tr(sourceText: "%1 MB" , disambiguation: "MetricBinaryDialect" ).arg(a: numString); |
226 | case KFormat::UnitGigaByte: |
227 | //: MetricBinaryDialect size in 10^9 bytes |
228 | return tr(sourceText: "%1 GB" , disambiguation: "MetricBinaryDialect" ).arg(a: numString); |
229 | case KFormat::UnitTeraByte: |
230 | //: MetricBinaryDialect size in 10^12 bytes |
231 | return tr(sourceText: "%1 TB" , disambiguation: "MetricBinaryDialect" ).arg(a: numString); |
232 | case KFormat::UnitPetaByte: |
233 | //: MetricBinaryDialect size in 10^15 bytes |
234 | return tr(sourceText: "%1 PB" , disambiguation: "MetricBinaryDialect" ).arg(a: numString); |
235 | case KFormat::UnitExaByte: |
236 | //: MetricBinaryDialect size in 10^18 byte |
237 | return tr(sourceText: "%1 EB" , disambiguation: "MetricBinaryDialect" ).arg(a: numString); |
238 | case KFormat::UnitZettaByte: |
239 | //: MetricBinaryDialect size in 10^21 bytes |
240 | return tr(sourceText: "%1 ZB" , disambiguation: "MetricBinaryDialect" ).arg(a: numString); |
241 | case KFormat::UnitYottaByte: |
242 | //: MetricBinaryDialect size in 10^24 bytes |
243 | return tr(sourceText: "%1 YB" , disambiguation: "MetricBinaryDialect" ).arg(a: numString); |
244 | } |
245 | } else if (dialect == KFormat::JEDECBinaryDialect) { |
246 | switch (unit) { |
247 | case KFormat::UnitByte: |
248 | //: JEDECBinaryDialect memory size in bytes |
249 | return tr(sourceText: "%1 B" , disambiguation: "JEDECBinaryDialect" ).arg(a: numString); |
250 | case KFormat::UnitKiloByte: |
251 | //: JEDECBinaryDialect memory size in 1024 bytes |
252 | return tr(sourceText: "%1 KB" , disambiguation: "JEDECBinaryDialect" ).arg(a: numString); |
253 | case KFormat::UnitMegaByte: |
254 | //: JEDECBinaryDialect memory size in 10^20 bytes |
255 | return tr(sourceText: "%1 MB" , disambiguation: "JEDECBinaryDialect" ).arg(a: numString); |
256 | case KFormat::UnitGigaByte: |
257 | //: JEDECBinaryDialect memory size in 10^30 bytes |
258 | return tr(sourceText: "%1 GB" , disambiguation: "JEDECBinaryDialect" ).arg(a: numString); |
259 | case KFormat::UnitTeraByte: |
260 | //: JEDECBinaryDialect memory size in 10^40 bytes |
261 | return tr(sourceText: "%1 TB" , disambiguation: "JEDECBinaryDialect" ).arg(a: numString); |
262 | case KFormat::UnitPetaByte: |
263 | //: JEDECBinaryDialect memory size in 10^50 bytes |
264 | return tr(sourceText: "%1 PB" , disambiguation: "JEDECBinaryDialect" ).arg(a: numString); |
265 | case KFormat::UnitExaByte: |
266 | //: JEDECBinaryDialect memory size in 10^60 bytes |
267 | return tr(sourceText: "%1 EB" , disambiguation: "JEDECBinaryDialect" ).arg(a: numString); |
268 | case KFormat::UnitZettaByte: |
269 | //: JEDECBinaryDialect memory size in 10^70 bytes |
270 | return tr(sourceText: "%1 ZB" , disambiguation: "JEDECBinaryDialect" ).arg(a: numString); |
271 | case KFormat::UnitYottaByte: |
272 | //: JEDECBinaryDialect memory size in 10^80 bytes |
273 | return tr(sourceText: "%1 YB" , disambiguation: "JEDECBinaryDialect" ).arg(a: numString); |
274 | } |
275 | } else { // KFormat::IECBinaryDialect, KFormat::DefaultBinaryDialect |
276 | switch (unit) { |
277 | case KFormat::UnitByte: |
278 | //: IECBinaryDialect size in bytes |
279 | return tr(sourceText: "%1 B" , disambiguation: "IECBinaryDialect" ).arg(a: numString); |
280 | case KFormat::UnitKiloByte: |
281 | //: IECBinaryDialect size in 1024 bytes |
282 | return tr(sourceText: "%1 KiB" , disambiguation: "IECBinaryDialect" ).arg(a: numString); |
283 | case KFormat::UnitMegaByte: |
284 | //: IECBinaryDialect size in 10^20 bytes |
285 | return tr(sourceText: "%1 MiB" , disambiguation: "IECBinaryDialect" ).arg(a: numString); |
286 | case KFormat::UnitGigaByte: |
287 | //: IECBinaryDialect size in 10^30 bytes |
288 | return tr(sourceText: "%1 GiB" , disambiguation: "IECBinaryDialect" ).arg(a: numString); |
289 | case KFormat::UnitTeraByte: |
290 | //: IECBinaryDialect size in 10^40 bytes |
291 | return tr(sourceText: "%1 TiB" , disambiguation: "IECBinaryDialect" ).arg(a: numString); |
292 | case KFormat::UnitPetaByte: |
293 | //: IECBinaryDialect size in 10^50 bytes |
294 | return tr(sourceText: "%1 PiB" , disambiguation: "IECBinaryDialect" ).arg(a: numString); |
295 | case KFormat::UnitExaByte: |
296 | //: IECBinaryDialect size in 10^60 bytes |
297 | return tr(sourceText: "%1 EiB" , disambiguation: "IECBinaryDialect" ).arg(a: numString); |
298 | case KFormat::UnitZettaByte: |
299 | //: IECBinaryDialect size in 10^70 bytes |
300 | return tr(sourceText: "%1 ZiB" , disambiguation: "IECBinaryDialect" ).arg(a: numString); |
301 | case KFormat::UnitYottaByte: |
302 | //: IECBinaryDialect size in 10^80 bytes |
303 | return tr(sourceText: "%1 YiB" , disambiguation: "IECBinaryDialect" ).arg(a: numString); |
304 | } |
305 | } |
306 | |
307 | // Should never reach here |
308 | Q_ASSERT(false); |
309 | return numString; |
310 | } |
311 | |
312 | constexpr quint64 MSecsInYear = (quint64)1000 * 60 * 60 * 24 * 365; // approximation of a year |
313 | constexpr quint64 MSecsInDay = 1000 * 60 * 60 * 24; |
314 | constexpr quint64 MSecsInHour = 3600000; |
315 | constexpr quint64 MSecsInMinute = 60000; |
316 | constexpr quint64 MSecsInSecond = 1000; |
317 | |
318 | enum DurationUnits { |
319 | Years = 0, |
320 | Days, |
321 | Hours, |
322 | Minutes, |
323 | Seconds, |
324 | }; |
325 | |
326 | static QString formatSingleAbbreviatedDuration(DurationUnits units, int n) |
327 | { |
328 | switch (units) { |
329 | case Years: |
330 | //: @item:intext abbreviated amount of years |
331 | //~ singular %n yr |
332 | //~ plural %n yr |
333 | return KFormatPrivate::tr(sourceText: "%n yr" , disambiguation: nullptr, n); |
334 | case Days: |
335 | //: @item:intext abbreviated amount of days |
336 | //~ singular %n d |
337 | //~ plural %n d |
338 | return KFormatPrivate::tr(sourceText: "%n d" , disambiguation: nullptr, n); |
339 | case Hours: |
340 | //: @item:intext abbreviated amount of hours |
341 | //~ singular %n hr |
342 | //~ plural %n hr |
343 | return KFormatPrivate::tr(sourceText: "%n hr" , disambiguation: nullptr, n); |
344 | case Minutes: |
345 | //: @item:intext abbreviated amount of minutes |
346 | //~ singular %n min |
347 | //~ plural %n min |
348 | return KFormatPrivate::tr(sourceText: "%n min" , disambiguation: nullptr, n); |
349 | case Seconds: |
350 | //: @item:intext abbreviated amount of seconds |
351 | //~ singular %n sec |
352 | //~ plural %n sec |
353 | return KFormatPrivate::tr(sourceText: "%n sec" , disambiguation: nullptr, n); |
354 | } |
355 | Q_ASSERT(false); |
356 | return QString(); |
357 | } |
358 | QString KFormatPrivate::formatDuration(quint64 msecs, KFormat::DurationFormatOptions options) const |
359 | { |
360 | quint64 ms = msecs; |
361 | if (options & KFormat::HideSeconds) { |
362 | // round to nearest minute |
363 | ms = qRound64(d: ms / (qreal)MSecsInMinute) * MSecsInMinute; |
364 | } else if (!(options & KFormat::ShowMilliseconds)) { |
365 | // round to nearest second |
366 | ms = qRound64(d: ms / (qreal)MSecsInSecond) * MSecsInSecond; |
367 | } |
368 | |
369 | if ((options & KFormat::InitialDuration) == KFormat::InitialDuration) { |
370 | int hours = ms / MSecsInHour; |
371 | ms = ms % MSecsInHour; |
372 | int minutes = ms / MSecsInMinute; |
373 | ms = ms % MSecsInMinute; |
374 | int seconds = ms / MSecsInSecond; |
375 | ms = ms % MSecsInSecond; |
376 | |
377 | if ((options & KFormat::FoldHours) == KFormat::FoldHours && (options & KFormat::ShowMilliseconds) == KFormat::ShowMilliseconds) { |
378 | //: @item:intext Duration format minutes, seconds and milliseconds |
379 | return tr(sourceText: "%1m%2.%3s" ).arg(a: hours * 60 + minutes, fieldWidth: 1, base: 10, fillChar: QLatin1Char('0')).arg(a: seconds, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')).arg(a: ms, fieldwidth: 3, base: 10, fillChar: QLatin1Char('0')); |
380 | } else if ((options & KFormat::FoldHours) == KFormat::FoldHours) { |
381 | //: @item:intext Duration format minutes and seconds |
382 | return tr(sourceText: "%1m%2s" ).arg(a: hours * 60 + minutes, fieldWidth: 1, base: 10, fillChar: QLatin1Char('0')).arg(a: seconds, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')); |
383 | } else if ((options & KFormat::HideSeconds) == KFormat::HideSeconds) { |
384 | //: @item:intext Duration format hours and minutes |
385 | return tr(sourceText: "%1h%2m" ).arg(a: hours, fieldWidth: 1, base: 10, fillChar: QLatin1Char('0')).arg(a: minutes, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')); |
386 | } else if ((options & KFormat::ShowMilliseconds) == KFormat::ShowMilliseconds) { |
387 | //: @item:intext Duration format hours, minutes, seconds, milliseconds |
388 | return tr(sourceText: "%1h%2m%3.%4s" ) |
389 | .arg(a: hours, fieldWidth: 1, base: 10, fillChar: QLatin1Char('0')) |
390 | .arg(a: minutes, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')) |
391 | .arg(a: seconds, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')) |
392 | .arg(a: ms, fieldwidth: 3, base: 10, fillChar: QLatin1Char('0')); |
393 | } |
394 | |
395 | //: @item:intext Duration format hours, minutes, seconds |
396 | return tr(sourceText: "%1h%2m%3s" ).arg(a: hours, fieldWidth: 1, base: 10, fillChar: QLatin1Char('0')).arg(a: minutes, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')).arg(a: seconds, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')); |
397 | } |
398 | |
399 | int days = 0; |
400 | int years = 0; |
401 | if (options & KFormat::AbbreviatedDuration) { |
402 | years = ms / MSecsInYear; |
403 | ms = ms % MSecsInYear; |
404 | days = ms / MSecsInDay; |
405 | ms = ms % MSecsInDay; |
406 | } |
407 | int hours = ms / MSecsInHour; |
408 | ms = ms % MSecsInHour; |
409 | int minutes = ms / MSecsInMinute; |
410 | ms = ms % MSecsInMinute; |
411 | int seconds = ms / MSecsInSecond; |
412 | ms = ms % MSecsInSecond; |
413 | |
414 | if (options & KFormat::AbbreviatedDuration) { |
415 | if (options & KFormat::FoldHours) { |
416 | minutes += 60 * hours + 60 * 24 * days + 365 * 60 * 24 * years; |
417 | hours = 0; |
418 | days = 0; |
419 | years = 0; |
420 | } |
421 | |
422 | if (days == 0 && hours == 0 && minutes == 0) { |
423 | return (options & KFormat::HideSeconds) ? formatSingleAbbreviatedDuration(units: Minutes, n: minutes) : formatSingleAbbreviatedDuration(units: Seconds, n: seconds); |
424 | } |
425 | |
426 | if (years != 0) { |
427 | if (days == 0) { |
428 | return formatSingleAbbreviatedDuration(units: Years, n: years); |
429 | } |
430 | |
431 | //: @item:intext abbreviated amount of years and abbreviated amount of days |
432 | return tr(sourceText: "%1 %2" ).arg(args: formatSingleAbbreviatedDuration(units: Years, n: years), args: formatSingleAbbreviatedDuration(units: Days, n: days)); |
433 | } |
434 | |
435 | if (days != 0) { |
436 | if (hours == 0) { |
437 | return formatSingleAbbreviatedDuration(units: Days, n: days); |
438 | } |
439 | |
440 | //: @item:intext abbreviated amount of days and abbreviated amount of hours |
441 | return tr(sourceText: "%1 %2" ).arg(args: formatSingleAbbreviatedDuration(units: Days, n: days), args: formatSingleAbbreviatedDuration(units: Hours, n: hours)); |
442 | } |
443 | |
444 | if (hours != 0) { |
445 | //: @item:intext abbreviated amount of hours and abbreviated amount of minutes |
446 | return tr(sourceText: "%1 %2" ).arg(args: formatSingleAbbreviatedDuration(units: Hours, n: hours), args: formatSingleAbbreviatedDuration(units: Minutes, n: minutes)); |
447 | } |
448 | |
449 | if (options & KFormat::HideSeconds) { |
450 | return formatSingleAbbreviatedDuration(units: Minutes, n: minutes); |
451 | } |
452 | |
453 | //: @item:intext abbreviated amount of minutes and abbreviated amount of seconds |
454 | return tr(sourceText: "%1 %2" ).arg(args: formatSingleAbbreviatedDuration(units: Minutes, n: minutes), args: formatSingleAbbreviatedDuration(units: Seconds, n: seconds)); |
455 | } |
456 | |
457 | if ((options & KFormat::FoldHours) == KFormat::FoldHours && (options & KFormat::ShowMilliseconds) == KFormat::ShowMilliseconds) { |
458 | //: @item:intext Duration format minutes, seconds and milliseconds |
459 | return tr(sourceText: "%1:%2.%3" ).arg(a: hours * 60 + minutes, fieldWidth: 1, base: 10, fillChar: QLatin1Char('0')).arg(a: seconds, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')).arg(a: ms, fieldwidth: 3, base: 10, fillChar: QLatin1Char('0')); |
460 | } else if ((options & KFormat::FoldHours) == KFormat::FoldHours) { |
461 | //: @item:intext Duration format minutes and seconds |
462 | return tr(sourceText: "%1:%2" ).arg(a: hours * 60 + minutes, fieldWidth: 1, base: 10, fillChar: QLatin1Char('0')).arg(a: seconds, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')); |
463 | } else if ((options & KFormat::HideSeconds) == KFormat::HideSeconds) { |
464 | //: @item:intext Duration format hours and minutes |
465 | return tr(sourceText: "%1:%2" ).arg(a: hours, fieldWidth: 1, base: 10, fillChar: QLatin1Char('0')).arg(a: minutes, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')); |
466 | } else if ((options & KFormat::ShowMilliseconds) == KFormat::ShowMilliseconds) { |
467 | //: @item:intext Duration format hours, minutes, seconds, milliseconds |
468 | return tr(sourceText: "%1:%2:%3.%4" ) |
469 | .arg(a: hours, fieldWidth: 1, base: 10, fillChar: QLatin1Char('0')) |
470 | .arg(a: minutes, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')) |
471 | .arg(a: seconds, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')) |
472 | .arg(a: ms, fieldwidth: 3, base: 10, fillChar: QLatin1Char('0')); |
473 | } |
474 | |
475 | //: @item:intext Duration format hours, minutes, seconds |
476 | return tr(sourceText: "%1:%2:%3" ).arg(a: hours, fieldWidth: 1, base: 10, fillChar: QLatin1Char('0')).arg(a: minutes, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')).arg(a: seconds, fieldWidth: 2, base: 10, fillChar: QLatin1Char('0')); |
477 | } |
478 | |
479 | QString KFormatPrivate::formatDecimalDuration(quint64 msecs, int decimalPlaces) const |
480 | { |
481 | if (msecs >= MSecsInDay) { |
482 | //: @item:intext %1 is a real number, e.g. 1.23 days |
483 | return tr(sourceText: "%1 days" ).arg(a: m_locale.toString(f: msecs / (+MSecsInDay * 1.0), format: 'f', precision: decimalPlaces)); |
484 | } else if (msecs >= MSecsInHour) { |
485 | //: @item:intext %1 is a real number, e.g. 1.23 hours |
486 | return tr(sourceText: "%1 hours" ).arg(a: m_locale.toString(f: msecs / (+MSecsInHour * 1.0), format: 'f', precision: decimalPlaces)); |
487 | } else if (msecs >= MSecsInMinute) { |
488 | //: @item:intext %1 is a real number, e.g. 1.23 minutes |
489 | return tr(sourceText: "%1 minutes" ).arg(a: m_locale.toString(f: msecs / (+MSecsInMinute * 1.0), format: 'f', precision: decimalPlaces)); |
490 | } else if (msecs >= MSecsInSecond) { |
491 | //: @item:intext %1 is a real number, e.g. 1.23 seconds |
492 | return tr(sourceText: "%1 seconds" ).arg(a: m_locale.toString(f: msecs / (+MSecsInSecond * 1.0), format: 'f', precision: decimalPlaces)); |
493 | } |
494 | //: @item:intext %1 is a whole number |
495 | //~ singular %n millisecond |
496 | //~ plural %n milliseconds |
497 | return tr(sourceText: "%n millisecond(s)" , disambiguation: nullptr, n: msecs); |
498 | } |
499 | |
500 | static QString formatSingleDuration(DurationUnits units, int n) |
501 | { |
502 | // NB: n is guaranteed to be non-negative |
503 | switch (units) { |
504 | case Years: |
505 | //: @item:intext %n is a whole number |
506 | //~ singular %n year |
507 | //~ plural %n years |
508 | return KFormatPrivate::tr(sourceText: "%n year(s)" , disambiguation: nullptr, n); |
509 | case Days: |
510 | //: @item:intext %n is a whole number |
511 | //~ singular %n day |
512 | //~ plural %n days |
513 | return KFormatPrivate::tr(sourceText: "%n day(s)" , disambiguation: nullptr, n); |
514 | case Hours: |
515 | //: @item:intext %n is a whole number |
516 | //~ singular %n hour |
517 | //~ plural %n hours |
518 | return KFormatPrivate::tr(sourceText: "%n hour(s)" , disambiguation: nullptr, n); |
519 | case Minutes: |
520 | //: @item:intext %n is a whole number |
521 | //~ singular %n minute |
522 | //~ plural %n minutes |
523 | return KFormatPrivate::tr(sourceText: "%n minute(s)" , disambiguation: nullptr, n); |
524 | case Seconds: |
525 | //: @item:intext %n is a whole number |
526 | //~ singular %n second |
527 | //~ plural %n seconds |
528 | return KFormatPrivate::tr(sourceText: "%n second(s)" , disambiguation: nullptr, n); |
529 | } |
530 | Q_ASSERT(false); |
531 | return QString(); |
532 | } |
533 | |
534 | QString KFormatPrivate::formatSpelloutDuration(quint64 msecs) const |
535 | { |
536 | quint64 ms = msecs; |
537 | int days = ms / MSecsInDay; |
538 | ms = ms % (MSecsInDay); |
539 | int hours = ms / MSecsInHour; |
540 | ms = ms % MSecsInHour; |
541 | int minutes = ms / MSecsInMinute; |
542 | ms = ms % MSecsInMinute; |
543 | int seconds = qRound(d: ms / 1000.0); |
544 | |
545 | // Handle correctly problematic case #1 (look at KFormatTest::prettyFormatDuration()) |
546 | if (seconds == 60) { |
547 | return formatSpelloutDuration(msecs: msecs - ms + MSecsInMinute); |
548 | } |
549 | |
550 | if (days && hours) { |
551 | /*: @item:intext days and hours. This uses the previous item:intext messages. |
552 | If this does not fit the grammar of your language please contact the i18n team to solve the problem */ |
553 | return tr(sourceText: "%1 and %2" ).arg(args: formatSingleDuration(units: Days, n: days), args: formatSingleDuration(units: Hours, n: hours)); |
554 | } else if (days) { |
555 | return formatSingleDuration(units: Days, n: days); |
556 | } else if (hours && minutes) { |
557 | /*: @item:intext hours and minutes. This uses the previous item:intext messages. |
558 | If this does not fit the grammar of your language please contact the i18n team to solve the problem */ |
559 | return tr(sourceText: "%1 and %2" ).arg(args: formatSingleDuration(units: Hours, n: hours), args: formatSingleDuration(units: Minutes, n: minutes)); |
560 | } else if (hours) { |
561 | return formatSingleDuration(units: Hours, n: hours); |
562 | } else if (minutes && seconds) { |
563 | /*: @item:intext minutes and seconds. This uses the previous item:intext messages. |
564 | If this does not fit the grammar of your language please contact the i18n team to solve the problem */ |
565 | return tr(sourceText: "%1 and %2" ).arg(args: formatSingleDuration(units: Minutes, n: minutes), args: formatSingleDuration(units: Seconds, n: seconds)); |
566 | } else if (minutes) { |
567 | return formatSingleDuration(units: Minutes, n: minutes); |
568 | } else { |
569 | return formatSingleDuration(units: Seconds, n: seconds); |
570 | } |
571 | } |
572 | |
573 | QString KFormatPrivate::formatRelativeDate(const QDate &date, QLocale::FormatType format) const |
574 | { |
575 | if (!date.isValid()) { |
576 | return tr(sourceText: "Invalid date" , disambiguation: "used when a relative date string can't be generated because the date is invalid" ); |
577 | } |
578 | |
579 | const qint64 daysTo = QDate::currentDate().daysTo(d: date); |
580 | if (daysTo > 2 || daysTo < -2) { |
581 | return m_locale.toString(date, format); |
582 | } |
583 | |
584 | switch (daysTo) { |
585 | case 2: |
586 | return tr(sourceText: "In two days" ); |
587 | case 1: |
588 | return tr(sourceText: "Tomorrow" ); |
589 | case 0: |
590 | return tr(sourceText: "Today" ); |
591 | case -1: |
592 | return tr(sourceText: "Yesterday" ); |
593 | case -2: |
594 | return tr(sourceText: "Two days ago" ); |
595 | } |
596 | Q_UNREACHABLE(); |
597 | } |
598 | |
599 | QString KFormatPrivate::formatRelativeDateTime(const QDateTime &dateTime, QLocale::FormatType format) const |
600 | { |
601 | const QDateTime now = QDateTime::currentDateTime(); |
602 | |
603 | const auto secsToNow = dateTime.secsTo(now); |
604 | constexpr int secsInAHour = 60 * 60; |
605 | if (secsToNow >= 0 && secsToNow < secsInAHour) { |
606 | const int minutesToNow = secsToNow / 60; |
607 | if (minutesToNow <= 1) { |
608 | return tr(sourceText: "Just now" ); |
609 | } else if (format == QLocale::NarrowFormat) { |
610 | //: @item:intext %1 is a whole number, abbreviate minute value |
611 | //~ singular %n min ago |
612 | //~ plural %n mins ago |
613 | return tr(sourceText: "%n min(s) ago" , disambiguation: nullptr, n: minutesToNow); |
614 | } |
615 | //: @item:intext %1 is a whole number |
616 | //~ singular %n minute ago |
617 | //~ plural %n minutes ago |
618 | return tr(sourceText: "%n minute(s) ago" , disambiguation: nullptr, n: minutesToNow); |
619 | } |
620 | if (secsToNow <= 0 && -secsToNow < secsInAHour) { |
621 | const int minutesFromNow = -secsToNow / 60; |
622 | if (minutesFromNow < 1) { |
623 | return tr(sourceText: "Now" ); |
624 | } else if (format == QLocale::NarrowFormat) { |
625 | //: @item:intext %1 is a whole number, abbreviate minute value |
626 | //~ singular %n min |
627 | //~ plural %n mins |
628 | return tr(sourceText: "%n min(s)" , disambiguation: nullptr, n: minutesFromNow); |
629 | } |
630 | //: @item:intext %1 is a whole number |
631 | //~ singular In %n minute |
632 | //~ plural In %n minutes |
633 | return tr(sourceText: "In %n minute(s)" , disambiguation: nullptr, n: minutesFromNow); |
634 | } |
635 | |
636 | const auto timeFormatType = format == QLocale::FormatType::LongFormat ? QLocale::FormatType::ShortFormat : format; |
637 | const qint64 daysToNow = dateTime.daysTo(now); |
638 | QString dateString; |
639 | if (daysToNow < 2 && daysToNow > -2) { |
640 | dateString = formatRelativeDate(date: dateTime.date(), format); |
641 | } else { |
642 | dateString = m_locale.toString(date: dateTime.date(), format); |
643 | } |
644 | |
645 | /*: relative datetime with %1 result of QLocale.toString(date, format) or formatRelativeDate |
646 | and %2 result of QLocale.toString(time, timeformatType) |
647 | If this does not fit the grammar of your language please contact the i18n team to solve the problem */ |
648 | QString formattedDate = tr(sourceText: "%1 at %2" ).arg(args&: dateString, args: m_locale.toString(time: dateTime.time(), format: timeFormatType)); |
649 | |
650 | return formattedDate.replace(i: 0, len: 1, after: formattedDate.at(i: 0).toUpper()); |
651 | } |
652 | |
653 | [[nodiscard]] static bool needsTimeZone(const QDateTime &dt) |
654 | { |
655 | if (dt.timeSpec() == Qt::LocalTime) { |
656 | return false; |
657 | } |
658 | return dt.timeZone().offsetFromUtc(atDateTime: dt) != QTimeZone::systemTimeZone().offsetFromUtc(atDateTime: dt); |
659 | } |
660 | |
661 | QString KFormatPrivate::formatTime(const QDateTime &dateTime, QLocale::FormatType format, KFormat::TimeFormatOptions options) const |
662 | { |
663 | auto output = m_locale.toString(time: dateTime.time(), format); |
664 | if (options == KFormat::DoNotAddTimeZone || dateTime.timeSpec() == Qt::LocalTime |
665 | || ((options & KFormat::AddTimezoneAbbreviationIfNeeded) && !needsTimeZone(dt: dateTime))) { |
666 | return output; |
667 | } |
668 | |
669 | QString tzAbbr; |
670 | const auto tz = dateTime.timeZone(); |
671 | if (tz.hasDaylightTime()) { |
672 | tzAbbr = tz.displayName(timeType: tz.isDaylightTime(atDateTime: dateTime) ? QTimeZone::DaylightTime : QTimeZone::StandardTime, nameType: QTimeZone::ShortName, locale: m_locale); |
673 | } else { |
674 | tzAbbr = tz.displayName(timeType: QTimeZone::GenericTime, nameType: QTimeZone::ShortName, locale: m_locale); |
675 | } |
676 | if (tzAbbr.isEmpty()) { |
677 | return output; |
678 | } |
679 | /*: %1 is a formatted time (from QLocale.toString), %2 is a localized timezone abbreviation (from QTimeZone::displayName(QTimeZone::ShortName)). */ |
680 | return tr(sourceText: "%1 %2" ).arg(args&: output, args&: tzAbbr); |
681 | } |
682 | |
683 | QString KFormatPrivate::formatDistance(double distance, KFormat::DistanceFormatOptions options) const |
684 | { |
685 | if (((options & KFormat::MetricDistanceUnits) == 0) |
686 | && (m_locale.measurementSystem() == QLocale::ImperialUSSystem || m_locale.measurementSystem() == QLocale::ImperialUSSystem)) { |
687 | return formatImperialDistance(distance); |
688 | } |
689 | return formatMetricDistance(distance); |
690 | } |
691 | |
692 | [[nodiscard]] QString KFormatPrivate::formatImperialDistance(double distance) const |
693 | { |
694 | const auto feet = distance / 0.3048; |
695 | if (feet < 500.0) { |
696 | return KFormatPrivate::tr(sourceText: "%1 ft" , disambiguation: "distance in feet" ).arg(a: m_locale.toString(i: (int)std::round(x: feet))); |
697 | } |
698 | const auto miles = distance / 1609.344; |
699 | if (miles < 10.0) { |
700 | return KFormatPrivate::tr(sourceText: "%1 mi" , disambiguation: "distance in miles" ).arg(a: m_locale.toString(f: (int)(std::round(x: miles * 10.0)) / 10.0)); |
701 | } |
702 | return KFormatPrivate::tr(sourceText: "%1 mi" , disambiguation: "distance in miles" ).arg(a: m_locale.toString(i: (int)std::round(x: miles))); |
703 | } |
704 | |
705 | [[nodiscard]] QString KFormatPrivate::formatMetricDistance(double distance) const |
706 | { |
707 | if (distance < 1000.0) { |
708 | return KFormatPrivate::tr(sourceText: "%1 m" , disambiguation: "distance in meter" ).arg(a: m_locale.toString(i: (int)std::round(x: distance))); |
709 | } |
710 | if (distance < 10000.0) { |
711 | return KFormatPrivate::tr(sourceText: "%1 km" , disambiguation: "distance in kilometer" ).arg(a: m_locale.toString(f: (int)(std::round(x: distance / 100.0)) / 10.0)); |
712 | } |
713 | return KFormatPrivate::tr(sourceText: "%1 km" , disambiguation: "distance in kilometer" ).arg(a: m_locale.toString(i: (int)std::round(x: distance / 1000.0))); |
714 | } |
715 | |