| 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 | |