diff --git a/autotests/kformattest.h b/autotests/kformattest.h --- a/autotests/kformattest.h +++ b/autotests/kformattest.h @@ -37,6 +37,7 @@ void formatDecimalDuration(); void formatSpelloutDuration(); void formatRelativeDate(); + void formatValue(); }; #endif // KFORMATTEST_H diff --git a/autotests/kformattest.cpp b/autotests/kformattest.cpp --- a/autotests/kformattest.cpp +++ b/autotests/kformattest.cpp @@ -94,6 +94,28 @@ QCOMPARE(format.formatByteSize(1000, 1, KFormat::JEDECBinaryDialect, KFormat::UnitKiloByte), QStringLiteral("1.0 KB")); } +void KFormatTest::formatValue() +{ + QLocale locale(QLocale::c()); + locale.setNumberOptions(QLocale::DefaultNumberOptions); // Qt >= 5.6 sets QLocale::OmitGroupSeparator for the C locale + KFormat format(locale); + + // Check examples from the documentation + QCOMPARE(format.formatValue(1000, KFormat::Unit::Byte, 1, KFormat::UnitPrefix::Kilo, KFormat::MetricBinaryDialect), QStringLiteral("1.0 kB")); + QCOMPARE(format.formatValue(1000, KFormat::Unit::Byte, 1, KFormat::UnitPrefix::Kilo, KFormat::IECBinaryDialect), QStringLiteral("1.0 KiB")); + QCOMPARE(format.formatValue(1000, KFormat::Unit::Byte, 1, KFormat::UnitPrefix::Kilo, KFormat::JEDECBinaryDialect), QStringLiteral("1.0 KB")); + + // Check examples from the documentation + QCOMPARE(format.formatValue(1000, KFormat::Unit::Bit, 1, KFormat::UnitPrefix::Kilo, KFormat::MetricBinaryDialect), QStringLiteral("1.0 kbit")); + QCOMPARE(format.formatValue(1000, QStringLiteral("bit"), 1, KFormat::UnitPrefix::Kilo), QStringLiteral("1.0 kbit")); + QCOMPARE(format.formatValue(1000, QStringLiteral("bit/s"), 1, KFormat::UnitPrefix::Kilo), QStringLiteral("1.0 kbit/s")); + + QCOMPARE(format.formatValue(100, QStringLiteral("bit/s")), QStringLiteral("100.0 bit/s")); + QCOMPARE(format.formatValue(1000, QStringLiteral("bit/s")), QStringLiteral("1.0 kbit/s")); + QCOMPARE(format.formatValue(10e3, QStringLiteral("bit/s")), QStringLiteral("10.0 kbit/s")); + QCOMPARE(format.formatValue(10e6, QStringLiteral("bit/s")), QStringLiteral("10.0 Mbit/s")); +} + enum TimeConstants { MSecsInDay = 86400000, MSecsInHour = 3600000, diff --git a/src/lib/util/kformat.h b/src/lib/util/kformat.h --- a/src/lib/util/kformat.h +++ b/src/lib/util/kformat.h @@ -110,6 +110,58 @@ UnitLastUnit = UnitYottaByte }; + /** + * These units are used in KDE by the formatValue() function. + * + * @see formatValue + * @since 5.48 + */ + enum class Unit { + Other, + Bit, ///< "bit" + Byte, ///< "B" + Meter, ///< "m" + Hertz, ///< "Hz" + }; + + /** + * These prefixes are used in KDE by the formatValue() + * function. + * + * IEC prefixes are only defined for integral units of information, e.g. + * bits and bytes. + * + * @see BinarySizeUnits + * @see formatValue + * @since 5.48 + */ + enum class UnitPrefix { + /// Auto-choose a unit such that the result is in the range [0, 1000 or 1024) + AutoAdjust = -128, + + Yocto = 0, ///< --/-/y 10^-24 + Zepto, ///< --/-/z 10^-21 + Atto, ///< --/-/a 10^-18 + Femto, ///< --/-/f 10^-15 + Pico, ///< --/-/p 10^-12 + Nano, ///< --/-/n 10^-9 + Micro, ///< --/-/µ 10^-6 + Mili, ///< --/-/m 10^-3 + Centi, ///< --/-/c 0.01 + Deci, ///< --/-/d 0.1 + Unity, ///< "" 1 + Deca, ///< --/-/da 10 + Hecto, ///< --/-/h 100 + Kilo, ///< Ki/K/k 1024/1000 + Mega, ///< Mi/M/M 2^20/10^06 + Giga, ///< Gi/G/G 2^30/10^09 + Tera, ///< Ti/T/T 2^40/10^12 + Peta, ///< Pi/P/P 2^50/10^15 + Exa, ///< Ei/E/E 2^60/10^18 + Zetta, ///< Zi/Z/Z 2^70/10^21 + Yotta, ///< Yi/Y/Y 2^80/10^24 + }; + /** * This enum chooses what dialect is used for binary units. * @@ -288,6 +340,66 @@ QString formatRelativeDateTime(const QDateTime &dateTime, QLocale::FormatType format) const; + /** + * Converts @p value to the appropriate string representation + * + * Example: + * @code + * // sets formatted to "1.0 kbit" + * auto formatted = format.formatValue(1000, KFormat::Unit::Bit, 1, KFormat::UnitPrefix::Kilo); + * @endcode + * + * @param value value to be formatted + * @param precision number of places after the decimal point to use. KDE uses + * 1 by default so when in doubt use 1. + * @param unit unit to use in result. + * @param prefix specific prefix to use in result. Use UnitPrefix::AutoAdjust + * to automatically select an appropriate prefix. + * @param dialect prefix standard to use. Use DefaultBinaryDialect to + * use the localized user selection unless you need to use a specific + * unit type. Only meaningful for KFormat::Unit::Byte, and ignored for + * all other units. + * @return converted size as a translated string including prefix and unit. + * E.g. "1.23 KiB", "2 GB" (JEDEC), "4.2 kB" (Metric), "1.2 kbit". + * @see Unit + * @see UnitPrefix + * @see BinaryUnitDialect + * @since 5.48 + */ + QString formatValue(double value, + KFormat::Unit unit, + int precision = 1, + KFormat::UnitPrefix prefix = KFormat::UnitPrefix::AutoAdjust, + KFormat::BinaryUnitDialect dialect = KFormat::DefaultBinaryDialect) const; + + /** + * Converts @p value to the appropriate string representation + * + * Example: + * @code + * QString bits, slow, fast; + * // sets bits to "1.0 kbit", slow to "1.0 kbit/s" and fast to "12.3 Mbit/s". + * bits = format.formatValue(1000, QStringLiteral("bit"), 1, KFormat::UnitPrefix::Kilo); + * slow = format.formatValue(1000, QStringLiteral("bit/s"); + * fast = format.formatValue(12.3e6, QStringLiteral("bit/s"); + * @endcode + * + * @param value value to be formatted + * @param precision number of places after the decimal point to use. KDE uses + * 1 by default so when in doubt use 1. + * @param unit unit to use in result. + * @param prefix specific prefix to use in result. Use UnitPrefix::AutoAdjust + * to automatically select an appropriate prefix. + * @return converted size as a translated string including prefix and unit. + * E.g. "1.2 kbit", "2.4 kB", "12.3 Mbit/s" + * @see UnitPrefix + * @since 5.48 + */ + QString formatValue(double value, + const QString& unit, + int precision = 1, + KFormat::UnitPrefix prefix = KFormat::UnitPrefix::AutoAdjust) const; + private: QSharedDataPointer d; }; diff --git a/src/lib/util/kformat.cpp b/src/lib/util/kformat.cpp --- a/src/lib/util/kformat.cpp +++ b/src/lib/util/kformat.cpp @@ -52,6 +52,23 @@ return d->formatByteSize(size, precision, dialect, units); } +QString KFormat::formatValue(double value, + KFormat::Unit unit, + int precision, + KFormat::UnitPrefix prefix, + KFormat::BinaryUnitDialect dialect) const +{ + return d->formatValue(value, unit, QString(), precision, prefix, dialect); +} + +QString KFormat::formatValue(double value, + const QString& unit, + int precision, + KFormat::UnitPrefix prefix) const +{ + return d->formatValue(value, KFormat::Unit::Other, unit, precision, prefix, MetricBinaryDialect); +} + QString KFormat::formatDuration(quint64 msecs, KFormat::DurationFormatOptions options) const { diff --git a/src/lib/util/kformatprivate.cpp b/src/lib/util/kformatprivate.cpp --- a/src/lib/util/kformatprivate.cpp +++ b/src/lib/util/kformatprivate.cpp @@ -37,6 +37,144 @@ { } +constexpr double bpow(int exp) { + return (exp > 0) ? 2.0 * bpow(exp - 1) : + (exp < 0) ? 0.5 * bpow(exp + 1) : + 1.0; +} + +QString KFormatPrivate::formatValue(double value, + KFormat::Unit unit, + QString unitString, + int precision, + KFormat::UnitPrefix prefix, + KFormat::BinaryUnitDialect dialect) const +{ + if (dialect <= KFormat::DefaultBinaryDialect || dialect > KFormat::LastBinaryDialect) { + dialect = KFormat::IECBinaryDialect; + } + + if (static_cast(prefix) < static_cast(KFormat::UnitPrefix::Yocto) || + static_cast(prefix) > static_cast(KFormat::UnitPrefix::Yotta)) { + prefix = KFormat::UnitPrefix::AutoAdjust; + } + + double multiplier = 1024.0; + if (dialect == KFormat::MetricBinaryDialect) { + multiplier = 1000.0; + } + + int power = 0; + if (prefix == KFormat::UnitPrefix::AutoAdjust) { + double adjustValue = qAbs(value); + while (adjustValue >= multiplier) { + adjustValue /= multiplier; + power += 1; + } + while (adjustValue && adjustValue < 1.0) { + adjustValue *= multiplier; + power -= 1; + } + const KFormat::UnitPrefix map[] = { + KFormat::UnitPrefix::Yocto, // -8 + KFormat::UnitPrefix::Zepto, + KFormat::UnitPrefix::Atto, + KFormat::UnitPrefix::Femto, + KFormat::UnitPrefix::Pico, + KFormat::UnitPrefix::Nano, + KFormat::UnitPrefix::Micro, + KFormat::UnitPrefix::Mili, + KFormat::UnitPrefix::Unity, // 0 + KFormat::UnitPrefix::Kilo, + KFormat::UnitPrefix::Mega, + KFormat::UnitPrefix::Giga, + KFormat::UnitPrefix::Tera, + KFormat::UnitPrefix::Peta, + KFormat::UnitPrefix::Exa, + KFormat::UnitPrefix::Zetta, + KFormat::UnitPrefix::Yotta, // 8 + }; + power = std::max(-8, std::min(8, power)); + prefix = map[power + 8]; + } + + if (prefix == KFormat::UnitPrefix::Unity && + unit == KFormat::Unit::Byte) { + precision = 0; + } + + struct PrefixMapEntry + { + KFormat::UnitPrefix prefix; + double decimalFactor; + double binaryFactor; + QChar prefixChar; + }; + + const PrefixMapEntry map[] = { + { KFormat::UnitPrefix::Yocto, 1e-24, bpow(-80), u'y' }, + { KFormat::UnitPrefix::Zepto, 1e-21, bpow(-70), u'z' }, + { KFormat::UnitPrefix::Atto, 1e-18, bpow(-60), u'a' }, + { KFormat::UnitPrefix::Femto, 1e-15, bpow(-50), u'f' }, + { KFormat::UnitPrefix::Pico, 1e-12, bpow(-40), u'p' }, + { KFormat::UnitPrefix::Nano, 1e-9, bpow(-30), u'n' }, + { KFormat::UnitPrefix::Micro, 1e-6, bpow(-20), u'µ' }, + { KFormat::UnitPrefix::Mili, 1e-3, bpow(-10), u'm' }, + { KFormat::UnitPrefix::Unity, 1.0, 1.0, u'\0' }, + { KFormat::UnitPrefix::Kilo, 1e3, bpow(10), u'k' }, + { KFormat::UnitPrefix::Mega, 1e6, bpow(20), u'M' }, + { KFormat::UnitPrefix::Giga, 1e9, bpow(30), u'G' }, + { KFormat::UnitPrefix::Tera, 1e12, bpow(40), u'T' }, + { KFormat::UnitPrefix::Peta, 1e15, bpow(50), u'P' }, + { KFormat::UnitPrefix::Exa, 1e18, bpow(60), u'E' }, + { KFormat::UnitPrefix::Zetta, 1e21, bpow(70), u'Z' }, + { KFormat::UnitPrefix::Yotta, 1e24, bpow(80), u'Y' }, + }; + + auto entry = std::find_if(std::begin(map), std::end(map), + [prefix](const PrefixMapEntry& e) { return e.prefix == prefix; }); + + switch (unit) { + case KFormat::Unit::Bit: + unitString = QStringLiteral("bit"); + break; + case KFormat::Unit::Byte: + unitString = QStringLiteral("B"); + break; + case KFormat::Unit::Meter: + unitString = QStringLiteral("m"); + break; + case KFormat::Unit::Hertz: + unitString = QStringLiteral("Hz"); + break; + case KFormat::Unit::Other: + break; + } + + if (prefix == KFormat::UnitPrefix::Unity) { + QString numString = m_locale.toString(value, 'f', precision); + //: value without prefix, format " " + return tr("%1 %2", "no Prefix").arg(numString, unitString); + } + + QString prefixString; + if (dialect == KFormat::MetricBinaryDialect) { + value /= entry->decimalFactor; + prefixString = entry->prefixChar; + } else { + value /= entry->binaryFactor; + prefixString = QString(entry->prefixChar).toUpper(); + if (dialect == KFormat::IECBinaryDialect) { + prefixString += u'i'; + } + } + + QString numString = m_locale.toString(value, 'f', precision); + + //: value with prefix, format " " + return tr("%1 %2%3", "MetricBinaryDialect").arg(numString, prefixString, unitString); +} + QString KFormatPrivate::formatByteSize(double size, int precision, KFormat::BinaryUnitDialect dialect, KFormat::BinarySizeUnits units) const { diff --git a/src/lib/util/kformatprivate_p.h b/src/lib/util/kformatprivate_p.h --- a/src/lib/util/kformatprivate_p.h +++ b/src/lib/util/kformatprivate_p.h @@ -43,6 +43,13 @@ KFormat::BinaryUnitDialect dialect, KFormat::BinarySizeUnits units) const; + QString formatValue(double value, + KFormat::Unit unit, + QString unitString, + int precision, + KFormat::UnitPrefix prefix, + KFormat::BinaryUnitDialect dialect) const; + QString formatDuration(quint64 msecs, KFormat::DurationFormatOptions options) const;