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
19#include <math.h>
20
21KFormatPrivate::KFormatPrivate(const QLocale &locale)
22 : m_locale(locale)
23{
24}
25
26KFormatPrivate::~KFormatPrivate()
27{
28}
29
30constexpr double bpow(int exp)
31{
32 return (exp > 0) ? 2.0 * bpow(exp: exp - 1) : (exp < 0) ? 0.5 * bpow(exp: exp + 1) : 1.0;
33}
34
35QString KFormatPrivate::formatValue(double value,
36 KFormat::Unit unit,
37 QString unitString,
38 int precision,
39 KFormat::UnitPrefix prefix,
40 KFormat::BinaryUnitDialect dialect) const
41{
42 if (dialect <= KFormat::DefaultBinaryDialect || dialect > KFormat::LastBinaryDialect) {
43 dialect = KFormat::IECBinaryDialect;
44 }
45
46 if (static_cast<int>(prefix) < static_cast<int>(KFormat::UnitPrefix::Yocto) || static_cast<int>(prefix) > static_cast<int>(KFormat::UnitPrefix::Yotta)) {
47 prefix = KFormat::UnitPrefix::AutoAdjust;
48 }
49
50 double multiplier = 1024.0;
51 if (dialect == KFormat::MetricBinaryDialect) {
52 multiplier = 1000.0;
53 }
54
55 if (prefix == KFormat::UnitPrefix::AutoAdjust) {
56 int power = 0;
57 double adjustValue = qAbs(value);
58 while (adjustValue >= multiplier) {
59 adjustValue /= multiplier;
60 power += 1;
61 }
62 while (adjustValue && adjustValue < 1.0) {
63 adjustValue *= multiplier;
64 power -= 1;
65 }
66 const KFormat::UnitPrefix map[] = {
67 KFormat::UnitPrefix::Yocto, // -8
68 KFormat::UnitPrefix::Zepto,
69 KFormat::UnitPrefix::Atto,
70 KFormat::UnitPrefix::Femto,
71 KFormat::UnitPrefix::Pico,
72 KFormat::UnitPrefix::Nano,
73 KFormat::UnitPrefix::Micro,
74 KFormat::UnitPrefix::Milli,
75 KFormat::UnitPrefix::Unity, // 0
76 KFormat::UnitPrefix::Kilo,
77 KFormat::UnitPrefix::Mega,
78 KFormat::UnitPrefix::Giga,
79 KFormat::UnitPrefix::Tera,
80 KFormat::UnitPrefix::Peta,
81 KFormat::UnitPrefix::Exa,
82 KFormat::UnitPrefix::Zetta,
83 KFormat::UnitPrefix::Yotta, // 8
84 };
85 power = std::max(a: -8, b: std::min(a: 8, b: power));
86 prefix = map[power + 8];
87 }
88
89 if (prefix == KFormat::UnitPrefix::Unity && unit == KFormat::Unit::Byte) {
90 precision = 0;
91 }
92
93 struct PrefixMapEntry {
94 KFormat::UnitPrefix prefix;
95 double decimalFactor;
96 double binaryFactor;
97 QString prefixCharSI;
98 QString prefixCharIEC;
99 };
100
101 const PrefixMapEntry map[] = {
102 {KFormat::UnitPrefix::Yocto, 1e-24, bpow(exp: -80), tr("y", "SI prefix for 10^⁻24"), QString()},
103 {KFormat::UnitPrefix::Zepto, 1e-21, bpow(exp: -70), tr("z", "SI prefix for 10^⁻21"), QString()},
104 {KFormat::UnitPrefix::Atto, 1e-18, bpow(exp: -60), tr("a", "SI prefix for 10^⁻18"), QString()},
105 {KFormat::UnitPrefix::Femto, 1e-15, bpow(exp: -50), tr("f", "SI prefix for 10^⁻15"), QString()},
106 {KFormat::UnitPrefix::Pico, 1e-12, bpow(exp: -40), tr("p", "SI prefix for 10^⁻12"), QString()},
107 {KFormat::UnitPrefix::Nano, 1e-9, bpow(exp: -30), tr("n", "SI prefix for 10^⁻9"), QString()},
108 {KFormat::UnitPrefix::Micro, 1e-6, bpow(exp: -20), tr("µ", "SI prefix for 10^⁻6"), QString()},
109 {KFormat::UnitPrefix::Milli, 1e-3, bpow(exp: -10), tr("m", "SI prefix for 10^⁻3"), QString()},
110 {KFormat::UnitPrefix::Unity, 1.0, 1.0, QString(), QString()},
111 {KFormat::UnitPrefix::Kilo, 1e3, bpow(exp: 10), tr("k", "SI prefix for 10^3"), tr("Ki", "IEC binary prefix for 2^10")},
112 {KFormat::UnitPrefix::Mega, 1e6, bpow(exp: 20), tr("M", "SI prefix for 10^6"), tr("Mi", "IEC binary prefix for 2^20")},
113 {KFormat::UnitPrefix::Giga, 1e9, bpow(exp: 30), tr("G", "SI prefix for 10^9"), tr("Gi", "IEC binary prefix for 2^30")},
114 {KFormat::UnitPrefix::Tera, 1e12, bpow(exp: 40), tr("T", "SI prefix for 10^12"), tr("Ti", "IEC binary prefix for 2^40")},
115 {KFormat::UnitPrefix::Peta, 1e15, bpow(exp: 50), tr("P", "SI prefix for 10^15"), tr("Pi", "IEC binary prefix for 2^50")},
116 {KFormat::UnitPrefix::Exa, 1e18, bpow(exp: 60), tr("E", "SI prefix for 10^18"), tr("Ei", "IEC binary prefix for 2^60")},
117 {KFormat::UnitPrefix::Zetta, 1e21, bpow(exp: 70), tr("Z", "SI prefix for 10^21"), tr("Zi", "IEC binary prefix for 2^70")},
118 {KFormat::UnitPrefix::Yotta, 1e24, bpow(exp: 80), tr("Y", "SI prefix for 10^24"), tr("Yi", "IEC binary prefix for 2^80")},
119 };
120
121 auto entry = std::find_if(std::begin(map), std::end(map), [prefix](const PrefixMapEntry &e) {
122 return e.prefix == prefix;
123 });
124
125 switch (unit) {
126 case KFormat::Unit::Bit:
127 unitString = tr("bit", "Symbol of binary digit");
128 break;
129 case KFormat::Unit::Byte:
130 unitString = tr("B", "Symbol of byte");
131 break;
132 case KFormat::Unit::Meter:
133 unitString = tr("m", "Symbol of meter");
134 break;
135 case KFormat::Unit::Hertz:
136 unitString = tr("Hz", "Symbol of hertz");
137 break;
138 case KFormat::Unit::Other:
139 break;
140 }
141
142 if (prefix == KFormat::UnitPrefix::Unity) {
143 QString numString = m_locale.toString(value, 'f', precision);
144 //: value without prefix, format "<val> <unit>"
145 return tr("%1 %2", "no Prefix").arg(numString, unitString);
146 }
147
148 QString prefixString;
149 if (dialect == KFormat::MetricBinaryDialect) {
150 value /= entry->decimalFactor;
151 prefixString = entry->prefixCharSI;
152 } else {
153 value /= entry->binaryFactor;
154 if (dialect == KFormat::IECBinaryDialect) {
155 prefixString = entry->prefixCharIEC;
156 } else {
157 prefixString = entry->prefixCharSI.toUpper();
158 }
159 }
160
161 QString numString = m_locale.toString(value, 'f', precision);
162
163 //: value with prefix, format "<val> <prefix><unit>"
164 return tr("%1 %2%3", "MetricBinaryDialect").arg(numString, prefixString, unitString);
165}
166
167QString KFormatPrivate::formatByteSize(double size, int precision, KFormat::BinaryUnitDialect dialect, KFormat::BinarySizeUnits units) const
168{
169 // Current KDE default is IECBinaryDialect
170 const auto fallbackDialect = KFormat::IECBinaryDialect;
171
172 if (dialect <= KFormat::DefaultBinaryDialect || dialect > KFormat::LastBinaryDialect) {
173 const auto kdeglobals = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, QStringLiteral("kdeglobals"));
174 QSettings settings(kdeglobals, QSettings::IniFormat);
175 dialect = static_cast<KFormat::BinaryUnitDialect>(settings.value("Locale/BinaryUnitDialect", fallbackDialect).toInt());
176 }
177
178 // Current KDE default is to auto-adjust so the size falls in the range 0 to 1000/1024
179 if (units < KFormat::DefaultBinaryUnits || units > KFormat::UnitLastUnit) {
180 units = KFormat::DefaultBinaryUnits;
181 }
182
183 int unit = 0; // Selects what unit to use
184 double multiplier = 1024.0;
185
186 if (dialect == KFormat::MetricBinaryDialect) {
187 multiplier = 1000.0;
188 }
189
190 // If a specific unit conversion is given, use it directly. Otherwise
191 // search until the result is in [0, multiplier] (or out of our range).
192 if (units == KFormat::DefaultBinaryUnits) {
193 while (qAbs(size) >= multiplier && unit < int(KFormat::UnitYottaByte)) {
194 size /= multiplier;
195 ++unit;
196 }
197 } else {
198 // A specific unit is in use
199 unit = static_cast<int>(units);
200 if (unit > 0) {
201 size /= pow(x: multiplier, y: unit);
202 }
203 }
204
205 // Bytes, no rounding
206 if (unit == 0) {
207 precision = 0;
208 }
209
210 QString numString = m_locale.toString(size, 'f', precision);
211
212 // Do not remove "//:" comments below, they are used by the translators.
213 // NB: we cannot pass pluralization arguments, as the size may be negative
214 if (dialect == KFormat::MetricBinaryDialect) {
215 switch (unit) {
216 case KFormat::UnitByte:
217 //: MetricBinaryDialect size in bytes
218 return tr("%1 B", "MetricBinaryDialect").arg(numString);
219 case KFormat::UnitKiloByte:
220 //: MetricBinaryDialect size in 1000 bytes
221 return tr("%1 kB", "MetricBinaryDialect").arg(numString);
222 case KFormat::UnitMegaByte:
223 //: MetricBinaryDialect size in 10^6 bytes
224 return tr("%1 MB", "MetricBinaryDialect").arg(numString);
225 case KFormat::UnitGigaByte:
226 //: MetricBinaryDialect size in 10^9 bytes
227 return tr("%1 GB", "MetricBinaryDialect").arg(numString);
228 case KFormat::UnitTeraByte:
229 //: MetricBinaryDialect size in 10^12 bytes
230 return tr("%1 TB", "MetricBinaryDialect").arg(numString);
231 case KFormat::UnitPetaByte:
232 //: MetricBinaryDialect size in 10^15 bytes
233 return tr("%1 PB", "MetricBinaryDialect").arg(numString);
234 case KFormat::UnitExaByte:
235 //: MetricBinaryDialect size in 10^18 byte
236 return tr("%1 EB", "MetricBinaryDialect").arg(numString);
237 case KFormat::UnitZettaByte:
238 //: MetricBinaryDialect size in 10^21 bytes
239 return tr("%1 ZB", "MetricBinaryDialect").arg(numString);
240 case KFormat::UnitYottaByte:
241 //: MetricBinaryDialect size in 10^24 bytes
242 return tr("%1 YB", "MetricBinaryDialect").arg(numString);
243 }
244 } else if (dialect == KFormat::JEDECBinaryDialect) {
245 switch (unit) {
246 case KFormat::UnitByte:
247 //: JEDECBinaryDialect memory size in bytes
248 return tr("%1 B", "JEDECBinaryDialect").arg(numString);
249 case KFormat::UnitKiloByte:
250 //: JEDECBinaryDialect memory size in 1024 bytes
251 return tr("%1 KB", "JEDECBinaryDialect").arg(numString);
252 case KFormat::UnitMegaByte:
253 //: JEDECBinaryDialect memory size in 10^20 bytes
254 return tr("%1 MB", "JEDECBinaryDialect").arg(numString);
255 case KFormat::UnitGigaByte:
256 //: JEDECBinaryDialect memory size in 10^30 bytes
257 return tr("%1 GB", "JEDECBinaryDialect").arg(numString);
258 case KFormat::UnitTeraByte:
259 //: JEDECBinaryDialect memory size in 10^40 bytes
260 return tr("%1 TB", "JEDECBinaryDialect").arg(numString);
261 case KFormat::UnitPetaByte:
262 //: JEDECBinaryDialect memory size in 10^50 bytes
263 return tr("%1 PB", "JEDECBinaryDialect").arg(numString);
264 case KFormat::UnitExaByte:
265 //: JEDECBinaryDialect memory size in 10^60 bytes
266 return tr("%1 EB", "JEDECBinaryDialect").arg(numString);
267 case KFormat::UnitZettaByte:
268 //: JEDECBinaryDialect memory size in 10^70 bytes
269 return tr("%1 ZB", "JEDECBinaryDialect").arg(numString);
270 case KFormat::UnitYottaByte:
271 //: JEDECBinaryDialect memory size in 10^80 bytes
272 return tr("%1 YB", "JEDECBinaryDialect").arg(numString);
273 }
274 } else { // KFormat::IECBinaryDialect, KFormat::DefaultBinaryDialect
275 switch (unit) {
276 case KFormat::UnitByte:
277 //: IECBinaryDialect size in bytes
278 return tr("%1 B", "IECBinaryDialect").arg(numString);
279 case KFormat::UnitKiloByte:
280 //: IECBinaryDialect size in 1024 bytes
281 return tr("%1 KiB", "IECBinaryDialect").arg(numString);
282 case KFormat::UnitMegaByte:
283 //: IECBinaryDialect size in 10^20 bytes
284 return tr("%1 MiB", "IECBinaryDialect").arg(numString);
285 case KFormat::UnitGigaByte:
286 //: IECBinaryDialect size in 10^30 bytes
287 return tr("%1 GiB", "IECBinaryDialect").arg(numString);
288 case KFormat::UnitTeraByte:
289 //: IECBinaryDialect size in 10^40 bytes
290 return tr("%1 TiB", "IECBinaryDialect").arg(numString);
291 case KFormat::UnitPetaByte:
292 //: IECBinaryDialect size in 10^50 bytes
293 return tr("%1 PiB", "IECBinaryDialect").arg(numString);
294 case KFormat::UnitExaByte:
295 //: IECBinaryDialect size in 10^60 bytes
296 return tr("%1 EiB", "IECBinaryDialect").arg(numString);
297 case KFormat::UnitZettaByte:
298 //: IECBinaryDialect size in 10^70 bytes
299 return tr("%1 ZiB", "IECBinaryDialect").arg(numString);
300 case KFormat::UnitYottaByte:
301 //: IECBinaryDialect size in 10^80 bytes
302 return tr("%1 YiB", "IECBinaryDialect").arg(numString);
303 }
304 }
305
306 // Should never reach here
307 Q_ASSERT(false);
308 return numString;
309}
310
311enum TimeConstants {
312 MSecsInDay = 86400000,
313 MSecsInHour = 3600000,
314 MSecsInMinute = 60000,
315 MSecsInSecond = 1000,
316};
317
318QString KFormatPrivate::formatDuration(quint64 msecs, KFormat::DurationFormatOptions options) const
319{
320 quint64 ms = msecs;
321 if (options & KFormat::HideSeconds) {
322 // round to nearest minute
323 ms = qRound64(ms / (qreal)MSecsInMinute) * MSecsInMinute;
324 } else if (!(options & KFormat::ShowMilliseconds)) {
325 // round to nearest second
326 ms = qRound64(ms / (qreal)MSecsInSecond) * MSecsInSecond;
327 }
328
329 int hours = ms / MSecsInHour;
330 ms = ms % MSecsInHour;
331 int minutes = ms / MSecsInMinute;
332 ms = ms % MSecsInMinute;
333 int seconds = ms / MSecsInSecond;
334 ms = ms % MSecsInSecond;
335
336 if ((options & KFormat::InitialDuration) == KFormat::InitialDuration) {
337 if ((options & KFormat::FoldHours) == KFormat::FoldHours && (options & KFormat::ShowMilliseconds) == KFormat::ShowMilliseconds) {
338 //: @item:intext Duration format minutes, seconds and milliseconds
339 return tr("%1m%2.%3s").arg(hours * 60 + minutes, 1, 10, QLatin1Char('0')).arg(seconds, 2, 10, QLatin1Char('0')).arg(ms, 3, 10, QLatin1Char('0'));
340 } else if ((options & KFormat::FoldHours) == KFormat::FoldHours) {
341 //: @item:intext Duration format minutes and seconds
342 return tr("%1m%2s").arg(hours * 60 + minutes, 1, 10, QLatin1Char('0')).arg(seconds, 2, 10, QLatin1Char('0'));
343 } else if ((options & KFormat::HideSeconds) == KFormat::HideSeconds) {
344 //: @item:intext Duration format hours and minutes
345 return tr("%1h%2m").arg(hours, 1, 10, QLatin1Char('0')).arg(minutes, 2, 10, QLatin1Char('0'));
346 } else if ((options & KFormat::ShowMilliseconds) == KFormat::ShowMilliseconds) {
347 //: @item:intext Duration format hours, minutes, seconds, milliseconds
348 return tr("%1h%2m%3.%4s")
349 .arg(hours, 1, 10, QLatin1Char('0'))
350 .arg(minutes, 2, 10, QLatin1Char('0'))
351 .arg(seconds, 2, 10, QLatin1Char('0'))
352 .arg(ms, 3, 10, QLatin1Char('0'));
353 } else { // Default
354 //: @item:intext Duration format hours, minutes, seconds
355 return tr("%1h%2m%3s").arg(hours, 1, 10, QLatin1Char('0')).arg(minutes, 2, 10, QLatin1Char('0')).arg(seconds, 2, 10, QLatin1Char('0'));
356 }
357
358 } else {
359 if ((options & KFormat::FoldHours) == KFormat::FoldHours && (options & KFormat::ShowMilliseconds) == KFormat::ShowMilliseconds) {
360 //: @item:intext Duration format minutes, seconds and milliseconds
361 return tr("%1:%2.%3").arg(hours * 60 + minutes, 1, 10, QLatin1Char('0')).arg(seconds, 2, 10, QLatin1Char('0')).arg(ms, 3, 10, QLatin1Char('0'));
362 } else if ((options & KFormat::FoldHours) == KFormat::FoldHours) {
363 //: @item:intext Duration format minutes and seconds
364 return tr("%1:%2").arg(hours * 60 + minutes, 1, 10, QLatin1Char('0')).arg(seconds, 2, 10, QLatin1Char('0'));
365 } else if ((options & KFormat::HideSeconds) == KFormat::HideSeconds) {
366 //: @item:intext Duration format hours and minutes
367 return tr("%1:%2").arg(hours, 1, 10, QLatin1Char('0')).arg(minutes, 2, 10, QLatin1Char('0'));
368 } else if ((options & KFormat::ShowMilliseconds) == KFormat::ShowMilliseconds) {
369 //: @item:intext Duration format hours, minutes, seconds, milliseconds
370 return tr("%1:%2:%3.%4")
371 .arg(hours, 1, 10, QLatin1Char('0'))
372 .arg(minutes, 2, 10, QLatin1Char('0'))
373 .arg(seconds, 2, 10, QLatin1Char('0'))
374 .arg(ms, 3, 10, QLatin1Char('0'));
375 } else { // Default
376 //: @item:intext Duration format hours, minutes, seconds
377 return tr("%1:%2:%3").arg(hours, 1, 10, QLatin1Char('0')).arg(minutes, 2, 10, QLatin1Char('0')).arg(seconds, 2, 10, QLatin1Char('0'));
378 }
379 }
380
381 Q_UNREACHABLE();
382 return QString();
383}
384
385QString KFormatPrivate::formatDecimalDuration(quint64 msecs, int decimalPlaces) const
386{
387 if (msecs >= MSecsInDay) {
388 //: @item:intext %1 is a real number, e.g. 1.23 days
389 return tr("%1 days").arg(m_locale.toString(msecs / (MSecsInDay * 1.0), 'f', decimalPlaces));
390 } else if (msecs >= MSecsInHour) {
391 //: @item:intext %1 is a real number, e.g. 1.23 hours
392 return tr("%1 hours").arg(m_locale.toString(msecs / (MSecsInHour * 1.0), 'f', decimalPlaces));
393 } else if (msecs >= MSecsInMinute) {
394 //: @item:intext %1 is a real number, e.g. 1.23 minutes
395 return tr("%1 minutes").arg(m_locale.toString(msecs / (MSecsInMinute * 1.0), 'f', decimalPlaces));
396 } else if (msecs >= MSecsInSecond) {
397 //: @item:intext %1 is a real number, e.g. 1.23 seconds
398 return tr("%1 seconds").arg(m_locale.toString(msecs / (MSecsInSecond * 1.0), 'f', decimalPlaces));
399 }
400 //: @item:intext %1 is a whole number
401 //~ singular %n millisecond
402 //~ plural %n milliseconds
403 return tr("%n millisecond(s)", nullptr, msecs);
404}
405
406enum DurationUnits {
407 Days = 0,
408 Hours,
409 Minutes,
410 Seconds,
411};
412
413static QString formatSingleDuration(DurationUnits units, int n)
414{
415 // NB: n is guaranteed to be non-negative
416 switch (units) {
417 case Days:
418 //: @item:intext %n is a whole number
419 //~ singular %n day
420 //~ plural %n days
421 return KFormatPrivate::tr("%n day(s)", nullptr, n);
422 case Hours:
423 //: @item:intext %n is a whole number
424 //~ singular %n hour
425 //~ plural %n hours
426 return KFormatPrivate::tr("%n hour(s)", nullptr, n);
427 case Minutes:
428 //: @item:intext %n is a whole number
429 //~ singular %n minute
430 //~ plural %n minutes
431 return KFormatPrivate::tr("%n minute(s)", nullptr, n);
432 case Seconds:
433 //: @item:intext %n is a whole number
434 //~ singular %n second
435 //~ plural %n seconds
436 return KFormatPrivate::tr("%n second(s)", nullptr, n);
437 }
438 Q_ASSERT(false);
439 return QString();
440}
441
442QString KFormatPrivate::formatSpelloutDuration(quint64 msecs) const
443{
444 quint64 ms = msecs;
445 int days = ms / MSecsInDay;
446 ms = ms % (MSecsInDay);
447 int hours = ms / MSecsInHour;
448 ms = ms % MSecsInHour;
449 int minutes = ms / MSecsInMinute;
450 ms = ms % MSecsInMinute;
451 int seconds = qRound(ms / 1000.0);
452
453 // Handle correctly problematic case #1 (look at KFormatTest::prettyFormatDuration())
454 if (seconds == 60) {
455 return formatSpelloutDuration(msecs - ms + MSecsInMinute);
456 }
457
458 if (days && hours) {
459 /*: @item:intext days and hours. This uses the previous item:intext messages.
460 If this does not fit the grammar of your language please contact the i18n team to solve the problem */
461 return tr("%1 and %2").arg(formatSingleDuration(Days, days), formatSingleDuration(Hours, hours));
462 } else if (days) {
463 return formatSingleDuration(Days, days);
464 } else if (hours && minutes) {
465 /*: @item:intext hours and minutes. This uses the previous item:intext messages.
466 If this does not fit the grammar of your language please contact the i18n team to solve the problem */
467 return tr("%1 and %2").arg(formatSingleDuration(Hours, hours), formatSingleDuration(Minutes, minutes));
468 } else if (hours) {
469 return formatSingleDuration(Hours, hours);
470 } else if (minutes && seconds) {
471 /*: @item:intext minutes and seconds. This uses the previous item:intext messages.
472 If this does not fit the grammar of your language please contact the i18n team to solve the problem */
473 return tr("%1 and %2").arg(formatSingleDuration(Minutes, minutes), formatSingleDuration(Seconds, seconds));
474 } else if (minutes) {
475 return formatSingleDuration(Minutes, minutes);
476 } else {
477 return formatSingleDuration(Seconds, seconds);
478 }
479}
480
481QString KFormatPrivate::formatRelativeDate(const QDate &date, QLocale::FormatType format) const
482{
483 if (!date.isValid()) {
484 return tr("Invalid date", "used when a relative date string can't be generated because the date is invalid");
485 }
486
487 const qint64 daysTo = QDate::currentDate().daysTo(date);
488 if (daysTo > 2 || daysTo < -2) {
489 return m_locale.toString(date, format);
490 }
491
492 switch (daysTo) {
493 case 2:
494 return tr("In two days");
495 case 1:
496 return tr("Tomorrow");
497 case 0:
498 return tr("Today");
499 case -1:
500 return tr("Yesterday");
501 case -2:
502 return tr("Two days ago");
503 }
504 Q_UNREACHABLE();
505}
506
507QString KFormatPrivate::formatRelativeDateTime(const QDateTime &dateTime, QLocale::FormatType format) const
508{
509 const QDateTime now = QDateTime::currentDateTime();
510
511 const auto secsToNow = dateTime.secsTo(now);
512 constexpr int secsInAHour = 60 * 60;
513 if (secsToNow >= 0 && secsToNow < secsInAHour) {
514 const int minutesToNow = secsToNow / 60;
515 if (minutesToNow <= 1) {
516 return tr("Just now");
517 } else {
518 //: @item:intext %1 is a whole number
519 //~ singular %n minute ago
520 //~ plural %n minutes ago
521 return tr("%n minute(s) ago", nullptr, minutesToNow);
522 }
523 }
524
525 const auto timeFormatType = format == QLocale::FormatType::LongFormat ? QLocale::FormatType::ShortFormat : format;
526 const qint64 daysToNow = dateTime.daysTo(now);
527 QString dateString;
528 if (daysToNow < 2 && daysToNow > -2) {
529 dateString = formatRelativeDate(dateTime.date(), format);
530 } else {
531 dateString = m_locale.toString(dateTime.date(), format);
532 }
533
534 /*: relative datetime with %1 result of QLocale.toString(date, format) or formatRelativeDate
535 and %2 result of QLocale.toString(time, timeformatType)
536 If this does not fit the grammar of your language please contact the i18n team to solve the problem */
537 QString formattedDate = tr("%1 at %2").arg(dateString, m_locale.toString(dateTime.time(), timeFormatType));
538
539 return formattedDate.replace(0, 1, formattedDate.at(0).toUpper());
540}
541

source code of kcoreaddons/src/lib/util/kformatprivate.cpp