From a1d481495178227a635c8a41956b7ceef87c07fb Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Mon, 22 Jun 2026 22:03:00 +0200 Subject: [PATCH 01/57] Bump version to 2.0.0 --- README.md | 2 +- build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 112d7cc..9d4a11e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The library is published to Maven Central. ```kotlin dependencies { - implementation("nl.jacobras:Human-Readable:1.13.0") + implementation("nl.jacobras:Human-Readable:2.0.0") // Not yet published! } ``` diff --git a/build.gradle.kts b/build.gradle.kts index 72bb3c3..eb5b604 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { } group = "nl.jacobras" -version = "1.13.0" +version = "2.0.0" mavenPublishing { publishToMavenCentral() From d43cbe7bee4a3fb287f89d47afb624c98f15f220 Mon Sep 17 00:00:00 2001 From: jacobras Date: Sat, 20 Jun 2026 17:14:08 +0200 Subject: [PATCH 02/57] Custom i18n impl Fixes #139 --- .github/workflows/build.yml | 2 - README.md | 33 +++--- build.gradle.kts | 19 +-- demo/src/webMain/kotlin/App.kt | 8 +- gradle/libs.versions.toml | 1 + .../humanreadable/extendLibresPlurals.kt | 87 -------------- .../i18n/SystemLanguageTag.apple.kt | 7 ++ .../jacobras/humanreadable/HumanReadable.kt | 64 +++++++--- .../humanreadable/HumanReadableDistance.kt | 10 +- .../humanreadable/HumanReadableDuration.kt | 10 +- .../humanreadable/HumanReadableFileSize.kt | 14 +-- .../humanreadable/HumanReadableNumber.kt | 13 +- .../HumanReadableRelativeTime.kt | 8 +- .../jacobras/humanreadable/SafelyTranslate.kt | 25 ---- .../nl/jacobras/humanreadable/TimeUnit.kt | 66 ++++------- .../humanreadable/extendLibresPlurals.kt | 6 - .../i18n/HumanReadableStrings.kt | 81 +++++++++++++ .../humanreadable/i18n/Localisation.kt | 59 +++++++++ .../nl/jacobras/humanreadable/i18n/Plural.kt | 15 +++ .../humanreadable/i18n/SystemLanguageTag.kt | 6 + .../jacobras/humanreadable/i18n/TenseForms.kt | 112 ++++++++++++++++++ .../humanreadable/i18n/translations.kt | 52 ++++++++ .../i18n/translations/ArStrings.kt | 67 +++++++++++ .../i18n/translations/CsStrings.kt | 54 +++++++++ .../i18n/translations/DeStrings.kt | 34 ++++++ .../i18n/translations/ElStrings.kt | 24 ++++ .../i18n/translations/EnStrings.kt | 25 ++++ .../i18n/translations/EsStrings.kt | 24 ++++ .../i18n/translations/FiStrings.kt | 32 +++++ .../i18n/translations/FrStrings.kt | 32 +++++ .../i18n/translations/IdStrings.kt | 24 ++++ .../i18n/translations/ItStrings.kt | 24 ++++ .../i18n/translations/JaStrings.kt | 22 ++++ .../i18n/translations/KkStrings.kt | 22 ++++ .../i18n/translations/KoStrings.kt | 22 ++++ .../i18n/translations/NlStrings.kt | 24 ++++ .../i18n/translations/PlStrings.kt | 40 +++++++ .../i18n/translations/PtStrings.kt | 30 +++++ .../i18n/translations/RuStrings.kt | 40 +++++++ .../i18n/translations/TrStrings.kt | 24 ++++ .../i18n/translations/UkStrings.kt | 30 +++++ .../i18n/translations/UzStrings.kt | 24 ++++ .../i18n/translations/ViStrings.kt | 24 ++++ .../i18n/translations/ZhStrings.kt | 22 ++++ .../libres/strings/data_units_ar.xml | 8 -- .../libres/strings/data_units_en.xml | 8 -- .../libres/strings/data_units_fi.xml | 8 -- .../libres/strings/data_units_fr.xml | 8 -- .../libres/strings/distance_units_ar.xml | 7 -- .../libres/strings/distance_units_en.xml | 7 -- .../libres/strings/number_units_cs.xml | 5 - .../libres/strings/number_units_de.xml | 5 - .../libres/strings/number_units_el.xml | 5 - .../libres/strings/number_units_en.xml | 5 - .../libres/strings/number_units_es.xml | 5 - .../libres/strings/number_units_fi.xml | 5 - .../libres/strings/number_units_fr.xml | 5 - .../libres/strings/number_units_id.xml | 5 - .../libres/strings/number_units_it.xml | 5 - .../libres/strings/number_units_nl.xml | 5 - .../libres/strings/number_units_pl.xml | 5 - .../libres/strings/number_units_pt.xml | 5 - .../libres/strings/number_units_ru.xml | 5 - .../libres/strings/number_units_tr.xml | 5 - .../libres/strings/number_units_uk.xml | 5 - .../libres/strings/number_units_uz.xml | 5 - .../libres/strings/number_units_vi.xml | 5 - .../libres/strings/time_units_ar.xml | 111 ----------------- .../libres/strings/time_units_cs.xml | 92 -------------- .../libres/strings/time_units_de.xml | 59 --------- .../libres/strings/time_units_el.xml | 37 ------ .../libres/strings/time_units_en.xml | 87 -------------- .../libres/strings/time_units_es.xml | 35 ------ .../libres/strings/time_units_fi.xml | 35 ------ .../libres/strings/time_units_fr.xml | 35 ------ .../libres/strings/time_units_id.xml | 28 ----- .../libres/strings/time_units_it.xml | 35 ------ .../libres/strings/time_units_ja.xml | 28 ----- .../libres/strings/time_units_kk.xml | 69 ----------- .../libres/strings/time_units_ko.xml | 28 ----- .../libres/strings/time_units_nl.xml | 35 ------ .../libres/strings/time_units_pl.xml | 66 ----------- .../libres/strings/time_units_pt.xml | 42 ------- .../libres/strings/time_units_ru.xml | 66 ----------- .../libres/strings/time_units_tr.xml | 35 ------ .../libres/strings/time_units_uk.xml | 42 ------- .../libres/strings/time_units_uz.xml | 35 ------ .../libres/strings/time_units_vi.xml | 28 ----- .../libres/strings/time_units_zh.xml | 28 ----- .../HumanReadableAbbreviationTests.kt | 3 +- .../HumanReadableDurationTests.kt | 3 +- .../HumanReadableFileSizeTests.kt | 3 +- .../HumanReadableNumberEdgeCaseTests.kt | 3 +- .../HumanReadableRelativeTimeTests.kt | 3 +- .../humanreadable/localized/LocalizedTests.kt | 45 ++++--- .../humanreadable/extendLibresPlurals.kt | 5 - .../i18n/SystemLanguageTag.jvm.kt | 5 + .../humanreadable/i18n/LocalisationTest.kt | 59 +++++++++ .../i18n/SystemLanguageTag.web.kt | 7 ++ 99 files changed, 1214 insertions(+), 1471 deletions(-) delete mode 100644 src/appleAndJsMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt create mode 100644 src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.apple.kt delete mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/SafelyTranslate.kt delete mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Plural.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/TenseForms.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt delete mode 100644 src/commonMain/libres/strings/data_units_ar.xml delete mode 100644 src/commonMain/libres/strings/data_units_en.xml delete mode 100644 src/commonMain/libres/strings/data_units_fi.xml delete mode 100644 src/commonMain/libres/strings/data_units_fr.xml delete mode 100644 src/commonMain/libres/strings/distance_units_ar.xml delete mode 100644 src/commonMain/libres/strings/distance_units_en.xml delete mode 100644 src/commonMain/libres/strings/number_units_cs.xml delete mode 100644 src/commonMain/libres/strings/number_units_de.xml delete mode 100644 src/commonMain/libres/strings/number_units_el.xml delete mode 100644 src/commonMain/libres/strings/number_units_en.xml delete mode 100644 src/commonMain/libres/strings/number_units_es.xml delete mode 100644 src/commonMain/libres/strings/number_units_fi.xml delete mode 100644 src/commonMain/libres/strings/number_units_fr.xml delete mode 100644 src/commonMain/libres/strings/number_units_id.xml delete mode 100644 src/commonMain/libres/strings/number_units_it.xml delete mode 100644 src/commonMain/libres/strings/number_units_nl.xml delete mode 100644 src/commonMain/libres/strings/number_units_pl.xml delete mode 100644 src/commonMain/libres/strings/number_units_pt.xml delete mode 100644 src/commonMain/libres/strings/number_units_ru.xml delete mode 100644 src/commonMain/libres/strings/number_units_tr.xml delete mode 100644 src/commonMain/libres/strings/number_units_uk.xml delete mode 100644 src/commonMain/libres/strings/number_units_uz.xml delete mode 100644 src/commonMain/libres/strings/number_units_vi.xml delete mode 100644 src/commonMain/libres/strings/time_units_ar.xml delete mode 100644 src/commonMain/libres/strings/time_units_cs.xml delete mode 100644 src/commonMain/libres/strings/time_units_de.xml delete mode 100644 src/commonMain/libres/strings/time_units_el.xml delete mode 100644 src/commonMain/libres/strings/time_units_en.xml delete mode 100644 src/commonMain/libres/strings/time_units_es.xml delete mode 100644 src/commonMain/libres/strings/time_units_fi.xml delete mode 100644 src/commonMain/libres/strings/time_units_fr.xml delete mode 100644 src/commonMain/libres/strings/time_units_id.xml delete mode 100644 src/commonMain/libres/strings/time_units_it.xml delete mode 100644 src/commonMain/libres/strings/time_units_ja.xml delete mode 100644 src/commonMain/libres/strings/time_units_kk.xml delete mode 100644 src/commonMain/libres/strings/time_units_ko.xml delete mode 100644 src/commonMain/libres/strings/time_units_nl.xml delete mode 100644 src/commonMain/libres/strings/time_units_pl.xml delete mode 100644 src/commonMain/libres/strings/time_units_pt.xml delete mode 100644 src/commonMain/libres/strings/time_units_ru.xml delete mode 100644 src/commonMain/libres/strings/time_units_tr.xml delete mode 100644 src/commonMain/libres/strings/time_units_uk.xml delete mode 100644 src/commonMain/libres/strings/time_units_uz.xml delete mode 100644 src/commonMain/libres/strings/time_units_vi.xml delete mode 100644 src/commonMain/libres/strings/time_units_zh.xml delete mode 100644 src/jvmMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt create mode 100644 src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.jvm.kt create mode 100644 src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt create mode 100644 src/webMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.web.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2aaf05c..8323a67 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,8 +38,6 @@ jobs: require_tests: true report_paths: "**/build/test-results/**/TEST-*.xml" - # compileProductionExecutableKotlinJs - - name: Build production JS file for size comparison run: ./gradlew compileProductionExecutableKotlinJs diff --git a/README.md b/README.md index 9d4a11e..1724416 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ HumanReadable.duration(7.days) // "1 week" HumanReadable.duration(544.hours) // "3 weeks" ``` -**Note**: The formatter switches to a bigger unit (minute, hour, day, ...) as soon as it can. See [Precision](#datetime-precision). +**Note**: The formatter switches to a bigger unit (minute, hour, day, ...) as soon as it can. +See [Precision](#datetime-precision). ### 📂 File size @@ -54,7 +55,7 @@ HumanReadable.fileSize(21_947_282_882, decimals = 2) // "20.44 GB" in English / ### 🔢 Number abbreviation -Available since version 1.8, localized since 1.10. +Available since version 1.8, localised since 1.10. ```kotlin HumanReadable.abbreviation(3_000) // "3K" @@ -93,10 +94,11 @@ HumanReadable.distance(value = 5350, unit = DistanceUnit.Foot) // "1.0 mi" HumanReadable.distance(value = 28512, unit = DistanceUnit.Foot, decimals = 2) // "5.40 mi" ``` -**Note:** numbers in meters and feet are always formatted with 0 decimals. The passed in +**Note:** numbers in meters and feet are always formatted with zero decimals. The passed in number of decimals is only used for the larger units kilometers and miles. ## Date/time precision + The formatter switches to a bigger unit (minute, hour, day, ...) as soon as it can. For example: @@ -112,29 +114,29 @@ This behaviour may become configurable in future releases. ## Localisation -The library uses the small [Libres](https://github.com/Skeptick/libres) library for its string resources. It detects the -current locale by default, but it's changeable on runtime. -See [Libres: Changing Localization](https://github.com/Skeptick/libres/blob/master/docs/LOCALIZATION.md#changing-localization). - -You don't need to manually import Libres, as Gradle already pulls it in along with HumanReadable. +The library uses an internal i18n mechanism. It detects the current locale by default, but it's changeable at +runtime via `HumanReadable.languageTag`: ```kotlin HumanReadable.timeAgo(instant) // "3 days ago" -LibresSettings.languageCode = "nl" +HumanReadable.languageTag = "nl" HumanReadable.timeAgo(instant) // "3 dagen geleden" -LibresSettings.languageCode = "fr" +HumanReadable.languageTag = "fr" HumanReadable.timeAgo(instant) // "il y a 3 jours" ``` +If the requested locale is not supported, the library will fall back to `HumanReadable.fallbackLanguageTag`, which by +default is set to English. + ### Supported languages * Arabic (since 1.13.0) * Czech * Chinese (since 1.3.0) * Dutch -* English (**default**) +* **English** * Finnish (since 1.7.0) * French * German @@ -157,7 +159,10 @@ HumanReadable.timeAgo(instant) // "il y a 3 jours" Missing a language? Feel free to open an issue about it. Or, add it yourself: -1. Fork the code and navigate to [src/commonMain/libres/strings/](https://github.com/jacobras/Human-Readable/tree/main/src/commonMain/libres/strings) -2. Add a file named `time_units_[LANGUAGE CODE].xml` (see [Unicode: CLDR chart](https://www.unicode.org/cldr/charts/42/supplemental/language_plural_rules.html) for the code & plural categories). -3. If the language deviates from English data units (like French does), also add `data_units_[LANGUAGE CODE].xml`. +1. Fork the code and navigate to `src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations` +2. Add a file named `XxStrings.kt` (where `Xx` is + the [language code](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html)). Follow the + example of other translations. +3. Add a new entry to the `translations` map in + `src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations.kt`. 4. Open a PR. \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index eb5b604..1397973 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,6 @@ plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.compose.compiler) apply false id("com.vanniktech.maven.publish") version "0.37.0" - id("io.github.skeptick.libres") version "1.2.4" signing } @@ -86,21 +85,15 @@ kotlin { } sourceSets { - val commonMain by getting { - dependencies { - implementation(libs.kotlinX.datetime) - } + commonMain.dependencies { + implementation(libs.kotlinX.datetime) } commonTest.dependencies { implementation(kotlin("test")) implementation(libs.assertK) } - val wasmJsMain by getting - val appleAndJsMain by creating { - dependsOn(commonMain) - appleMain.get().dependsOn(this) - jsMain.get().dependsOn(this) - wasmJsMain.dependsOn(this) + webMain.dependencies { + implementation(libs.kotlinX.browser) } } } @@ -111,10 +104,6 @@ tasks.withType().configureEach { mustRunAfter(signingTasks) } -libres { - generatedClassName = "HumanReadableRes" -} - signing { setRequired { !gradle.taskGraph.allTasks.any { it is PublishToMavenLocal } diff --git a/demo/src/webMain/kotlin/App.kt b/demo/src/webMain/kotlin/App.kt index 6ea7c2b..1a0baaf 100644 --- a/demo/src/webMain/kotlin/App.kt +++ b/demo/src/webMain/kotlin/App.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import io.github.skeptick.libres.LibresSettings +import nl.jacobras.humanreadable.HumanReadable import ui.AbbreviationDemo import ui.DistanceDemo import ui.FileSizeDemo @@ -43,7 +43,7 @@ import ui.TimeDemo @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable internal fun App() { - var selectedLanguageCode by remember { mutableStateOf(LibresSettings.languageCode ?: "en") } + var selectedLanguageCode by remember { mutableStateOf(HumanReadable.languageTag) } val layoutDirection = if (selectedLanguageCode == "ar") { LayoutDirection.Rtl } else { @@ -52,7 +52,7 @@ internal fun App() { fun onSelectLanguage(code: String) { selectedLanguageCode = code - LibresSettings.languageCode = code + HumanReadable.languageTag = code } MaterialTheme { @@ -74,7 +74,7 @@ internal fun App() { ) { Text( text = buildAnnotatedString { - append("LibresSettings.languageCode = \"") + append("HumanReadable.languageTag = \"") withStyle(monoBodyStringBold) { append(selectedLanguageCode) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7345a8f..c7bdda2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ compose-plugin = "1.12.0-alpha02" [libraries] assertK = { module = "com.willowtreeapps.assertk:assertk", version = "0.28.1" } +kotlinX-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version = "0.5.0" } kotlinX-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.8.0" } [plugins] diff --git a/src/appleAndJsMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt b/src/appleAndJsMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt deleted file mode 100644 index 732d740..0000000 --- a/src/appleAndJsMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt +++ /dev/null @@ -1,87 +0,0 @@ -package nl.jacobras.humanreadable - -import io.github.skeptick.libres.strings.PluralForm -import io.github.skeptick.libres.strings.PluralRule -import io.github.skeptick.libres.strings.PluralRules - -/** - * Only used by Apple & JS. - */ -internal actual fun extendLibresPlurals() { - // Make all Libres plurals default to the English implementation - // Workaround until https://github.com/Skeptick/libres/issues/53 is fixed - PluralRules["nl"] = PluralRules["en"] - PluralRules["de"] = PluralRules["en"] - PluralRules["es"] = PluralRules["en"] - PluralRules["it"] = PluralRules["en"] - PluralRules["tr"] = PluralRules["en"] - PluralRules["el"] = PluralRules["en"] - - // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged - PluralRules["cs"] = PluralRule { number -> - when (number) { - 1 -> PluralForm.One - in (2..4) -> PluralForm.Few - else -> PluralForm.Other - } - } - - // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged - PluralRules["id"] = PluralRule { PluralForm.Other } - - // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged - PluralRules["pl"] = PluralRule { number -> - when { - number == 1 -> PluralForm.One - (number % 10) in 2..4 && (number % 100) !in 12..14 -> PluralForm.Few - else -> PluralForm.Many - } - } - - // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged - PluralRules["zh"] = PluralRule { PluralForm.Other } - - // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged - PluralRules["uz"] = PluralRule { number -> - when (number) { - 1 -> PluralForm.One - else -> PluralForm.Other - } - } - - // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged - PluralRules["ja"] = PluralRule { PluralForm.Other } - - // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged - PluralRules["ko"] = PluralRule { PluralForm.Other } - - // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged - PluralRules["vi"] = PluralRule { PluralForm.Other } - - // TODO: Remove when https://github.com/Skeptick/libres/pull/56 is merged - PluralRules["fi"] = PluralRule { number -> - when (number) { - 1 -> PluralForm.One - else -> PluralForm.Other - } - } - - PluralRules["pt"] = PluralRule { number -> - when { - number == 0 || number == 1 -> PluralForm.One - number % 1_000_000 == 0 -> PluralForm.Many - else -> PluralForm.Other - } - } - - PluralRules["ar"] = PluralRule { number -> - when { - number == 0 -> PluralForm.Zero - number == 1 -> PluralForm.One - number == 2 -> PluralForm.Two - number % 100 in 3..10 -> PluralForm.Few - number % 100 in 11..99 -> PluralForm.Many - else -> PluralForm.Other - } - } -} \ No newline at end of file diff --git a/src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.apple.kt b/src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.apple.kt new file mode 100644 index 0000000..0834531 --- /dev/null +++ b/src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.apple.kt @@ -0,0 +1,7 @@ +package nl.jacobras.humanreadable.i18n + +import platform.Foundation.NSLocale +import platform.Foundation.currentLocale +import platform.Foundation.languageCode + +internal actual fun systemLanguageTag(): String = NSLocale.currentLocale.languageCode \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 89a8777..7157d8c 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -3,7 +3,11 @@ package nl.jacobras.humanreadable import nl.jacobras.humanreadable.HumanReadable.duration +import nl.jacobras.humanreadable.HumanReadable.fallbackLanguageTag +import nl.jacobras.humanreadable.HumanReadable.languageTag import nl.jacobras.humanreadable.HumanReadable.number +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.Localisation import kotlin.time.Clock import kotlin.time.Duration import kotlin.time.ExperimentalTime @@ -14,13 +18,35 @@ import kotlin.time.Instant */ public object HumanReadable { - init { - extendLibresPlurals() - } + internal val localisation = Localisation() + internal val strings: HumanReadableStrings + get() = localisation.currentStrings + + /** + * The language tag (e.g. `"en"`, `"fr"`) to use when formatting. Defaults to the detected system + * language when it is supported, otherwise [fallbackLanguageTag]. + */ + public var languageTag: String + get() = localisation.languageTag + set(value) { + localisation.requestedLanguageTag = value + } + + /** + * The fallback language tag (e.g. `"en"`, `"fr"`) to use when [languageTag] is not supported. + * If an unsupported language is passed in, nothing changes. + * + * Defaults to English. + */ + public var fallbackLanguageTag: String + get() = localisation.fallbackLanguageTag + set(value) { + localisation.fallbackLanguageTag = value + } /** * Returns the difference between now and [instant], in human-readable format. Also supports - * instants in the future. For example: an instant that's 5 hours ago will return "5 hours ago". + * instants in the future. For example, an instant that's 5 hours ago will return "5 hours ago". * * @param instant The [Instant] to format. * @return a formatted string @@ -30,32 +56,32 @@ public object HumanReadable { instant: Instant, baseInstant: Instant = Clock.System.now() ): String { - return safelyTranslate { formatTimeAgo(instant, baseInstant) } + return formatTimeAgo(instant, baseInstant) } /** * Returns the given [duration] in human-readable format. - * For example: a duration of 3 seconds returns "3 seconds". + * For example, a duration of 3 seconds returns "3 seconds". * * @param duration The [Duration] to format. * @return a formatted string */ public fun duration(duration: Duration): String { - return safelyTranslate { formatDuration(duration, RelativeTime.Present) } + return formatDuration(duration, RelativeTime.Present) } /** - * Returns the given [bytes] size in human-readable format. For example: + * Returns the given [bytes] size in human-readable format. For example, * a size of 3_500_000 bytes returns "3.5 MB". Assumes base 1024. * - * For example, 3_5000_000 bytes returns: "3.5 MB" for EN or "3.5 Mo" for FR. + * For example, `3_5000_000` returns: "3.5 MB" for EN or "3.5 Mo" for FR. * * @param bytes The size in bytes to format. * @param decimals The number of decimals to use in formatting. * @return a formatted string */ public fun fileSize(bytes: Long, decimals: Int = 1): String { - return safelyTranslate { formatFileSize(bytes, decimals) } + return formatFileSize(bytes, decimals) } /** @@ -63,20 +89,20 @@ public object HumanReadable { * * Supported abbreviations: K (1,000), M (1,000,000), B (1,000,000,000) and T (1,000,000,000,000). * - * For example: 10394 returns "10K" and "4234321" returns "4M". + * For example, `10394` returns "10K" and `4234321` returns "4M". * * @param number The number to abbreviate. * @param decimals The number of decimals to use in formatting. * @return a formatted string */ public fun abbreviation(number: Number, decimals: Int = 0): String { - return safelyTranslate { formatAbbreviation(number.toDouble(), decimals) } + return formatAbbreviation(number.toDouble(), decimals) } /** * Formats the given [number]. * - * For example: 1_000_000.34 returns: + * For example, `1_000_000.34` returns: * - "1,000,000.34" for EN * - "1 000 000.34" for FR * - "1.000.000,34" for NL @@ -86,19 +112,19 @@ public object HumanReadable { * @return a formatted string */ public fun number(number: Number, decimals: Int = 0): String { - return safelyTranslate { number.toDouble().formatNumber(decimals) } + return number.toDouble().formatNumber(decimals) } /** * Formats the given [value] of the given [unit] to a readable distance. * * Metric examples: - * - 956 with DistanceUnit METERS returns "956 m" for EN. - * - 1534 with DistanceUnit METERS returns "1,5 km" for EN. + * - `956` with DistanceUnit METERS returns "956 m" for EN. + * - `1534` with DistanceUnit METERS returns "1,5 km" for EN. * * Imperial examples: - * - 5200 with DistanceUnit FEET returns "5,200 ft" for EN. - * - 5350 returns with DistanceUnit FEET returns "1.0 mi" for EN. + * - `5200` with DistanceUnit FEET returns "5,200 ft" for EN. + * - `5350` returns with DistanceUnit FEET returns "1.0 mi" for EN. * * In other languages, the numbers are formatted accordingly, see [number]. * @@ -111,6 +137,6 @@ public object HumanReadable { * @return a formatted string */ public fun distance(value: Number, unit: DistanceUnit, decimals: Int = 1): String { - return safelyTranslate { formatDistance(value, unit, decimalsForLargeUnits = decimals) } + return formatDistance(value, unit, decimalsForLargeUnits = decimals) } } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDistance.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDistance.kt index 375bdc4..8cc3ebc 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDistance.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDistance.kt @@ -1,6 +1,6 @@ package nl.jacobras.humanreadable -import HumanReadableRes as Res +import nl.jacobras.humanreadable.HumanReadable.strings /** * Returns the given distance [value] in human-readable format. @@ -24,20 +24,20 @@ internal fun formatDistance( private fun toMetric(meters: Double, decimalsForLargeUnits: Int): String { return if (meters < 1000) { // Use meters - meters.formatNumber(decimals = 0) + " " + Res.string.meter_abbreviation + meters.formatNumber(decimals = 0) + " " + strings.distance.meterAbbreviation } else { // Convert to kilometers - (meters / 1000).formatNumber(decimals = decimalsForLargeUnits) + " " + Res.string.kilometer_abbreviation + (meters / 1000).formatNumber(decimals = decimalsForLargeUnits) + " " + strings.distance.kilometerAbbreviation } } private fun toImperial(feet: Double, decimalsForLargeUnits: Int): String { return if (feet < MILE_IN_FEET) { // Less than a mile, use feet - feet.formatNumber(decimals = 0) + " " + Res.string.feet_abbreviation + feet.formatNumber(decimals = 0) + " " + strings.distance.feetAbbreviation } else { // Convert to miles - (feet / MILE_IN_FEET).formatNumber(decimals = decimalsForLargeUnits) + " " + Res.string.mile_abbreviation + (feet / MILE_IN_FEET).formatNumber(decimals = decimalsForLargeUnits) + " " + strings.distance.mileAbbreviation } } diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt index 9f38aab..56f8733 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt @@ -1,6 +1,5 @@ package nl.jacobras.humanreadable -import io.github.skeptick.libres.LibresSettings import kotlin.math.roundToInt import kotlin.time.Duration @@ -45,8 +44,11 @@ internal fun formatDuration( } /** - * Formats a [count] with its [unit]. Normally produces "$count $unit", but for Arabic the - * singular (1) and dual (2) forms are encoded in the unit word itself, so the numeral is omitted. + * Formats a [count] with its [unit]. + * + * Note about Arabic: normally this produces "$count $unit", but for Arabic the + * singular (1) and dual (2) forms are encoded in the unit word itself, so the + * numeral is omitted. */ private fun formatUnit( count: Int, @@ -54,7 +56,7 @@ private fun formatUnit( relativeTime: RelativeTime ): String { val unitText = unit.format(count, relativeTime) - return if (LibresSettings.languageCode == "ar" && (count == 1 || count == 2)) { + return if (HumanReadable.localisation.languageTag == "ar" && (count == 1 || count == 2)) { unitText } else { "$count $unitText" diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableFileSize.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableFileSize.kt index 24ebe15..b72b0d8 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableFileSize.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableFileSize.kt @@ -1,6 +1,6 @@ package nl.jacobras.humanreadable -import HumanReadableRes as Res +import nl.jacobras.humanreadable.HumanReadable.strings /** * Returns the given [bytes] size in human-readable format. @@ -8,19 +8,19 @@ import HumanReadableRes as Res internal fun formatFileSize(bytes: Long, decimals: Int): String { return when { bytes < 1024 -> { - "$bytes ${Res.string.byte_symbol}" + "$bytes ${strings.fileSize.byteSymbol}" } bytes < 1_048_576 -> { - "${(bytes / 1_024.0).formatNumber(decimals)} ${Res.string.kilobyte_symbol}" + "${(bytes / 1_024.0).formatNumber(decimals)} ${strings.fileSize.kilobyteSymbol}" } bytes < 1.07374182E9 -> { - "${(bytes / 1_048_576.0).formatNumber(decimals)} ${Res.string.megabyte_symbol}" + "${(bytes / 1_048_576.0).formatNumber(decimals)} ${strings.fileSize.megabyteSymbol}" } bytes < 1.09951163E12 -> { - "${(bytes / 1.07374182E9).formatNumber(decimals)} ${Res.string.gigabyte_symbol}" + "${(bytes / 1.07374182E9).formatNumber(decimals)} ${strings.fileSize.gigabyteSymbol}" } else -> { - "${(bytes / 1.09951163E12).formatNumber(decimals)} ${Res.string.terabyte_symbol}" + "${(bytes / 1.09951163E12).formatNumber(decimals)} ${strings.fileSize.terabyteSymbol}" } } -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt index ea759e8..81b1240 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt @@ -1,14 +1,14 @@ package nl.jacobras.humanreadable -import HumanReadableRes as Res +import nl.jacobras.humanreadable.HumanReadable.strings import kotlin.math.pow import kotlin.math.roundToLong internal fun Double.formatNumber( decimals: Int ): String { - val groupSeparator = Res.string.groupSeparator.formatWithSpaceIfNeeded() - val decimalSymbol = Res.string.decimalSymbol + val groupSeparator = strings.number.groupSeparator + val decimalSymbol = strings.number.decimalSymbol val rounded = formatWithDecimals(decimals) val parts = rounded.split('.') @@ -52,11 +52,4 @@ private fun Double.formatWithDecimals(decimals: Int): String { } else { "$mainRes.$fractionRes" } -} - -/** - * Workaround for Libres returning an empty string if it contains only a space. - */ -private fun String.formatWithSpaceIfNeeded(): String { - return ifEmpty { " " } } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTime.kt index 819e95b..56c5a4e 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTime.kt @@ -1,8 +1,8 @@ package nl.jacobras.humanreadable +import nl.jacobras.humanreadable.HumanReadable.strings import kotlin.time.ExperimentalTime import kotlin.time.Instant -import HumanReadableRes as Res /** * Returns the difference between [baseInstant] and [instant], in human-readable format. @@ -20,11 +20,11 @@ internal fun formatTimeAgo( val secondsAgo = diff.inWholeSeconds return when { - secondsAgo < 0 -> Res.string.time_in_future.format( + secondsAgo < 0 -> strings.dateTime.timeInFuture( formatDuration(diff.absoluteValue, relativeTime = RelativeTime.Future) ) - secondsAgo <= 1 -> Res.string.now - else -> Res.string.time_ago.format( + secondsAgo <= 1 -> strings.dateTime.now + else -> strings.dateTime.timeAgo( formatDuration(diff.absoluteValue, relativeTime = RelativeTime.Past) ) } diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/SafelyTranslate.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/SafelyTranslate.kt deleted file mode 100644 index 945b79c..0000000 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/SafelyTranslate.kt +++ /dev/null @@ -1,25 +0,0 @@ -package nl.jacobras.humanreadable - -import io.github.skeptick.libres.strings.VoidPluralString - -/** - * Prevents crashing consumer app on a translation issue. - */ -internal fun safelyTranslate(block: () -> String): String { - return try { - block() - } catch (e: IllegalStateException) { - "Translation error: ${e.message ?: "IllegalStateException"}" - } -} - -/** - * @return `null` if the given resource doesn't exist in the current locale. - */ -internal fun VoidPluralString.optionallyFormat(number: Int): String? { - return try { - format(number).takeIf { it.isNotEmpty() } - } catch (e: IllegalStateException) { - return null - } -} \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/TimeUnit.kt index 60f29fa..a80064c 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/TimeUnit.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/TimeUnit.kt @@ -1,54 +1,32 @@ package nl.jacobras.humanreadable -import io.github.skeptick.libres.strings.VoidPluralString -import HumanReadableRes as Res +import nl.jacobras.humanreadable.HumanReadable.localisation +import nl.jacobras.humanreadable.HumanReadable.strings +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.TenseForms internal enum class TimeUnit( - val past: () -> VoidPluralString, - val present: () -> VoidPluralString, - val future: () -> VoidPluralString + val forms: (DateTimeStrings) -> TenseForms ) { - Seconds( - past = { Res.string.seconds_past }, - present = { Res.string.seconds }, - future = { Res.string.seconds_future } - ), - Minutes( - past = { Res.string.minutes_past }, - present = { Res.string.minutes }, - future = { Res.string.minutes_future } - ), - Hours( - past = { Res.string.hours_past }, - present = { Res.string.hours }, - future = { Res.string.hours_future } - ), - Days( - past = { Res.string.days_past }, - present = { Res.string.days }, - future = { Res.string.days_future } - ), - Weeks( - past = { Res.string.weeks_past }, - present = { Res.string.weeks }, - future = { Res.string.weeks_future } - ), - Months( - past = { Res.string.months_past }, - present = { Res.string.months }, - future = { Res.string.months_future } - ), - Years( - past = { Res.string.years_past }, - present = { Res.string.years }, - future = { Res.string.years_future } - ); + Seconds({ it.seconds }), + Minutes({ it.minutes }), + Hours({ it.hours }), + Days({ it.days }), + Weeks({ it.weeks }), + Months({ it.months }), + Years({ it.years }); fun format(value: Int, relativeTime: RelativeTime): String { - return when (relativeTime) { - RelativeTime.Past -> past().optionallyFormat(value) ?: present().format(value) - RelativeTime.Present -> present().format(value) - RelativeTime.Future -> future().optionallyFormat(value) ?: present().format(value) + val dateTimeStrings = strings.dateTime + val tenseForms = forms(dateTimeStrings) + val pluralCategory = dateTimeStrings.plural(value) + val correctTense = when (relativeTime) { + RelativeTime.Past -> tenseForms.past + RelativeTime.Future -> tenseForms.future + RelativeTime.Present -> tenseForms.present } + return correctTense[pluralCategory] + ?: tenseForms.present[pluralCategory] + ?: error("No translation for $value $this in '${localisation.languageTag}'") } } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt deleted file mode 100644 index d006f43..0000000 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt +++ /dev/null @@ -1,6 +0,0 @@ -package nl.jacobras.humanreadable - -/** - * Workaround until https://github.com/Skeptick/libres/issues/53 is fixed. - */ -internal expect fun extendLibresPlurals() \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt new file mode 100644 index 0000000..d78a032 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt @@ -0,0 +1,81 @@ +package nl.jacobras.humanreadable.i18n + +/** + * All localised strings for a single language, grouped per type. + */ +internal data class HumanReadableStrings( + val dateTime: DateTimeStrings, + val number: NumberStrings = NumberStrings(), + val fileSize: FileSizeStrings = FileSizeStrings(), + val distance: DistanceStrings = DistanceStrings() +) + +/** + * Relative-time and duration strings. + */ +internal data class DateTimeStrings( + /** + * Resolves a count to its plural category for this language. + * + * See [Unicode Plural Rules](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html). + */ + val plural: (Int) -> Plural, + val seconds: TenseForms, + val minutes: TenseForms, + val hours: TenseForms, + val days: TenseForms, + val weeks: TenseForms, + val months: TenseForms, + val years: TenseForms, + + /** + * Wraps a formatted duration as past relative time, e.g. "3 days ago". + */ + val timeAgo: (String) -> String, + + /** + * Wraps a formatted duration as future relative time, e.g. "in 3 days". + */ + val timeInFuture: (String) -> String, + + val now: String +) + +/** + * Number-formatting symbols. + * + * Only override what differs from the default values. + */ +internal data class NumberStrings( + /** + * Separator used for grouping thousands. + */ + val groupSeparator: String = ",", + + val decimalSymbol: String = "." +) + +/** + * File-size unit symbols. + * + * Only override what differs from the default values. + */ +internal data class FileSizeStrings( + val byteSymbol: String = "B", + val kilobyteSymbol: String = "kB", + val megabyteSymbol: String = "MB", + val gigabyteSymbol: String = "GB", + val terabyteSymbol: String = "TB" +) + +/** + * Distance unit abbreviations. + * + * Only override what differs from the default values. + */ +internal data class DistanceStrings( + val meterAbbreviation: String = "m", + val kilometerAbbreviation: String = "km", + val feetAbbreviation: String = "ft", + val mileAbbreviation: String = "mi" +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt new file mode 100644 index 0000000..bebf9c9 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt @@ -0,0 +1,59 @@ +package nl.jacobras.humanreadable.i18n + +/** + * Holds language settings and a reference to the current translation strings. + */ +internal class Localisation { + + private var currentTagAndStrings: Pair? = null + + /** + * The requested language tag (e.g. `"en"`, `"fr"`). + * + * @see languageTag for the actual language tag used, which may differ if the requested one is not supported. + */ + var requestedLanguageTag = systemLanguageTag() + set(value) { + field = value + currentTagAndStrings = null + } + + /** + * The fallback language tag to use if [requestedLanguageTag] is not supported. + * + * If this field is changed to a language tag that is not supported, nothing changes. + */ + var fallbackLanguageTag = "en" + set(value) { + if (translations.containsKey(value)) { + field = value + currentTagAndStrings = null + } + } + + /** + * The currently used language tag (e.g. `"en"`, `"fr"`). + */ + val languageTag: String + get() { + val (tag, _) = resolve() + return tag + } + + /** + * The current set of translation strings. + */ + val currentStrings: HumanReadableStrings + get() { + val (_, strings) = resolve() + return strings + } + + private fun resolve(): Pair { + return currentTagAndStrings ?: run { + val tag = requestedLanguageTag.takeIf { it in translations } ?: fallbackLanguageTag + val strings = translations[tag] ?: error("Internal error: unsupported language tag $tag") + (tag to strings).also { currentTagAndStrings = it } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Plural.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Plural.kt new file mode 100644 index 0000000..d6b69e6 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Plural.kt @@ -0,0 +1,15 @@ +package nl.jacobras.humanreadable.i18n + +/** + * CLDR plural categories. + * + * See [Unicode Plural Rules](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html). + */ +internal enum class Plural { + Zero, + One, + Two, + Few, + Many, + Other +} \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.kt new file mode 100644 index 0000000..fad8ee9 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.kt @@ -0,0 +1,6 @@ +package nl.jacobras.humanreadable.i18n + +/** + * Returns the current platform language tag (e.g. "en", "fr"). + */ +internal expect fun systemLanguageTag(): String \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/TenseForms.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/TenseForms.kt new file mode 100644 index 0000000..93cf6ca --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/TenseForms.kt @@ -0,0 +1,112 @@ +@file:Suppress("LongParameterList") + +package nl.jacobras.humanreadable.i18n + +/** + * The word forms for a single unit (e.g. "minute") in a single language. + * + * @property present The word forms for the present tense. E.g. "minute" for [Plural.One] + * and "minutes" for [Plural.Other]. + * @property past The word forms for the past tense, if they differ from [present]. + * @property future The word forms for the future tense, if they differ from [present]. + */ +internal data class TenseForms( + val present: Map, + val past: Map = emptyMap(), + val future: Map = emptyMap() +) + +/** + * Builds a map of plural category -> word entries, ignoring empty ones. + */ +private fun buildMap( + zero: String = "", + one: String = "", + two: String = "", + few: String = "", + many: String = "", + other: String = "" +): Map = buildMap { + if (zero.isNotEmpty()) put(Plural.Zero, zero) + if (one.isNotEmpty()) put(Plural.One, one) + if (two.isNotEmpty()) put(Plural.Two, two) + if (few.isNotEmpty()) put(Plural.Few, few) + if (many.isNotEmpty()) put(Plural.Many, many) + if (other.isNotEmpty()) put(Plural.Other, other) +} + +/** + * Shortcut for languages that don't need past/future grammatical-case overrides. + * + * Use [multipleTenses] when past/future grammatical-case overrides are needed. + */ +internal fun presentTense( + zero: String = "", + one: String = "", + two: String = "", + few: String = "", + many: String = "", + other: String = "" +): TenseForms = TenseForms(present = buildMap(zero, one, two, few, many, other)) + +/** + * Builds [TenseForms] for a unit that needs past/future grammatical-case overrides. For + * present-only units prefer [presentTense]. + */ +internal fun multipleTenses(block: TenseFormsBuilder.() -> Unit): TenseForms { + return TenseFormsBuilder().apply(block).build() +} + +/** DSL for building [TenseForms] with present and optional past/future overrides. */ +@DslMarker +private annotation class TenseFormsDsl + +/** + * Builder for [TenseForms]. + * + * - Use [present] to set the base forms. + * - Use [past]/[future] to override the past/future forms. + * + * For languages where past/future are identical (but different from present), use [pastOrFuture]. + */ +@TenseFormsDsl +internal class TenseFormsBuilder { + private var present: Map = emptyMap() + private var past: Map = emptyMap() + private var future: Map = emptyMap() + + fun present( + zero: String = "", one: String = "", two: String = "", + few: String = "", many: String = "", other: String = "" + ) { + present = buildMap(zero, one, two, few, many, other) + } + + fun past( + zero: String = "", one: String = "", two: String = "", + few: String = "", many: String = "", other: String = "" + ) { + past = buildMap(zero, one, two, few, many, other) + } + + fun future( + zero: String = "", one: String = "", two: String = "", + few: String = "", many: String = "", other: String = "" + ) { + future = buildMap(zero, one, two, few, many, other) + } + + /** + * Sets both [past] and [future] to the same forms, for languages where they are identical.\ + */ + fun pastOrFuture( + zero: String = "", one: String = "", two: String = "", + few: String = "", many: String = "", other: String = "" + ) { + val forms = buildMap(zero, one, two, few, many, other) + past = forms + future = forms + } + + fun build(): TenseForms = TenseForms(present = present, past = past, future = future) +} \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations.kt new file mode 100644 index 0000000..38c50cc --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations.kt @@ -0,0 +1,52 @@ +package nl.jacobras.humanreadable.i18n + +import nl.jacobras.humanreadable.i18n.translations.ArStrings +import nl.jacobras.humanreadable.i18n.translations.CsStrings +import nl.jacobras.humanreadable.i18n.translations.DeStrings +import nl.jacobras.humanreadable.i18n.translations.ElStrings +import nl.jacobras.humanreadable.i18n.translations.EnStrings +import nl.jacobras.humanreadable.i18n.translations.EsStrings +import nl.jacobras.humanreadable.i18n.translations.FiStrings +import nl.jacobras.humanreadable.i18n.translations.FrStrings +import nl.jacobras.humanreadable.i18n.translations.IdStrings +import nl.jacobras.humanreadable.i18n.translations.ItStrings +import nl.jacobras.humanreadable.i18n.translations.JaStrings +import nl.jacobras.humanreadable.i18n.translations.KkStrings +import nl.jacobras.humanreadable.i18n.translations.KoStrings +import nl.jacobras.humanreadable.i18n.translations.NlStrings +import nl.jacobras.humanreadable.i18n.translations.PlStrings +import nl.jacobras.humanreadable.i18n.translations.PtStrings +import nl.jacobras.humanreadable.i18n.translations.RuStrings +import nl.jacobras.humanreadable.i18n.translations.TrStrings +import nl.jacobras.humanreadable.i18n.translations.UkStrings +import nl.jacobras.humanreadable.i18n.translations.UzStrings +import nl.jacobras.humanreadable.i18n.translations.ViStrings +import nl.jacobras.humanreadable.i18n.translations.ZhStrings + +/** + * All supported translations. Make sure to add a new entry here if you add a new language. + */ +internal val translations: Map = mapOf( + "ar" to ArStrings, + "cs" to CsStrings, + "de" to DeStrings, + "el" to ElStrings, + "en" to EnStrings, + "es" to EsStrings, + "fi" to FiStrings, + "fr" to FrStrings, + "id" to IdStrings, + "it" to ItStrings, + "ja" to JaStrings, + "kk" to KkStrings, + "ko" to KoStrings, + "nl" to NlStrings, + "pl" to PlStrings, + "pt" to PtStrings, + "ru" to RuStrings, + "tr" to TrStrings, + "uk" to UkStrings, + "uz" to UzStrings, + "vi" to ViStrings, + "zh" to ZhStrings +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt new file mode 100644 index 0000000..08b8c5f --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt @@ -0,0 +1,67 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.multipleTenses +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.DistanceStrings +import nl.jacobras.humanreadable.i18n.FileSizeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings + +internal val ArStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> + when { + count == 0 -> Plural.Zero + count == 1 -> Plural.One + count == 2 -> Plural.Two + count % 100 in 3..10 -> Plural.Few + count % 100 in 11..99 -> Plural.Many + else -> Plural.Other + } + }, + seconds = multipleTenses { + present(zero = "ثانية", one = "ثانية", two = "ثانيتان", few = "ثوان", many = "ثانية", other = "ثانية") + pastOrFuture(two = "ثانيتين") + }, + minutes = multipleTenses { + present(zero = "دقيقة", one = "دقيقة", two = "دقيقتان", few = "دقائق", many = "دقيقة", other = "دقيقة") + pastOrFuture(two = "دقيقتين") + }, + hours = multipleTenses { + present(zero = "ساعة", one = "ساعة", two = "ساعتان", few = "ساعات", many = "ساعة", other = "ساعة") + pastOrFuture(two = "ساعتين") + }, + days = multipleTenses { + present(zero = "يوم", one = "يوم", two = "يومان", few = "أيام", many = "يوم", other = "يوم") + pastOrFuture(two = "يومين") + }, + weeks = multipleTenses { + present(zero = "أسبوع", one = "أسبوع", two = "أسبوعان", few = "أسابيع", many = "أسبوع", other = "أسبوع") + pastOrFuture(two = "أسبوعين") + }, + months = multipleTenses { + present(zero = "شهر", one = "شهر", two = "شهران", few = "أشهر", many = "شهر", other = "شهر") + pastOrFuture(two = "شهرين") + }, + years = multipleTenses { + present(zero = "سنة", one = "سنة", two = "سنتان", few = "سنوات", many = "سنة", other = "سنة") + pastOrFuture(two = "سنتين") + }, + timeAgo = { "قبل $it" }, + timeInFuture = { "بعد $it" }, + now = "الآن" + ), + fileSize = FileSizeStrings( + byteSymbol = "ب", + kilobyteSymbol = "ك.ب", + megabyteSymbol = "م.ب", + gigabyteSymbol = "ج.ب", + terabyteSymbol = "ت.ب" + ), + distance = DistanceStrings( + meterAbbreviation = "م", + kilometerAbbreviation = "كم", + feetAbbreviation = "قدم", + mileAbbreviation = "ميل" + ) +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt new file mode 100644 index 0000000..8adad39 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt @@ -0,0 +1,54 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.multipleTenses +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val CsStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> + when (count) { + 1 -> Plural.One + in 2..4 -> Plural.Few + else -> Plural.Other + } + }, + seconds = multipleTenses { + present(one = "sekunda", few = "sekundy", other = "sekund") + past(one = "sekundou", few = "sekundami", other = "sekundami") + future(one = "sekundu", few = "sekundy", other = "sekund") + }, + minutes = multipleTenses { + present(one = "minuta", few = "minuty", other = "minut") + past(one = "minutou", few = "minutami", other = "minutami") + future(one = "minutu", few = "minuty", other = "minut") + }, + hours = multipleTenses { + present(one = "hodina", few = "hodiny", other = "hodin") + past(one = "hodinou", few = "hodinami", other = "hodinami") + future(one = "hodinu", few = "hodiny", other = "hodin") + }, + days = multipleTenses { + present(one = "den", few = "dny", other = "dní") + past(one = "dnem", few = "dny", other = "dny") + }, + weeks = multipleTenses { + present(one = "týden", few = "týdny", other = "týdnů") + past(one = "týdnem", few = "týdny", other = "týdny") + }, + months = multipleTenses { + present(one = "měsíc", few = "měsíce", other = "měsíců") + past(one = "měsícem", few = "měsíci", other = "měsíci") + }, + years = multipleTenses { + present(one = "rok", few = "roky", other = "let") + past(one = "rokem", few = "roky", other = "lety") + }, + timeAgo = { "před $it" }, + timeInFuture = { "za $it" }, + now = "nyní" + ), + number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt new file mode 100644 index 0000000..171964a --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt @@ -0,0 +1,34 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.multipleTenses +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val DeStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> if (count == 1) Plural.One else Plural.Other }, + seconds = presentTense(one = "Sekunde", other = "Sekunden"), + minutes = presentTense(one = "Minute", other = "Minuten"), + hours = presentTense(one = "Stunde", other = "Stunden"), + days = multipleTenses { + present(one = "Tag", other = "Tage") + pastOrFuture(other = "Tagen") + }, + weeks = presentTense(one = "Woche", other = "Wochen"), + months = multipleTenses { + present(one = "Monat", other = "Monate") + pastOrFuture(other = "Monaten") + }, + years = multipleTenses { + present(one = "Jahr", other = "Jahre") + pastOrFuture(other = "Jahren") + }, + timeAgo = { "vor $it" }, + timeInFuture = { "in $it" }, + now = "jetzt" + ), + number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt new file mode 100644 index 0000000..35e7a52 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt @@ -0,0 +1,24 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val ElStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> if (count == 1) Plural.One else Plural.Other }, + seconds = presentTense(one = "δευτερόλεπτο", other = "δευτερόλεπτα"), + minutes = presentTense(one = "λεπτό", other = "λεπτά"), + hours = presentTense(one = "ώρα", other = "ώρες"), + days = presentTense(one = "μέρα", other = "μέρες"), + weeks = presentTense(one = "εβδομάδα", other = "εβδομάδες"), + months = presentTense(one = "μήνα", other = "μήνες"), + years = presentTense(one = "έτος", other = "έτη"), + timeAgo = { "$it πριν" }, + timeInFuture = { "σε $it" }, + now = "τώρα" + ), + number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt new file mode 100644 index 0000000..039bb29 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt @@ -0,0 +1,25 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings + +/** + * English is the default language that will be used if no better translation is found. + */ +internal val EnStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> if (count == 1) Plural.One else Plural.Other }, + seconds = presentTense(one = "second", other = "seconds"), + minutes = presentTense(one = "minute", other = "minutes"), + hours = presentTense(one = "hour", other = "hours"), + days = presentTense(one = "day", other = "days"), + weeks = presentTense(one = "week", other = "weeks"), + months = presentTense(one = "month", other = "months"), + years = presentTense(one = "year", other = "years"), + timeAgo = { "$it ago" }, + timeInFuture = { "in $it" }, + now = "now" + ) +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt new file mode 100644 index 0000000..d855b25 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt @@ -0,0 +1,24 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val EsStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> if (count == 1) Plural.One else Plural.Other }, + seconds = presentTense(one = "segundo", other = "segundos"), + minutes = presentTense(one = "minuto", other = "minutos"), + hours = presentTense(one = "hora", other = "horas"), + days = presentTense(one = "día", other = "días"), + weeks = presentTense(one = "semana", other = "semanas"), + months = presentTense(one = "mes", other = "meses"), + years = presentTense(one = "año", other = "años"), + timeAgo = { "hace $it" }, + timeInFuture = { "en $it" }, + now = "ahora" + ), + number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt new file mode 100644 index 0000000..49e406c --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt @@ -0,0 +1,32 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.FileSizeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val FiStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> if (count == 1) Plural.One else Plural.Other }, + seconds = presentTense(one = "sekunti", other = "sekuntia"), + minutes = presentTense(one = "minuutti", other = "minuuttia"), + hours = presentTense(one = "tunti", other = "tuntia"), + days = presentTense(one = "päivä", other = "päivää"), + weeks = presentTense(one = "viikko", other = "viikkoa"), + months = presentTense(one = "kuukausi", other = "kuukautta"), + years = presentTense(one = "vuosi", other = "vuotta"), + timeAgo = { "$it sitten" }, + timeInFuture = { "$it tulevaisuudessa" }, + now = "nyt" + ), + number = NumberStrings(groupSeparator = " ", decimalSymbol = ","), + fileSize = FileSizeStrings( + byteSymbol = "t", + kilobyteSymbol = "kt", + megabyteSymbol = "Mt", + gigabyteSymbol = "Gt", + terabyteSymbol = "Tt" + ) +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt new file mode 100644 index 0000000..2459c60 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt @@ -0,0 +1,32 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.FileSizeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val FrStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> if (count == 0 || count == 1) Plural.One else Plural.Other }, + seconds = presentTense(one = "seconde", other = "secondes"), + minutes = presentTense(one = "minute", other = "minutes"), + hours = presentTense(one = "heure", other = "heures"), + days = presentTense(one = "jour", other = "jours"), + weeks = presentTense(one = "semaine", other = "semaines"), + months = presentTense(one = "mois", other = "mois"), + years = presentTense(one = "an", other = "ans"), + timeAgo = { "il y a $it" }, + timeInFuture = { "dans $it" }, + now = "maintenant" + ), + number = NumberStrings(groupSeparator = " ", decimalSymbol = ","), + fileSize = FileSizeStrings( + byteSymbol = "o", + kilobyteSymbol = "ko", + megabyteSymbol = "Mo", + gigabyteSymbol = "Go", + terabyteSymbol = "To" + ) +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt new file mode 100644 index 0000000..f774a2d --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt @@ -0,0 +1,24 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val IdStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { Plural.Other }, + seconds = presentTense(other = "detik"), + minutes = presentTense(other = "menit"), + hours = presentTense(other = "jam"), + days = presentTense(other = "hari"), + weeks = presentTense(other = "minggu"), + months = presentTense(other = "bulan"), + years = presentTense(other = "tahun"), + timeAgo = { "$it yang lalu" }, + timeInFuture = { "dalam $it" }, + now = "sekarang" + ), + number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt new file mode 100644 index 0000000..2590659 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt @@ -0,0 +1,24 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val ItStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> if (count == 1) Plural.One else Plural.Other }, + seconds = presentTense(one = "secondo", other = "secondi"), + minutes = presentTense(one = "minuto", other = "minuti"), + hours = presentTense(one = "ora", other = "ore"), + days = presentTense(one = "giorno", other = "giorni"), + weeks = presentTense(one = "settimana", other = "settimane"), + months = presentTense(one = "mese", other = "mesi"), + years = presentTense(one = "anno", other = "anni"), + timeAgo = { "$it fa" }, + timeInFuture = { "tra $it" }, + now = "adesso" + ), + number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt new file mode 100644 index 0000000..e565608 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt @@ -0,0 +1,22 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings + +internal val JaStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { Plural.Other }, + seconds = presentTense(other = "秒"), + minutes = presentTense(other = "分"), + hours = presentTense(other = "時間"), + days = presentTense(other = "日"), + weeks = presentTense(other = "週"), + months = presentTense(other = "ヶ月"), + years = presentTense(other = "年"), + timeAgo = { "${it}前" }, + timeInFuture = { "${it}後" }, + now = "今" + ) +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt new file mode 100644 index 0000000..573b8b0 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt @@ -0,0 +1,22 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.multipleTenses +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings + +internal val KkStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> if (count == 1) Plural.One else Plural.Other }, + seconds = multipleTenses { present(one = "секунд", other = "секунд"); future(one = "секундтан", other = "секундтан") }, + minutes = multipleTenses { present(one = "минут", other = "минут"); future(one = "минуттан", other = "минуттан") }, + hours = multipleTenses { present(one = "сағат", other = "сағат"); future(one = "сағаттан", other = "сағаттан") }, + days = multipleTenses { present(one = "күн", other = "күн"); future(one = "күннен", other = "күннен") }, + weeks = multipleTenses { present(one = "апта", other = "апта"); future(one = "аптадан", other = "аптадан") }, + months = multipleTenses { present(one = "ай", other = "ай"); future(one = "айдан", other = "айдан") }, + years = multipleTenses { present(one = "жыл", other = "жыл"); future(one = "жылдан", other = "жылдан") }, + timeAgo = { "$it бұрын" }, + timeInFuture = { "$it кейін" }, + now = "қазір" + ) +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt new file mode 100644 index 0000000..5ff304a --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt @@ -0,0 +1,22 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings + +internal val KoStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { Plural.Other }, + seconds = presentTense(other = "초"), + minutes = presentTense(other = "분"), + hours = presentTense(other = "시"), + days = presentTense(other = "일"), + weeks = presentTense(other = "주"), + months = presentTense(other = "월"), + years = presentTense(other = "년"), + timeAgo = { "$it 전" }, + timeInFuture = { "$it 후" }, + now = "지금" + ) +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt new file mode 100644 index 0000000..02c96cd --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt @@ -0,0 +1,24 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val NlStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> if (count == 1) Plural.One else Plural.Other }, + seconds = presentTense(one = "seconde", other = "seconden"), + minutes = presentTense(one = "minuut", other = "minuten"), + hours = presentTense(one = "uur", other = "uur"), + days = presentTense(one = "dag", other = "dagen"), + weeks = presentTense(one = "week", other = "weken"), + months = presentTense(one = "maand", other = "maanden"), + years = presentTense(one = "jaar", other = "jaar"), + timeAgo = { "$it geleden" }, + timeInFuture = { "over $it" }, + now = "nu" + ), + number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt new file mode 100644 index 0000000..0a94a4f --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt @@ -0,0 +1,40 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.multipleTenses +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val PlStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> + when { + count == 1 -> Plural.One + count % 10 in 2..4 && count % 100 !in 12..14 -> Plural.Few + else -> Plural.Many + } + }, + seconds = multipleTenses { + present(one = "sekunda", few = "sekundy", many = "sekund") + pastOrFuture(one = "sekundę") + }, + minutes = multipleTenses { + present(one = "minuta", few = "minuty", many = "minut") + pastOrFuture(one = "minutę") + }, + hours = multipleTenses { + present(one = "godzina", few = "godziny", many = "godzin") + pastOrFuture(one = "godzinę") + }, + days = presentTense(one = "dzień", few = "dni", many = "dni"), + weeks = presentTense(one = "tydzień", few = "tygodnie", many = "tygodni"), + months = presentTense(one = "miesiąc", few = "miesiące", many = "miesięcy"), + years = presentTense(one = "rok", few = "lata", many = "lat"), + timeAgo = { "$it temu" }, + timeInFuture = { "za $it" }, + now = "teraz" + ), + number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt new file mode 100644 index 0000000..da28777 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt @@ -0,0 +1,30 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val PtStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> + when { + count == 0 || count == 1 -> Plural.One + count % 1_000_000 == 0 -> Plural.Many + else -> Plural.Other + } + }, + seconds = presentTense(one = "segundo", many = "de segundos", other = "segundos"), + minutes = presentTense(one = "minuto", many = "de minutos", other = "minutos"), + hours = presentTense(one = "hora", many = "de horas", other = "horas"), + days = presentTense(one = "dia", many = "de dias", other = "dias"), + weeks = presentTense(one = "semana", many = "de semanas", other = "semanas"), + months = presentTense(one = "mês", many = "de meses", other = "meses"), + years = presentTense(one = "ano", many = "de anos", other = "anos"), + timeAgo = { "há $it" }, + timeInFuture = { "em $it" }, + now = "agora" + ), + number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt new file mode 100644 index 0000000..cc30ef3 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt @@ -0,0 +1,40 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.multipleTenses +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val RuStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> + when { + count % 10 == 1 && count % 100 != 11 -> Plural.One + count % 10 in 2..4 && count % 100 !in 12..14 -> Plural.Few + else -> Plural.Many + } + }, + seconds = multipleTenses { + present(one = "секунда", few = "секунды", many = "секунд") + pastOrFuture(one = "секунду") + }, + minutes = multipleTenses { + present(one = "минута", few = "минуты", many = "минут") + pastOrFuture(one = "минуту") + }, + hours = presentTense(one = "час", few = "часа", many = "часов"), + days = presentTense(one = "день", few = "дня", many = "дней"), + weeks = multipleTenses { + present(one = "неделя", few = "недели", many = "недель") + pastOrFuture(one = "неделю") + }, + months = presentTense(one = "месяц", few = "месяца", many = "месяцев"), + years = presentTense(one = "год", few = "года", many = "лет"), + timeAgo = { "$it назад" }, + timeInFuture = { "через $it" }, + now = "сейчас" + ), + number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt new file mode 100644 index 0000000..ec3d2f6 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt @@ -0,0 +1,24 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val TrStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> if (count == 1) Plural.One else Plural.Other }, + seconds = presentTense(other = "saniye"), + minutes = presentTense(other = "dakika"), + hours = presentTense(other = "saat"), + days = presentTense(other = "gün"), + weeks = presentTense(other = "hafta"), + months = presentTense(other = "ay"), + years = presentTense(other = "yıl"), + timeAgo = { "$it önce" }, + timeInFuture = { "$it sonra" }, + now = "şimdi" + ), + number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt new file mode 100644 index 0000000..c9e1c86 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt @@ -0,0 +1,30 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val UkStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> + when { + count % 10 == 1 && count % 100 != 11 -> Plural.One + count % 10 in 2..4 && count % 100 !in 12..14 -> Plural.Few + else -> Plural.Many + } + }, + seconds = presentTense(one = "секунду", few = "секунди", many = "секунд"), + minutes = presentTense(one = "хвилину", few = "хвилини", many = "хвилин"), + hours = presentTense(one = "годину", few = "години", many = "годин"), + days = presentTense(one = "день", few = "дні", many = "днів"), + weeks = presentTense(one = "тиждень", few = "тижні", many = "тижнів"), + months = presentTense(one = "місяць", few = "місяці", many = "місяців"), + years = presentTense(one = "рік", few = "роки", many = "років"), + timeAgo = { "$it тому" }, + timeInFuture = { "через $it" }, + now = "зараз" + ), + number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt new file mode 100644 index 0000000..608a715 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt @@ -0,0 +1,24 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val UzStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { count -> if (count == 1) Plural.One else Plural.Other }, + seconds = presentTense(other = "soniya"), + minutes = presentTense(other = "daqiqa"), + hours = presentTense(other = "soat"), + days = presentTense(other = "kun"), + weeks = presentTense(other = "hafta"), + months = presentTense(other = "oy"), + years = presentTense(other = "yil"), + timeAgo = { "$it oldin" }, + timeInFuture = { "$it keyin" }, + now = "hozir" + ), + number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt new file mode 100644 index 0000000..8913b3c --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt @@ -0,0 +1,24 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.NumberStrings + +internal val ViStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { Plural.Other }, + seconds = presentTense(other = "giây"), + minutes = presentTense(other = "phút"), + hours = presentTense(other = "giờ"), + days = presentTense(other = "ngày"), + weeks = presentTense(other = "tuần"), + months = presentTense(other = "tháng"), + years = presentTense(other = "năm"), + timeAgo = { "$it trước" }, + timeInFuture = { "vào $it" }, + now = "bây giờ" + ), + number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") +) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt new file mode 100644 index 0000000..e71559b --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt @@ -0,0 +1,22 @@ +package nl.jacobras.humanreadable.i18n.translations + +import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.DateTimeStrings +import nl.jacobras.humanreadable.i18n.HumanReadableStrings + +internal val ZhStrings = HumanReadableStrings( + dateTime = DateTimeStrings( + plural = { Plural.Other }, + seconds = presentTense(other = "秒"), + minutes = presentTense(other = "分钟"), + hours = presentTense(other = "小时"), + days = presentTense(other = "天"), + weeks = presentTense(other = "周"), + months = presentTense(other = "个月"), + years = presentTense(other = "年"), + timeAgo = { "${it}之前" }, + timeInFuture = { "${it}之后" }, + now = "现在" + ) +) \ No newline at end of file diff --git a/src/commonMain/libres/strings/data_units_ar.xml b/src/commonMain/libres/strings/data_units_ar.xml deleted file mode 100644 index 5a567c8..0000000 --- a/src/commonMain/libres/strings/data_units_ar.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - ب - ك.ب - م.ب - ج.ب - ت.ب - diff --git a/src/commonMain/libres/strings/data_units_en.xml b/src/commonMain/libres/strings/data_units_en.xml deleted file mode 100644 index 52054d5..0000000 --- a/src/commonMain/libres/strings/data_units_en.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - B - kB - MB - GB - TB - \ No newline at end of file diff --git a/src/commonMain/libres/strings/data_units_fi.xml b/src/commonMain/libres/strings/data_units_fi.xml deleted file mode 100644 index b7f8224..0000000 --- a/src/commonMain/libres/strings/data_units_fi.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - t - kt - Mt - Gt - Tt - \ No newline at end of file diff --git a/src/commonMain/libres/strings/data_units_fr.xml b/src/commonMain/libres/strings/data_units_fr.xml deleted file mode 100644 index d1c3382..0000000 --- a/src/commonMain/libres/strings/data_units_fr.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - o - ko - Mo - Go - To - \ No newline at end of file diff --git a/src/commonMain/libres/strings/distance_units_ar.xml b/src/commonMain/libres/strings/distance_units_ar.xml deleted file mode 100644 index 54999bb..0000000 --- a/src/commonMain/libres/strings/distance_units_ar.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - م - كم - قدم - ميل - diff --git a/src/commonMain/libres/strings/distance_units_en.xml b/src/commonMain/libres/strings/distance_units_en.xml deleted file mode 100644 index 87b95cb..0000000 --- a/src/commonMain/libres/strings/distance_units_en.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - m - km - ft - mi - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_cs.xml b/src/commonMain/libres/strings/number_units_cs.xml deleted file mode 100644 index 4a32de0..0000000 --- a/src/commonMain/libres/strings/number_units_cs.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_de.xml b/src/commonMain/libres/strings/number_units_de.xml deleted file mode 100644 index defdcc7..0000000 --- a/src/commonMain/libres/strings/number_units_de.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - . - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_el.xml b/src/commonMain/libres/strings/number_units_el.xml deleted file mode 100644 index defdcc7..0000000 --- a/src/commonMain/libres/strings/number_units_el.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - . - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_en.xml b/src/commonMain/libres/strings/number_units_en.xml deleted file mode 100644 index 60a9cff..0000000 --- a/src/commonMain/libres/strings/number_units_en.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - , - . - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_es.xml b/src/commonMain/libres/strings/number_units_es.xml deleted file mode 100644 index defdcc7..0000000 --- a/src/commonMain/libres/strings/number_units_es.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - . - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_fi.xml b/src/commonMain/libres/strings/number_units_fi.xml deleted file mode 100644 index 4a32de0..0000000 --- a/src/commonMain/libres/strings/number_units_fi.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_fr.xml b/src/commonMain/libres/strings/number_units_fr.xml deleted file mode 100644 index a2acbea..0000000 --- a/src/commonMain/libres/strings/number_units_fr.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - , - diff --git a/src/commonMain/libres/strings/number_units_id.xml b/src/commonMain/libres/strings/number_units_id.xml deleted file mode 100644 index defdcc7..0000000 --- a/src/commonMain/libres/strings/number_units_id.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - . - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_it.xml b/src/commonMain/libres/strings/number_units_it.xml deleted file mode 100644 index defdcc7..0000000 --- a/src/commonMain/libres/strings/number_units_it.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - . - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_nl.xml b/src/commonMain/libres/strings/number_units_nl.xml deleted file mode 100644 index defdcc7..0000000 --- a/src/commonMain/libres/strings/number_units_nl.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - . - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_pl.xml b/src/commonMain/libres/strings/number_units_pl.xml deleted file mode 100644 index 4a32de0..0000000 --- a/src/commonMain/libres/strings/number_units_pl.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_pt.xml b/src/commonMain/libres/strings/number_units_pt.xml deleted file mode 100644 index 4a32de0..0000000 --- a/src/commonMain/libres/strings/number_units_pt.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_ru.xml b/src/commonMain/libres/strings/number_units_ru.xml deleted file mode 100644 index 4a32de0..0000000 --- a/src/commonMain/libres/strings/number_units_ru.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_tr.xml b/src/commonMain/libres/strings/number_units_tr.xml deleted file mode 100644 index defdcc7..0000000 --- a/src/commonMain/libres/strings/number_units_tr.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - . - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_uk.xml b/src/commonMain/libres/strings/number_units_uk.xml deleted file mode 100644 index 4a32de0..0000000 --- a/src/commonMain/libres/strings/number_units_uk.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_uz.xml b/src/commonMain/libres/strings/number_units_uz.xml deleted file mode 100644 index defdcc7..0000000 --- a/src/commonMain/libres/strings/number_units_uz.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - . - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/number_units_vi.xml b/src/commonMain/libres/strings/number_units_vi.xml deleted file mode 100644 index defdcc7..0000000 --- a/src/commonMain/libres/strings/number_units_vi.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - . - , - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_ar.xml b/src/commonMain/libres/strings/time_units_ar.xml deleted file mode 100644 index b03787e..0000000 --- a/src/commonMain/libres/strings/time_units_ar.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - ثانية - ثانية - ثانيتان - ثوان - ثانية - ثانية - - - ثانيتين - - - ثانيتين - - - - دقيقة - دقيقة - دقيقتان - دقائق - دقيقة - دقيقة - - - دقيقتين - - - دقيقتين - - - - ساعة - ساعة - ساعتان - ساعات - ساعة - ساعة - - - ساعتين - - - ساعتين - - - - يوم - يوم - يومان - أيام - يوم - يوم - - - يومين - - - يومين - - - - أسبوع - أسبوع - أسبوعان - أسابيع - أسبوع - أسبوع - - - أسبوعين - - - أسبوعين - - - - شهر - شهر - شهران - أشهر - شهر - شهر - - - شهرين - - - شهرين - - - - سنة - سنة - سنتان - سنوات - سنة - سنة - - - سنتين - - - سنتين - - - قبل ${time} - بعد ${time} - الآن - diff --git a/src/commonMain/libres/strings/time_units_cs.xml b/src/commonMain/libres/strings/time_units_cs.xml deleted file mode 100644 index 8a90a57..0000000 --- a/src/commonMain/libres/strings/time_units_cs.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - sekunda - sekundy - sekund - - - sekundou - sekundami - sekundami - - - sekundu - sekundy - sekund - - - minuta - minuty - minut - - - minutou - minutami - minutami - - - minutu - minuty - minut - - - hodina - hodiny - hodin - - - hodinou - hodinami - hodinami - - - hodinu - hodiny - hodin - - - den - dny - dní - - - dnem - dny - dny - - - týden - týdny - týdnů - - - týdnem - týdny - týdny - - - měsíc - měsíce - měsíců - - - měsícem - měsíci - měsíci - - - rok - roky - let - - - rokem - roky - lety - - - před ${time} - za ${time} - nyní - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_de.xml b/src/commonMain/libres/strings/time_units_de.xml deleted file mode 100644 index c9bb630..0000000 --- a/src/commonMain/libres/strings/time_units_de.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - Sekunde - Sekunden - - - - Minute - Minuten - - - - Stunde - Stunden - - - - Tag - Tage - - - Tagen - - - Tagen - - - - Woche - Wochen - - - - Monat - Monate - - - Monaten - - - Monaten - - - - Jahr - Jahre - - - Jahren - - - Jahren - - - vor ${time} - in ${time} - jetzt - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_el.xml b/src/commonMain/libres/strings/time_units_el.xml deleted file mode 100644 index 5a71bd2..0000000 --- a/src/commonMain/libres/strings/time_units_el.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - δευτερόλεπτο - δευτερόλεπτα - - - λεπτό - λεπτά - - - ώρα - ώρες - - - μέρα - μέρες - - - εβδομάδα - εβδομάδες - - - μήνα - μήνες - - - έτος - έτη - - ${time} πριν - σε ${time} - τώρα - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_en.xml b/src/commonMain/libres/strings/time_units_en.xml deleted file mode 100644 index 756f7e2..0000000 --- a/src/commonMain/libres/strings/time_units_en.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - second - seconds - - - - - - - - - - minute - minutes - - - - - - - - - - hour - hours - - - - - - - - - - day - days - - - - - - - - - - week - weeks - - - - - - - - - - month - months - - - - - - - - - - year - years - - - - - - - - - ${time} ago - in ${time} - now - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_es.xml b/src/commonMain/libres/strings/time_units_es.xml deleted file mode 100644 index eb909a0..0000000 --- a/src/commonMain/libres/strings/time_units_es.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - segundo - segundos - - - minuto - minutos - - - hora - horas - - - día - días - - - semana - semanas - - - mes - meses - - - año - años - - - hace ${time} - en ${time} - ahora - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_fi.xml b/src/commonMain/libres/strings/time_units_fi.xml deleted file mode 100644 index f6d36b8..0000000 --- a/src/commonMain/libres/strings/time_units_fi.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - sekunti - sekuntia - - - minuutti - minuuttia - - - tunti - tuntia - - - päivä - päivää - - - viikko - viikkoa - - - kuukausi - kuukautta - - - vuosi - vuotta - - - ${time} sitten - ${time} tulevaisuudessa - nyt - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_fr.xml b/src/commonMain/libres/strings/time_units_fr.xml deleted file mode 100644 index 83786e1..0000000 --- a/src/commonMain/libres/strings/time_units_fr.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - seconde - secondes - - - minute - minutes - - - heure - heures - - - jour - jours - - - semaine - semaines - - - mois - mois - - - an - ans - - - il y a ${time} - dans ${time} - maintenant - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_id.xml b/src/commonMain/libres/strings/time_units_id.xml deleted file mode 100644 index 57b0d70..0000000 --- a/src/commonMain/libres/strings/time_units_id.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - detik - - - menit - - - jam - - - hari - - - minggu - - - bulan - - - tahun - - - ${time} yang lalu - dalam ${time} - sekarang - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_it.xml b/src/commonMain/libres/strings/time_units_it.xml deleted file mode 100644 index 778d7ad..0000000 --- a/src/commonMain/libres/strings/time_units_it.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - secondo - secondi - - - minuto - minuti - - - ora - ore - - - giorno - giorni - - - settimana - settimane - - - mese - mesi - - - anno - anni - - - ${time} fa - tra ${time} - adesso - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_ja.xml b/src/commonMain/libres/strings/time_units_ja.xml deleted file mode 100644 index 922a845..0000000 --- a/src/commonMain/libres/strings/time_units_ja.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - 時間 - - - - - - - - - ヶ月 - - - - - - ${time}前 - ${time}後 - - diff --git a/src/commonMain/libres/strings/time_units_kk.xml b/src/commonMain/libres/strings/time_units_kk.xml deleted file mode 100644 index da15910..0000000 --- a/src/commonMain/libres/strings/time_units_kk.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - секунд - секунд - - - секундтан - секундтан - - - - минут - минут - - - минуттан - минуттан - - - - сағат - сағат - - - сағаттан - сағаттан - - - - күн - күн - - - күннен - күннен - - - - апта - апта - - - аптадан - аптадан - - - - ай - ай - - - айдан - айдан - - - - жыл - жыл - - - жылдан - жылдан - - - ${time} бұрын - ${time} кейін - қазір - diff --git a/src/commonMain/libres/strings/time_units_ko.xml b/src/commonMain/libres/strings/time_units_ko.xml deleted file mode 100644 index c413341..0000000 --- a/src/commonMain/libres/strings/time_units_ko.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - ${time} 전 - ${time} 후 - 지금 - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_nl.xml b/src/commonMain/libres/strings/time_units_nl.xml deleted file mode 100644 index d3d0091..0000000 --- a/src/commonMain/libres/strings/time_units_nl.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - seconde - seconden - - - minuut - minuten - - - uur - uur - - - dag - dagen - - - week - weken - - - maand - maanden - - - jaar - jaar - - - ${time} geleden - over ${time} - nu - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_pl.xml b/src/commonMain/libres/strings/time_units_pl.xml deleted file mode 100644 index 7b4cef8..0000000 --- a/src/commonMain/libres/strings/time_units_pl.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - sekunda - sekundy - sekund - - - sekundę - - - sekundę - - - - minuta - minuty - minut - - - minutę - - - minutę - - - - godzina - godziny - godzin - - - godzinę - - - godzinę - - - - dzień - dni - dni - - - - tydzień - tygodnie - tygodni - - - - miesiąc - miesiące - miesięcy - - - - rok - lata - lat - - - ${time} temu - za ${time} - teraz - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_pt.xml b/src/commonMain/libres/strings/time_units_pt.xml deleted file mode 100644 index cb075cd..0000000 --- a/src/commonMain/libres/strings/time_units_pt.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - segundo - de segundos - segundos - - - minuto - de minutos - minutos - - - hora - de horas - horas - - - dia - de dias - dias - - - semana - de semanas - semanas - - - mês - de meses - meses - - - ano - de anos - anos - - - há ${time} - em ${time} - agora - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_ru.xml b/src/commonMain/libres/strings/time_units_ru.xml deleted file mode 100644 index 8e82b2b..0000000 --- a/src/commonMain/libres/strings/time_units_ru.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - секунда - секунды - секунд - - - секунду - - - секунду - - - - минута - минуты - минут - - - минуту - - - минуту - - - - час - часа - часов - - - - день - дня - дней - - - - неделя - недели - недель - - - неделю - - - неделю - - - - месяц - месяца - месяцев - - - - год - года - лет - - - ${time} назад - через ${time} - сейчас - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_tr.xml b/src/commonMain/libres/strings/time_units_tr.xml deleted file mode 100644 index 9906153..0000000 --- a/src/commonMain/libres/strings/time_units_tr.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - saniye - saniye - - - dakika - dakika - - - saat - saat - - - gün - gün - - - hafta - hafta - - - ay - ay - - - yıl - yıl - - - ${time} önce - ${time} sonra - şimdi - \ No newline at end of file diff --git a/src/commonMain/libres/strings/time_units_uk.xml b/src/commonMain/libres/strings/time_units_uk.xml deleted file mode 100644 index 67cd739..0000000 --- a/src/commonMain/libres/strings/time_units_uk.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - секунду - секунди - секунд - - - хвилину - хвилини - хвилин - - - годину - години - годин - - - день - дні - днів - - - тиждень - тижні - тижнів - - - місяць - місяці - місяців - - - рік - роки - років - - - ${time} тому - через ${time} - зараз - diff --git a/src/commonMain/libres/strings/time_units_uz.xml b/src/commonMain/libres/strings/time_units_uz.xml deleted file mode 100644 index 36d680a..0000000 --- a/src/commonMain/libres/strings/time_units_uz.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - soniya - soniya - - - daqiqa - daqiqa - - - soat - soat - - - kun - kun - - - hafta - hafta - - - oy - oy - - - yil - yil - - - ${time} oldin - ${time} keyin - hozir - diff --git a/src/commonMain/libres/strings/time_units_vi.xml b/src/commonMain/libres/strings/time_units_vi.xml deleted file mode 100644 index 66020e0..0000000 --- a/src/commonMain/libres/strings/time_units_vi.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - giây - - - phút - - - giờ - - - ngày - - - tuần - - - tháng - - - năm - - - ${time} trước - vào ${time} - bây giờ - diff --git a/src/commonMain/libres/strings/time_units_zh.xml b/src/commonMain/libres/strings/time_units_zh.xml deleted file mode 100644 index 8031f32..0000000 --- a/src/commonMain/libres/strings/time_units_zh.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - 分钟 - - - 小时 - - - - - - - - - 个月 - - - - - - ${time}之前 - ${time}之后 - 现在 - \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt index ee0a2c2..fdcab9d 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt @@ -2,13 +2,12 @@ package nl.jacobras.humanreadable import assertk.assertThat import assertk.assertions.isEqualTo -import io.github.skeptick.libres.LibresSettings import kotlin.test.Test class HumanReadableAbbreviationTests { init { - LibresSettings.languageCode = "en" + HumanReadable.languageTag = "en" } @Test diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt index bec2beb..2521be3 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt @@ -2,7 +2,6 @@ package nl.jacobras.humanreadable import assertk.assertThat import assertk.assertions.isEqualTo -import io.github.skeptick.libres.LibresSettings import kotlin.test.Test import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours @@ -12,7 +11,7 @@ import kotlin.time.Duration.Companion.seconds class HumanReadableDurationTests { init { - LibresSettings.languageCode = "en" + HumanReadable.languageTag = "en" } @Test diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt index ae5e781..5ec9d71 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt @@ -2,13 +2,12 @@ package nl.jacobras.humanreadable import assertk.assertThat import assertk.assertions.isEqualTo -import io.github.skeptick.libres.LibresSettings import kotlin.test.Test class HumanReadableFileSizeTests { init { - LibresSettings.languageCode = "en" + HumanReadable.languageTag = "en" } @Test diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt index 06b9f18..b70a932 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt @@ -2,12 +2,11 @@ package nl.jacobras.humanreadable import assertk.assertThat import assertk.assertions.isEqualTo -import io.github.skeptick.libres.LibresSettings import kotlin.test.Test class HumanReadableNumberEdgeCaseTests { init { - LibresSettings.languageCode = "en" + HumanReadable.languageTag = "en" } @Test diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt index 946431c..0b9ead2 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt @@ -2,7 +2,6 @@ package nl.jacobras.humanreadable import assertk.assertThat import assertk.assertions.isEqualTo -import io.github.skeptick.libres.LibresSettings import kotlin.test.Test import kotlin.time.Clock import kotlin.time.Duration.Companion.days @@ -14,7 +13,7 @@ import kotlin.time.Instant class HumanReadableRelativeTimeTests { init { - LibresSettings.languageCode = "en" + HumanReadable.languageTag = "en" } private val now: Instant diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalizedTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalizedTests.kt index 70f61b7..f3d1b4b 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalizedTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalizedTests.kt @@ -2,7 +2,6 @@ package nl.jacobras.humanreadable.localized import assertk.assertThat import assertk.assertions.isEqualTo -import io.github.skeptick.libres.LibresSettings import nl.jacobras.humanreadable.DistanceUnit import nl.jacobras.humanreadable.HumanReadable import kotlin.test.Test @@ -49,7 +48,7 @@ class LocalizedTests { @Test fun ar_arabic() { - LibresSettings.languageCode = "ar" + HumanReadable.languageTag = "ar" assertThat(HumanReadable.duration(0.seconds)).isEqualTo("0 ثانية") assertThat(HumanReadable.duration(1.seconds)).isEqualTo("ثانية") assertThat(HumanReadable.duration(2.seconds)).isEqualTo("ثانيتان") @@ -75,7 +74,7 @@ class LocalizedTests { @Test fun cs_czech() { - LibresSettings.languageCode = "cs" + HumanReadable.languageTag = "cs" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 sekundy") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34") @@ -102,7 +101,7 @@ class LocalizedTests { @Test fun de_german() { - LibresSettings.languageCode = "de" + HumanReadable.languageTag = "de" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 Sekunden") assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2 Monate") @@ -115,7 +114,7 @@ class LocalizedTests { @Test fun el_greek() { - LibresSettings.languageCode = "el" + HumanReadable.languageTag = "el" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 δευτερόλεπτα") assertThat(HumanReadable.timeAgo(oneWeekAgo)).isEqualTo("1 εβδομάδα πριν") assertThat(HumanReadable.timeAgo(oneYearFromNow)).isEqualTo("σε 1 έτος") @@ -132,7 +131,7 @@ class LocalizedTests { @Test fun en_english() { - LibresSettings.languageCode = "en" + HumanReadable.languageTag = "en" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 seconds") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") @@ -147,7 +146,7 @@ class LocalizedTests { @Test fun es_spanish() { - LibresSettings.languageCode = "es" + HumanReadable.languageTag = "es" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 segundos") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -156,7 +155,7 @@ class LocalizedTests { @Test fun fi_finnish() { - LibresSettings.languageCode = "fi" + HumanReadable.languageTag = "fi" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 sekuntia") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34") @@ -165,7 +164,7 @@ class LocalizedTests { @Test fun fr_french() { - LibresSettings.languageCode = "fr" + HumanReadable.languageTag = "fr" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 secondes") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34") @@ -176,7 +175,7 @@ class LocalizedTests { @Test fun id_indonesian() { - LibresSettings.languageCode = "id" + HumanReadable.languageTag = "id" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 detik") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -185,7 +184,7 @@ class LocalizedTests { @Test fun it_italian() { - LibresSettings.languageCode = "it" + HumanReadable.languageTag = "it" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 secondi") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -194,7 +193,7 @@ class LocalizedTests { @Test fun ja_japanese() { - LibresSettings.languageCode = "ja" + HumanReadable.languageTag = "ja" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 秒") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") @@ -203,7 +202,7 @@ class LocalizedTests { @Test fun kk_kazakh() { - LibresSettings.languageCode = "kk" + HumanReadable.languageTag = "kk" assertThat(HumanReadable.timeAgo(now)).isEqualTo("қазір") assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 секунд") assertThat(HumanReadable.timeAgo(twoSecondsAgo, baseInstant = now)).isEqualTo("2 секунд бұрын") @@ -230,7 +229,7 @@ class LocalizedTests { @Test fun ko_korean() { - LibresSettings.languageCode = "ko" + HumanReadable.languageTag = "ko" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 초") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") @@ -239,7 +238,7 @@ class LocalizedTests { @Test fun nl_dutch() { - LibresSettings.languageCode = "nl" + HumanReadable.languageTag = "nl" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 seconden") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -254,7 +253,7 @@ class LocalizedTests { @Test fun pl_polish() { - LibresSettings.languageCode = "pl" + HumanReadable.languageTag = "pl" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 sekundy") assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 minuta") @@ -267,7 +266,7 @@ class LocalizedTests { @Test fun pt_portuguese() { - LibresSettings.languageCode = "pt" + HumanReadable.languageTag = "pt" assertThat(HumanReadable.duration(0.seconds)).isEqualTo("0 segundo") assertThat(HumanReadable.duration(1.seconds)).isEqualTo("1 segundo") assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 segundos") @@ -278,7 +277,7 @@ class LocalizedTests { @Test fun ru_russian() { - LibresSettings.languageCode = "ru" + HumanReadable.languageTag = "ru" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 секунды") assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 минута") @@ -291,7 +290,7 @@ class LocalizedTests { @Test fun tr_turkish() { - LibresSettings.languageCode = "tr" + HumanReadable.languageTag = "tr" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 saniye") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -300,7 +299,7 @@ class LocalizedTests { @Test fun uk_ukrainian() { - LibresSettings.languageCode = "uk" + HumanReadable.languageTag = "uk" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 секунди") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34") @@ -309,7 +308,7 @@ class LocalizedTests { @Test fun uz_uzbek() { - LibresSettings.languageCode = "uz" + HumanReadable.languageTag = "uz" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 soniya") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -318,7 +317,7 @@ class LocalizedTests { @Test fun vi_vietnamese() { - LibresSettings.languageCode = "vi" + HumanReadable.languageTag = "vi" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 giây") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -327,7 +326,7 @@ class LocalizedTests { @Test fun zh_chinese() { - LibresSettings.languageCode = "zh" + HumanReadable.languageTag = "zh" assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 秒") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") diff --git a/src/jvmMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt b/src/jvmMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt deleted file mode 100644 index 699cbd0..0000000 --- a/src/jvmMain/kotlin/nl/jacobras/humanreadable/extendLibresPlurals.kt +++ /dev/null @@ -1,5 +0,0 @@ -package nl.jacobras.humanreadable - -internal actual fun extendLibresPlurals() { - // NOOP, only needed for Apple & JS. -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.jvm.kt b/src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.jvm.kt new file mode 100644 index 0000000..d7d65c1 --- /dev/null +++ b/src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.jvm.kt @@ -0,0 +1,5 @@ +package nl.jacobras.humanreadable.i18n + +import java.util.Locale + +internal actual fun systemLanguageTag(): String = Locale.getDefault().language \ No newline at end of file diff --git a/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt b/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt new file mode 100644 index 0000000..d45926e --- /dev/null +++ b/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt @@ -0,0 +1,59 @@ +package nl.jacobras.humanreadable.i18n + +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.jupiter.api.BeforeEach +import java.util.* +import kotlin.test.Test + +class LocalisationTest { + + @BeforeEach + fun setup() { + Locale.setDefault(Locale.ENGLISH) + } + + @Test + fun `languageTag defaults to system language`() { + // Given a system language of French + Locale.setDefault(Locale.FRANCE) + + val localisation = Localisation() + + // Then the language tag should be French + assertThat(localisation.languageTag).isEqualTo("fr") + } + + @Test + fun `languageTag falls back to English (by default) if system language is not supported`() { + // Given a system language of a language that is not supported + Locale.setDefault(Locale.forLanguageTag("xx-XX")) + + val localisation = Localisation() + + // Then the language tag should be English (the default fallback) + assertThat(localisation.languageTag).isEqualTo("en") + } + + @Test + fun `languageTag falls back to a custom fallback if system language is not supported`() { + // Given a system language of a language that is not supported + Locale.setDefault(Locale.forLanguageTag("xx-XX")) + + // and the fallback language tag is set to French + val localisation = Localisation() + localisation.fallbackLanguageTag = "fr" + + // Then the used language tag should be French + assertThat(localisation.languageTag).isEqualTo("fr") + } + + @Test + fun `setting a non-supported fallback language tag changes nothing`() { + val localisation = Localisation() + localisation.fallbackLanguageTag = "xx" + + // Then the used language tag stays English (the default fallback) + assertThat(localisation.languageTag).isEqualTo("en") + } +} \ No newline at end of file diff --git a/src/webMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.web.kt b/src/webMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.web.kt new file mode 100644 index 0000000..a3994f8 --- /dev/null +++ b/src/webMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.web.kt @@ -0,0 +1,7 @@ +package nl.jacobras.humanreadable.i18n + +import kotlinx.browser.window + +private fun navigatorLanguage(): String = window.navigator.language + +internal actual fun systemLanguageTag(): String = navigatorLanguage().substringBefore('-') \ No newline at end of file From 3f1893ae0f97646eb37072394c75f17e220fcd55 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Tue, 23 Jun 2026 18:57:22 +0200 Subject: [PATCH 03/57] Fix setting language tag with region --- .../jacobras/humanreadable/i18n/Localisation.kt | 5 +++-- .../humanreadable/i18n/LocalisationTest.kt | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt index bebf9c9..b46d679 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt @@ -8,13 +8,14 @@ internal class Localisation { private var currentTagAndStrings: Pair? = null /** - * The requested language tag (e.g. `"en"`, `"fr"`). + * The requested language tag (e.g. `"en"`, `"fr"`). A language tag with a region, e.g. `"en-US"`, may also + * be passed in. The region code is ignored. * * @see languageTag for the actual language tag used, which may differ if the requested one is not supported. */ var requestedLanguageTag = systemLanguageTag() set(value) { - field = value + field = value.substringBefore("-") currentTagAndStrings = null } diff --git a/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt b/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt index d45926e..7269615 100644 --- a/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt +++ b/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt @@ -35,6 +35,22 @@ class LocalisationTest { assertThat(localisation.languageTag).isEqualTo("en") } + @Test + fun `setting requested language tag`() { + val localisation = Localisation() + localisation.requestedLanguageTag = "de" + + assertThat(localisation.languageTag).isEqualTo("de") + } + + @Test + fun `setting requested language tag with region`() { + val localisation = Localisation() + localisation.requestedLanguageTag = "fr-FR" + + assertThat(localisation.languageTag).isEqualTo("fr") + } + @Test fun `languageTag falls back to a custom fallback if system language is not supported`() { // Given a system language of a language that is not supported From ba6f27ddfe06641f7f43dea3af44fcaf06e280f0 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Tue, 23 Jun 2026 18:58:40 +0200 Subject: [PATCH 04/57] Fix setting capitalised language tag --- .../kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt | 2 +- .../nl/jacobras/humanreadable/i18n/LocalisationTest.kt | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt index b46d679..6f6fc4c 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt @@ -15,7 +15,7 @@ internal class Localisation { */ var requestedLanguageTag = systemLanguageTag() set(value) { - field = value.substringBefore("-") + field = value.lowercase().substringBefore("-") currentTagAndStrings = null } diff --git a/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt b/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt index 7269615..08b4d13 100644 --- a/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt +++ b/src/jvmTest/kotlin/nl/jacobras/humanreadable/i18n/LocalisationTest.kt @@ -51,6 +51,14 @@ class LocalisationTest { assertThat(localisation.languageTag).isEqualTo("fr") } + @Test + fun `setting capitalised requested language tag`() { + val localisation = Localisation() + localisation.requestedLanguageTag = "DE" + + assertThat(localisation.languageTag).isEqualTo("de") + } + @Test fun `languageTag falls back to a custom fallback if system language is not supported`() { // Given a system language of a language that is not supported From 1048cea9c444d7b45ef0a4449d4a58677f3dbf4a Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Tue, 23 Jun 2026 19:28:06 +0200 Subject: [PATCH 05/57] Add tvOS + watchOS support Fixes #60 --- README.md | 2 ++ build.gradle.kts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index 1724416..5c322a4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ ![Android](http://img.shields.io/badge/-android-6EDB8D.svg?style=flat) ![iOS](http://img.shields.io/badge/-ios-CDCDCD.svg?style=flat) +![tvOS](http://img.shields.io/badge/-tvos-808080.svg?style=flat) +![watchOS](http://img.shields.io/badge/-watchos-D32D41.svg?style=flat) ![JS](http://img.shields.io/badge/-js-F8DB5D.svg?style=flat) ![wasm](https://img.shields.io/badge/-wasm-624DE9.svg?style=flat) diff --git a/build.gradle.kts b/build.gradle.kts index 1397973..71f5dae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,6 +51,11 @@ kotlin { iosX64() iosArm64() iosSimulatorArm64() + tvosArm64() + tvosSimulatorArm64() + watchosArm32() + watchosArm64() + watchosSimulatorArm64() js { browser { testTask { From 995c893e06f0b56cb68459d677ecb78bfceb5d2c Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 24 Jun 2026 20:50:02 +0200 Subject: [PATCH 06/57] Update spelling --- .../localized/{LocalizedTests.kt => LocalisedTests.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/commonTest/kotlin/nl/jacobras/humanreadable/localized/{LocalizedTests.kt => LocalisedTests.kt} (99%) diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalizedTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt similarity index 99% rename from src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalizedTests.kt rename to src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt index f3d1b4b..66f4876 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalizedTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt @@ -16,7 +16,7 @@ import kotlin.time.Instant * Smoke test that verifies all supported languages have working plural formatting. */ @OptIn(ExperimentalTime::class) -class LocalizedTests { +class LocalisedTests { private val now: Instant = Clock.System.now() private val oneSecondFromNow = now + 1.seconds From 71b8d42483d0a9d49c887797e3c061117edab07d Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Tue, 23 Jun 2026 22:31:55 +0200 Subject: [PATCH 07/57] Fix a bunch of translation issues --- .../humanreadable/HumanReadableDuration.kt | 11 +- .../i18n/translations/ElStrings.kt | 6 +- .../i18n/translations/FiStrings.kt | 39 +++-- .../i18n/translations/KkStrings.kt | 15 +- .../i18n/translations/KoStrings.kt | 4 +- .../i18n/translations/UkStrings.kt | 16 +- .../i18n/translations/UzStrings.kt | 37 ++++- .../i18n/translations/ViStrings.kt | 2 +- .../humanreadable/localized/LocalisedTests.kt | 140 ++++++++++++------ 9 files changed, 190 insertions(+), 80 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt index 56f8733..d3635d3 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt @@ -49,6 +49,8 @@ internal fun formatDuration( * Note about Arabic: normally this produces "$count $unit", but for Arabic the * singular (1) and dual (2) forms are encoded in the unit word itself, so the * numeral is omitted. + * + * Note about Korean: the number and unit are written without a separating space, e.g. "2초". */ private fun formatUnit( count: Int, @@ -56,9 +58,10 @@ private fun formatUnit( relativeTime: RelativeTime ): String { val unitText = unit.format(count, relativeTime) - return if (HumanReadable.localisation.languageTag == "ar" && (count == 1 || count == 2)) { - unitText - } else { - "$count $unitText" + val languageTag = HumanReadable.localisation.languageTag + return when (languageTag) { + "ar" if (count == 1 || count == 2) -> unitText + "ko" -> "$count$unitText" + else -> "$count $unitText" } } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt index 35e7a52..60fc6eb 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt @@ -2,6 +2,7 @@ package nl.jacobras.humanreadable.i18n.translations import nl.jacobras.humanreadable.i18n.Plural import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.multipleTenses import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.HumanReadableStrings import nl.jacobras.humanreadable.i18n.NumberStrings @@ -14,7 +15,10 @@ internal val ElStrings = HumanReadableStrings( hours = presentTense(one = "ώρα", other = "ώρες"), days = presentTense(one = "μέρα", other = "μέρες"), weeks = presentTense(one = "εβδομάδα", other = "εβδομάδες"), - months = presentTense(one = "μήνα", other = "μήνες"), + months = multipleTenses { + present(one = "μήνας", other = "μήνες") + pastOrFuture(one = "μήνα") + }, years = presentTense(one = "έτος", other = "έτη"), timeAgo = { "$it πριν" }, timeInFuture = { "σε $it" }, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt index 49e406c..463a925 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt @@ -1,7 +1,7 @@ package nl.jacobras.humanreadable.i18n.translations import nl.jacobras.humanreadable.i18n.Plural -import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.multipleTenses import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.FileSizeStrings import nl.jacobras.humanreadable.i18n.HumanReadableStrings @@ -10,15 +10,36 @@ import nl.jacobras.humanreadable.i18n.NumberStrings internal val FiStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = presentTense(one = "sekunti", other = "sekuntia"), - minutes = presentTense(one = "minuutti", other = "minuuttia"), - hours = presentTense(one = "tunti", other = "tuntia"), - days = presentTense(one = "päivä", other = "päivää"), - weeks = presentTense(one = "viikko", other = "viikkoa"), - months = presentTense(one = "kuukausi", other = "kuukautta"), - years = presentTense(one = "vuosi", other = "vuotta"), + seconds = multipleTenses { + present(one = "sekunti", other = "sekuntia") + future(one = "sekunnin", other = "sekunnin") + }, + minutes = multipleTenses { + present(one = "minuutti", other = "minuuttia") + future(one = "minuutin", other = "minuutin") + }, + hours = multipleTenses { + present(one = "tunti", other = "tuntia") + future(one = "tunnin", other = "tunnin") + }, + days = multipleTenses { + present(one = "päivä", other = "päivää") + future(one = "päivän", other = "päivän") + }, + weeks = multipleTenses { + present(one = "viikko", other = "viikkoa") + future(one = "viikon", other = "viikon") + }, + months = multipleTenses { + present(one = "kuukausi", other = "kuukautta") + future(one = "kuukauden", other = "kuukauden") + }, + years = multipleTenses { + present(one = "vuosi", other = "vuotta") + future(one = "vuoden", other = "vuoden") + }, timeAgo = { "$it sitten" }, - timeInFuture = { "$it tulevaisuudessa" }, + timeInFuture = { "$it kuluttua" }, now = "nyt" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ","), diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt index 573b8b0..b0782eb 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt @@ -8,9 +8,18 @@ import nl.jacobras.humanreadable.i18n.HumanReadableStrings internal val KkStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = multipleTenses { present(one = "секунд", other = "секунд"); future(one = "секундтан", other = "секундтан") }, - minutes = multipleTenses { present(one = "минут", other = "минут"); future(one = "минуттан", other = "минуттан") }, - hours = multipleTenses { present(one = "сағат", other = "сағат"); future(one = "сағаттан", other = "сағаттан") }, + seconds = multipleTenses { + present(one = "секунд", other = "секунд") + future(one = "секундтан", other = "секундтан") + }, + minutes = multipleTenses { + present(one = "минут", other = "минут") + future(one = "минуттан", other = "минуттан") + }, + hours = multipleTenses { + present(one = "сағат", other = "сағат") + future(one = "сағаттан", other = "сағаттан") + }, days = multipleTenses { present(one = "күн", other = "күн"); future(one = "күннен", other = "күннен") }, weeks = multipleTenses { present(one = "апта", other = "апта"); future(one = "аптадан", other = "аптадан") }, months = multipleTenses { present(one = "ай", other = "ай"); future(one = "айдан", other = "айдан") }, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt index 5ff304a..cdfbbd2 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt @@ -10,10 +10,10 @@ internal val KoStrings = HumanReadableStrings( plural = { Plural.Other }, seconds = presentTense(other = "초"), minutes = presentTense(other = "분"), - hours = presentTense(other = "시"), + hours = presentTense(other = "시간"), days = presentTense(other = "일"), weeks = presentTense(other = "주"), - months = presentTense(other = "월"), + months = presentTense(other = "개월"), years = presentTense(other = "년"), timeAgo = { "$it 전" }, timeInFuture = { "$it 후" }, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt index c9e1c86..b3af0df 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt @@ -2,6 +2,7 @@ package nl.jacobras.humanreadable.i18n.translations import nl.jacobras.humanreadable.i18n.Plural import nl.jacobras.humanreadable.i18n.presentTense +import nl.jacobras.humanreadable.i18n.multipleTenses import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.HumanReadableStrings import nl.jacobras.humanreadable.i18n.NumberStrings @@ -15,9 +16,18 @@ internal val UkStrings = HumanReadableStrings( else -> Plural.Many } }, - seconds = presentTense(one = "секунду", few = "секунди", many = "секунд"), - minutes = presentTense(one = "хвилину", few = "хвилини", many = "хвилин"), - hours = presentTense(one = "годину", few = "години", many = "годин"), + seconds = multipleTenses { + present(one = "секунда", few = "секунди", many = "секунд") + pastOrFuture(one = "секунду") + }, + minutes = multipleTenses { + present(one = "хвилина", few = "хвилини", many = "хвилин") + pastOrFuture(one = "хвилину") + }, + hours = multipleTenses { + present(one = "година", few = "години", many = "годин") + pastOrFuture(one = "годину") + }, days = presentTense(one = "день", few = "дні", many = "днів"), weeks = presentTense(one = "тиждень", few = "тижні", many = "тижнів"), months = presentTense(one = "місяць", few = "місяці", many = "місяців"), diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt index 608a715..9e16a73 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt @@ -1,21 +1,42 @@ package nl.jacobras.humanreadable.i18n.translations import nl.jacobras.humanreadable.i18n.Plural -import nl.jacobras.humanreadable.i18n.presentTense import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.HumanReadableStrings import nl.jacobras.humanreadable.i18n.NumberStrings +import nl.jacobras.humanreadable.i18n.multipleTenses internal val UzStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = presentTense(other = "soniya"), - minutes = presentTense(other = "daqiqa"), - hours = presentTense(other = "soat"), - days = presentTense(other = "kun"), - weeks = presentTense(other = "hafta"), - months = presentTense(other = "oy"), - years = presentTense(other = "yil"), + seconds = multipleTenses { + present(one = "soniya", other = "soniya") + future(one = "soniyadan", other = "soniyadan") + }, + minutes = multipleTenses { + present(one = "daqiqa", other = "daqiqa") + future(one = "daqiqadan", other = "daqiqadan") + }, + hours = multipleTenses { + present(one = "soat", other = "soat") + future(one = "soatdan", other = "soatdan") + }, + days = multipleTenses { + present(one = "kun", other = "kun") + future(one = "kundan", other = "kundan") + }, + weeks = multipleTenses { + present(one = "hafta", other = "hafta") + future(one = "haftadan", other = "haftadan") + }, + months = multipleTenses { + present(one = "oy", other = "oy") + future(one = "oydan", other = "oydan") + }, + years = multipleTenses { + present(one = "yil", other = "yil") + future(one = "yildan", other = "yildan") + }, timeAgo = { "$it oldin" }, timeInFuture = { "$it keyin" }, now = "hozir" diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt index 8913b3c..1a74a08 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt @@ -17,7 +17,7 @@ internal val ViStrings = HumanReadableStrings( months = presentTense(other = "tháng"), years = presentTense(other = "năm"), timeAgo = { "$it trước" }, - timeInFuture = { "vào $it" }, + timeInFuture = { "sau $it" }, now = "bây giờ" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt index 66f4876..17323b7 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt @@ -19,26 +19,39 @@ import kotlin.time.Instant class LocalisedTests { private val now: Instant = Clock.System.now() - private val oneSecondFromNow = now + 1.seconds + private val zeroSeconds = 0.seconds + private val oneSecond = 1.seconds + private val oneSecondFromNow = now + oneSecond private val twoSeconds = 2.seconds private val twoSecondsAgo = now - twoSeconds private val twoSecondsFromNow = now + twoSeconds + private val threeSeconds = 3.seconds + private val elevenSeconds = 11.seconds private val oneMinute = 1.minutes private val oneMinuteAgo = now - oneMinute private val oneMinuteFromNow = now + oneMinute private val twoMinutes = 2.minutes private val twoMinutesAgo = now - twoMinutes + private val twentyMinutes = 20.minutes private val oneHour = 60.minutes private val oneHourAgo = now - oneHour private val oneHourFromNow = now + oneHour + private val twoHours = 120.minutes + private val twoHoursAgo = now - twoHours + private val twoHoursFromNow = now + twoHours private val oneDay = 1.days private val oneDayAgo = now - oneDay private val oneDayFromNow = now + oneDay private val twoDays = 2.days private val twoDaysAgo = now - twoDays + private val threeDays = 3.days + private val fiveDays = 5.days private val oneWeek = 7.days private val oneWeekAgo = now - oneWeek private val oneWeekFromNow = now + oneWeek + private val oneMonth = 31.days + private val oneMonthAgo = now - oneMonth + private val oneMonthFromNow = now + oneMonth private val twoMonths = 60.days private val twoMonthsAgo = now - twoMonths private val twoMonthsFromNow = now + twoMonths @@ -49,18 +62,18 @@ class LocalisedTests { @Test fun ar_arabic() { HumanReadable.languageTag = "ar" - assertThat(HumanReadable.duration(0.seconds)).isEqualTo("0 ثانية") - assertThat(HumanReadable.duration(1.seconds)).isEqualTo("ثانية") - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("ثانيتان") - assertThat(HumanReadable.duration(3.seconds)).isEqualTo("3 ثوان") - assertThat(HumanReadable.duration(11.seconds)).isEqualTo("11 ثانية") + assertThat(HumanReadable.duration(zeroSeconds)).isEqualTo("0 ثانية") + assertThat(HumanReadable.duration(oneSecond)).isEqualTo("ثانية") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("ثانيتان") + assertThat(HumanReadable.duration(threeSeconds)).isEqualTo("3 ثوان") + assertThat(HumanReadable.duration(elevenSeconds)).isEqualTo("11 ثانية") + assertThat(HumanReadable.duration(threeDays)).isEqualTo("3 أيام") + assertThat(HumanReadable.duration(twoMonths)).isEqualTo("شهران") + assertThat(HumanReadable.duration(oneYear)).isEqualTo("سنة") assertThat(HumanReadable.timeAgo(now, baseInstant = now)).isEqualTo("الآن") assertThat(HumanReadable.timeAgo(twoSecondsAgo, baseInstant = now)).isEqualTo("قبل ثانيتين") assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("بعد دقيقة") - assertThat(HumanReadable.duration(3.days)).isEqualTo("3 أيام") - assertThat(HumanReadable.duration(twoMonths)).isEqualTo("شهران") - assertThat(HumanReadable.duration(oneYear)).isEqualTo("سنة") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4.34") @@ -75,7 +88,10 @@ class LocalisedTests { @Test fun cs_czech() { HumanReadable.languageTag = "cs" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 sekundy") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 sekundy") + assertThat(HumanReadable.duration(oneDay)).isEqualTo("1 den") + assertThat(HumanReadable.duration(twoDays)).isEqualTo("2 dny") + assertThat(HumanReadable.duration(fiveDays)).isEqualTo("5 dní") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -85,15 +101,12 @@ class LocalisedTests { assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("před 1 minutou") assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("za 1 minutu") assertThat(HumanReadable.timeAgo(twoMinutesAgo, baseInstant = now)).isEqualTo("před 2 minutami") - assertThat(HumanReadable.timeAgo(now - 20.minutes, baseInstant = now)).isEqualTo("před 20 minutami") + assertThat(HumanReadable.timeAgo(now - twentyMinutes, baseInstant = now)).isEqualTo("před 20 minutami") assertThat(HumanReadable.timeAgo(oneHourAgo, baseInstant = now)).isEqualTo("před 1 hodinou") assertThat(HumanReadable.timeAgo(oneHourFromNow, baseInstant = now)).isEqualTo("za 1 hodinu") - assertThat(HumanReadable.duration(oneDay)).isEqualTo("1 den") - assertThat(HumanReadable.duration(twoDays)).isEqualTo("2 dny") assertThat(HumanReadable.timeAgo(oneDayAgo, baseInstant = now)).isEqualTo("před 1 dnem") assertThat(HumanReadable.timeAgo(twoDaysAgo, baseInstant = now)).isEqualTo("před 2 dny") - assertThat(HumanReadable.duration(5.days)).isEqualTo("5 dní") - assertThat(HumanReadable.timeAgo(now - 5.days, baseInstant = now)).isEqualTo("před 5 dny") + assertThat(HumanReadable.timeAgo(now - fiveDays, baseInstant = now)).isEqualTo("před 5 dny") assertThat(HumanReadable.timeAgo(oneWeekAgo, baseInstant = now)).isEqualTo("před 1 týdnem") assertThat(HumanReadable.timeAgo(twoMonthsAgo, baseInstant = now)).isEqualTo("před 2 měsíci") assertThat(HumanReadable.timeAgo(oneYearAgo, baseInstant = now)).isEqualTo("před 1 rokem") @@ -102,9 +115,9 @@ class LocalisedTests { @Test fun de_german() { HumanReadable.languageTag = "de" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 Sekunden") - + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 Sekunden") assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2 Monate") + assertThat(HumanReadable.timeAgo(twoMonthsAgo, baseInstant = now)).isEqualTo("vor 2 Monaten") assertThat(HumanReadable.timeAgo(twoMonthsFromNow, baseInstant = now)).isEqualTo("in 2 Monaten") @@ -115,9 +128,14 @@ class LocalisedTests { @Test fun el_greek() { HumanReadable.languageTag = "el" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 δευτερόλεπτα") - assertThat(HumanReadable.timeAgo(oneWeekAgo)).isEqualTo("1 εβδομάδα πριν") - assertThat(HumanReadable.timeAgo(oneYearFromNow)).isEqualTo("σε 1 έτος") + assertThat(HumanReadable.timeAgo(oneWeekAgo, baseInstant = now)).isEqualTo("1 εβδομάδα πριν") + assertThat(HumanReadable.timeAgo(oneYearFromNow, baseInstant = now)).isEqualTo("σε 1 έτος") + assertThat(HumanReadable.timeAgo(oneMonthAgo, baseInstant = now)).isEqualTo("1 μήνα πριν") + assertThat(HumanReadable.timeAgo(oneMonthFromNow, baseInstant = now)).isEqualTo("σε 1 μήνα") + + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 δευτερόλεπτα") + assertThat(HumanReadable.duration(oneMonth)).isEqualTo("1 μήνας") + assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2 μήνες") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -132,7 +150,7 @@ class LocalisedTests { @Test fun en_english() { HumanReadable.languageTag = "en" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 seconds") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 seconds") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4.34") @@ -147,7 +165,7 @@ class LocalisedTests { @Test fun es_spanish() { HumanReadable.languageTag = "es" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 segundos") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 segundos") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -156,7 +174,11 @@ class LocalisedTests { @Test fun fi_finnish() { HumanReadable.languageTag = "fi" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 sekuntia") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 sekuntia") + + assertThat(HumanReadable.timeAgo(twoMinutesAgo, baseInstant = now)).isEqualTo("2 minuuttia sitten") + assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("1 minuutin kuluttua") + assertThat(HumanReadable.timeAgo(twoHoursFromNow, baseInstant = now)).isEqualTo("2 tunnin kuluttua") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -165,7 +187,7 @@ class LocalisedTests { @Test fun fr_french() { HumanReadable.languageTag = "fr" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 secondes") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 secondes") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -176,7 +198,7 @@ class LocalisedTests { @Test fun id_indonesian() { HumanReadable.languageTag = "id" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 detik") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 detik") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -185,7 +207,7 @@ class LocalisedTests { @Test fun it_italian() { HumanReadable.languageTag = "it" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 secondi") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 secondi") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -194,7 +216,7 @@ class LocalisedTests { @Test fun ja_japanese() { HumanReadable.languageTag = "ja" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 秒") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 秒") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4.34") @@ -204,33 +226,39 @@ class LocalisedTests { fun kk_kazakh() { HumanReadable.languageTag = "kk" assertThat(HumanReadable.timeAgo(now)).isEqualTo("қазір") - assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 секунд") assertThat(HumanReadable.timeAgo(twoSecondsAgo, baseInstant = now)).isEqualTo("2 секунд бұрын") assertThat(HumanReadable.timeAgo(twoSecondsFromNow, baseInstant = now)).isEqualTo("2 секундтан кейін") - assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 минут") assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("1 минут бұрын") assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("1 минуттан кейін") - assertThat(HumanReadable.duration(oneHour)).isEqualTo("1 сағат") assertThat(HumanReadable.timeAgo(oneHourAgo, baseInstant = now)).isEqualTo("1 сағат бұрын") assertThat(HumanReadable.timeAgo(oneHourFromNow, baseInstant = now)).isEqualTo("1 сағаттан кейін") - assertThat(HumanReadable.duration(oneDay)).isEqualTo("1 күн") assertThat(HumanReadable.timeAgo(oneDayAgo, baseInstant = now)).isEqualTo("1 күн бұрын") assertThat(HumanReadable.timeAgo(oneDayFromNow, baseInstant = now)).isEqualTo("1 күннен кейін") - assertThat(HumanReadable.duration(oneWeek)).isEqualTo("1 апта") assertThat(HumanReadable.timeAgo(oneWeekAgo, baseInstant = now)).isEqualTo("1 апта бұрын") assertThat(HumanReadable.timeAgo(oneWeekFromNow, baseInstant = now)).isEqualTo("1 аптадан кейін") - assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2 ай") assertThat(HumanReadable.timeAgo(twoMonthsAgo, baseInstant = now)).isEqualTo("2 ай бұрын") assertThat(HumanReadable.timeAgo(twoMonthsFromNow, baseInstant = now)).isEqualTo("2 айдан кейін") - assertThat(HumanReadable.duration(oneYear)).isEqualTo("1 жыл") assertThat(HumanReadable.timeAgo(oneYearAgo, baseInstant = now)).isEqualTo("1 жыл бұрын") assertThat(HumanReadable.timeAgo(oneYearFromNow, baseInstant = now)).isEqualTo("1 жылдан кейін") + + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 секунд") + assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 минут") + assertThat(HumanReadable.duration(oneHour)).isEqualTo("1 сағат") + assertThat(HumanReadable.duration(oneDay)).isEqualTo("1 күн") + assertThat(HumanReadable.duration(oneWeek)).isEqualTo("1 апта") + assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2 ай") + assertThat(HumanReadable.duration(oneYear)).isEqualTo("1 жыл") } @Test fun ko_korean() { HumanReadable.languageTag = "ko" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 초") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2초") + assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2개월") + + assertThat(HumanReadable.timeAgo(oneHourAgo, baseInstant = now)).isEqualTo("1시간 전") + assertThat(HumanReadable.timeAgo(twoHoursAgo, baseInstant = now)).isEqualTo("2시간 전") + assertThat(HumanReadable.timeAgo(twoMonthsFromNow, baseInstant = now)).isEqualTo("2개월 후") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4.34") @@ -239,7 +267,7 @@ class LocalisedTests { @Test fun nl_dutch() { HumanReadable.languageTag = "nl" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 seconden") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 seconden") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -254,9 +282,9 @@ class LocalisedTests { @Test fun pl_polish() { HumanReadable.languageTag = "pl" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 sekundy") - + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 sekundy") assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 minuta") + assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("1 minutę temu") assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("za 1 minutę") @@ -267,9 +295,9 @@ class LocalisedTests { @Test fun pt_portuguese() { HumanReadable.languageTag = "pt" - assertThat(HumanReadable.duration(0.seconds)).isEqualTo("0 segundo") - assertThat(HumanReadable.duration(1.seconds)).isEqualTo("1 segundo") - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 segundos") + assertThat(HumanReadable.duration(zeroSeconds)).isEqualTo("0 segundo") + assertThat(HumanReadable.duration(oneSecond)).isEqualTo("1 segundo") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 segundos") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -278,9 +306,9 @@ class LocalisedTests { @Test fun ru_russian() { HumanReadable.languageTag = "ru" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 секунды") - + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 секунды") assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 минута") + assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("1 минуту назад") assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("через 1 минуту") @@ -291,7 +319,7 @@ class LocalisedTests { @Test fun tr_turkish() { HumanReadable.languageTag = "tr" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 saniye") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 saniye") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -300,7 +328,13 @@ class LocalisedTests { @Test fun uk_ukrainian() { HumanReadable.languageTag = "uk" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 секунди") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 секунди") + assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 хвилина") + assertThat(HumanReadable.duration(oneHour)).isEqualTo("1 година") + + assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("1 хвилину тому") + assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("через 1 хвилину") + assertThat(HumanReadable.timeAgo(oneHourAgo, baseInstant = now)).isEqualTo("1 годину тому") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -309,7 +343,12 @@ class LocalisedTests { @Test fun uz_uzbek() { HumanReadable.languageTag = "uz" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 soniya") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 soniya") + assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 daqiqa") + + assertThat(HumanReadable.timeAgo(oneMinuteAgo, baseInstant = now)).isEqualTo("1 daqiqa oldin") + assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("1 daqiqadan keyin") + assertThat(HumanReadable.timeAgo(twoHoursFromNow, baseInstant = now)).isEqualTo("2 soatdan keyin") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -318,7 +357,10 @@ class LocalisedTests { @Test fun vi_vietnamese() { HumanReadable.languageTag = "vi" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 giây") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 giây") + + assertThat(HumanReadable.timeAgo(twoSecondsAgo, baseInstant = now)).isEqualTo("2 giây trước") + assertThat(HumanReadable.timeAgo(oneMinuteFromNow, baseInstant = now)).isEqualTo("sau 1 phút") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -327,9 +369,9 @@ class LocalisedTests { @Test fun zh_chinese() { HumanReadable.languageTag = "zh" - assertThat(HumanReadable.duration(2.seconds)).isEqualTo("2 秒") + assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 秒") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4.34") } -} +} \ No newline at end of file From 8787ac126d720e8df48f0ebb2ed45fbf5f393378 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 24 Jun 2026 20:52:10 +0200 Subject: [PATCH 08/57] Improve test stability --- .../humanreadable/HumanReadableRelativeTimeTests.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt index 0b9ead2..6c841f6 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt @@ -2,6 +2,7 @@ package nl.jacobras.humanreadable import assertk.assertThat import assertk.assertions.isEqualTo +import nl.jacobras.humanreadable.localized.LocalisedTests import kotlin.test.Test import kotlin.time.Clock import kotlin.time.Duration.Companion.days @@ -9,6 +10,11 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.ExperimentalTime import kotlin.time.Instant +/** + * Main test for the specific configuration options related to relative time formatting. + * + * All in English, see [LocalisedTests] for specific tests for the supported languages. + */ @OptIn(ExperimentalTime::class) class HumanReadableRelativeTimeTests { @@ -21,16 +27,16 @@ class HumanReadableRelativeTimeTests { @Test fun inFuture() { - assertThat(HumanReadable.timeAgo(now + 200.seconds)).isEqualTo("in 3 minutes") + assertThat(HumanReadable.timeAgo(now + 200.seconds, baseInstant = now)).isEqualTo("in 3 minutes") } @Test fun now() { - assertThat(HumanReadable.timeAgo(now)).isEqualTo("now") + assertThat(HumanReadable.timeAgo(now, baseInstant = now)).isEqualTo("now") } @Test fun inPast() { - assertThat(HumanReadable.timeAgo(now - 3.days)).isEqualTo("3 days ago") + assertThat(HumanReadable.timeAgo(now - 3.days, baseInstant = now)).isEqualTo("3 days ago") } } \ No newline at end of file From d2a696ac33c313f447bf0ccdbb262281747ca647 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 24 Jun 2026 20:55:18 +0200 Subject: [PATCH 09/57] Update Kdoc --- .../kotlin/nl/jacobras/humanreadable/HumanReadable.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 7157d8c..693b7cc 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -45,7 +45,7 @@ public object HumanReadable { } /** - * Returns the difference between now and [instant], in human-readable format. Also supports + * Returns the difference between [baseInstant] and [instant], in human-readable format. Also supports * instants in the future. For example, an instant that's 5 hours ago will return "5 hours ago". * * @param instant The [Instant] to format. From db349dee18d98feb0bf0680d689b483491f28678 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 24 Jun 2026 20:57:45 +0200 Subject: [PATCH 10/57] Package time things together --- .../kotlin/nl/jacobras/humanreadable/HumanReadable.kt | 3 +++ .../jacobras/humanreadable/{ => time}/HumanReadableDuration.kt | 3 ++- .../humanreadable/{ => time}/HumanReadableRelativeTime.kt | 2 +- .../nl/jacobras/humanreadable/{ => time}/RelativeTime.kt | 2 +- .../kotlin/nl/jacobras/humanreadable/{ => time}/TimeUnit.kt | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) rename src/commonMain/kotlin/nl/jacobras/humanreadable/{ => time}/HumanReadableDuration.kt (95%) rename src/commonMain/kotlin/nl/jacobras/humanreadable/{ => time}/HumanReadableRelativeTime.kt (96%) rename src/commonMain/kotlin/nl/jacobras/humanreadable/{ => time}/RelativeTime.kt (84%) rename src/commonMain/kotlin/nl/jacobras/humanreadable/{ => time}/TimeUnit.kt (96%) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 693b7cc..0c30be5 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -8,6 +8,9 @@ import nl.jacobras.humanreadable.HumanReadable.languageTag import nl.jacobras.humanreadable.HumanReadable.number import nl.jacobras.humanreadable.i18n.HumanReadableStrings import nl.jacobras.humanreadable.i18n.Localisation +import nl.jacobras.humanreadable.time.RelativeTime +import nl.jacobras.humanreadable.time.formatDuration +import nl.jacobras.humanreadable.time.formatTimeAgo import kotlin.time.Clock import kotlin.time.Duration import kotlin.time.ExperimentalTime diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt similarity index 95% rename from src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt rename to src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index d3635d3..b424da0 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -1,5 +1,6 @@ -package nl.jacobras.humanreadable +package nl.jacobras.humanreadable.time +import nl.jacobras.humanreadable.HumanReadable import kotlin.math.roundToInt import kotlin.time.Duration diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt similarity index 96% rename from src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTime.kt rename to src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt index 56c5a4e..fb070ec 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt @@ -1,4 +1,4 @@ -package nl.jacobras.humanreadable +package nl.jacobras.humanreadable.time import nl.jacobras.humanreadable.HumanReadable.strings import kotlin.time.ExperimentalTime diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/RelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/RelativeTime.kt similarity index 84% rename from src/commonMain/kotlin/nl/jacobras/humanreadable/RelativeTime.kt rename to src/commonMain/kotlin/nl/jacobras/humanreadable/time/RelativeTime.kt index a36cbcb..9868bc6 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/RelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/RelativeTime.kt @@ -1,4 +1,4 @@ -package nl.jacobras.humanreadable +package nl.jacobras.humanreadable.time /** * Indicates in what time frame the requested time unit needs diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt similarity index 96% rename from src/commonMain/kotlin/nl/jacobras/humanreadable/TimeUnit.kt rename to src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt index a80064c..90f0ce4 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/TimeUnit.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt @@ -1,4 +1,4 @@ -package nl.jacobras.humanreadable +package nl.jacobras.humanreadable.time import nl.jacobras.humanreadable.HumanReadable.localisation import nl.jacobras.humanreadable.HumanReadable.strings From b508c5bba57c350fb5521cf359d229ee94445aba Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 24 Jun 2026 21:57:44 +0200 Subject: [PATCH 11/57] Add `Rounding` parameter --- .../jacobras/humanreadable/HumanReadable.kt | 21 ++++++-- .../time/HumanReadableDuration.kt | 46 ++++++++--------- .../time/HumanReadableRelativeTime.kt | 15 ++++-- .../jacobras/humanreadable/time/Rounding.kt | 24 +++++++++ .../HumanReadableDurationTests.kt | 50 +++++++++++-------- .../humanreadable/localized/LocalisedTests.kt | 7 ++- 6 files changed, 107 insertions(+), 56 deletions(-) create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 0c30be5..648987b 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -9,6 +9,7 @@ import nl.jacobras.humanreadable.HumanReadable.number import nl.jacobras.humanreadable.i18n.HumanReadableStrings import nl.jacobras.humanreadable.i18n.Localisation import nl.jacobras.humanreadable.time.RelativeTime +import nl.jacobras.humanreadable.time.Rounding import nl.jacobras.humanreadable.time.formatDuration import nl.jacobras.humanreadable.time.formatTimeAgo import kotlin.time.Clock @@ -57,9 +58,14 @@ public object HumanReadable { @OptIn(ExperimentalTime::class) public fun timeAgo( instant: Instant, - baseInstant: Instant = Clock.System.now() + baseInstant: Instant = Clock.System.now(), + rounding: Rounding = Rounding.HalfUp ): String { - return formatTimeAgo(instant, baseInstant) + return formatTimeAgo( + instant = instant, + baseInstant = baseInstant, + rounding = rounding + ) } /** @@ -69,8 +75,15 @@ public object HumanReadable { * @param duration The [Duration] to format. * @return a formatted string */ - public fun duration(duration: Duration): String { - return formatDuration(duration, RelativeTime.Present) + public fun duration( + duration: Duration, + rounding: Rounding = Rounding.HalfUp + ): String { + return formatDuration( + duration = duration, + relativeTime = RelativeTime.Present, + rounding = rounding + ) } /** diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index b424da0..d41c6f7 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -9,38 +9,25 @@ import kotlin.time.Duration */ internal fun formatDuration( duration: Duration, - relativeTime: RelativeTime + relativeTime: RelativeTime, + rounding: Rounding ): String { val secondsAgo = duration.inWholeSeconds.toInt() + val minutesAgo = duration.inWholeMinutes.toInt() val hoursAgo = duration.inWholeHours.toInt() val daysAgo = duration.inWholeDays.toInt() - val weeksAgo = (duration.inWholeDays / 7f).roundToInt() - val monthsAgo = (duration.inWholeDays / 30.5f).roundToInt() - val yearsAgo = (duration.inWholeDays / 365).toInt() + val weeksAgo = (duration.inWholeDays / 7f).round(rounding) + val monthsAgo = (duration.inWholeDays / 30.5f).round(rounding) + val yearsAgo = (duration.inWholeDays / 365f).round(rounding) return when { - secondsAgo < 60 -> { - formatUnit(secondsAgo, TimeUnit.Seconds, relativeTime) - } - secondsAgo < 3600 -> { - val minutes = duration.inWholeMinutes.toInt() - formatUnit(minutes, TimeUnit.Minutes, relativeTime) - } - daysAgo < 1 -> { - formatUnit(hoursAgo, TimeUnit.Hours, relativeTime) - } - daysAgo < 7 -> { - formatUnit(daysAgo, TimeUnit.Days, relativeTime) - } - daysAgo < 30 -> { - formatUnit(weeksAgo, TimeUnit.Weeks, relativeTime) - } - monthsAgo < 12 || yearsAgo == 0 -> { - formatUnit(monthsAgo, TimeUnit.Months, relativeTime) - } - else -> { - formatUnit(yearsAgo, TimeUnit.Years, relativeTime) - } + yearsAgo > 0 -> formatUnit(yearsAgo, TimeUnit.Years, relativeTime) + monthsAgo > 0 -> formatUnit(monthsAgo, TimeUnit.Months, relativeTime) + weeksAgo > 0 -> formatUnit(weeksAgo, TimeUnit.Weeks, relativeTime) + daysAgo > 0 -> formatUnit(daysAgo, TimeUnit.Days, relativeTime) + hoursAgo > 0 -> formatUnit(hoursAgo, TimeUnit.Hours, relativeTime) + minutesAgo > 0 -> formatUnit(minutesAgo, TimeUnit.Minutes, relativeTime) + else -> formatUnit(secondsAgo, TimeUnit.Seconds, relativeTime) } } @@ -65,4 +52,11 @@ private fun formatUnit( "ko" -> "$count$unitText" else -> "$count $unitText" } +} + +private fun Float.round(rounding: Rounding): Int { + return when (rounding) { + Rounding.Floor -> toInt() + Rounding.HalfUp -> roundToInt() + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt index fb070ec..a686a11 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt @@ -14,18 +14,27 @@ import kotlin.time.Instant @OptIn(ExperimentalTime::class) internal fun formatTimeAgo( instant: Instant, - baseInstant: Instant + baseInstant: Instant, + rounding: Rounding ): String { val diff = baseInstant - instant val secondsAgo = diff.inWholeSeconds return when { secondsAgo < 0 -> strings.dateTime.timeInFuture( - formatDuration(diff.absoluteValue, relativeTime = RelativeTime.Future) + formatDuration( + duration = diff.absoluteValue, + relativeTime = RelativeTime.Future, + rounding = rounding + ) ) secondsAgo <= 1 -> strings.dateTime.now else -> strings.dateTime.timeAgo( - formatDuration(diff.absoluteValue, relativeTime = RelativeTime.Past) + formatDuration( + duration = diff.absoluteValue, + relativeTime = RelativeTime.Past, + rounding = rounding + ) ) } } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt new file mode 100644 index 0000000..08955d9 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt @@ -0,0 +1,24 @@ +package nl.jacobras.humanreadable.time + +public sealed interface Rounding { + + /** + * Rounds down. + * + * Some examples: + * - `11.days` results in "1 week". + * - `13.days` results in "1 week". + * - `14.days` results in "2 weeks". + */ + public data object Floor : Rounding + + /** + * Rounds up. + * + * Some examples: + * - `11.days` results in "2 weeks". + * - `13.days` results in "2 weeks". + * - `14.days` results in "2 weeks". + */ + public data object HalfUp : Rounding +} \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt index 2521be3..aced8a5 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt @@ -2,12 +2,19 @@ package nl.jacobras.humanreadable import assertk.assertThat import assertk.assertions.isEqualTo +import nl.jacobras.humanreadable.localized.LocalisedTests +import nl.jacobras.humanreadable.time.Rounding import kotlin.test.Test import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +/** + * Main test for the specific configuration options related to duration formatting. + * + * All in English, see [LocalisedTests] for specific tests for the supported languages. + */ class HumanReadableDurationTests { init { @@ -15,48 +22,49 @@ class HumanReadableDurationTests { } @Test - fun seconds() { + fun defaultRounding() { assertThat(HumanReadable.duration(1.seconds)).isEqualTo("1 second") assertThat(HumanReadable.duration(3.seconds)).isEqualTo("3 seconds") - } - @Test - fun minutes() { assertThat(HumanReadable.duration(1.minutes)).isEqualTo("1 minute") assertThat(HumanReadable.duration(3.minutes)).isEqualTo("3 minutes") - } - @Test - fun hours() { assertThat(HumanReadable.duration(1.hours)).isEqualTo("1 hour") assertThat(HumanReadable.duration(3.hours)).isEqualTo("3 hours") assertThat(HumanReadable.duration(23.hours)).isEqualTo("23 hours") - } - @Test - fun days() { assertThat(HumanReadable.duration(1.days)).isEqualTo("1 day") assertThat(HumanReadable.duration(3.days)).isEqualTo("3 days") - } - @Test - fun weeks() { assertThat(HumanReadable.duration(7.days)).isEqualTo("1 week") assertThat(HumanReadable.duration(10.days)).isEqualTo("1 week") assertThat(HumanReadable.duration(11.days)).isEqualTo("2 weeks") - assertThat(HumanReadable.duration(21.days)).isEqualTo("3 weeks") - } - @Test - fun months() { + assertThat(HumanReadable.duration(21.days)).isEqualTo("1 month") + assertThat(HumanReadable.duration(29.days)).isEqualTo("1 month") assertThat(HumanReadable.duration(30.days)).isEqualTo("1 month") + assertThat(HumanReadable.duration(31.days)).isEqualTo("1 month") assertThat(HumanReadable.duration(90.days)).isEqualTo("3 months") - assertThat(HumanReadable.duration(364.days)).isEqualTo("12 months") - } - @Test - fun years() { + assertThat(HumanReadable.duration(360.days)).isEqualTo("1 year") assertThat(HumanReadable.duration(365.days)).isEqualTo("1 year") + assertThat(HumanReadable.duration(555.days)).isEqualTo("2 years") assertThat(HumanReadable.duration(1095.days)).isEqualTo("3 years") } + + @Test + fun floorRounding() { + assertThat(HumanReadable.duration(10.days, rounding = Rounding.Floor)).isEqualTo("1 week") + assertThat(HumanReadable.duration(11.days, rounding = Rounding.Floor)).isEqualTo("1 week") + + assertThat(HumanReadable.duration(21.days, rounding = Rounding.Floor)).isEqualTo("3 weeks") + assertThat(HumanReadable.duration(29.days, rounding = Rounding.Floor)).isEqualTo("4 weeks") + assertThat(HumanReadable.duration(30.days, rounding = Rounding.Floor)).isEqualTo("4 weeks") + assertThat(HumanReadable.duration(31.days, rounding = Rounding.Floor)).isEqualTo("1 month") + + assertThat(HumanReadable.duration(360.days, rounding = Rounding.Floor)).isEqualTo("11 months") + assertThat(HumanReadable.duration(365.days, rounding = Rounding.Floor)).isEqualTo("1 year") + assertThat(HumanReadable.duration(555.days, rounding = Rounding.Floor)).isEqualTo("1 year") + assertThat(HumanReadable.duration(1095.days, rounding = Rounding.Floor)).isEqualTo("3 years") + } } \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt index 17323b7..e62f74f 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt @@ -4,6 +4,7 @@ import assertk.assertThat import assertk.assertions.isEqualTo import nl.jacobras.humanreadable.DistanceUnit import nl.jacobras.humanreadable.HumanReadable +import nl.jacobras.humanreadable.time.Rounding import kotlin.test.Test import kotlin.time.Clock import kotlin.time.Duration.Companion.days @@ -91,7 +92,7 @@ class LocalisedTests { assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 sekundy") assertThat(HumanReadable.duration(oneDay)).isEqualTo("1 den") assertThat(HumanReadable.duration(twoDays)).isEqualTo("2 dny") - assertThat(HumanReadable.duration(fiveDays)).isEqualTo("5 dní") + assertThat(HumanReadable.duration(fiveDays, rounding = Rounding.Floor)).isEqualTo("5 dní") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34") assertThat(HumanReadable.number(-4.34, decimals = 2)).isEqualTo("-4,34") @@ -106,7 +107,9 @@ class LocalisedTests { assertThat(HumanReadable.timeAgo(oneHourFromNow, baseInstant = now)).isEqualTo("za 1 hodinu") assertThat(HumanReadable.timeAgo(oneDayAgo, baseInstant = now)).isEqualTo("před 1 dnem") assertThat(HumanReadable.timeAgo(twoDaysAgo, baseInstant = now)).isEqualTo("před 2 dny") - assertThat(HumanReadable.timeAgo(now - fiveDays, baseInstant = now)).isEqualTo("před 5 dny") + assertThat( + HumanReadable.timeAgo(now - fiveDays, baseInstant = now, rounding = Rounding.Floor) + ).isEqualTo("před 5 dny") assertThat(HumanReadable.timeAgo(oneWeekAgo, baseInstant = now)).isEqualTo("před 1 týdnem") assertThat(HumanReadable.timeAgo(twoMonthsAgo, baseInstant = now)).isEqualTo("před 2 měsíci") assertThat(HumanReadable.timeAgo(oneYearAgo, baseInstant = now)).isEqualTo("před 1 rokem") From d1f52d0b20e67a7695a2ef4cc4dd2cef8f8805af Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 24 Jun 2026 22:24:37 +0200 Subject: [PATCH 12/57] Add `UpIfClose` rounding --- .../time/HumanReadableDuration.kt | 26 +++++++++++--- .../jacobras/humanreadable/time/Rounding.kt | 12 +++++++ .../HumanReadableDurationTests.kt | 35 +++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index d41c6f7..c0a56d9 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -25,9 +25,27 @@ internal fun formatDuration( monthsAgo > 0 -> formatUnit(monthsAgo, TimeUnit.Months, relativeTime) weeksAgo > 0 -> formatUnit(weeksAgo, TimeUnit.Weeks, relativeTime) daysAgo > 0 -> formatUnit(daysAgo, TimeUnit.Days, relativeTime) - hoursAgo > 0 -> formatUnit(hoursAgo, TimeUnit.Hours, relativeTime) - minutesAgo > 0 -> formatUnit(minutesAgo, TimeUnit.Minutes, relativeTime) - else -> formatUnit(secondsAgo, TimeUnit.Seconds, relativeTime) + hoursAgo > 0 -> { + if (rounding == Rounding.UpIfClose && hoursAgo >= 23) { + formatUnit(1, TimeUnit.Days, relativeTime) + } else { + formatUnit(hoursAgo, TimeUnit.Hours, relativeTime) + } + } + minutesAgo > 0 -> { + if (rounding == Rounding.UpIfClose && minutesAgo >= 55) { + formatUnit(1, TimeUnit.Hours, relativeTime) + } else { + formatUnit(minutesAgo, TimeUnit.Minutes, relativeTime) + } + } + else -> { + if (rounding == Rounding.UpIfClose && secondsAgo >= 55) { + formatUnit(1, TimeUnit.Minutes, relativeTime) + } else { + formatUnit(secondsAgo, TimeUnit.Seconds, relativeTime) + } + } } } @@ -57,6 +75,6 @@ private fun formatUnit( private fun Float.round(rounding: Rounding): Int { return when (rounding) { Rounding.Floor -> toInt() - Rounding.HalfUp -> roundToInt() + Rounding.HalfUp, Rounding.UpIfClose -> roundToInt() } } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt index 08955d9..386a843 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt @@ -21,4 +21,16 @@ public sealed interface Rounding { * - `14.days` results in "2 weeks". */ public data object HalfUp : Rounding + + /** + * Rounds up if the duration is close to the next unit. + * + * "Close" is defined as: + * - `55.seconds` rounds up to a minute + * - `55.minutes` rounds up to an hour + * - `23.hours` rounds up to a day + * + * Larger units are rounded half up, e.g. 10.5 days rounds up to 11 days. + */ + public data object UpIfClose : Rounding } \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt index aced8a5..9d43d22 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt @@ -25,9 +25,11 @@ class HumanReadableDurationTests { fun defaultRounding() { assertThat(HumanReadable.duration(1.seconds)).isEqualTo("1 second") assertThat(HumanReadable.duration(3.seconds)).isEqualTo("3 seconds") + assertThat(HumanReadable.duration(55.seconds)).isEqualTo("55 seconds") assertThat(HumanReadable.duration(1.minutes)).isEqualTo("1 minute") assertThat(HumanReadable.duration(3.minutes)).isEqualTo("3 minutes") + assertThat(HumanReadable.duration(55.minutes)).isEqualTo("55 minutes") assertThat(HumanReadable.duration(1.hours)).isEqualTo("1 hour") assertThat(HumanReadable.duration(3.hours)).isEqualTo("3 hours") @@ -67,4 +69,37 @@ class HumanReadableDurationTests { assertThat(HumanReadable.duration(555.days, rounding = Rounding.Floor)).isEqualTo("1 year") assertThat(HumanReadable.duration(1095.days, rounding = Rounding.Floor)).isEqualTo("3 years") } + + @Test + fun upIfCloseRounding() { + assertThat(HumanReadable.duration(1.seconds, rounding = Rounding.UpIfClose)).isEqualTo("1 second") + assertThat(HumanReadable.duration(3.seconds, rounding = Rounding.UpIfClose)).isEqualTo("3 seconds") + + assertThat(HumanReadable.duration(55.seconds, rounding = Rounding.UpIfClose)).isEqualTo("1 minute") + assertThat(HumanReadable.duration(1.minutes, rounding = Rounding.UpIfClose)).isEqualTo("1 minute") + assertThat(HumanReadable.duration(3.minutes, rounding = Rounding.UpIfClose)).isEqualTo("3 minutes") + + assertThat(HumanReadable.duration(55.minutes, rounding = Rounding.UpIfClose)).isEqualTo("1 hour") + assertThat(HumanReadable.duration(1.hours, rounding = Rounding.UpIfClose)).isEqualTo("1 hour") + assertThat(HumanReadable.duration(3.hours, rounding = Rounding.UpIfClose)).isEqualTo("3 hours") + + assertThat(HumanReadable.duration(23.hours, rounding = Rounding.UpIfClose)).isEqualTo("1 day") + assertThat(HumanReadable.duration(1.days, rounding = Rounding.UpIfClose)).isEqualTo("1 day") + assertThat(HumanReadable.duration(3.days, rounding = Rounding.UpIfClose)).isEqualTo("3 days") + + assertThat(HumanReadable.duration(7.days, rounding = Rounding.UpIfClose)).isEqualTo("1 week") + assertThat(HumanReadable.duration(10.days, rounding = Rounding.UpIfClose)).isEqualTo("1 week") + assertThat(HumanReadable.duration(11.days, rounding = Rounding.UpIfClose)).isEqualTo("2 weeks") + + assertThat(HumanReadable.duration(21.days, rounding = Rounding.UpIfClose)).isEqualTo("1 month") + assertThat(HumanReadable.duration(29.days, rounding = Rounding.UpIfClose)).isEqualTo("1 month") + assertThat(HumanReadable.duration(30.days, rounding = Rounding.UpIfClose)).isEqualTo("1 month") + assertThat(HumanReadable.duration(31.days, rounding = Rounding.UpIfClose)).isEqualTo("1 month") + assertThat(HumanReadable.duration(90.days, rounding = Rounding.UpIfClose)).isEqualTo("3 months") + + assertThat(HumanReadable.duration(360.days, rounding = Rounding.UpIfClose)).isEqualTo("1 year") + assertThat(HumanReadable.duration(365.days, rounding = Rounding.UpIfClose)).isEqualTo("1 year") + assertThat(HumanReadable.duration(555.days, rounding = Rounding.UpIfClose)).isEqualTo("2 years") + assertThat(HumanReadable.duration(1095.days, rounding = Rounding.UpIfClose)).isEqualTo("3 years") + } } \ No newline at end of file From 424cde98cfa9ef61b2da27be5ea92ef005d1300d Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 24 Jun 2026 22:25:50 +0200 Subject: [PATCH 13/57] Group time tests together --- .../humanreadable/{ => time}/HumanReadableDurationTests.kt | 4 ++-- .../{ => time}/HumanReadableRelativeTimeTests.kt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) rename src/commonTest/kotlin/nl/jacobras/humanreadable/{ => time}/HumanReadableDurationTests.kt (98%) rename src/commonTest/kotlin/nl/jacobras/humanreadable/{ => time}/HumanReadableRelativeTimeTests.kt (92%) diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt similarity index 98% rename from src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt rename to src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 9d43d22..28b5d31 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -1,9 +1,9 @@ -package nl.jacobras.humanreadable +package nl.jacobras.humanreadable.time import assertk.assertThat import assertk.assertions.isEqualTo +import nl.jacobras.humanreadable.HumanReadable import nl.jacobras.humanreadable.localized.LocalisedTests -import nl.jacobras.humanreadable.time.Rounding import kotlin.test.Test import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt similarity index 92% rename from src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt rename to src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt index 6c841f6..6df2d66 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableRelativeTimeTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt @@ -1,7 +1,8 @@ -package nl.jacobras.humanreadable +package nl.jacobras.humanreadable.time import assertk.assertThat import assertk.assertions.isEqualTo +import nl.jacobras.humanreadable.HumanReadable import nl.jacobras.humanreadable.localized.LocalisedTests import kotlin.test.Test import kotlin.time.Clock From 74fb39c6412ca3337fa19e8d17f197d1f1ea2b3c Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 24 Jun 2026 22:47:37 +0200 Subject: [PATCH 14/57] Add `units` parameter to limit time units used in formatting --- .../jacobras/humanreadable/HumanReadable.kt | 22 ++++++++++------- .../humanreadable/HumanReadableNumber.kt | 1 - .../time/HumanReadableDuration.kt | 24 +++++++++++++------ .../time/HumanReadableRelativeTime.kt | 9 ++++--- .../jacobras/humanreadable/time/TimeUnit.kt | 10 +++++--- .../time/HumanReadableDurationTests.kt | 11 +++++++++ 6 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 648987b..47339a2 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -8,10 +8,7 @@ import nl.jacobras.humanreadable.HumanReadable.languageTag import nl.jacobras.humanreadable.HumanReadable.number import nl.jacobras.humanreadable.i18n.HumanReadableStrings import nl.jacobras.humanreadable.i18n.Localisation -import nl.jacobras.humanreadable.time.RelativeTime -import nl.jacobras.humanreadable.time.Rounding -import nl.jacobras.humanreadable.time.formatDuration -import nl.jacobras.humanreadable.time.formatTimeAgo +import nl.jacobras.humanreadable.time.* import kotlin.time.Clock import kotlin.time.Duration import kotlin.time.ExperimentalTime @@ -53,18 +50,23 @@ public object HumanReadable { * instants in the future. For example, an instant that's 5 hours ago will return "5 hours ago". * * @param instant The [Instant] to format. + * @param baseInstant The base/starting [Instant], defaulting to "now". + * @param rounding The [Rounding] strategy to use. + * @param units The [TimeUnit]s to limit to during formatting. * @return a formatted string */ @OptIn(ExperimentalTime::class) public fun timeAgo( instant: Instant, baseInstant: Instant = Clock.System.now(), - rounding: Rounding = Rounding.HalfUp + rounding: Rounding = Rounding.HalfUp, + units: Set = TimeUnit.all ): String { return formatTimeAgo( instant = instant, baseInstant = baseInstant, - rounding = rounding + rounding = rounding, + units = units ) } @@ -73,16 +75,20 @@ public object HumanReadable { * For example, a duration of 3 seconds returns "3 seconds". * * @param duration The [Duration] to format. + * @param rounding The [Rounding] strategy to use. + * @param units The [TimeUnit]s to limit to during formatting. * @return a formatted string */ public fun duration( duration: Duration, - rounding: Rounding = Rounding.HalfUp + rounding: Rounding = Rounding.HalfUp, + units: Set = TimeUnit.all ): String { return formatDuration( duration = duration, relativeTime = RelativeTime.Present, - rounding = rounding + rounding = rounding, + units = units ) } diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt index 81b1240..09d7db1 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadableNumber.kt @@ -28,7 +28,6 @@ internal fun Double.formatNumber( val formattedIntegerPart = if (isNegative) "-$formattedNumberPart" else formattedNumberPart - // Format the decimal part val decimalPart = if (parts.size > 1) parts[1] else "" val formattedDecimalPart = if (decimals > 0) { diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index c0a56d9..d666304 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -1,6 +1,7 @@ package nl.jacobras.humanreadable.time import nl.jacobras.humanreadable.HumanReadable +import nl.jacobras.humanreadable.formatNumber import kotlin.math.roundToInt import kotlin.time.Duration @@ -10,7 +11,8 @@ import kotlin.time.Duration internal fun formatDuration( duration: Duration, relativeTime: RelativeTime, - rounding: Rounding + rounding: Rounding, + units: Set ): String { val secondsAgo = duration.inWholeSeconds.toInt() val minutesAgo = duration.inWholeMinutes.toInt() @@ -21,18 +23,26 @@ internal fun formatDuration( val yearsAgo = (duration.inWholeDays / 365f).round(rounding) return when { - yearsAgo > 0 -> formatUnit(yearsAgo, TimeUnit.Years, relativeTime) - monthsAgo > 0 -> formatUnit(monthsAgo, TimeUnit.Months, relativeTime) - weeksAgo > 0 -> formatUnit(weeksAgo, TimeUnit.Weeks, relativeTime) - daysAgo > 0 -> formatUnit(daysAgo, TimeUnit.Days, relativeTime) - hoursAgo > 0 -> { + units.contains(TimeUnit.Years) && yearsAgo > 0 -> { + formatUnit(yearsAgo, TimeUnit.Years, relativeTime) + } + units.contains(TimeUnit.Months) && monthsAgo > 0 -> { + formatUnit(monthsAgo, TimeUnit.Months, relativeTime) + } + units.contains(TimeUnit.Weeks) && weeksAgo > 0 -> { + formatUnit(weeksAgo, TimeUnit.Weeks, relativeTime) + } + units.contains(TimeUnit.Days) && daysAgo > 0 -> { + formatUnit(daysAgo, TimeUnit.Days, relativeTime) + } + units.contains(TimeUnit.Hours) && hoursAgo > 0 -> { if (rounding == Rounding.UpIfClose && hoursAgo >= 23) { formatUnit(1, TimeUnit.Days, relativeTime) } else { formatUnit(hoursAgo, TimeUnit.Hours, relativeTime) } } - minutesAgo > 0 -> { + units.contains(TimeUnit.Minutes) && minutesAgo > 0 -> { if (rounding == Rounding.UpIfClose && minutesAgo >= 55) { formatUnit(1, TimeUnit.Hours, relativeTime) } else { diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt index a686a11..0775d57 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt @@ -15,7 +15,8 @@ import kotlin.time.Instant internal fun formatTimeAgo( instant: Instant, baseInstant: Instant, - rounding: Rounding + rounding: Rounding, + units: Set ): String { val diff = baseInstant - instant val secondsAgo = diff.inWholeSeconds @@ -25,7 +26,8 @@ internal fun formatTimeAgo( formatDuration( duration = diff.absoluteValue, relativeTime = RelativeTime.Future, - rounding = rounding + rounding = rounding, + units = units ) ) secondsAgo <= 1 -> strings.dateTime.now @@ -33,7 +35,8 @@ internal fun formatTimeAgo( formatDuration( duration = diff.absoluteValue, relativeTime = RelativeTime.Past, - rounding = rounding + rounding = rounding, + units = units ) ) } diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt index 90f0ce4..7f3be5a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt @@ -5,8 +5,8 @@ import nl.jacobras.humanreadable.HumanReadable.strings import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.TenseForms -internal enum class TimeUnit( - val forms: (DateTimeStrings) -> TenseForms +public enum class TimeUnit( + internal val forms: (DateTimeStrings) -> TenseForms ) { Seconds({ it.seconds }), Minutes({ it.minutes }), @@ -16,7 +16,7 @@ internal enum class TimeUnit( Months({ it.months }), Years({ it.years }); - fun format(value: Int, relativeTime: RelativeTime): String { + internal fun format(value: Int, relativeTime: RelativeTime): String { val dateTimeStrings = strings.dateTime val tenseForms = forms(dateTimeStrings) val pluralCategory = dateTimeStrings.plural(value) @@ -29,4 +29,8 @@ internal enum class TimeUnit( ?: tenseForms.present[pluralCategory] ?: error("No translation for $value $this in '${localisation.languageTag}'") } + + internal companion object { + internal val all = entries.toSet() + } } \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 28b5d31..2426765 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -102,4 +102,15 @@ class HumanReadableDurationTests { assertThat(HumanReadable.duration(555.days, rounding = Rounding.UpIfClose)).isEqualTo("2 years") assertThat(HumanReadable.duration(1095.days, rounding = Rounding.UpIfClose)).isEqualTo("3 years") } + + @Test + fun limitedUnits() { + assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Years))).isEqualTo("1 year") + assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Months))).isEqualTo("13 months") + assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Weeks))).isEqualTo("57 weeks") + assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Days))).isEqualTo("400 days") + assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Hours))).isEqualTo("9,600 hours") + assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Minutes))).isEqualTo("576,000 minutes") + assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Seconds))).isEqualTo("34,560,000 seconds") + } } \ No newline at end of file From c60488405c98dc4cccfc8e1e05cc5d5a0131e327 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 24 Jun 2026 22:47:51 +0200 Subject: [PATCH 15/57] Fix duration formatting of numbers > 1000 --- .../humanreadable/time/HumanReadableDuration.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index d666304..351310b 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -73,12 +73,18 @@ private fun formatUnit( unit: TimeUnit, relativeTime: RelativeTime ): String { + val formattedCount = if (count > 1000) { + count.toDouble().formatNumber(decimals = 0) + } else { + count.toString() + } + val unitText = unit.format(count, relativeTime) val languageTag = HumanReadable.localisation.languageTag return when (languageTag) { "ar" if (count == 1 || count == 2) -> unitText - "ko" -> "$count$unitText" - else -> "$count $unitText" + "ko" -> "$formattedCount$unitText" + else -> "$formattedCount $unitText" } } From 7055a6592abd32ea08e254b8ba2c790e2bbbc4e2 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Fri, 26 Jun 2026 10:07:44 +0200 Subject: [PATCH 16/57] Tweak `UpIfClose` rounding to floor by default --- .../time/HumanReadableDuration.kt | 4 +- .../jacobras/humanreadable/time/Rounding.kt | 19 +-- .../time/HumanReadableDurationTests.kt | 157 +++++++++--------- 3 files changed, 91 insertions(+), 89 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index 351310b..2a5e080 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -90,7 +90,7 @@ private fun formatUnit( private fun Float.round(rounding: Rounding): Int { return when (rounding) { - Rounding.Floor -> toInt() - Rounding.HalfUp, Rounding.UpIfClose -> roundToInt() + Rounding.Floor, Rounding.UpIfClose -> toInt() + Rounding.HalfUp -> roundToInt() } } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt index 386a843..72fe9e7 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt @@ -6,9 +6,8 @@ public sealed interface Rounding { * Rounds down. * * Some examples: - * - `11.days` results in "1 week". - * - `13.days` results in "1 week". - * - `14.days` results in "2 weeks". + * - `12.days` results in "1 week". + * - `46.days` results in "1 month". */ public data object Floor : Rounding @@ -16,9 +15,8 @@ public sealed interface Rounding { * Rounds up. * * Some examples: - * - `11.days` results in "2 weeks". - * - `13.days` results in "2 weeks". - * - `14.days` results in "2 weeks". + * - `12.days` results in "2 weeks". + * - `46.days` results in "2 months". */ public data object HalfUp : Rounding @@ -26,11 +24,12 @@ public sealed interface Rounding { * Rounds up if the duration is close to the next unit. * * "Close" is defined as: - * - `55.seconds` rounds up to a minute - * - `55.minutes` rounds up to an hour - * - `23.hours` rounds up to a day + * - `55.seconds` rounds up to a minute. + * - `55.minutes` rounds up to an hour. + * - `23.hours` rounds up to a day. + * - `13.days` rounds up to two weeks. * - * Larger units are rounded half up, e.g. 10.5 days rounds up to 11 days. + * Anything else is rounded **down**. */ public data object UpIfClose : Rounding } \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 2426765..1154842 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -3,7 +3,10 @@ package nl.jacobras.humanreadable.time import assertk.assertThat import assertk.assertions.isEqualTo import nl.jacobras.humanreadable.HumanReadable +import nl.jacobras.humanreadable.HumanReadable.duration import nl.jacobras.humanreadable.localized.LocalisedTests +import nl.jacobras.humanreadable.time.Rounding.Floor +import nl.jacobras.humanreadable.time.Rounding.UpIfClose import kotlin.test.Test import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours @@ -23,94 +26,94 @@ class HumanReadableDurationTests { @Test fun defaultRounding() { - assertThat(HumanReadable.duration(1.seconds)).isEqualTo("1 second") - assertThat(HumanReadable.duration(3.seconds)).isEqualTo("3 seconds") - assertThat(HumanReadable.duration(55.seconds)).isEqualTo("55 seconds") - - assertThat(HumanReadable.duration(1.minutes)).isEqualTo("1 minute") - assertThat(HumanReadable.duration(3.minutes)).isEqualTo("3 minutes") - assertThat(HumanReadable.duration(55.minutes)).isEqualTo("55 minutes") - - assertThat(HumanReadable.duration(1.hours)).isEqualTo("1 hour") - assertThat(HumanReadable.duration(3.hours)).isEqualTo("3 hours") - assertThat(HumanReadable.duration(23.hours)).isEqualTo("23 hours") - - assertThat(HumanReadable.duration(1.days)).isEqualTo("1 day") - assertThat(HumanReadable.duration(3.days)).isEqualTo("3 days") - - assertThat(HumanReadable.duration(7.days)).isEqualTo("1 week") - assertThat(HumanReadable.duration(10.days)).isEqualTo("1 week") - assertThat(HumanReadable.duration(11.days)).isEqualTo("2 weeks") - - assertThat(HumanReadable.duration(21.days)).isEqualTo("1 month") - assertThat(HumanReadable.duration(29.days)).isEqualTo("1 month") - assertThat(HumanReadable.duration(30.days)).isEqualTo("1 month") - assertThat(HumanReadable.duration(31.days)).isEqualTo("1 month") - assertThat(HumanReadable.duration(90.days)).isEqualTo("3 months") - - assertThat(HumanReadable.duration(360.days)).isEqualTo("1 year") - assertThat(HumanReadable.duration(365.days)).isEqualTo("1 year") - assertThat(HumanReadable.duration(555.days)).isEqualTo("2 years") - assertThat(HumanReadable.duration(1095.days)).isEqualTo("3 years") + assertThat(duration(1.seconds)).isEqualTo("1 second") + assertThat(duration(3.seconds)).isEqualTo("3 seconds") + assertThat(duration(55.seconds)).isEqualTo("55 seconds") + + assertThat(duration(1.minutes)).isEqualTo("1 minute") + assertThat(duration(3.minutes)).isEqualTo("3 minutes") + assertThat(duration(55.minutes)).isEqualTo("55 minutes") + + assertThat(duration(1.hours)).isEqualTo("1 hour") + assertThat(duration(3.hours)).isEqualTo("3 hours") + assertThat(duration(23.hours)).isEqualTo("23 hours") + + assertThat(duration(1.days)).isEqualTo("1 day") + assertThat(duration(3.days)).isEqualTo("3 days") + + assertThat(duration(7.days)).isEqualTo("1 week") + assertThat(duration(10.days)).isEqualTo("1 week") + assertThat(duration(11.days)).isEqualTo("2 weeks") + + assertThat(duration(21.days)).isEqualTo("1 month") + assertThat(duration(29.days)).isEqualTo("1 month") + assertThat(duration(30.days)).isEqualTo("1 month") + assertThat(duration(31.days)).isEqualTo("1 month") + assertThat(duration(90.days)).isEqualTo("3 months") + + assertThat(duration(360.days)).isEqualTo("1 year") + assertThat(duration(365.days)).isEqualTo("1 year") + assertThat(duration(555.days)).isEqualTo("2 years") + assertThat(duration(1095.days)).isEqualTo("3 years") } @Test fun floorRounding() { - assertThat(HumanReadable.duration(10.days, rounding = Rounding.Floor)).isEqualTo("1 week") - assertThat(HumanReadable.duration(11.days, rounding = Rounding.Floor)).isEqualTo("1 week") - - assertThat(HumanReadable.duration(21.days, rounding = Rounding.Floor)).isEqualTo("3 weeks") - assertThat(HumanReadable.duration(29.days, rounding = Rounding.Floor)).isEqualTo("4 weeks") - assertThat(HumanReadable.duration(30.days, rounding = Rounding.Floor)).isEqualTo("4 weeks") - assertThat(HumanReadable.duration(31.days, rounding = Rounding.Floor)).isEqualTo("1 month") - - assertThat(HumanReadable.duration(360.days, rounding = Rounding.Floor)).isEqualTo("11 months") - assertThat(HumanReadable.duration(365.days, rounding = Rounding.Floor)).isEqualTo("1 year") - assertThat(HumanReadable.duration(555.days, rounding = Rounding.Floor)).isEqualTo("1 year") - assertThat(HumanReadable.duration(1095.days, rounding = Rounding.Floor)).isEqualTo("3 years") + assertThat(duration(10.days, rounding = Floor)).isEqualTo("1 week") + assertThat(duration(11.days, rounding = Floor)).isEqualTo("1 week") + + assertThat(duration(21.days, rounding = Floor)).isEqualTo("3 weeks") + assertThat(duration(29.days, rounding = Floor)).isEqualTo("4 weeks") + assertThat(duration(30.days, rounding = Floor)).isEqualTo("4 weeks") + assertThat(duration(31.days, rounding = Floor)).isEqualTo("1 month") + + assertThat(duration(360.days, rounding = Floor)).isEqualTo("11 months") + assertThat(duration(365.days, rounding = Floor)).isEqualTo("1 year") + assertThat(duration(555.days, rounding = Floor)).isEqualTo("1 year") + assertThat(duration(1095.days, rounding = Floor)).isEqualTo("3 years") } @Test fun upIfCloseRounding() { - assertThat(HumanReadable.duration(1.seconds, rounding = Rounding.UpIfClose)).isEqualTo("1 second") - assertThat(HumanReadable.duration(3.seconds, rounding = Rounding.UpIfClose)).isEqualTo("3 seconds") - - assertThat(HumanReadable.duration(55.seconds, rounding = Rounding.UpIfClose)).isEqualTo("1 minute") - assertThat(HumanReadable.duration(1.minutes, rounding = Rounding.UpIfClose)).isEqualTo("1 minute") - assertThat(HumanReadable.duration(3.minutes, rounding = Rounding.UpIfClose)).isEqualTo("3 minutes") - - assertThat(HumanReadable.duration(55.minutes, rounding = Rounding.UpIfClose)).isEqualTo("1 hour") - assertThat(HumanReadable.duration(1.hours, rounding = Rounding.UpIfClose)).isEqualTo("1 hour") - assertThat(HumanReadable.duration(3.hours, rounding = Rounding.UpIfClose)).isEqualTo("3 hours") - - assertThat(HumanReadable.duration(23.hours, rounding = Rounding.UpIfClose)).isEqualTo("1 day") - assertThat(HumanReadable.duration(1.days, rounding = Rounding.UpIfClose)).isEqualTo("1 day") - assertThat(HumanReadable.duration(3.days, rounding = Rounding.UpIfClose)).isEqualTo("3 days") - - assertThat(HumanReadable.duration(7.days, rounding = Rounding.UpIfClose)).isEqualTo("1 week") - assertThat(HumanReadable.duration(10.days, rounding = Rounding.UpIfClose)).isEqualTo("1 week") - assertThat(HumanReadable.duration(11.days, rounding = Rounding.UpIfClose)).isEqualTo("2 weeks") - - assertThat(HumanReadable.duration(21.days, rounding = Rounding.UpIfClose)).isEqualTo("1 month") - assertThat(HumanReadable.duration(29.days, rounding = Rounding.UpIfClose)).isEqualTo("1 month") - assertThat(HumanReadable.duration(30.days, rounding = Rounding.UpIfClose)).isEqualTo("1 month") - assertThat(HumanReadable.duration(31.days, rounding = Rounding.UpIfClose)).isEqualTo("1 month") - assertThat(HumanReadable.duration(90.days, rounding = Rounding.UpIfClose)).isEqualTo("3 months") - - assertThat(HumanReadable.duration(360.days, rounding = Rounding.UpIfClose)).isEqualTo("1 year") - assertThat(HumanReadable.duration(365.days, rounding = Rounding.UpIfClose)).isEqualTo("1 year") - assertThat(HumanReadable.duration(555.days, rounding = Rounding.UpIfClose)).isEqualTo("2 years") - assertThat(HumanReadable.duration(1095.days, rounding = Rounding.UpIfClose)).isEqualTo("3 years") + assertThat(duration(1.seconds, rounding = UpIfClose)).isEqualTo("1 second") + assertThat(duration(3.seconds, rounding = UpIfClose)).isEqualTo("3 seconds") + + assertThat(duration(55.seconds, rounding = UpIfClose)).isEqualTo("1 minute") + assertThat(duration(1.minutes, rounding = UpIfClose)).isEqualTo("1 minute") + assertThat(duration(3.minutes, rounding = UpIfClose)).isEqualTo("3 minutes") + + assertThat(duration(55.minutes, rounding = UpIfClose)).isEqualTo("1 hour") + assertThat(duration(1.hours, rounding = UpIfClose)).isEqualTo("1 hour") + assertThat(duration(3.hours, rounding = UpIfClose)).isEqualTo("3 hours") + + assertThat(duration(23.hours, rounding = UpIfClose)).isEqualTo("1 day") + assertThat(duration(1.days, rounding = UpIfClose)).isEqualTo("1 day") + assertThat(duration(3.days, rounding = UpIfClose)).isEqualTo("3 days") + + assertThat(duration(7.days, rounding = UpIfClose)).isEqualTo("1 week") + assertThat(duration(10.days, rounding = UpIfClose)).isEqualTo("1 week") + assertThat(duration(11.days, rounding = UpIfClose)).isEqualTo("1 week") + + assertThat(duration(21.days, rounding = UpIfClose)).isEqualTo("3 weeks") + assertThat(duration(29.days, rounding = UpIfClose)).isEqualTo("4 weeks") + assertThat(duration(30.days, rounding = UpIfClose)).isEqualTo("4 weeks") + assertThat(duration(31.days, rounding = UpIfClose)).isEqualTo("1 month") + assertThat(duration(90.days, rounding = UpIfClose)).isEqualTo("2 months") + + assertThat(duration(360.days, rounding = UpIfClose)).isEqualTo("11 months") + assertThat(duration(365.days, rounding = UpIfClose)).isEqualTo("1 year") + assertThat(duration(555.days, rounding = UpIfClose)).isEqualTo("1 year") + assertThat(duration(1095.days, rounding = UpIfClose)).isEqualTo("3 years") } @Test fun limitedUnits() { - assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Years))).isEqualTo("1 year") - assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Months))).isEqualTo("13 months") - assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Weeks))).isEqualTo("57 weeks") - assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Days))).isEqualTo("400 days") - assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Hours))).isEqualTo("9,600 hours") - assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Minutes))).isEqualTo("576,000 minutes") - assertThat(HumanReadable.duration(400.days, units = setOf(TimeUnit.Seconds))).isEqualTo("34,560,000 seconds") + assertThat(duration(400.days, units = setOf(TimeUnit.Years))).isEqualTo("1 year") + assertThat(duration(400.days, units = setOf(TimeUnit.Months))).isEqualTo("13 months") + assertThat(duration(400.days, units = setOf(TimeUnit.Weeks))).isEqualTo("57 weeks") + assertThat(duration(400.days, units = setOf(TimeUnit.Days))).isEqualTo("400 days") + assertThat(duration(400.days, units = setOf(TimeUnit.Hours))).isEqualTo("9,600 hours") + assertThat(duration(400.days, units = setOf(TimeUnit.Minutes))).isEqualTo("576,000 minutes") + assertThat(duration(400.days, units = setOf(TimeUnit.Seconds))).isEqualTo("34,560,000 seconds") } } \ No newline at end of file From 79b7bcd29d094a7ad2328322163115481b2884d9 Mon Sep 17 00:00:00 2001 From: jacobras Date: Fri, 26 Jun 2026 14:21:27 +0200 Subject: [PATCH 17/57] Extract `UpIfClose` rounding params into constants --- .../humanreadable/time/HumanReadableDuration.kt | 6 +++--- .../kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index 2a5e080..07c4343 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -36,21 +36,21 @@ internal fun formatDuration( formatUnit(daysAgo, TimeUnit.Days, relativeTime) } units.contains(TimeUnit.Hours) && hoursAgo > 0 -> { - if (rounding == Rounding.UpIfClose && hoursAgo >= 23) { + if (rounding == Rounding.UpIfClose && hoursAgo >= TimeUnit.Hours.upIfCloseRollover) { formatUnit(1, TimeUnit.Days, relativeTime) } else { formatUnit(hoursAgo, TimeUnit.Hours, relativeTime) } } units.contains(TimeUnit.Minutes) && minutesAgo > 0 -> { - if (rounding == Rounding.UpIfClose && minutesAgo >= 55) { + if (rounding == Rounding.UpIfClose && minutesAgo >= TimeUnit.Minutes.upIfCloseRollover) { formatUnit(1, TimeUnit.Hours, relativeTime) } else { formatUnit(minutesAgo, TimeUnit.Minutes, relativeTime) } } else -> { - if (rounding == Rounding.UpIfClose && secondsAgo >= 55) { + if (rounding == Rounding.UpIfClose && secondsAgo >= TimeUnit.Seconds.upIfCloseRollover) { formatUnit(1, TimeUnit.Minutes, relativeTime) } else { formatUnit(secondsAgo, TimeUnit.Seconds, relativeTime) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt index 7f3be5a..ebc2804 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt @@ -4,6 +4,7 @@ import nl.jacobras.humanreadable.HumanReadable.localisation import nl.jacobras.humanreadable.HumanReadable.strings import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.TenseForms +import nl.jacobras.humanreadable.time.Rounding.UpIfClose public enum class TimeUnit( internal val forms: (DateTimeStrings) -> TenseForms @@ -16,6 +17,16 @@ public enum class TimeUnit( Months({ it.months }), Years({ it.years }); + /** + * When to roll over to the next larger unit for [UpIfClose] rounding. + */ + internal val upIfCloseRollover: Int + get() = when (this) { + Seconds, Minutes -> 55 + Hours -> 23 + else -> Int.MAX_VALUE + } + internal fun format(value: Int, relativeTime: RelativeTime): String { val dateTimeStrings = strings.dateTime val tenseForms = forms(dateTimeStrings) From 2ff414ba7162df1edd901a0fa8d3987c53ce213d Mon Sep 17 00:00:00 2001 From: jacobras Date: Fri, 26 Jun 2026 14:25:22 +0200 Subject: [PATCH 18/57] Expand configuration options --- .../jacobras/humanreadable/HumanReadable.kt | 42 +++++++++++----- .../humanreadable/time/FormatStyle.kt | 24 +++++++++ .../time/HumanReadableDuration.kt | 13 ++++- .../time/HumanReadableRelativeTime.kt | 25 +++++++--- .../nl/jacobras/humanreadable/time/Parts.kt | 49 +++++++++++++++++++ .../time/HumanReadableDurationTests.kt | 2 + 6 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/time/Parts.kt diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 47339a2..2c3db82 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -2,6 +2,7 @@ package nl.jacobras.humanreadable +import kotlinx.datetime.TimeZone import nl.jacobras.humanreadable.HumanReadable.duration import nl.jacobras.humanreadable.HumanReadable.fallbackLanguageTag import nl.jacobras.humanreadable.HumanReadable.languageTag @@ -51,22 +52,32 @@ public object HumanReadable { * * @param instant The [Instant] to format. * @param baseInstant The base/starting [Instant], defaulting to "now". - * @param rounding The [Rounding] strategy to use. - * @param units The [TimeUnit]s to limit to during formatting. + * @param formatStyle The [FormatStyle] to use, defaulting to [FormatStyle.Regular]. + * @param timeZone If set, today/tomorrow/yesterday will be used. + * @param parts Configures the formatting of multiple parts, defaulting to 1 part. + * @param units The [TimeUnit]s to limit to during formatting, not limited by default. + * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp]. * @return a formatted string */ @OptIn(ExperimentalTime::class) public fun timeAgo( instant: Instant, baseInstant: Instant = Clock.System.now(), - rounding: Rounding = Rounding.HalfUp, - units: Set = TimeUnit.all + formatStyle: FormatStyle = FormatStyle.Regular, + timeZone: TimeZone? = null, + parts: Parts = Parts(), + units: Set = TimeUnit.all, + rounding: Rounding = Rounding.HalfUp ): String { return formatTimeAgo( instant = instant, baseInstant = baseInstant, - rounding = rounding, - units = units + formatStyle = formatStyle, + timeZone = timeZone, + parts = parts, + units = units, + rounding = rounding + ) } @@ -75,20 +86,25 @@ public object HumanReadable { * For example, a duration of 3 seconds returns "3 seconds". * * @param duration The [Duration] to format. - * @param rounding The [Rounding] strategy to use. - * @param units The [TimeUnit]s to limit to during formatting. - * @return a formatted string + * @param formatStyle The [FormatStyle] to use, defaulting to [FormatStyle.Regular]. + * @param parts Configures the formatting of multiple parts, defaulting to 1 part. + * @param units The [TimeUnit]s to limit to during formatting, not limited by default. + * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp]. */ public fun duration( duration: Duration, - rounding: Rounding = Rounding.HalfUp, - units: Set = TimeUnit.all + formatStyle: FormatStyle = FormatStyle.Regular, + parts: Parts = Parts(), + units: Set = TimeUnit.all, + rounding: Rounding = Rounding.HalfUp ): String { return formatDuration( duration = duration, relativeTime = RelativeTime.Present, - rounding = rounding, - units = units + formatStyle = formatStyle, + parts = parts, + units = units, + rounding = rounding ) } diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt new file mode 100644 index 0000000..16a1df7 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt @@ -0,0 +1,24 @@ +package nl.jacobras.humanreadable.time + +public enum class FormatStyle { + + /** + * The default format style, e.g. "1 hour ago". + */ + Regular, + + /** + * Same as [Regular], but with an approxmiation word added if the duration is not exact. + * + * For example, with `Parts` configured to show max. 1 part): + * - `1.hours` will be "1 hour"; + * - `1.hours + 1.minutes` will be "about 1 hour"; + * - `1.hours + 59.minutes` will be "about 1 hour" (with `Floor` rounding). + */ + WithApproximation, + + /** + * Shorter format for limited space, e.g. "1h ago" or "10mo ago" + */ + Abbreviated +} \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index 07c4343..8de80d4 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -7,12 +7,21 @@ import kotlin.time.Duration /** * Returns the given [duration] in human-readable format. + * + * @param duration The duration to format. + * @param relativeTime Whether this is in the past, current or future (for grammatical correctness). + * @param formatStyle The [FormatStyle] to use. + * @param parts Configures the formatting of multiple parts (defaults to 1 part). + * @param units The [TimeUnit]s to limit to during formatting. + * @param rounding The [Rounding] strategy to use. */ internal fun formatDuration( duration: Duration, relativeTime: RelativeTime, - rounding: Rounding, - units: Set + formatStyle: FormatStyle, + parts: Parts, + units: Set, + rounding: Rounding ): String { val secondsAgo = duration.inWholeSeconds.toInt() val minutesAgo = duration.inWholeMinutes.toInt() diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt index 0775d57..2a42098 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt @@ -1,5 +1,6 @@ package nl.jacobras.humanreadable.time +import kotlinx.datetime.TimeZone import nl.jacobras.humanreadable.HumanReadable.strings import kotlin.time.ExperimentalTime import kotlin.time.Instant @@ -10,13 +11,21 @@ import kotlin.time.Instant * * @param instant The [Instant] to compare with [baseInstant]. * @param baseInstant The base/starting [Instant], usually "now". + * @param formatStyle The [FormatStyle] to use. + * @param timeZone If set, today/tomorrow/yesterday will be used. + * @param parts Configures the formatting of multiple parts (defaults to 1 part). + * @param units The [TimeUnit]s to limit to during formatting. + * @param rounding The [Rounding] strategy to use. */ @OptIn(ExperimentalTime::class) internal fun formatTimeAgo( instant: Instant, baseInstant: Instant, - rounding: Rounding, - units: Set + formatStyle: FormatStyle, + timeZone: TimeZone?, + parts: Parts, + units: Set, + rounding: Rounding ): String { val diff = baseInstant - instant val secondsAgo = diff.inWholeSeconds @@ -26,8 +35,10 @@ internal fun formatTimeAgo( formatDuration( duration = diff.absoluteValue, relativeTime = RelativeTime.Future, - rounding = rounding, - units = units + formatStyle = formatStyle, + parts = parts, + units = units, + rounding = rounding ) ) secondsAgo <= 1 -> strings.dateTime.now @@ -35,8 +46,10 @@ internal fun formatTimeAgo( formatDuration( duration = diff.absoluteValue, relativeTime = RelativeTime.Past, - rounding = rounding, - units = units + formatStyle = formatStyle, + parts = parts, + units = units, + rounding = rounding ) ) } diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Parts.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Parts.kt new file mode 100644 index 0000000..87f03b5 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Parts.kt @@ -0,0 +1,49 @@ +package nl.jacobras.humanreadable.time + +import kotlin.time.Duration + +/** + * Configures the formatting of multiple parts when formatting a duration. + * + * Note that only adjacent subparts are used: + * - "1 day, 20 hours" could be output; + * - "1 day, 20 minutes" will never be output (because minutes are not next to days). + */ +public data class Parts( + + /** + * Maximum number of parts to show. + * + * For example, `14.days + 5.hours + 6.minutes + 30.seconds` will be: + * - "14 days, 5 hours, 6 minutes" if this is set to 3; + * - "14 days, 5 hours" if this is set to 2; + * - "14 days" if this is set to 1. + */ + val max: Int = 1, + + /** + * Smallest duration to show. Anything smaller than this will be shown as "less than x". + * + * For example, if this is set to `45.seconds`, then: + * - `44.seconds` will be "less than 45 seconds" + * - `45.seconds` will be "45 seconds" + */ + val smallestDuration: Duration = Duration.ZERO, + + /** + * Controls up to how many of [TimeUnit] smaller subparts should be shown. + * + * For example, if this is set to `[TimeUnit.Minutes = 2]`: + * - `1.minutes + 4.seconds` will be "1 minute, 4 seconds" + * - `2.minutes + 4.seconds` will be "2 minutes" + * + * If this is set to `[TimeUnit.Hours = 20]`: + * - `19.hours + 4.minutes` will be "19 hours, 4 minutes" + * - `20.hours + 4.minutes` will be "20 hours" + */ + val subpartCutOffs: Map = mapOf( + TimeUnit.Minutes to 2, + TimeUnit.Hours to 2, + TimeUnit.Days to 2 + ) +) \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 1154842..2952d48 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -49,6 +49,7 @@ class HumanReadableDurationTests { assertThat(duration(29.days)).isEqualTo("1 month") assertThat(duration(30.days)).isEqualTo("1 month") assertThat(duration(31.days)).isEqualTo("1 month") + assertThat(duration(46.days)).isEqualTo("2 months") assertThat(duration(90.days)).isEqualTo("3 months") assertThat(duration(360.days)).isEqualTo("1 year") @@ -66,6 +67,7 @@ class HumanReadableDurationTests { assertThat(duration(29.days, rounding = Floor)).isEqualTo("4 weeks") assertThat(duration(30.days, rounding = Floor)).isEqualTo("4 weeks") assertThat(duration(31.days, rounding = Floor)).isEqualTo("1 month") + assertThat(duration(46.days, rounding = Floor)).isEqualTo("1 month") assertThat(duration(360.days, rounding = Floor)).isEqualTo("11 months") assertThat(duration(365.days, rounding = Floor)).isEqualTo("1 year") From 1a9425832c566a9184386f1fabc4a1cf979d4083 Mon Sep 17 00:00:00 2001 From: jacobras Date: Fri, 26 Jun 2026 14:27:00 +0200 Subject: [PATCH 19/57] Add tests for new configuration options --- .../time/HumanReadableDurationTests.kt | 77 ++++++++++++++++++- .../time/HumanReadableRelativeTimeTests.kt | 26 +++++++ 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 2952d48..3ed3f69 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -5,8 +5,8 @@ import assertk.assertions.isEqualTo import nl.jacobras.humanreadable.HumanReadable import nl.jacobras.humanreadable.HumanReadable.duration import nl.jacobras.humanreadable.localized.LocalisedTests -import nl.jacobras.humanreadable.time.Rounding.Floor -import nl.jacobras.humanreadable.time.Rounding.UpIfClose +import nl.jacobras.humanreadable.time.FormatStyle.* +import nl.jacobras.humanreadable.time.Rounding.* import kotlin.test.Test import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours @@ -118,4 +118,77 @@ class HumanReadableDurationTests { assertThat(duration(400.days, units = setOf(TimeUnit.Minutes))).isEqualTo("576,000 minutes") assertThat(duration(400.days, units = setOf(TimeUnit.Seconds))).isEqualTo("34,560,000 seconds") } + + @Test + fun multipleParts() { + assertThat( + duration(70.seconds, parts = Parts(max = 2)) + ).isEqualTo("1 minute, 10 seconds") + assertThat( + duration(90.minutes + 10.seconds, parts = Parts(max = 3)) + ).isEqualTo("1 hour, 30 minutes, 10 seconds") + assertThat( + duration(3.seconds, parts = Parts(max = 2)) + ).isEqualTo("3 seconds") + assertThat( + duration(1.hours + 10.seconds, parts = Parts(max = 2)) + ).isEqualTo("1 hour, 10 seconds") + assertThat( + duration(1.minutes + 55.seconds, rounding = HalfUp, parts = Parts(max = 2)) + ).isEqualTo("1 minute, 55 seconds") + assertThat( + duration(1.minutes + 55.seconds, rounding = UpIfClose, parts = Parts(max = 2)) + ).isEqualTo("2 minutes") + } + + @Test + fun smallestDuration() { + assertThat( + duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = Regular) + ).isEqualTo("less than 45 seconds") + assertThat( + duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = WithApproximation) + ).isEqualTo("less than 45 seconds") + assertThat( + duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = Abbreviated) + ).isEqualTo("<45s") + assertThat(duration(45.seconds, parts = Parts(smallestDuration = 45.seconds))).isEqualTo("45 seconds") + } + + @Test + fun subpartsCutOffs() { + assertThat( + duration(1.minutes + 4.seconds, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Minutes to 2))) + ).isEqualTo("1 minute, 4 seconds") + assertThat( + duration(2.minutes + 4.seconds, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Minutes to 2))) + ).isEqualTo("2 minutes") + + assertThat( + duration(19.hours + 4.minutes, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Hours to 2))) + ).isEqualTo("1 minute, 4 seconds") + assertThat( + duration(20.hours + 4.minutes, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Hours to 2))) + ).isEqualTo("2 minutes") + + assertThat( + duration(1.days + 5.hours, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Days to 2))) + ).isEqualTo("1 day, 5 hours") + assertThat( + duration(2.days + 5.hours, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Days to 2))) + ).isEqualTo("2 days") + } + + @Test + fun formatStyle() { + assertThat(duration(1.hours, formatStyle = WithApproximation)).isEqualTo("1 hour") + assertThat(duration(1.hours + 1.minutes, formatStyle = WithApproximation)).isEqualTo("about 1 hour") + + assertThat(duration(1.hours, formatStyle = Regular)).isEqualTo("1 hour") + assertThat(duration(1.hours, formatStyle = Abbreviated)).isEqualTo("1h") + assertThat(duration(14.days, formatStyle = Regular)).isEqualTo("2 weeks") + assertThat(duration(14.days, formatStyle = Abbreviated)).isEqualTo("2w") + assertThat(duration(180.days, formatStyle = Regular)).isEqualTo("5 months") + assertThat(duration(180.days, formatStyle = Abbreviated)).isEqualTo("5mo") + } } \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt index 6df2d66..23ca6ec 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt @@ -2,6 +2,7 @@ package nl.jacobras.humanreadable.time import assertk.assertThat import assertk.assertions.isEqualTo +import kotlinx.datetime.TimeZone import nl.jacobras.humanreadable.HumanReadable import nl.jacobras.humanreadable.localized.LocalisedTests import kotlin.test.Test @@ -40,4 +41,29 @@ class HumanReadableRelativeTimeTests { fun inPast() { assertThat(HumanReadable.timeAgo(now - 3.days, baseInstant = now)).isEqualTo("3 days ago") } + + @Test + fun todayTomorrowYesterday() { + assertThat( + HumanReadable.timeAgo( + instant = now - 1.days, + baseInstant = now, + timeZone = TimeZone.currentSystemDefault() + ) + ).isEqualTo("yesterday") + assertThat( + HumanReadable.timeAgo( + instant = now, + baseInstant = now, + timeZone = TimeZone.currentSystemDefault() + ) + ).isEqualTo("today") + assertThat( + HumanReadable.timeAgo( + instant = now + 1.days, + baseInstant = now, + timeZone = TimeZone.currentSystemDefault() + ) + ).isEqualTo("tomorrow") + } } \ No newline at end of file From 11c8e185f7cd1a894da036a75af66c5b7d583658 Mon Sep 17 00:00:00 2001 From: jacobras Date: Fri, 26 Jun 2026 15:03:13 +0200 Subject: [PATCH 20/57] Implement yesterday/today/tomorrow --- .../nl/jacobras/humanreadable/HumanReadable.kt | 16 ++++++++++++++++ .../humanreadable/i18n/HumanReadableStrings.kt | 5 ++++- .../humanreadable/i18n/translations/ArStrings.kt | 5 ++++- .../humanreadable/i18n/translations/CsStrings.kt | 5 ++++- .../humanreadable/i18n/translations/DeStrings.kt | 5 ++++- .../humanreadable/i18n/translations/ElStrings.kt | 5 ++++- .../humanreadable/i18n/translations/EnStrings.kt | 5 ++++- .../humanreadable/i18n/translations/EsStrings.kt | 5 ++++- .../humanreadable/i18n/translations/FiStrings.kt | 5 ++++- .../humanreadable/i18n/translations/FrStrings.kt | 5 ++++- .../humanreadable/i18n/translations/IdStrings.kt | 5 ++++- .../humanreadable/i18n/translations/ItStrings.kt | 5 ++++- .../humanreadable/i18n/translations/JaStrings.kt | 5 ++++- .../humanreadable/i18n/translations/KkStrings.kt | 5 ++++- .../humanreadable/i18n/translations/KoStrings.kt | 5 ++++- .../humanreadable/i18n/translations/NlStrings.kt | 5 ++++- .../humanreadable/i18n/translations/PlStrings.kt | 5 ++++- .../humanreadable/i18n/translations/PtStrings.kt | 5 ++++- .../humanreadable/i18n/translations/RuStrings.kt | 5 ++++- .../humanreadable/i18n/translations/TrStrings.kt | 5 ++++- .../humanreadable/i18n/translations/UkStrings.kt | 5 ++++- .../humanreadable/i18n/translations/UzStrings.kt | 5 ++++- .../humanreadable/i18n/translations/ViStrings.kt | 5 ++++- .../humanreadable/i18n/translations/ZhStrings.kt | 5 ++++- 24 files changed, 108 insertions(+), 23 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 2c3db82..31e8a52 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -2,7 +2,12 @@ package nl.jacobras.humanreadable +import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.TimeZone +import kotlinx.datetime.minus +import kotlinx.datetime.plus +import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.todayIn import nl.jacobras.humanreadable.HumanReadable.duration import nl.jacobras.humanreadable.HumanReadable.fallbackLanguageTag import nl.jacobras.humanreadable.HumanReadable.languageTag @@ -69,6 +74,17 @@ public object HumanReadable { units: Set = TimeUnit.all, rounding: Rounding = Rounding.HalfUp ): String { + if (timeZone != null) { + val localDate = instant.toLocalDateTime(timeZone).date + val today = Clock.System.todayIn(timeZone) + + when (localDate) { + today.minus(1, DateTimeUnit.DAY) -> return strings.dateTime.yesterday + today -> return strings.dateTime.today + today.plus(1, DateTimeUnit.DAY) -> return strings.dateTime.tomorrow + } + } + return formatTimeAgo( instant = instant, baseInstant = baseInstant, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt index d78a032..05ecdc7 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt @@ -38,7 +38,10 @@ internal data class DateTimeStrings( */ val timeInFuture: (String) -> String, - val now: String + val now: String, + val today: String, + val yesterday: String, + val tomorrow: String ) /** diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt index 08b8c5f..795e132 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt @@ -49,7 +49,10 @@ internal val ArStrings = HumanReadableStrings( }, timeAgo = { "قبل $it" }, timeInFuture = { "بعد $it" }, - now = "الآن" + now = "الآن", + today = "اليوم", + tomorrow = "أمس", + yesterday = "غداً" ), fileSize = FileSizeStrings( byteSymbol = "ب", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt index 8adad39..b5c4aba 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt @@ -48,7 +48,10 @@ internal val CsStrings = HumanReadableStrings( }, timeAgo = { "před $it" }, timeInFuture = { "za $it" }, - now = "nyní" + now = "nyní", + today = "dnes", + yesterday = "včera", + tomorrow = "zítra" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt index 171964a..4829aab 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt @@ -28,7 +28,10 @@ internal val DeStrings = HumanReadableStrings( }, timeAgo = { "vor $it" }, timeInFuture = { "in $it" }, - now = "jetzt" + now = "jetzt", + today = "heute", + yesterday = "gestern", + tomorrow = "morgen" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt index 60fc6eb..29ed1b6 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt @@ -22,7 +22,10 @@ internal val ElStrings = HumanReadableStrings( years = presentTense(one = "έτος", other = "έτη"), timeAgo = { "$it πριν" }, timeInFuture = { "σε $it" }, - now = "τώρα" + now = "τώρα", + today = "σήμερα", + yesterday = "χθες", + tomorrow = "αύριο" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt index 039bb29..a73787a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt @@ -20,6 +20,9 @@ internal val EnStrings = HumanReadableStrings( years = presentTense(one = "year", other = "years"), timeAgo = { "$it ago" }, timeInFuture = { "in $it" }, - now = "now" + now = "now", + today = "today", + yesterday = "yesterday", + tomorrow = "tomorrow" ) ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt index d855b25..1495127 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt @@ -18,7 +18,10 @@ internal val EsStrings = HumanReadableStrings( years = presentTense(one = "año", other = "años"), timeAgo = { "hace $it" }, timeInFuture = { "en $it" }, - now = "ahora" + now = "ahora", + today = "hoy", + yesterday = "ayer", + tomorrow = "mañana" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt index 463a925..a32907e 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt @@ -40,7 +40,10 @@ internal val FiStrings = HumanReadableStrings( }, timeAgo = { "$it sitten" }, timeInFuture = { "$it kuluttua" }, - now = "nyt" + now = "nyt", + today = "tänään", + yesterday = "eilen", + tomorrow = "huomenna" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ","), fileSize = FileSizeStrings( diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt index 2459c60..d3afb24 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt @@ -19,7 +19,10 @@ internal val FrStrings = HumanReadableStrings( years = presentTense(one = "an", other = "ans"), timeAgo = { "il y a $it" }, timeInFuture = { "dans $it" }, - now = "maintenant" + now = "maintenant", + today = "aujourd'hui", + yesterday = "hier", + tomorrow = "demain" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ","), fileSize = FileSizeStrings( diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt index f774a2d..ec54b2a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt @@ -18,7 +18,10 @@ internal val IdStrings = HumanReadableStrings( years = presentTense(other = "tahun"), timeAgo = { "$it yang lalu" }, timeInFuture = { "dalam $it" }, - now = "sekarang" + now = "sekarang", + today = "hari ini", + yesterday = "kemarin", + tomorrow = "besok" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt index 2590659..35564cc 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt @@ -18,7 +18,10 @@ internal val ItStrings = HumanReadableStrings( years = presentTense(one = "anno", other = "anni"), timeAgo = { "$it fa" }, timeInFuture = { "tra $it" }, - now = "adesso" + now = "adesso", + today = "oggi", + yesterday = "ieri", + tomorrow = "domani" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt index e565608..d741188 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt @@ -17,6 +17,9 @@ internal val JaStrings = HumanReadableStrings( years = presentTense(other = "年"), timeAgo = { "${it}前" }, timeInFuture = { "${it}後" }, - now = "今" + now = "今", + today = "今日", + yesterday = "昨日", + tomorrow = "明日" ) ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt index b0782eb..ce1f2e9 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt @@ -26,6 +26,9 @@ internal val KkStrings = HumanReadableStrings( years = multipleTenses { present(one = "жыл", other = "жыл"); future(one = "жылдан", other = "жылдан") }, timeAgo = { "$it бұрын" }, timeInFuture = { "$it кейін" }, - now = "қазір" + now = "қазір", + today = "бүгін", + yesterday = "кеше", + tomorrow = "ертең" ) ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt index cdfbbd2..79c7db9 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt @@ -17,6 +17,9 @@ internal val KoStrings = HumanReadableStrings( years = presentTense(other = "년"), timeAgo = { "$it 전" }, timeInFuture = { "$it 후" }, - now = "지금" + now = "지금", + today = "오늘", + yesterday = "어제", + tomorrow = "내일" ) ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt index 02c96cd..1a3522a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt @@ -18,7 +18,10 @@ internal val NlStrings = HumanReadableStrings( years = presentTense(one = "jaar", other = "jaar"), timeAgo = { "$it geleden" }, timeInFuture = { "over $it" }, - now = "nu" + now = "nu", + today = "vandaag", + yesterday = "gisteren", + tomorrow = "morgen" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt index 0a94a4f..102b593 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt @@ -34,7 +34,10 @@ internal val PlStrings = HumanReadableStrings( years = presentTense(one = "rok", few = "lata", many = "lat"), timeAgo = { "$it temu" }, timeInFuture = { "za $it" }, - now = "teraz" + now = "teraz", + today = "dzisiaj", + yesterday = "wczoraj", + tomorrow = "jutro" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt index da28777..d1864b4 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt @@ -24,7 +24,10 @@ internal val PtStrings = HumanReadableStrings( years = presentTense(one = "ano", many = "de anos", other = "anos"), timeAgo = { "há $it" }, timeInFuture = { "em $it" }, - now = "agora" + now = "agora", + today = "hoje", + yesterday = "ontem", + tomorrow = "amanhã" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt index cc30ef3..0ea6707 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt @@ -34,7 +34,10 @@ internal val RuStrings = HumanReadableStrings( years = presentTense(one = "год", few = "года", many = "лет"), timeAgo = { "$it назад" }, timeInFuture = { "через $it" }, - now = "сейчас" + now = "сейчас", + today = "сегодня", + yesterday = "вчера", + tomorrow = "завтра" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt index ec3d2f6..9e9464b 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt @@ -18,7 +18,10 @@ internal val TrStrings = HumanReadableStrings( years = presentTense(other = "yıl"), timeAgo = { "$it önce" }, timeInFuture = { "$it sonra" }, - now = "şimdi" + now = "şimdi", + today = "bugün", + yesterday = "dün", + tomorrow = "yarın" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt index b3af0df..b79b5ef 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt @@ -34,7 +34,10 @@ internal val UkStrings = HumanReadableStrings( years = presentTense(one = "рік", few = "роки", many = "років"), timeAgo = { "$it тому" }, timeInFuture = { "через $it" }, - now = "зараз" + now = "зараз", + today = "сьогодні", + yesterday = "вчора", + tomorrow = "завтра" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt index 9e16a73..d618e8a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt @@ -39,7 +39,10 @@ internal val UzStrings = HumanReadableStrings( }, timeAgo = { "$it oldin" }, timeInFuture = { "$it keyin" }, - now = "hozir" + now = "hozir", + today = "bugun", + yesterday = "kecha", + tomorrow = "ertaga" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt index 1a74a08..eaf7360 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt @@ -18,7 +18,10 @@ internal val ViStrings = HumanReadableStrings( years = presentTense(other = "năm"), timeAgo = { "$it trước" }, timeInFuture = { "sau $it" }, - now = "bây giờ" + now = "bây giờ", + today = "hôm nay", + yesterday = "hôm qua", + tomorrow = "ngày mai" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt index e71559b..4274cf7 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt @@ -17,6 +17,9 @@ internal val ZhStrings = HumanReadableStrings( years = presentTense(other = "年"), timeAgo = { "${it}之前" }, timeInFuture = { "${it}之后" }, - now = "现在" + now = "现在", + today = "今天", + yesterday = "昨天", + tomorrow = "明天" ) ) \ No newline at end of file From 217aafa208a1ba131b3cd6da69e690f3c67873d1 Mon Sep 17 00:00:00 2001 From: jacobras Date: Fri, 26 Jun 2026 15:03:47 +0200 Subject: [PATCH 21/57] Remove wildcard import --- .../kotlin/nl/jacobras/humanreadable/HumanReadable.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 31e8a52..0606aea 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -14,7 +14,13 @@ import nl.jacobras.humanreadable.HumanReadable.languageTag import nl.jacobras.humanreadable.HumanReadable.number import nl.jacobras.humanreadable.i18n.HumanReadableStrings import nl.jacobras.humanreadable.i18n.Localisation -import nl.jacobras.humanreadable.time.* +import nl.jacobras.humanreadable.time.FormatStyle +import nl.jacobras.humanreadable.time.Parts +import nl.jacobras.humanreadable.time.RelativeTime +import nl.jacobras.humanreadable.time.Rounding +import nl.jacobras.humanreadable.time.TimeUnit +import nl.jacobras.humanreadable.time.formatDuration +import nl.jacobras.humanreadable.time.formatTimeAgo import kotlin.time.Clock import kotlin.time.Duration import kotlin.time.ExperimentalTime From 4e2a1508a969e32d60a8f7dda7f960cdaa0c2d71 Mon Sep 17 00:00:00 2001 From: jacobras Date: Sat, 27 Jun 2026 12:41:30 +0200 Subject: [PATCH 22/57] Tweak `FormatStyle`, splitting up Short/Narrow and adding Digital variants --- .../jacobras/humanreadable/HumanReadable.kt | 8 +-- .../humanreadable/time/FormatStyle.kt | 43 +++++++++++++--- .../time/HumanReadableDurationTests.kt | 49 +++++++++++++------ 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 0606aea..5a9d517 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -63,7 +63,7 @@ public object HumanReadable { * * @param instant The [Instant] to format. * @param baseInstant The base/starting [Instant], defaulting to "now". - * @param formatStyle The [FormatStyle] to use, defaulting to [FormatStyle.Regular]. + * @param formatStyle The [FormatStyle] to use, defaulting to [FormatStyle.Long]. * @param timeZone If set, today/tomorrow/yesterday will be used. * @param parts Configures the formatting of multiple parts, defaulting to 1 part. * @param units The [TimeUnit]s to limit to during formatting, not limited by default. @@ -74,7 +74,7 @@ public object HumanReadable { public fun timeAgo( instant: Instant, baseInstant: Instant = Clock.System.now(), - formatStyle: FormatStyle = FormatStyle.Regular, + formatStyle: FormatStyle = FormatStyle.Long, timeZone: TimeZone? = null, parts: Parts = Parts(), units: Set = TimeUnit.all, @@ -108,14 +108,14 @@ public object HumanReadable { * For example, a duration of 3 seconds returns "3 seconds". * * @param duration The [Duration] to format. - * @param formatStyle The [FormatStyle] to use, defaulting to [FormatStyle.Regular]. + * @param formatStyle The [FormatStyle] to use, defaulting to [FormatStyle.Long]. * @param parts Configures the formatting of multiple parts, defaulting to 1 part. * @param units The [TimeUnit]s to limit to during formatting, not limited by default. * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp]. */ public fun duration( duration: Duration, - formatStyle: FormatStyle = FormatStyle.Regular, + formatStyle: FormatStyle = FormatStyle.Long, parts: Parts = Parts(), units: Set = TimeUnit.all, rounding: Rounding = Rounding.HalfUp diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt index 16a1df7..cf6cf2c 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt @@ -5,20 +5,51 @@ public enum class FormatStyle { /** * The default format style, e.g. "1 hour ago". */ - Regular, + Long, /** - * Same as [Regular], but with an approxmiation word added if the duration is not exact. + * Same as [Long], but with an approxmiation word added if the duration is not exact. * - * For example, with `Parts` configured to show max. 1 part): + * For example, with `Parts` configured to show max. 1 part: * - `1.hours` will be "1 hour"; * - `1.hours + 1.minutes` will be "about 1 hour"; * - `1.hours + 59.minutes` will be "about 1 hour" (with `Floor` rounding). */ - WithApproximation, + LongApproximate, /** - * Shorter format for limited space, e.g. "1h ago" or "10mo ago" + * Shorter format for limited space. + * + * Examples: + * - Time: "1 hr, 50 min" + * - Time and date: "3 wks, 4 days, 1 hr, 50 min" + */ + Short, + + /** + * Like [Short], but with digital time. + * + * Examples: + * - Time: "1:50:00" + * - Time and date: "3 wks, 4 days, 1:50:00" + */ + ShortDigital, + + /** + * Shortest format for limited space. + * + * Examples: + * - Time: "1h 50m" + * - Time and date: "3w, 4d, 1h 50m" + */ + Narrow, + + /** + * Like [Narrow], but with digital time. + * + * Examples: + * - Time: "1:50:00" + * - Time and date: "3w, 4d, 1:50:00" */ - Abbreviated + NarrowDigital } \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 3ed3f69..b0cfdd4 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -5,13 +5,18 @@ import assertk.assertions.isEqualTo import nl.jacobras.humanreadable.HumanReadable import nl.jacobras.humanreadable.HumanReadable.duration import nl.jacobras.humanreadable.localized.LocalisedTests -import nl.jacobras.humanreadable.time.FormatStyle.* -import nl.jacobras.humanreadable.time.Rounding.* +import nl.jacobras.humanreadable.time.Rounding.Floor +import nl.jacobras.humanreadable.time.Rounding.HalfUp +import nl.jacobras.humanreadable.time.Rounding.UpIfClose import kotlin.test.Test import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +import nl.jacobras.humanreadable.time.FormatStyle.Long as FormatLong +import nl.jacobras.humanreadable.time.FormatStyle.LongApproximate as FormatApproximate +import nl.jacobras.humanreadable.time.FormatStyle.Narrow as FormatNarrow +import nl.jacobras.humanreadable.time.FormatStyle.Short as FormatShort /** * Main test for the specific configuration options related to duration formatting. @@ -144,13 +149,13 @@ class HumanReadableDurationTests { @Test fun smallestDuration() { assertThat( - duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = Regular) + duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = FormatLong) ).isEqualTo("less than 45 seconds") assertThat( - duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = WithApproximation) + duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = FormatApproximate) ).isEqualTo("less than 45 seconds") assertThat( - duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = Abbreviated) + duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = FormatNarrow) ).isEqualTo("<45s") assertThat(duration(45.seconds, parts = Parts(smallestDuration = 45.seconds))).isEqualTo("45 seconds") } @@ -181,14 +186,30 @@ class HumanReadableDurationTests { @Test fun formatStyle() { - assertThat(duration(1.hours, formatStyle = WithApproximation)).isEqualTo("1 hour") - assertThat(duration(1.hours + 1.minutes, formatStyle = WithApproximation)).isEqualTo("about 1 hour") - - assertThat(duration(1.hours, formatStyle = Regular)).isEqualTo("1 hour") - assertThat(duration(1.hours, formatStyle = Abbreviated)).isEqualTo("1h") - assertThat(duration(14.days, formatStyle = Regular)).isEqualTo("2 weeks") - assertThat(duration(14.days, formatStyle = Abbreviated)).isEqualTo("2w") - assertThat(duration(180.days, formatStyle = Regular)).isEqualTo("5 months") - assertThat(duration(180.days, formatStyle = Abbreviated)).isEqualTo("5mo") + assertThat( + duration(1.hours + 50.minutes, formatStyle = FormatLong, parts = Parts(max = 2)) + ).isEqualTo("1 hour, 50 minutes") + assertThat( + duration(1.hours + 50.minutes, formatStyle = FormatShort, parts = Parts(max = 2)) + ).isEqualTo("1 hr, 50 min") + assertThat( + duration(1.hours + 50.minutes, formatStyle = FormatNarrow, parts = Parts(max = 2)) + ).isEqualTo("1h 50m") + + assertThat(duration(1.hours, formatStyle = FormatLong)).isEqualTo("1 hour") + assertThat(duration(1.hours, formatStyle = FormatShort)).isEqualTo("1 hr") + assertThat(duration(1.hours, formatStyle = FormatNarrow)).isEqualTo("1h") + assertThat(duration(14.days, formatStyle = FormatLong)).isEqualTo("2 weeks") + assertThat(duration(14.days, formatStyle = FormatShort)).isEqualTo("2 wks") + assertThat(duration(14.days, formatStyle = FormatNarrow)).isEqualTo("2w") + assertThat(duration(180.days, formatStyle = FormatLong)).isEqualTo("5 months") + assertThat(duration(180.days, formatStyle = FormatShort)).isEqualTo("5 mths") + assertThat(duration(180.days, formatStyle = FormatNarrow)).isEqualTo("5m") + } + + @Test + fun formatStyleApproximation() { + assertThat(duration(1.hours, formatStyle = FormatApproximate)).isEqualTo("1 hour") + assertThat(duration(1.hours + 1.minutes, formatStyle = FormatApproximate)).isEqualTo("about 1 hour") } } \ No newline at end of file From 042eafc91c3eb213c73c5668d8f16211146e941d Mon Sep 17 00:00:00 2001 From: jacobras Date: Sat, 27 Jun 2026 15:44:18 +0200 Subject: [PATCH 23/57] Add Unicode links for translators --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5c322a4..508b17e 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ HumanReadable.abbreviation(2_500_000, decimals = 1) // "2.5M" Available since version 1.10. ```kotlin -// English/default +// English HumanReadable.number(1_000_000.34) // "1,000,000.34" // French @@ -167,4 +167,8 @@ Missing a language? Feel free to open an issue about it. Or, add it yourself: example of other translations. 3. Add a new entry to the `translations` map in `src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations.kt`. -4. Open a PR. \ No newline at end of file +4. Open a PR. + +Follow the Unicode spec at +or , if you prefer the +JSON format. \ No newline at end of file From 1c188b09eb3f09093fc1cbf2aa4eff1c121e205c Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Tue, 30 Jun 2026 09:12:11 +0200 Subject: [PATCH 24/57] Make yesterday/today/tomorrow work for `LocalDate`s --- .../jacobras/humanreadable/HumanReadable.kt | 49 ++++++++------ .../time/HumanReadableRelativeTime.kt | 64 +++++++++++++++++-- .../time/HumanReadableRelativeTimeTests.kt | 22 ++++--- 3 files changed, 103 insertions(+), 32 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 5a9d517..8d062df 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -2,11 +2,8 @@ package nl.jacobras.humanreadable -import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone -import kotlinx.datetime.minus -import kotlinx.datetime.plus -import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.todayIn import nl.jacobras.humanreadable.HumanReadable.duration import nl.jacobras.humanreadable.HumanReadable.fallbackLanguageTag @@ -64,7 +61,6 @@ public object HumanReadable { * @param instant The [Instant] to format. * @param baseInstant The base/starting [Instant], defaulting to "now". * @param formatStyle The [FormatStyle] to use, defaulting to [FormatStyle.Long]. - * @param timeZone If set, today/tomorrow/yesterday will be used. * @param parts Configures the formatting of multiple parts, defaulting to 1 part. * @param units The [TimeUnit]s to limit to during formatting, not limited by default. * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp]. @@ -75,31 +71,48 @@ public object HumanReadable { instant: Instant, baseInstant: Instant = Clock.System.now(), formatStyle: FormatStyle = FormatStyle.Long, - timeZone: TimeZone? = null, parts: Parts = Parts(), units: Set = TimeUnit.all, rounding: Rounding = Rounding.HalfUp ): String { - if (timeZone != null) { - val localDate = instant.toLocalDateTime(timeZone).date - val today = Clock.System.todayIn(timeZone) - - when (localDate) { - today.minus(1, DateTimeUnit.DAY) -> return strings.dateTime.yesterday - today -> return strings.dateTime.today - today.plus(1, DateTimeUnit.DAY) -> return strings.dateTime.tomorrow - } - } - return formatTimeAgo( instant = instant, baseInstant = baseInstant, formatStyle = formatStyle, - timeZone = timeZone, parts = parts, units = units, rounding = rounding + ) + } + /** + * Returns the difference between [baseDate] and [date], in human-readable format. Also supports + * dates in the future or past. For example, a date that's 2 days ago will return "2 days ago". + * + * @param date The [LocalDate] to format. + * @param baseDate The base/starting [LocalDate], defaulting to "today". + * @param formatStyle The [FormatStyle] to use, defaulting to [FormatStyle.Long]. + * @param parts Configures the formatting of multiple parts, defaulting to 1 part. + * @param units The [TimeUnit]s to limit to during formatting, not limited by default. + * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp]. + * @return a formatted string + */ + @OptIn(ExperimentalTime::class) + public fun timeAgo( + date: LocalDate, + baseDate: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), + formatStyle: FormatStyle = FormatStyle.Long, + parts: Parts = Parts(), + units: Set = TimeUnit.all, + rounding: Rounding = Rounding.HalfUp + ): String { + return formatTimeAgo( + date = date, + baseDate = baseDate, + formatStyle = formatStyle, + parts = parts, + units = units, + rounding = rounding ) } diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt index 2a42098..596d650 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt @@ -1,18 +1,22 @@ package nl.jacobras.humanreadable.time -import kotlinx.datetime.TimeZone +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import kotlinx.datetime.plus import nl.jacobras.humanreadable.HumanReadable.strings +import kotlin.math.absoluteValue +import kotlin.time.Duration.Companion.seconds import kotlin.time.ExperimentalTime import kotlin.time.Instant /** * Returns the difference between [baseInstant] and [instant], in human-readable format. - * Also supports instants in the future. + * Also supports instants in the future or past. * * @param instant The [Instant] to compare with [baseInstant]. * @param baseInstant The base/starting [Instant], usually "now". * @param formatStyle The [FormatStyle] to use. - * @param timeZone If set, today/tomorrow/yesterday will be used. * @param parts Configures the formatting of multiple parts (defaults to 1 part). * @param units The [TimeUnit]s to limit to during formatting. * @param rounding The [Rounding] strategy to use. @@ -22,7 +26,6 @@ internal fun formatTimeAgo( instant: Instant, baseInstant: Instant, formatStyle: FormatStyle, - timeZone: TimeZone?, parts: Parts, units: Set, rounding: Rounding @@ -53,4 +56,57 @@ internal fun formatTimeAgo( ) ) } +} + +/** + * Returns the difference between [date] and [baseDate], in human-readable format. + * Also supports dates in the future or past. + * + * Will return "today", "tomorrow" or "yesterday" if the date is today, tomorrow, or yesterday. + * + * @param date The [LocalDate] to compare with [baseDate]. + * @param baseDate The base/starting [LocalDate], usually "today". + * @param formatStyle The [FormatStyle] to use. + * @param parts Configures the formatting of multiple parts (defaults to 1 part). + * @param units The [TimeUnit]s to limit to during formatting. + * @param rounding The [Rounding] strategy to use. + */ +internal fun formatTimeAgo( + date: LocalDate, + baseDate: LocalDate, + formatStyle: FormatStyle, + parts: Parts, + units: Set, + rounding: Rounding +): String { + when (date) { + baseDate.minus(1, DateTimeUnit.DAY) -> return strings.dateTime.yesterday + baseDate -> return strings.dateTime.today + baseDate.plus(1, DateTimeUnit.DAY) -> return strings.dateTime.tomorrow + } + val secondsAgo = (baseDate - date).seconds + + return when { + secondsAgo < 0 -> strings.dateTime.timeInFuture( + formatDuration( + duration = secondsAgo.absoluteValue.seconds, + relativeTime = RelativeTime.Future, + formatStyle = formatStyle, + parts = parts, + units = units, + rounding = rounding + ) + ) + secondsAgo <= 1 -> strings.dateTime.now + else -> strings.dateTime.timeAgo( + formatDuration( + duration = secondsAgo.absoluteValue.seconds, + relativeTime = RelativeTime.Past, + formatStyle = formatStyle, + parts = parts, + units = units, + rounding = rounding + ) + ) + } } \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt index 23ca6ec..4b3112b 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt @@ -2,7 +2,10 @@ package nl.jacobras.humanreadable.time import assertk.assertThat import assertk.assertions.isEqualTo -import kotlinx.datetime.TimeZone +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import kotlinx.datetime.plus import nl.jacobras.humanreadable.HumanReadable import nl.jacobras.humanreadable.localized.LocalisedTests import kotlin.test.Test @@ -44,25 +47,24 @@ class HumanReadableRelativeTimeTests { @Test fun todayTomorrowYesterday() { + val today = LocalDate.parse("2026-07-01") + assertThat( HumanReadable.timeAgo( - instant = now - 1.days, - baseInstant = now, - timeZone = TimeZone.currentSystemDefault() + date = today.minus(1, DateTimeUnit.DAY), + baseDate = today ) ).isEqualTo("yesterday") assertThat( HumanReadable.timeAgo( - instant = now, - baseInstant = now, - timeZone = TimeZone.currentSystemDefault() + date = today, + baseDate = today ) ).isEqualTo("today") assertThat( HumanReadable.timeAgo( - instant = now + 1.days, - baseInstant = now, - timeZone = TimeZone.currentSystemDefault() + date = today.plus(1, DateTimeUnit.DAY), + baseDate = today ) ).isEqualTo("tomorrow") } From dbb9ba05474a981749ffa4b8eadeece51768b392 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Mon, 29 Jun 2026 23:29:28 +0200 Subject: [PATCH 25/57] Implement new configurability options --- .../i18n/HumanReadableStrings.kt | 37 ++++- .../jacobras/humanreadable/i18n/TenseForms.kt | 5 +- .../i18n/translations/ArStrings.kt | 14 +- .../i18n/translations/CsStrings.kt | 14 +- .../i18n/translations/DeStrings.kt | 14 +- .../i18n/translations/ElStrings.kt | 14 +- .../i18n/translations/EnStrings.kt | 44 ++++-- .../i18n/translations/EsStrings.kt | 14 +- .../i18n/translations/FiStrings.kt | 14 +- .../i18n/translations/FrStrings.kt | 14 +- .../i18n/translations/IdStrings.kt | 14 +- .../i18n/translations/ItStrings.kt | 14 +- .../i18n/translations/JaStrings.kt | 14 +- .../i18n/translations/KkStrings.kt | 14 +- .../i18n/translations/KoStrings.kt | 14 +- .../i18n/translations/NlStrings.kt | 14 +- .../i18n/translations/PlStrings.kt | 14 +- .../i18n/translations/PtStrings.kt | 14 +- .../i18n/translations/RuStrings.kt | 14 +- .../i18n/translations/TrStrings.kt | 14 +- .../i18n/translations/UkStrings.kt | 14 +- .../i18n/translations/UzStrings.kt | 14 +- .../i18n/translations/ViStrings.kt | 14 +- .../i18n/translations/ZhStrings.kt | 14 +- .../humanreadable/time/FormatStyle.kt | 45 +++++- .../time/HumanReadableDuration.kt | 144 ++++++++++++------ .../jacobras/humanreadable/time/TimeUnit.kt | 85 +++++++++-- .../time/HumanReadableDurationTests.kt | 49 ++++-- 28 files changed, 465 insertions(+), 238 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt index 05ecdc7..0c84091 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt @@ -20,13 +20,32 @@ internal data class DateTimeStrings( * See [Unicode Plural Rules](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html). */ val plural: (Int) -> Plural, - val seconds: TenseForms, - val minutes: TenseForms, - val hours: TenseForms, - val days: TenseForms, - val weeks: TenseForms, - val months: TenseForms, - val years: TenseForms, + + val secondsLong: TenseForms, + val minutesLong: TenseForms, + val hoursLong: TenseForms, + val daysLong: TenseForms, + val weeksLong: TenseForms, + val monthsLong: TenseForms, + val yearsLong: TenseForms, + + // TODO: remove defaults + val secondsShort: TenseForms = presentTense(one = "hr"), + val minutesShort: TenseForms = presentTense(one = "h"), + val hoursShort: TenseForms = presentTense(one = "day", other = "days"), + val daysShort: TenseForms = presentTense(one = "day", other = "days"), + val weeksShort: TenseForms = presentTense(one = "d"), + val monthsShort: TenseForms = presentTense(one = "week", other = "weeks"), + val yearsShort: TenseForms = presentTense(one = "wk", other = "wks"), + + // TODO: remove defaults + val secondsNarrow: TenseForms = presentTense(one = "w"), + val minutesNarrow: TenseForms = presentTense(one = "month", other = "months"), + val hoursNarrow: TenseForms = presentTense(one = "mth", other = "mths"), + val daysNarrow: TenseForms = presentTense(one = "m"), + val weeksNarrow: TenseForms = presentTense(one = "year", other = "years"), + val monthsNarrow: TenseForms = presentTense(one = "yr", other = "yrs"), + val yearsNarrow: TenseForms = presentTense(one = "y"), /** * Wraps a formatted duration as past relative time, e.g. "3 days ago". @@ -41,7 +60,9 @@ internal data class DateTimeStrings( val now: String, val today: String, val yesterday: String, - val tomorrow: String + val tomorrow: String, + val lessThan: String = "!!TODO", + val about: String = "!!TODO" ) /** diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/TenseForms.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/TenseForms.kt index 93cf6ca..11e0418 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/TenseForms.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/TenseForms.kt @@ -38,7 +38,8 @@ private fun buildMap( /** * Shortcut for languages that don't need past/future grammatical-case overrides. * - * Use [multipleTenses] when past/future grammatical-case overrides are needed. + * - If there's no difference between the different categories, just fill [one]. + * - Use [multipleTenses] when past/future grammatical-case overrides are needed. */ internal fun presentTense( zero: String = "", @@ -97,7 +98,7 @@ internal class TenseFormsBuilder { } /** - * Sets both [past] and [future] to the same forms, for languages where they are identical.\ + * Sets both [past] and [future] to the same forms, for languages where they are identical. */ fun pastOrFuture( zero: String = "", one: String = "", two: String = "", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt index 795e132..f40753a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt @@ -19,31 +19,31 @@ internal val ArStrings = HumanReadableStrings( else -> Plural.Other } }, - seconds = multipleTenses { + secondsLong = multipleTenses { present(zero = "ثانية", one = "ثانية", two = "ثانيتان", few = "ثوان", many = "ثانية", other = "ثانية") pastOrFuture(two = "ثانيتين") }, - minutes = multipleTenses { + minutesLong = multipleTenses { present(zero = "دقيقة", one = "دقيقة", two = "دقيقتان", few = "دقائق", many = "دقيقة", other = "دقيقة") pastOrFuture(two = "دقيقتين") }, - hours = multipleTenses { + hoursLong = multipleTenses { present(zero = "ساعة", one = "ساعة", two = "ساعتان", few = "ساعات", many = "ساعة", other = "ساعة") pastOrFuture(two = "ساعتين") }, - days = multipleTenses { + daysLong = multipleTenses { present(zero = "يوم", one = "يوم", two = "يومان", few = "أيام", many = "يوم", other = "يوم") pastOrFuture(two = "يومين") }, - weeks = multipleTenses { + weeksLong = multipleTenses { present(zero = "أسبوع", one = "أسبوع", two = "أسبوعان", few = "أسابيع", many = "أسبوع", other = "أسبوع") pastOrFuture(two = "أسبوعين") }, - months = multipleTenses { + monthsLong = multipleTenses { present(zero = "شهر", one = "شهر", two = "شهران", few = "أشهر", many = "شهر", other = "شهر") pastOrFuture(two = "شهرين") }, - years = multipleTenses { + yearsLong = multipleTenses { present(zero = "سنة", one = "سنة", two = "سنتان", few = "سنوات", many = "سنة", other = "سنة") pastOrFuture(two = "سنتين") }, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt index b5c4aba..658a30c 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt @@ -15,34 +15,34 @@ internal val CsStrings = HumanReadableStrings( else -> Plural.Other } }, - seconds = multipleTenses { + secondsLong = multipleTenses { present(one = "sekunda", few = "sekundy", other = "sekund") past(one = "sekundou", few = "sekundami", other = "sekundami") future(one = "sekundu", few = "sekundy", other = "sekund") }, - minutes = multipleTenses { + minutesLong = multipleTenses { present(one = "minuta", few = "minuty", other = "minut") past(one = "minutou", few = "minutami", other = "minutami") future(one = "minutu", few = "minuty", other = "minut") }, - hours = multipleTenses { + hoursLong = multipleTenses { present(one = "hodina", few = "hodiny", other = "hodin") past(one = "hodinou", few = "hodinami", other = "hodinami") future(one = "hodinu", few = "hodiny", other = "hodin") }, - days = multipleTenses { + daysLong = multipleTenses { present(one = "den", few = "dny", other = "dní") past(one = "dnem", few = "dny", other = "dny") }, - weeks = multipleTenses { + weeksLong = multipleTenses { present(one = "týden", few = "týdny", other = "týdnů") past(one = "týdnem", few = "týdny", other = "týdny") }, - months = multipleTenses { + monthsLong = multipleTenses { present(one = "měsíc", few = "měsíce", other = "měsíců") past(one = "měsícem", few = "měsíci", other = "měsíci") }, - years = multipleTenses { + yearsLong = multipleTenses { present(one = "rok", few = "roky", other = "let") past(one = "rokem", few = "roky", other = "lety") }, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt index 4829aab..8ec251a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt @@ -10,19 +10,19 @@ import nl.jacobras.humanreadable.i18n.NumberStrings internal val DeStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = presentTense(one = "Sekunde", other = "Sekunden"), - minutes = presentTense(one = "Minute", other = "Minuten"), - hours = presentTense(one = "Stunde", other = "Stunden"), - days = multipleTenses { + secondsLong = presentTense(one = "Sekunde", other = "Sekunden"), + minutesLong = presentTense(one = "Minute", other = "Minuten"), + hoursLong = presentTense(one = "Stunde", other = "Stunden"), + daysLong = multipleTenses { present(one = "Tag", other = "Tage") pastOrFuture(other = "Tagen") }, - weeks = presentTense(one = "Woche", other = "Wochen"), - months = multipleTenses { + weeksLong = presentTense(one = "Woche", other = "Wochen"), + monthsLong = multipleTenses { present(one = "Monat", other = "Monate") pastOrFuture(other = "Monaten") }, - years = multipleTenses { + yearsLong = multipleTenses { present(one = "Jahr", other = "Jahre") pastOrFuture(other = "Jahren") }, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt index 29ed1b6..3c35f57 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt @@ -10,16 +10,16 @@ import nl.jacobras.humanreadable.i18n.NumberStrings internal val ElStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = presentTense(one = "δευτερόλεπτο", other = "δευτερόλεπτα"), - minutes = presentTense(one = "λεπτό", other = "λεπτά"), - hours = presentTense(one = "ώρα", other = "ώρες"), - days = presentTense(one = "μέρα", other = "μέρες"), - weeks = presentTense(one = "εβδομάδα", other = "εβδομάδες"), - months = multipleTenses { + secondsLong = presentTense(one = "δευτερόλεπτο", other = "δευτερόλεπτα"), + minutesLong = presentTense(one = "λεπτό", other = "λεπτά"), + hoursLong = presentTense(one = "ώρα", other = "ώρες"), + daysLong = presentTense(one = "μέρα", other = "μέρες"), + weeksLong = presentTense(one = "εβδομάδα", other = "εβδομάδες"), + monthsLong = multipleTenses { present(one = "μήνας", other = "μήνες") pastOrFuture(one = "μήνα") }, - years = presentTense(one = "έτος", other = "έτη"), + yearsLong = presentTense(one = "έτος", other = "έτη"), timeAgo = { "$it πριν" }, timeInFuture = { "σε $it" }, now = "τώρα", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt index a73787a..ff9af90 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EnStrings.kt @@ -1,9 +1,9 @@ package nl.jacobras.humanreadable.i18n.translations -import nl.jacobras.humanreadable.i18n.Plural -import nl.jacobras.humanreadable.i18n.presentTense import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.HumanReadableStrings +import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense /** * English is the default language that will be used if no better translation is found. @@ -11,18 +11,42 @@ import nl.jacobras.humanreadable.i18n.HumanReadableStrings internal val EnStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = presentTense(one = "second", other = "seconds"), - minutes = presentTense(one = "minute", other = "minutes"), - hours = presentTense(one = "hour", other = "hours"), - days = presentTense(one = "day", other = "days"), - weeks = presentTense(one = "week", other = "weeks"), - months = presentTense(one = "month", other = "months"), - years = presentTense(one = "year", other = "years"), + + secondsLong = presentTense(one = "second", other = "seconds"), + secondsShort = presentTense(one = "sec", other = "sec"), + secondsNarrow = presentTense(one = "s"), + + minutesLong = presentTense(one = "minute", other = "minutes"), + minutesShort = presentTense(one = "min"), + minutesNarrow = presentTense(one = "m"), + + hoursLong = presentTense(one = "hour", other = "hours"), + hoursShort = presentTense(one = "hr"), + hoursNarrow = presentTense(one = "h"), + + daysLong = presentTense(one = "day", other = "days"), + daysShort = presentTense(one = "day", other = "days"), + daysNarrow = presentTense(one = "d"), + + weeksLong = presentTense(one = "week", other = "weeks"), + weeksShort = presentTense(one = "wk", other = "wks"), + weeksNarrow = presentTense(one = "w"), + + monthsLong = presentTense(one = "month", other = "months"), + monthsShort = presentTense(one = "mth", other = "mths"), + monthsNarrow = presentTense(one = "m"), + + yearsLong = presentTense(one = "year", other = "years"), + yearsShort = presentTense(one = "yr", other = "yrs"), + yearsNarrow = presentTense(one = "y"), + timeAgo = { "$it ago" }, timeInFuture = { "in $it" }, now = "now", today = "today", yesterday = "yesterday", - tomorrow = "tomorrow" + tomorrow = "tomorrow", + lessThan = "less than", + about = "about" ) ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt index 1495127..354735b 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt @@ -9,13 +9,13 @@ import nl.jacobras.humanreadable.i18n.NumberStrings internal val EsStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = presentTense(one = "segundo", other = "segundos"), - minutes = presentTense(one = "minuto", other = "minutos"), - hours = presentTense(one = "hora", other = "horas"), - days = presentTense(one = "día", other = "días"), - weeks = presentTense(one = "semana", other = "semanas"), - months = presentTense(one = "mes", other = "meses"), - years = presentTense(one = "año", other = "años"), + secondsLong = presentTense(one = "segundo", other = "segundos"), + minutesLong = presentTense(one = "minuto", other = "minutos"), + hoursLong = presentTense(one = "hora", other = "horas"), + daysLong = presentTense(one = "día", other = "días"), + weeksLong = presentTense(one = "semana", other = "semanas"), + monthsLong = presentTense(one = "mes", other = "meses"), + yearsLong = presentTense(one = "año", other = "años"), timeAgo = { "hace $it" }, timeInFuture = { "en $it" }, now = "ahora", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt index a32907e..691643a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt @@ -10,31 +10,31 @@ import nl.jacobras.humanreadable.i18n.NumberStrings internal val FiStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = multipleTenses { + secondsLong = multipleTenses { present(one = "sekunti", other = "sekuntia") future(one = "sekunnin", other = "sekunnin") }, - minutes = multipleTenses { + minutesLong = multipleTenses { present(one = "minuutti", other = "minuuttia") future(one = "minuutin", other = "minuutin") }, - hours = multipleTenses { + hoursLong = multipleTenses { present(one = "tunti", other = "tuntia") future(one = "tunnin", other = "tunnin") }, - days = multipleTenses { + daysLong = multipleTenses { present(one = "päivä", other = "päivää") future(one = "päivän", other = "päivän") }, - weeks = multipleTenses { + weeksLong = multipleTenses { present(one = "viikko", other = "viikkoa") future(one = "viikon", other = "viikon") }, - months = multipleTenses { + monthsLong = multipleTenses { present(one = "kuukausi", other = "kuukautta") future(one = "kuukauden", other = "kuukauden") }, - years = multipleTenses { + yearsLong = multipleTenses { present(one = "vuosi", other = "vuotta") future(one = "vuoden", other = "vuoden") }, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt index d3afb24..363d04f 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt @@ -10,13 +10,13 @@ import nl.jacobras.humanreadable.i18n.NumberStrings internal val FrStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 0 || count == 1) Plural.One else Plural.Other }, - seconds = presentTense(one = "seconde", other = "secondes"), - minutes = presentTense(one = "minute", other = "minutes"), - hours = presentTense(one = "heure", other = "heures"), - days = presentTense(one = "jour", other = "jours"), - weeks = presentTense(one = "semaine", other = "semaines"), - months = presentTense(one = "mois", other = "mois"), - years = presentTense(one = "an", other = "ans"), + secondsLong = presentTense(one = "seconde", other = "secondes"), + minutesLong = presentTense(one = "minute", other = "minutes"), + hoursLong = presentTense(one = "heure", other = "heures"), + daysLong = presentTense(one = "jour", other = "jours"), + weeksLong = presentTense(one = "semaine", other = "semaines"), + monthsLong = presentTense(one = "mois", other = "mois"), + yearsLong = presentTense(one = "an", other = "ans"), timeAgo = { "il y a $it" }, timeInFuture = { "dans $it" }, now = "maintenant", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt index ec54b2a..a1fecb8 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt @@ -9,13 +9,13 @@ import nl.jacobras.humanreadable.i18n.NumberStrings internal val IdStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { Plural.Other }, - seconds = presentTense(other = "detik"), - minutes = presentTense(other = "menit"), - hours = presentTense(other = "jam"), - days = presentTense(other = "hari"), - weeks = presentTense(other = "minggu"), - months = presentTense(other = "bulan"), - years = presentTense(other = "tahun"), + secondsLong = presentTense(other = "detik"), + minutesLong = presentTense(other = "menit"), + hoursLong = presentTense(other = "jam"), + daysLong = presentTense(other = "hari"), + weeksLong = presentTense(other = "minggu"), + monthsLong = presentTense(other = "bulan"), + yearsLong = presentTense(other = "tahun"), timeAgo = { "$it yang lalu" }, timeInFuture = { "dalam $it" }, now = "sekarang", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt index 35564cc..3e023e8 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt @@ -9,13 +9,13 @@ import nl.jacobras.humanreadable.i18n.NumberStrings internal val ItStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = presentTense(one = "secondo", other = "secondi"), - minutes = presentTense(one = "minuto", other = "minuti"), - hours = presentTense(one = "ora", other = "ore"), - days = presentTense(one = "giorno", other = "giorni"), - weeks = presentTense(one = "settimana", other = "settimane"), - months = presentTense(one = "mese", other = "mesi"), - years = presentTense(one = "anno", other = "anni"), + secondsLong = presentTense(one = "secondo", other = "secondi"), + minutesLong = presentTense(one = "minuto", other = "minuti"), + hoursLong = presentTense(one = "ora", other = "ore"), + daysLong = presentTense(one = "giorno", other = "giorni"), + weeksLong = presentTense(one = "settimana", other = "settimane"), + monthsLong = presentTense(one = "mese", other = "mesi"), + yearsLong = presentTense(one = "anno", other = "anni"), timeAgo = { "$it fa" }, timeInFuture = { "tra $it" }, now = "adesso", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt index d741188..b1918a2 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt @@ -8,13 +8,13 @@ import nl.jacobras.humanreadable.i18n.HumanReadableStrings internal val JaStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { Plural.Other }, - seconds = presentTense(other = "秒"), - minutes = presentTense(other = "分"), - hours = presentTense(other = "時間"), - days = presentTense(other = "日"), - weeks = presentTense(other = "週"), - months = presentTense(other = "ヶ月"), - years = presentTense(other = "年"), + secondsLong = presentTense(other = "秒"), + minutesLong = presentTense(other = "分"), + hoursLong = presentTense(other = "時間"), + daysLong = presentTense(other = "日"), + weeksLong = presentTense(other = "週"), + monthsLong = presentTense(other = "ヶ月"), + yearsLong = presentTense(other = "年"), timeAgo = { "${it}前" }, timeInFuture = { "${it}後" }, now = "今", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt index ce1f2e9..994dddc 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt @@ -8,22 +8,22 @@ import nl.jacobras.humanreadable.i18n.HumanReadableStrings internal val KkStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = multipleTenses { + secondsLong = multipleTenses { present(one = "секунд", other = "секунд") future(one = "секундтан", other = "секундтан") }, - minutes = multipleTenses { + minutesLong = multipleTenses { present(one = "минут", other = "минут") future(one = "минуттан", other = "минуттан") }, - hours = multipleTenses { + hoursLong = multipleTenses { present(one = "сағат", other = "сағат") future(one = "сағаттан", other = "сағаттан") }, - days = multipleTenses { present(one = "күн", other = "күн"); future(one = "күннен", other = "күннен") }, - weeks = multipleTenses { present(one = "апта", other = "апта"); future(one = "аптадан", other = "аптадан") }, - months = multipleTenses { present(one = "ай", other = "ай"); future(one = "айдан", other = "айдан") }, - years = multipleTenses { present(one = "жыл", other = "жыл"); future(one = "жылдан", other = "жылдан") }, + daysLong = multipleTenses { present(one = "күн", other = "күн"); future(one = "күннен", other = "күннен") }, + weeksLong = multipleTenses { present(one = "апта", other = "апта"); future(one = "аптадан", other = "аптадан") }, + monthsLong = multipleTenses { present(one = "ай", other = "ай"); future(one = "айдан", other = "айдан") }, + yearsLong = multipleTenses { present(one = "жыл", other = "жыл"); future(one = "жылдан", other = "жылдан") }, timeAgo = { "$it бұрын" }, timeInFuture = { "$it кейін" }, now = "қазір", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt index 79c7db9..067d4dc 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt @@ -8,13 +8,13 @@ import nl.jacobras.humanreadable.i18n.HumanReadableStrings internal val KoStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { Plural.Other }, - seconds = presentTense(other = "초"), - minutes = presentTense(other = "분"), - hours = presentTense(other = "시간"), - days = presentTense(other = "일"), - weeks = presentTense(other = "주"), - months = presentTense(other = "개월"), - years = presentTense(other = "년"), + secondsLong = presentTense(other = "초"), + minutesLong = presentTense(other = "분"), + hoursLong = presentTense(other = "시간"), + daysLong = presentTense(other = "일"), + weeksLong = presentTense(other = "주"), + monthsLong = presentTense(other = "개월"), + yearsLong = presentTense(other = "년"), timeAgo = { "$it 전" }, timeInFuture = { "$it 후" }, now = "지금", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt index 1a3522a..febacd2 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt @@ -9,13 +9,13 @@ import nl.jacobras.humanreadable.i18n.NumberStrings internal val NlStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = presentTense(one = "seconde", other = "seconden"), - minutes = presentTense(one = "minuut", other = "minuten"), - hours = presentTense(one = "uur", other = "uur"), - days = presentTense(one = "dag", other = "dagen"), - weeks = presentTense(one = "week", other = "weken"), - months = presentTense(one = "maand", other = "maanden"), - years = presentTense(one = "jaar", other = "jaar"), + secondsLong = presentTense(one = "seconde", other = "seconden"), + minutesLong = presentTense(one = "minuut", other = "minuten"), + hoursLong = presentTense(one = "uur", other = "uur"), + daysLong = presentTense(one = "dag", other = "dagen"), + weeksLong = presentTense(one = "week", other = "weken"), + monthsLong = presentTense(one = "maand", other = "maanden"), + yearsLong = presentTense(one = "jaar", other = "jaar"), timeAgo = { "$it geleden" }, timeInFuture = { "over $it" }, now = "nu", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt index 102b593..e7d0f4f 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt @@ -16,22 +16,22 @@ internal val PlStrings = HumanReadableStrings( else -> Plural.Many } }, - seconds = multipleTenses { + secondsLong = multipleTenses { present(one = "sekunda", few = "sekundy", many = "sekund") pastOrFuture(one = "sekundę") }, - minutes = multipleTenses { + minutesLong = multipleTenses { present(one = "minuta", few = "minuty", many = "minut") pastOrFuture(one = "minutę") }, - hours = multipleTenses { + hoursLong = multipleTenses { present(one = "godzina", few = "godziny", many = "godzin") pastOrFuture(one = "godzinę") }, - days = presentTense(one = "dzień", few = "dni", many = "dni"), - weeks = presentTense(one = "tydzień", few = "tygodnie", many = "tygodni"), - months = presentTense(one = "miesiąc", few = "miesiące", many = "miesięcy"), - years = presentTense(one = "rok", few = "lata", many = "lat"), + daysLong = presentTense(one = "dzień", few = "dni", many = "dni"), + weeksLong = presentTense(one = "tydzień", few = "tygodnie", many = "tygodni"), + monthsLong = presentTense(one = "miesiąc", few = "miesiące", many = "miesięcy"), + yearsLong = presentTense(one = "rok", few = "lata", many = "lat"), timeAgo = { "$it temu" }, timeInFuture = { "za $it" }, now = "teraz", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt index d1864b4..e0f68bf 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt @@ -15,13 +15,13 @@ internal val PtStrings = HumanReadableStrings( else -> Plural.Other } }, - seconds = presentTense(one = "segundo", many = "de segundos", other = "segundos"), - minutes = presentTense(one = "minuto", many = "de minutos", other = "minutos"), - hours = presentTense(one = "hora", many = "de horas", other = "horas"), - days = presentTense(one = "dia", many = "de dias", other = "dias"), - weeks = presentTense(one = "semana", many = "de semanas", other = "semanas"), - months = presentTense(one = "mês", many = "de meses", other = "meses"), - years = presentTense(one = "ano", many = "de anos", other = "anos"), + secondsLong = presentTense(one = "segundo", many = "de segundos", other = "segundos"), + minutesLong = presentTense(one = "minuto", many = "de minutos", other = "minutos"), + hoursLong = presentTense(one = "hora", many = "de horas", other = "horas"), + daysLong = presentTense(one = "dia", many = "de dias", other = "dias"), + weeksLong = presentTense(one = "semana", many = "de semanas", other = "semanas"), + monthsLong = presentTense(one = "mês", many = "de meses", other = "meses"), + yearsLong = presentTense(one = "ano", many = "de anos", other = "anos"), timeAgo = { "há $it" }, timeInFuture = { "em $it" }, now = "agora", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt index 0ea6707..2874562 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt @@ -16,22 +16,22 @@ internal val RuStrings = HumanReadableStrings( else -> Plural.Many } }, - seconds = multipleTenses { + secondsLong = multipleTenses { present(one = "секунда", few = "секунды", many = "секунд") pastOrFuture(one = "секунду") }, - minutes = multipleTenses { + minutesLong = multipleTenses { present(one = "минута", few = "минуты", many = "минут") pastOrFuture(one = "минуту") }, - hours = presentTense(one = "час", few = "часа", many = "часов"), - days = presentTense(one = "день", few = "дня", many = "дней"), - weeks = multipleTenses { + hoursLong = presentTense(one = "час", few = "часа", many = "часов"), + daysLong = presentTense(one = "день", few = "дня", many = "дней"), + weeksLong = multipleTenses { present(one = "неделя", few = "недели", many = "недель") pastOrFuture(one = "неделю") }, - months = presentTense(one = "месяц", few = "месяца", many = "месяцев"), - years = presentTense(one = "год", few = "года", many = "лет"), + monthsLong = presentTense(one = "месяц", few = "месяца", many = "месяцев"), + yearsLong = presentTense(one = "год", few = "года", many = "лет"), timeAgo = { "$it назад" }, timeInFuture = { "через $it" }, now = "сейчас", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt index 9e9464b..f1c8b64 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt @@ -9,13 +9,13 @@ import nl.jacobras.humanreadable.i18n.NumberStrings internal val TrStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = presentTense(other = "saniye"), - minutes = presentTense(other = "dakika"), - hours = presentTense(other = "saat"), - days = presentTense(other = "gün"), - weeks = presentTense(other = "hafta"), - months = presentTense(other = "ay"), - years = presentTense(other = "yıl"), + secondsLong = presentTense(other = "saniye"), + minutesLong = presentTense(other = "dakika"), + hoursLong = presentTense(other = "saat"), + daysLong = presentTense(other = "gün"), + weeksLong = presentTense(other = "hafta"), + monthsLong = presentTense(other = "ay"), + yearsLong = presentTense(other = "yıl"), timeAgo = { "$it önce" }, timeInFuture = { "$it sonra" }, now = "şimdi", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt index b79b5ef..479384f 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt @@ -16,22 +16,22 @@ internal val UkStrings = HumanReadableStrings( else -> Plural.Many } }, - seconds = multipleTenses { + secondsLong = multipleTenses { present(one = "секунда", few = "секунди", many = "секунд") pastOrFuture(one = "секунду") }, - minutes = multipleTenses { + minutesLong = multipleTenses { present(one = "хвилина", few = "хвилини", many = "хвилин") pastOrFuture(one = "хвилину") }, - hours = multipleTenses { + hoursLong = multipleTenses { present(one = "година", few = "години", many = "годин") pastOrFuture(one = "годину") }, - days = presentTense(one = "день", few = "дні", many = "днів"), - weeks = presentTense(one = "тиждень", few = "тижні", many = "тижнів"), - months = presentTense(one = "місяць", few = "місяці", many = "місяців"), - years = presentTense(one = "рік", few = "роки", many = "років"), + daysLong = presentTense(one = "день", few = "дні", many = "днів"), + weeksLong = presentTense(one = "тиждень", few = "тижні", many = "тижнів"), + monthsLong = presentTense(one = "місяць", few = "місяці", many = "місяців"), + yearsLong = presentTense(one = "рік", few = "роки", many = "років"), timeAgo = { "$it тому" }, timeInFuture = { "через $it" }, now = "зараз", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt index d618e8a..7b3137b 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt @@ -9,31 +9,31 @@ import nl.jacobras.humanreadable.i18n.multipleTenses internal val UzStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { count -> if (count == 1) Plural.One else Plural.Other }, - seconds = multipleTenses { + secondsLong = multipleTenses { present(one = "soniya", other = "soniya") future(one = "soniyadan", other = "soniyadan") }, - minutes = multipleTenses { + minutesLong = multipleTenses { present(one = "daqiqa", other = "daqiqa") future(one = "daqiqadan", other = "daqiqadan") }, - hours = multipleTenses { + hoursLong = multipleTenses { present(one = "soat", other = "soat") future(one = "soatdan", other = "soatdan") }, - days = multipleTenses { + daysLong = multipleTenses { present(one = "kun", other = "kun") future(one = "kundan", other = "kundan") }, - weeks = multipleTenses { + weeksLong = multipleTenses { present(one = "hafta", other = "hafta") future(one = "haftadan", other = "haftadan") }, - months = multipleTenses { + monthsLong = multipleTenses { present(one = "oy", other = "oy") future(one = "oydan", other = "oydan") }, - years = multipleTenses { + yearsLong = multipleTenses { present(one = "yil", other = "yil") future(one = "yildan", other = "yildan") }, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt index eaf7360..abb06b0 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt @@ -9,13 +9,13 @@ import nl.jacobras.humanreadable.i18n.NumberStrings internal val ViStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { Plural.Other }, - seconds = presentTense(other = "giây"), - minutes = presentTense(other = "phút"), - hours = presentTense(other = "giờ"), - days = presentTense(other = "ngày"), - weeks = presentTense(other = "tuần"), - months = presentTense(other = "tháng"), - years = presentTense(other = "năm"), + secondsLong = presentTense(other = "giây"), + minutesLong = presentTense(other = "phút"), + hoursLong = presentTense(other = "giờ"), + daysLong = presentTense(other = "ngày"), + weeksLong = presentTense(other = "tuần"), + monthsLong = presentTense(other = "tháng"), + yearsLong = presentTense(other = "năm"), timeAgo = { "$it trước" }, timeInFuture = { "sau $it" }, now = "bây giờ", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt index 4274cf7..ca98cb6 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt @@ -8,13 +8,13 @@ import nl.jacobras.humanreadable.i18n.HumanReadableStrings internal val ZhStrings = HumanReadableStrings( dateTime = DateTimeStrings( plural = { Plural.Other }, - seconds = presentTense(other = "秒"), - minutes = presentTense(other = "分钟"), - hours = presentTense(other = "小时"), - days = presentTense(other = "天"), - weeks = presentTense(other = "周"), - months = presentTense(other = "个月"), - years = presentTense(other = "年"), + secondsLong = presentTense(other = "秒"), + minutesLong = presentTense(other = "分钟"), + hoursLong = presentTense(other = "小时"), + daysLong = presentTense(other = "天"), + weeksLong = presentTense(other = "周"), + monthsLong = presentTense(other = "个月"), + yearsLong = presentTense(other = "年"), timeAgo = { "${it}之前" }, timeInFuture = { "${it}之后" }, now = "现在", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt index cf6cf2c..75f08e7 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt @@ -1,11 +1,19 @@ package nl.jacobras.humanreadable.time -public enum class FormatStyle { +public enum class FormatStyle( + internal val dateTimeUnits: DateTimeUnits, + internal val timeStyle: TimeStyle, + internal val approximation: Boolean +) { /** * The default format style, e.g. "1 hour ago". */ - Long, + Long( + dateTimeUnits = DateTimeUnits.Long, + timeStyle = TimeStyle.Regular, + approximation = false + ), /** * Same as [Long], but with an approxmiation word added if the duration is not exact. @@ -15,7 +23,11 @@ public enum class FormatStyle { * - `1.hours + 1.minutes` will be "about 1 hour"; * - `1.hours + 59.minutes` will be "about 1 hour" (with `Floor` rounding). */ - LongApproximate, + LongApproximate( + dateTimeUnits = DateTimeUnits.Long, + timeStyle = TimeStyle.Regular, + approximation = true + ), /** * Shorter format for limited space. @@ -24,7 +36,11 @@ public enum class FormatStyle { * - Time: "1 hr, 50 min" * - Time and date: "3 wks, 4 days, 1 hr, 50 min" */ - Short, + Short( + dateTimeUnits = DateTimeUnits.Short, + timeStyle = TimeStyle.Regular, + approximation = false + ), /** * Like [Short], but with digital time. @@ -33,7 +49,11 @@ public enum class FormatStyle { * - Time: "1:50:00" * - Time and date: "3 wks, 4 days, 1:50:00" */ - ShortDigital, + ShortDigital( + dateTimeUnits = DateTimeUnits.Short, + timeStyle = TimeStyle.Digital, + approximation = false + ), /** * Shortest format for limited space. @@ -42,7 +62,11 @@ public enum class FormatStyle { * - Time: "1h 50m" * - Time and date: "3w, 4d, 1h 50m" */ - Narrow, + Narrow( + dateTimeUnits = DateTimeUnits.Narrow, + timeStyle = TimeStyle.Regular, + approximation = false + ), /** * Like [Narrow], but with digital time. @@ -51,5 +75,12 @@ public enum class FormatStyle { * - Time: "1:50:00" * - Time and date: "3w, 4d, 1:50:00" */ - NarrowDigital + NarrowDigital( + dateTimeUnits = DateTimeUnits.Narrow, + timeStyle = TimeStyle.Digital, + approximation = false + ); + + internal enum class DateTimeUnits { Long, Short, Narrow } + internal enum class TimeStyle { Regular, Digital } } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index 8de80d4..1aa9b07 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -1,8 +1,8 @@ package nl.jacobras.humanreadable.time import nl.jacobras.humanreadable.HumanReadable +import nl.jacobras.humanreadable.HumanReadable.strings import nl.jacobras.humanreadable.formatNumber -import kotlin.math.roundToInt import kotlin.time.Duration /** @@ -23,51 +23,107 @@ internal fun formatDuration( units: Set, rounding: Rounding ): String { - val secondsAgo = duration.inWholeSeconds.toInt() - val minutesAgo = duration.inWholeMinutes.toInt() - val hoursAgo = duration.inWholeHours.toInt() - val daysAgo = duration.inWholeDays.toInt() - val weeksAgo = (duration.inWholeDays / 7f).round(rounding) - val monthsAgo = (duration.inWholeDays / 30.5f).round(rounding) - val yearsAgo = (duration.inWholeDays / 365f).round(rounding) + val tooSmall = parts.smallestDuration > Duration.ZERO && duration < parts.smallestDuration - return when { - units.contains(TimeUnit.Years) && yearsAgo > 0 -> { - formatUnit(yearsAgo, TimeUnit.Years, relativeTime) + val parts = getNeededParts( + units = units, + duration = if (tooSmall) parts.smallestDuration else duration, + rounding = rounding, + parts = parts + ) + + return buildString { + if (tooSmall) { + // Too small? Add "less than ..." + when (formatStyle.dateTimeUnits) { + FormatStyle.DateTimeUnits.Long -> { + append(strings.dateTime.lessThan) + append(' ') + } + FormatStyle.DateTimeUnits.Short -> append('<') + FormatStyle.DateTimeUnits.Narrow -> append('<') + } + } else if (formatStyle.approximation && duration != parts.totalDuration) { + // Large enough, somewhere rounding occurred? Add "about ..." + append(strings.dateTime.about) + append(' ') } - units.contains(TimeUnit.Months) && monthsAgo > 0 -> { - formatUnit(monthsAgo, TimeUnit.Months, relativeTime) + + // Format all parts + for ((unit, value) in parts) { + if (unit != parts.keys.first()) { + when (formatStyle.dateTimeUnits) { + FormatStyle.DateTimeUnits.Long -> append(", ") + FormatStyle.DateTimeUnits.Short -> append(", ") + FormatStyle.DateTimeUnits.Narrow -> append(" ") + } + } + append(formatUnit(value, unit, relativeTime, formatStyle.dateTimeUnits)) } - units.contains(TimeUnit.Weeks) && weeksAgo > 0 -> { - formatUnit(weeksAgo, TimeUnit.Weeks, relativeTime) + + // Nothing to format? Add "0 ..." + if (isEmpty() && units.isNotEmpty()) { + append(formatUnit(0, units.first(), relativeTime, formatStyle.dateTimeUnits)) } - units.contains(TimeUnit.Days) && daysAgo > 0 -> { - formatUnit(daysAgo, TimeUnit.Days, relativeTime) + } +} + +private fun getNeededParts( + units: Set, + duration: Duration, + rounding: Rounding, + parts: Parts +): Map { + val unitsDescending = units.sorted().reversed() + val res = mutableMapOf() + var remainingDuration = duration + + // Initial parts division + for (unit in unitsDescending) { + if (res.size == parts.max) { + break } - units.contains(TimeUnit.Hours) && hoursAgo > 0 -> { - if (rounding == Rounding.UpIfClose && hoursAgo >= TimeUnit.Hours.upIfCloseRollover) { - formatUnit(1, TimeUnit.Days, relativeTime) - } else { - formatUnit(hoursAgo, TimeUnit.Hours, relativeTime) - } + + val value = unit.calculateValue(remainingDuration, rounding) + if (value > 0) { + res[unit] = value + remainingDuration -= unit.valueToDuration(value) } - units.contains(TimeUnit.Minutes) && minutesAgo > 0 -> { - if (rounding == Rounding.UpIfClose && minutesAgo >= TimeUnit.Minutes.upIfCloseRollover) { - formatUnit(1, TimeUnit.Hours, relativeTime) - } else { - formatUnit(minutesAgo, TimeUnit.Minutes, relativeTime) + } + + // Roll-overs + if (rounding == Rounding.UpIfClose) { + val unitsAscending = res.keys.sorted() + + for (unit in unitsAscending) { + val value = res[unit] ?: continue + if (value >= unit.upIfCloseRollover) { + val largerUnit = TimeUnit.entries.getOrNull(unit.ordinal + 1) + if (largerUnit != null) { + res[largerUnit] = (res[largerUnit] ?: 0) + 1 + res.remove(unit) + } } } - else -> { - if (rounding == Rounding.UpIfClose && secondsAgo >= TimeUnit.Seconds.upIfCloseRollover) { - formatUnit(1, TimeUnit.Minutes, relativeTime) - } else { - formatUnit(secondsAgo, TimeUnit.Seconds, relativeTime) + } + + // Sub-part cut-offs + if (parts.subpartCutOffs.isNotEmpty()) { + for ((cutoffUnit, cutoffValue) in parts.subpartCutOffs) { + val value = res[cutoffUnit] + if (value != null && value >= cutoffValue) { + res.keys.removeAll { it < cutoffUnit } } } } + return res } +private val Map.totalDuration: Duration + get() = entries.maxOf { (unit, value) -> + unit.valueToDuration(value) + } + /** * Formats a [count] with its [unit]. * @@ -80,26 +136,26 @@ internal fun formatDuration( private fun formatUnit( count: Int, unit: TimeUnit, - relativeTime: RelativeTime + relativeTime: RelativeTime, + formatStyle: FormatStyle.DateTimeUnits ): String { - val formattedCount = if (count > 1000) { + val formattedCount = if (count > 1_000) { count.toDouble().formatNumber(decimals = 0) } else { count.toString() } - val unitText = unit.format(count, relativeTime) + val unitText = unit.format(count, relativeTime, formatStyle) val languageTag = HumanReadable.localisation.languageTag return when (languageTag) { "ar" if (count == 1 || count == 2) -> unitText "ko" -> "$formattedCount$unitText" - else -> "$formattedCount $unitText" - } -} - -private fun Float.round(rounding: Rounding): Int { - return when (rounding) { - Rounding.Floor, Rounding.UpIfClose -> toInt() - Rounding.HalfUp -> roundToInt() + else -> { + if (formatStyle == FormatStyle.DateTimeUnits.Narrow) { + "$formattedCount$unitText" + } else { + "$formattedCount $unitText" + } + } } } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt index ebc2804..e9de28e 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt @@ -5,17 +5,70 @@ import nl.jacobras.humanreadable.HumanReadable.strings import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.TenseForms import nl.jacobras.humanreadable.time.Rounding.UpIfClose +import kotlin.math.roundToInt +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +// TODO: extensions instead of parameters? Cleaner public API public enum class TimeUnit( - internal val forms: (DateTimeStrings) -> TenseForms + internal val calculateValue: (Duration, Rounding) -> Int, + internal val valueToDuration: (Int) -> Duration, + internal val longForms: (DateTimeStrings) -> TenseForms, + internal val shortForms: (DateTimeStrings) -> TenseForms, + internal val narrowForms: (DateTimeStrings) -> TenseForms, ) { - Seconds({ it.seconds }), - Minutes({ it.minutes }), - Hours({ it.hours }), - Days({ it.days }), - Weeks({ it.weeks }), - Months({ it.months }), - Years({ it.years }); + Seconds( + calculateValue = { duration, _ -> duration.inWholeSeconds.toInt() }, + valueToDuration = { it.seconds }, + longForms = { it.secondsLong }, + shortForms = { it.secondsShort }, + narrowForms = { it.secondsNarrow } + ), + Minutes( + calculateValue = { duration, _ -> duration.inWholeMinutes.toInt() }, + valueToDuration = { it.minutes }, + longForms = { it.minutesLong }, + shortForms = { it.minutesShort }, + narrowForms = { it.minutesNarrow } + ), + Hours( + calculateValue = { duration, _ -> duration.inWholeHours.toInt() }, + valueToDuration = { it.hours }, + longForms = { it.hoursLong }, + shortForms = { it.hoursShort }, + narrowForms = { it.hoursNarrow } + ), + Days( + calculateValue = { duration, _ -> duration.inWholeDays.toInt() }, + valueToDuration = { it.days }, + longForms = { it.daysLong }, + shortForms = { it.daysShort }, + narrowForms = { it.daysNarrow } + ), + Weeks( + calculateValue = { duration, rounding -> (duration.inWholeDays / 7f).round(rounding) }, + valueToDuration = { (it * 7).days }, + longForms = { it.weeksLong }, + shortForms = { it.weeksShort }, + narrowForms = { it.weeksNarrow } + ), + Months( + calculateValue = { duration, rounding -> (duration.inWholeDays / 30.5f).round(rounding) }, + valueToDuration = { (it * 30.5).days }, + longForms = { it.monthsLong }, + shortForms = { it.monthsShort }, + narrowForms = { it.monthsNarrow } + ), + Years( + calculateValue = { duration, rounding -> (duration.inWholeDays / 365f).round(rounding) }, + valueToDuration = { (it * 365).days }, + longForms = { it.yearsLong }, + shortForms = { it.yearsShort }, + narrowForms = { it.yearsNarrow } + ); /** * When to roll over to the next larger unit for [UpIfClose] rounding. @@ -27,9 +80,13 @@ public enum class TimeUnit( else -> Int.MAX_VALUE } - internal fun format(value: Int, relativeTime: RelativeTime): String { + internal fun format(value: Int, relativeTime: RelativeTime, formatStyle: FormatStyle.DateTimeUnits): String { val dateTimeStrings = strings.dateTime - val tenseForms = forms(dateTimeStrings) + val tenseForms = when(formatStyle) { + FormatStyle.DateTimeUnits.Long -> longForms(dateTimeStrings) + FormatStyle.DateTimeUnits.Short -> shortForms(dateTimeStrings) + FormatStyle.DateTimeUnits.Narrow -> narrowForms(dateTimeStrings) + } val pluralCategory = dateTimeStrings.plural(value) val correctTense = when (relativeTime) { RelativeTime.Past -> tenseForms.past @@ -38,10 +95,18 @@ public enum class TimeUnit( } return correctTense[pluralCategory] ?: tenseForms.present[pluralCategory] + ?: tenseForms.present.values.firstOrNull() ?: error("No translation for $value $this in '${localisation.languageTag}'") } internal companion object { internal val all = entries.toSet() } +} + +private fun Float.round(rounding: Rounding): Int { + return when (rounding) { + Rounding.Floor, UpIfClose -> toInt() + Rounding.HalfUp -> roundToInt() + } } \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index b0cfdd4..9ce74f3 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -1,6 +1,7 @@ package nl.jacobras.humanreadable.time import assertk.assertThat +import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import nl.jacobras.humanreadable.HumanReadable import nl.jacobras.humanreadable.HumanReadable.duration @@ -14,7 +15,7 @@ import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import nl.jacobras.humanreadable.time.FormatStyle.Long as FormatLong -import nl.jacobras.humanreadable.time.FormatStyle.LongApproximate as FormatApproximate +import nl.jacobras.humanreadable.time.FormatStyle.LongApproximate as FormatLongApproximate import nl.jacobras.humanreadable.time.FormatStyle.Narrow as FormatNarrow import nl.jacobras.humanreadable.time.FormatStyle.Short as FormatShort @@ -122,6 +123,8 @@ class HumanReadableDurationTests { assertThat(duration(400.days, units = setOf(TimeUnit.Hours))).isEqualTo("9,600 hours") assertThat(duration(400.days, units = setOf(TimeUnit.Minutes))).isEqualTo("576,000 minutes") assertThat(duration(400.days, units = setOf(TimeUnit.Seconds))).isEqualTo("34,560,000 seconds") + assertThat(duration(400.days, units = emptySet())).isEmpty() + assertThat(duration(1.days, units = setOf(TimeUnit.Months))).isEqualTo("0 months") } @Test @@ -130,7 +133,7 @@ class HumanReadableDurationTests { duration(70.seconds, parts = Parts(max = 2)) ).isEqualTo("1 minute, 10 seconds") assertThat( - duration(90.minutes + 10.seconds, parts = Parts(max = 3)) + duration(90.minutes + 10.seconds, parts = Parts(max = 3, subpartCutOffs = emptyMap())) ).isEqualTo("1 hour, 30 minutes, 10 seconds") assertThat( duration(3.seconds, parts = Parts(max = 2)) @@ -144,6 +147,17 @@ class HumanReadableDurationTests { assertThat( duration(1.minutes + 55.seconds, rounding = UpIfClose, parts = Parts(max = 2)) ).isEqualTo("2 minutes") + + // Edge cases + assertThat( + duration(59.minutes + 55.seconds, rounding = UpIfClose, parts = Parts(max = 2)) + ).isEqualTo("1 hour") + assertThat( + duration(23.hours + 55.minutes, rounding = UpIfClose, parts = Parts(max = 2)) + ).isEqualTo("1 day") + assertThat( + duration(6.days + 23.hours, rounding = UpIfClose, parts = Parts(max = 2)) + ).isEqualTo("7 days") } @Test @@ -152,11 +166,23 @@ class HumanReadableDurationTests { duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = FormatLong) ).isEqualTo("less than 45 seconds") assertThat( - duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = FormatApproximate) + duration(10.minutes, parts = Parts(smallestDuration = 15.minutes), formatStyle = FormatLong) + ).isEqualTo("less than 15 minutes") + + assertThat( + duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = FormatLongApproximate) ).isEqualTo("less than 45 seconds") + assertThat( + duration(10.minutes, parts = Parts(smallestDuration = 15.minutes), formatStyle = FormatLongApproximate) + ).isEqualTo("less than 15 minutes") + assertThat( duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = FormatNarrow) ).isEqualTo("<45s") + assertThat( + duration(10.minutes, parts = Parts(smallestDuration = 15.minutes), formatStyle = FormatNarrow) + ).isEqualTo("<15m") + assertThat(duration(45.seconds, parts = Parts(smallestDuration = 45.seconds))).isEqualTo("45 seconds") } @@ -171,10 +197,10 @@ class HumanReadableDurationTests { assertThat( duration(19.hours + 4.minutes, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Hours to 2))) - ).isEqualTo("1 minute, 4 seconds") + ).isEqualTo("19 hours") assertThat( duration(20.hours + 4.minutes, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Hours to 2))) - ).isEqualTo("2 minutes") + ).isEqualTo("20 hours") assertThat( duration(1.days + 5.hours, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Days to 2))) @@ -202,14 +228,17 @@ class HumanReadableDurationTests { assertThat(duration(14.days, formatStyle = FormatLong)).isEqualTo("2 weeks") assertThat(duration(14.days, formatStyle = FormatShort)).isEqualTo("2 wks") assertThat(duration(14.days, formatStyle = FormatNarrow)).isEqualTo("2w") - assertThat(duration(180.days, formatStyle = FormatLong)).isEqualTo("5 months") - assertThat(duration(180.days, formatStyle = FormatShort)).isEqualTo("5 mths") - assertThat(duration(180.days, formatStyle = FormatNarrow)).isEqualTo("5m") + assertThat(duration(180.days, formatStyle = FormatLong)).isEqualTo("6 months") + assertThat(duration(180.days, formatStyle = FormatShort)).isEqualTo("6 mths") + assertThat(duration(180.days, formatStyle = FormatNarrow)).isEqualTo("6m") } @Test fun formatStyleApproximation() { - assertThat(duration(1.hours, formatStyle = FormatApproximate)).isEqualTo("1 hour") - assertThat(duration(1.hours + 1.minutes, formatStyle = FormatApproximate)).isEqualTo("about 1 hour") + assertThat(duration(1.hours, formatStyle = FormatLongApproximate)).isEqualTo("1 hour") + assertThat(duration(1.hours + 1.minutes, formatStyle = FormatLongApproximate)).isEqualTo("about 1 hour") + assertThat( + duration(1.hours + 1.minutes, formatStyle = FormatLongApproximate, parts = Parts(smallestDuration = 1.days)) + ).isEqualTo("less than 1 day") } } \ No newline at end of file From 750b555626f4193389ba4cc61e86b7476e9381f8 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 1 Jul 2026 08:11:28 +0200 Subject: [PATCH 26/57] Fix `Rounding.HalfUp` --- .../time/HumanReadableDuration.kt | 15 ++++- .../jacobras/humanreadable/time/Rounding.kt | 9 ++- .../jacobras/humanreadable/time/TimeUnit.kt | 10 +-- .../time/HumanReadableDurationTests.kt | 66 ++++++++++--------- 4 files changed, 61 insertions(+), 39 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index 1aa9b07..62aa2b7 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -77,20 +77,31 @@ private fun getNeededParts( val unitsDescending = units.sorted().reversed() val res = mutableMapOf() var remainingDuration = duration + var lastUnit: TimeUnit? = null + var remainingBeforeLast = remainingDuration - // Initial parts division + // Main division into parts for (unit in unitsDescending) { if (res.size == parts.max) { break } - val value = unit.calculateValue(remainingDuration, rounding) + // Initial division using Floor rounding, because actual rounding happens only in the last part. + val value = unit.calculateValue(remainingDuration, Rounding.Floor) if (value > 0) { + lastUnit = unit + remainingBeforeLast = remainingDuration res[unit] = value remainingDuration -= unit.valueToDuration(value) } } + // Round the last part. Only needed for HalfUp, because Floor is already done above + // and UpIfClose is done below in the roll-overs section. + if (lastUnit != null && rounding == Rounding.HalfUp) { + res[lastUnit] = lastUnit.calculateValue(remainingBeforeLast, rounding) + } + // Roll-overs if (rounding == Rounding.UpIfClose) { val unitsAscending = res.keys.sorted() diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt index 72fe9e7..a4c10bf 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt @@ -1,5 +1,10 @@ package nl.jacobras.humanreadable.time +/** + * Rounding method. + * + * Note about multi-parts (when [Parts.max] > 1): rounding is done on the smallest included unit. + */ public sealed interface Rounding { /** @@ -17,6 +22,8 @@ public sealed interface Rounding { * Some examples: * - `12.days` results in "2 weeks". * - `46.days` results in "2 months". + * + * Note that when [Parts.max] is larger than 1, the rounding is done on the smallest unit. */ public data object HalfUp : Rounding @@ -29,7 +36,7 @@ public sealed interface Rounding { * - `23.hours` rounds up to a day. * - `13.days` rounds up to two weeks. * - * Anything else is rounded **down**. + * Anything else is rounded **down** (see [Floor]). */ public data object UpIfClose : Rounding } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt index e9de28e..ec01d6b 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt @@ -21,28 +21,28 @@ public enum class TimeUnit( internal val narrowForms: (DateTimeStrings) -> TenseForms, ) { Seconds( - calculateValue = { duration, _ -> duration.inWholeSeconds.toInt() }, + calculateValue = { duration, rounding -> duration.inWholeSeconds.toFloat().round(rounding) }, valueToDuration = { it.seconds }, longForms = { it.secondsLong }, shortForms = { it.secondsShort }, narrowForms = { it.secondsNarrow } ), Minutes( - calculateValue = { duration, _ -> duration.inWholeMinutes.toInt() }, + calculateValue = { duration, rounding -> (duration.inWholeSeconds / 60f).round(rounding) }, valueToDuration = { it.minutes }, longForms = { it.minutesLong }, shortForms = { it.minutesShort }, narrowForms = { it.minutesNarrow } ), Hours( - calculateValue = { duration, _ -> duration.inWholeHours.toInt() }, + calculateValue = { duration, rounding -> (duration.inWholeMinutes / 60f).round(rounding) }, valueToDuration = { it.hours }, longForms = { it.hoursLong }, shortForms = { it.hoursShort }, narrowForms = { it.hoursNarrow } ), Days( - calculateValue = { duration, _ -> duration.inWholeDays.toInt() }, + calculateValue = { duration, rounding -> duration.inWholeDays.toFloat().round(rounding) }, valueToDuration = { it.days }, longForms = { it.daysLong }, shortForms = { it.daysShort }, @@ -82,7 +82,7 @@ public enum class TimeUnit( internal fun format(value: Int, relativeTime: RelativeTime, formatStyle: FormatStyle.DateTimeUnits): String { val dateTimeStrings = strings.dateTime - val tenseForms = when(formatStyle) { + val tenseForms = when (formatStyle) { FormatStyle.DateTimeUnits.Long -> longForms(dateTimeStrings) FormatStyle.DateTimeUnits.Short -> shortForms(dateTimeStrings) FormatStyle.DateTimeUnits.Narrow -> narrowForms(dateTimeStrings) diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 9ce74f3..8d57a1e 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -31,43 +31,46 @@ class HumanReadableDurationTests { } @Test - fun defaultRounding() { - assertThat(duration(1.seconds)).isEqualTo("1 second") - assertThat(duration(3.seconds)).isEqualTo("3 seconds") - assertThat(duration(55.seconds)).isEqualTo("55 seconds") - - assertThat(duration(1.minutes)).isEqualTo("1 minute") - assertThat(duration(3.minutes)).isEqualTo("3 minutes") - assertThat(duration(55.minutes)).isEqualTo("55 minutes") - - assertThat(duration(1.hours)).isEqualTo("1 hour") - assertThat(duration(3.hours)).isEqualTo("3 hours") - assertThat(duration(23.hours)).isEqualTo("23 hours") - - assertThat(duration(1.days)).isEqualTo("1 day") - assertThat(duration(3.days)).isEqualTo("3 days") - - assertThat(duration(7.days)).isEqualTo("1 week") - assertThat(duration(10.days)).isEqualTo("1 week") - assertThat(duration(11.days)).isEqualTo("2 weeks") - - assertThat(duration(21.days)).isEqualTo("1 month") - assertThat(duration(29.days)).isEqualTo("1 month") - assertThat(duration(30.days)).isEqualTo("1 month") - assertThat(duration(31.days)).isEqualTo("1 month") - assertThat(duration(46.days)).isEqualTo("2 months") - assertThat(duration(90.days)).isEqualTo("3 months") - - assertThat(duration(360.days)).isEqualTo("1 year") - assertThat(duration(365.days)).isEqualTo("1 year") - assertThat(duration(555.days)).isEqualTo("2 years") - assertThat(duration(1095.days)).isEqualTo("3 years") + fun halfUpRounding() { + assertThat(duration(1.seconds, rounding = HalfUp)).isEqualTo("1 second") + assertThat(duration(3.seconds, rounding = HalfUp)).isEqualTo("3 seconds") + assertThat(duration(55.seconds, rounding = HalfUp)).isEqualTo("55 seconds") + + assertThat(duration(1.minutes, rounding = HalfUp)).isEqualTo("1 minute") + assertThat(duration(1.minutes + 55.seconds, rounding = HalfUp)).isEqualTo("2 minutes") + assertThat(duration(3.minutes, rounding = HalfUp)).isEqualTo("3 minutes") + assertThat(duration(55.minutes, rounding = HalfUp)).isEqualTo("55 minutes") + + assertThat(duration(1.hours, rounding = HalfUp)).isEqualTo("1 hour") + assertThat(duration(3.hours, rounding = HalfUp)).isEqualTo("3 hours") + assertThat(duration(23.hours, rounding = HalfUp)).isEqualTo("23 hours") + + assertThat(duration(1.days, rounding = HalfUp)).isEqualTo("1 day") + assertThat(duration(3.days, rounding = HalfUp)).isEqualTo("3 days") + + assertThat(duration(7.days, rounding = HalfUp)).isEqualTo("1 week") + assertThat(duration(10.days, rounding = HalfUp)).isEqualTo("1 week") + assertThat(duration(11.days, rounding = HalfUp)).isEqualTo("2 weeks") + assertThat(duration(12.days, rounding = HalfUp)).isEqualTo("2 weeks") + + assertThat(duration(21.days, rounding = HalfUp)).isEqualTo("3 weeks") + assertThat(duration(29.days, rounding = HalfUp)).isEqualTo("4 weeks") + assertThat(duration(30.days, rounding = HalfUp)).isEqualTo("4 weeks") + assertThat(duration(31.days, rounding = HalfUp)).isEqualTo("1 month") + assertThat(duration(46.days, rounding = HalfUp)).isEqualTo("2 months") + assertThat(duration(90.days, rounding = HalfUp)).isEqualTo("3 months") + + assertThat(duration(360.days, rounding = HalfUp)).isEqualTo("12 months") + assertThat(duration(365.days, rounding = HalfUp)).isEqualTo("1 year") + assertThat(duration(555.days, rounding = HalfUp)).isEqualTo("2 years") + assertThat(duration(1095.days, rounding = HalfUp)).isEqualTo("3 years") } @Test fun floorRounding() { assertThat(duration(10.days, rounding = Floor)).isEqualTo("1 week") assertThat(duration(11.days, rounding = Floor)).isEqualTo("1 week") + assertThat(duration(12.days, rounding = Floor)).isEqualTo("1 week") assertThat(duration(21.days, rounding = Floor)).isEqualTo("3 weeks") assertThat(duration(29.days, rounding = Floor)).isEqualTo("4 weeks") @@ -101,6 +104,7 @@ class HumanReadableDurationTests { assertThat(duration(7.days, rounding = UpIfClose)).isEqualTo("1 week") assertThat(duration(10.days, rounding = UpIfClose)).isEqualTo("1 week") assertThat(duration(11.days, rounding = UpIfClose)).isEqualTo("1 week") + assertThat(duration(12.days, rounding = UpIfClose)).isEqualTo("1 week") assertThat(duration(21.days, rounding = UpIfClose)).isEqualTo("3 weeks") assertThat(duration(29.days, rounding = UpIfClose)).isEqualTo("4 weeks") From 3f940639af225e5f0f05da0dc6c90cfe45fa2f7e Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 1 Jul 2026 08:35:08 +0200 Subject: [PATCH 27/57] Implement digital time formatting --- README.md | 2 + .../jacobras/humanreadable/HumanReadable.kt | 12 +- .../humanreadable/time/FormatStyle.kt | 110 ++++++------------ .../time/HumanReadableDuration.kt | 46 +++++--- .../time/HumanReadableRelativeTime.kt | 8 +- .../jacobras/humanreadable/time/TimeUnit.kt | 8 +- .../time/HumanReadableDurationTests.kt | 104 ++++++++++++----- 7 files changed, 154 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index 508b17e..39177e9 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ number of decimals is only used for the larger units kilometers and miles. ## Date/time precision +// TODO global config options + The formatter switches to a bigger unit (minute, hour, day, ...) as soon as it can. For example: diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 8d062df..20ef550 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -70,7 +70,7 @@ public object HumanReadable { public fun timeAgo( instant: Instant, baseInstant: Instant = Clock.System.now(), - formatStyle: FormatStyle = FormatStyle.Long, + formatStyle: FormatStyle = FormatStyle(), parts: Parts = Parts(), units: Set = TimeUnit.all, rounding: Rounding = Rounding.HalfUp @@ -91,7 +91,7 @@ public object HumanReadable { * * @param date The [LocalDate] to format. * @param baseDate The base/starting [LocalDate], defaulting to "today". - * @param formatStyle The [FormatStyle] to use, defaulting to [FormatStyle.Long]. + * @param formatStyle The [FormatStyle] to use, defaulting to "long". * @param parts Configures the formatting of multiple parts, defaulting to 1 part. * @param units The [TimeUnit]s to limit to during formatting, not limited by default. * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp]. @@ -101,7 +101,7 @@ public object HumanReadable { public fun timeAgo( date: LocalDate, baseDate: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), - formatStyle: FormatStyle = FormatStyle.Long, + formatStyle: FormatStyle = FormatStyle(), parts: Parts = Parts(), units: Set = TimeUnit.all, rounding: Rounding = Rounding.HalfUp @@ -121,14 +121,14 @@ public object HumanReadable { * For example, a duration of 3 seconds returns "3 seconds". * * @param duration The [Duration] to format. - * @param formatStyle The [FormatStyle] to use, defaulting to [FormatStyle.Long]. + * @param formatStyle The [FormatStyle] to use, defaulting to "long". * @param parts Configures the formatting of multiple parts, defaulting to 1 part. * @param units The [TimeUnit]s to limit to during formatting, not limited by default. * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp]. */ public fun duration( duration: Duration, - formatStyle: FormatStyle = FormatStyle.Long, + formatStyle: FormatStyle = FormatStyle(), parts: Parts = Parts(), units: Set = TimeUnit.all, rounding: Rounding = Rounding.HalfUp @@ -136,7 +136,7 @@ public object HumanReadable { return formatDuration( duration = duration, relativeTime = RelativeTime.Present, - formatStyle = formatStyle, + format = formatStyle, parts = parts, units = units, rounding = rounding diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt index 75f08e7..51b583a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt @@ -1,86 +1,46 @@ package nl.jacobras.humanreadable.time -public enum class FormatStyle( - internal val dateTimeUnits: DateTimeUnits, - internal val timeStyle: TimeStyle, - internal val approximation: Boolean +/** + * Determines the format style of the output. + * + * @property date The date notation to use. + * @property time The time notation to use. + * @property indicateApproximation Whether to indicate that the output is approximate (e.g. "about 5 minutes ago"). + */ +public data class FormatStyle( + val date: Date = Date.Long, + val time: Time = Time.Regular, + val indicateApproximation: Boolean = false ) { - /** - * The default format style, e.g. "1 hour ago". - */ - Long( - dateTimeUnits = DateTimeUnits.Long, - timeStyle = TimeStyle.Regular, - approximation = false - ), + public enum class Date { - /** - * Same as [Long], but with an approxmiation word added if the duration is not exact. - * - * For example, with `Parts` configured to show max. 1 part: - * - `1.hours` will be "1 hour"; - * - `1.hours + 1.minutes` will be "about 1 hour"; - * - `1.hours + 59.minutes` will be "about 1 hour" (with `Floor` rounding). - */ - LongApproximate( - dateTimeUnits = DateTimeUnits.Long, - timeStyle = TimeStyle.Regular, - approximation = true - ), + /** + * Long format style, e.g. "1 hour, 50 minutes". + */ + Long, - /** - * Shorter format for limited space. - * - * Examples: - * - Time: "1 hr, 50 min" - * - Time and date: "3 wks, 4 days, 1 hr, 50 min" - */ - Short( - dateTimeUnits = DateTimeUnits.Short, - timeStyle = TimeStyle.Regular, - approximation = false - ), + /** + * Shorter format style, e.g. "1 hr, 50 min". + */ + Short, - /** - * Like [Short], but with digital time. - * - * Examples: - * - Time: "1:50:00" - * - Time and date: "3 wks, 4 days, 1:50:00" - */ - ShortDigital( - dateTimeUnits = DateTimeUnits.Short, - timeStyle = TimeStyle.Digital, - approximation = false - ), + /** + * Shortest format style, e.g. "1h 50m". + */ + Narrow + } - /** - * Shortest format for limited space. - * - * Examples: - * - Time: "1h 50m" - * - Time and date: "3w, 4d, 1h 50m" - */ - Narrow( - dateTimeUnits = DateTimeUnits.Narrow, - timeStyle = TimeStyle.Regular, - approximation = false - ), + public enum class Time { - /** - * Like [Narrow], but with digital time. - * - * Examples: - * - Time: "1:50:00" - * - Time and date: "3w, 4d, 1:50:00" - */ - NarrowDigital( - dateTimeUnits = DateTimeUnits.Narrow, - timeStyle = TimeStyle.Digital, - approximation = false - ); + /** + * Regular format style, e.g. "1 hour, 50 minutes". + */ + Regular, - internal enum class DateTimeUnits { Long, Short, Narrow } - internal enum class TimeStyle { Regular, Digital } + /** + * Digital format style, e.g. "1:50:00". + */ + Digital + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index 62aa2b7..3b978d8 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -10,7 +10,7 @@ import kotlin.time.Duration * * @param duration The duration to format. * @param relativeTime Whether this is in the past, current or future (for grammatical correctness). - * @param formatStyle The [FormatStyle] to use. + * @param format The [FormatStyle] to use. * @param parts Configures the formatting of multiple parts (defaults to 1 part). * @param units The [TimeUnit]s to limit to during formatting. * @param rounding The [Rounding] strategy to use. @@ -18,7 +18,7 @@ import kotlin.time.Duration internal fun formatDuration( duration: Duration, relativeTime: RelativeTime, - formatStyle: FormatStyle, + format: FormatStyle, parts: Parts, units: Set, rounding: Rounding @@ -26,7 +26,11 @@ internal fun formatDuration( val tooSmall = parts.smallestDuration > Duration.ZERO && duration < parts.smallestDuration val parts = getNeededParts( - units = units, + units = if (format.time == FormatStyle.Time.Digital) { + units.minus(setOf(TimeUnit.Hours, TimeUnit.Minutes, TimeUnit.Seconds)) + } else { + units + }, duration = if (tooSmall) parts.smallestDuration else duration, rounding = rounding, parts = parts @@ -35,15 +39,15 @@ internal fun formatDuration( return buildString { if (tooSmall) { // Too small? Add "less than ..." - when (formatStyle.dateTimeUnits) { - FormatStyle.DateTimeUnits.Long -> { + when (format.date) { + FormatStyle.Date.Long -> { append(strings.dateTime.lessThan) append(' ') } - FormatStyle.DateTimeUnits.Short -> append('<') - FormatStyle.DateTimeUnits.Narrow -> append('<') + FormatStyle.Date.Short -> append('<') + FormatStyle.Date.Narrow -> append('<') } - } else if (formatStyle.approximation && duration != parts.totalDuration) { + } else if (format.indicateApproximation && duration != parts.totalDuration) { // Large enough, somewhere rounding occurred? Add "about ..." append(strings.dateTime.about) append(' ') @@ -52,18 +56,28 @@ internal fun formatDuration( // Format all parts for ((unit, value) in parts) { if (unit != parts.keys.first()) { - when (formatStyle.dateTimeUnits) { - FormatStyle.DateTimeUnits.Long -> append(", ") - FormatStyle.DateTimeUnits.Short -> append(", ") - FormatStyle.DateTimeUnits.Narrow -> append(" ") + when (format.date) { + FormatStyle.Date.Long -> append(", ") + FormatStyle.Date.Short -> append(", ") + FormatStyle.Date.Narrow -> append(" ") } } - append(formatUnit(value, unit, relativeTime, formatStyle.dateTimeUnits)) + append(formatUnit(value, unit, relativeTime, format.date)) + } + + // Digital time? + if (format.time == FormatStyle.Time.Digital) { + if (!isEmpty()) append(", ") + + val hours = (duration.inWholeHours % 24).toString().padStart(2, '0') + val minutes = (duration.inWholeMinutes % 60).toString().padStart(2, '0') + val seconds = (duration.inWholeSeconds % 60).toString().padStart(2, '0') + append("$hours:$minutes:$seconds") } // Nothing to format? Add "0 ..." if (isEmpty() && units.isNotEmpty()) { - append(formatUnit(0, units.first(), relativeTime, formatStyle.dateTimeUnits)) + append(formatUnit(0, units.first(), relativeTime, format.date)) } } } @@ -148,7 +162,7 @@ private fun formatUnit( count: Int, unit: TimeUnit, relativeTime: RelativeTime, - formatStyle: FormatStyle.DateTimeUnits + formatStyle: FormatStyle.Date ): String { val formattedCount = if (count > 1_000) { count.toDouble().formatNumber(decimals = 0) @@ -162,7 +176,7 @@ private fun formatUnit( "ar" if (count == 1 || count == 2) -> unitText "ko" -> "$formattedCount$unitText" else -> { - if (formatStyle == FormatStyle.DateTimeUnits.Narrow) { + if (formatStyle == FormatStyle.Date.Narrow) { "$formattedCount$unitText" } else { "$formattedCount $unitText" diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt index 596d650..04906db 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt @@ -38,7 +38,7 @@ internal fun formatTimeAgo( formatDuration( duration = diff.absoluteValue, relativeTime = RelativeTime.Future, - formatStyle = formatStyle, + format = formatStyle, parts = parts, units = units, rounding = rounding @@ -49,7 +49,7 @@ internal fun formatTimeAgo( formatDuration( duration = diff.absoluteValue, relativeTime = RelativeTime.Past, - formatStyle = formatStyle, + format = formatStyle, parts = parts, units = units, rounding = rounding @@ -91,7 +91,7 @@ internal fun formatTimeAgo( formatDuration( duration = secondsAgo.absoluteValue.seconds, relativeTime = RelativeTime.Future, - formatStyle = formatStyle, + format = formatStyle, parts = parts, units = units, rounding = rounding @@ -102,7 +102,7 @@ internal fun formatTimeAgo( formatDuration( duration = secondsAgo.absoluteValue.seconds, relativeTime = RelativeTime.Past, - formatStyle = formatStyle, + format = formatStyle, parts = parts, units = units, rounding = rounding diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt index ec01d6b..b7e670d 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt @@ -80,12 +80,12 @@ public enum class TimeUnit( else -> Int.MAX_VALUE } - internal fun format(value: Int, relativeTime: RelativeTime, formatStyle: FormatStyle.DateTimeUnits): String { + internal fun format(value: Int, relativeTime: RelativeTime, formatStyle: FormatStyle.Date): String { val dateTimeStrings = strings.dateTime val tenseForms = when (formatStyle) { - FormatStyle.DateTimeUnits.Long -> longForms(dateTimeStrings) - FormatStyle.DateTimeUnits.Short -> shortForms(dateTimeStrings) - FormatStyle.DateTimeUnits.Narrow -> narrowForms(dateTimeStrings) + FormatStyle.Date.Long -> longForms(dateTimeStrings) + FormatStyle.Date.Short -> shortForms(dateTimeStrings) + FormatStyle.Date.Narrow -> narrowForms(dateTimeStrings) } val pluralCategory = dateTimeStrings.plural(value) val correctTense = when (relativeTime) { diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 8d57a1e..3bca504 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -14,10 +14,6 @@ import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -import nl.jacobras.humanreadable.time.FormatStyle.Long as FormatLong -import nl.jacobras.humanreadable.time.FormatStyle.LongApproximate as FormatLongApproximate -import nl.jacobras.humanreadable.time.FormatStyle.Narrow as FormatNarrow -import nl.jacobras.humanreadable.time.FormatStyle.Short as FormatShort /** * Main test for the specific configuration options related to duration formatting. @@ -26,6 +22,10 @@ import nl.jacobras.humanreadable.time.FormatStyle.Short as FormatShort */ class HumanReadableDurationTests { + private val longStyle = FormatStyle(date = FormatStyle.Date.Long) + private val shortStyle = FormatStyle(date = FormatStyle.Date.Short) + private val narrowStyle = FormatStyle(date = FormatStyle.Date.Narrow) + init { HumanReadable.languageTag = "en" } @@ -167,25 +167,18 @@ class HumanReadableDurationTests { @Test fun smallestDuration() { assertThat( - duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = FormatLong) + duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = longStyle) ).isEqualTo("less than 45 seconds") assertThat( - duration(10.minutes, parts = Parts(smallestDuration = 15.minutes), formatStyle = FormatLong) + duration(10.minutes, parts = Parts(smallestDuration = 15.minutes), formatStyle = longStyle) ).isEqualTo("less than 15 minutes") assertThat( - duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = FormatLongApproximate) - ).isEqualTo("less than 45 seconds") - assertThat( - duration(10.minutes, parts = Parts(smallestDuration = 15.minutes), formatStyle = FormatLongApproximate) - ).isEqualTo("less than 15 minutes") - + duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = shortStyle) + ).isEqualTo("<45 sec") assertThat( - duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = FormatNarrow) + duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = narrowStyle) ).isEqualTo("<45s") - assertThat( - duration(10.minutes, parts = Parts(smallestDuration = 15.minutes), formatStyle = FormatNarrow) - ).isEqualTo("<15m") assertThat(duration(45.seconds, parts = Parts(smallestDuration = 45.seconds))).isEqualTo("45 seconds") } @@ -217,32 +210,81 @@ class HumanReadableDurationTests { @Test fun formatStyle() { assertThat( - duration(1.hours + 50.minutes, formatStyle = FormatLong, parts = Parts(max = 2)) + duration(1.hours + 50.minutes, formatStyle = longStyle, parts = Parts(max = 2)) ).isEqualTo("1 hour, 50 minutes") assertThat( - duration(1.hours + 50.minutes, formatStyle = FormatShort, parts = Parts(max = 2)) + duration(1.hours + 50.minutes, formatStyle = shortStyle, parts = Parts(max = 2)) ).isEqualTo("1 hr, 50 min") assertThat( - duration(1.hours + 50.minutes, formatStyle = FormatNarrow, parts = Parts(max = 2)) + duration(1.hours + 50.minutes, formatStyle = narrowStyle, parts = Parts(max = 2)) ).isEqualTo("1h 50m") - assertThat(duration(1.hours, formatStyle = FormatLong)).isEqualTo("1 hour") - assertThat(duration(1.hours, formatStyle = FormatShort)).isEqualTo("1 hr") - assertThat(duration(1.hours, formatStyle = FormatNarrow)).isEqualTo("1h") - assertThat(duration(14.days, formatStyle = FormatLong)).isEqualTo("2 weeks") - assertThat(duration(14.days, formatStyle = FormatShort)).isEqualTo("2 wks") - assertThat(duration(14.days, formatStyle = FormatNarrow)).isEqualTo("2w") - assertThat(duration(180.days, formatStyle = FormatLong)).isEqualTo("6 months") - assertThat(duration(180.days, formatStyle = FormatShort)).isEqualTo("6 mths") - assertThat(duration(180.days, formatStyle = FormatNarrow)).isEqualTo("6m") + assertThat(duration(1.hours, formatStyle = longStyle)).isEqualTo("1 hour") + assertThat(duration(1.hours, formatStyle = shortStyle)).isEqualTo("1 hr") + assertThat(duration(1.hours, formatStyle = narrowStyle)).isEqualTo("1h") + assertThat(duration(14.days, formatStyle = longStyle)).isEqualTo("2 weeks") + assertThat(duration(14.days, formatStyle = shortStyle)).isEqualTo("2 wks") + assertThat(duration(14.days, formatStyle = narrowStyle)).isEqualTo("2w") + assertThat(duration(180.days, formatStyle = longStyle)).isEqualTo("6 months") + assertThat(duration(180.days, formatStyle = shortStyle)).isEqualTo("6 mths") + assertThat(duration(180.days, formatStyle = narrowStyle)).isEqualTo("6m") } @Test fun formatStyleApproximation() { - assertThat(duration(1.hours, formatStyle = FormatLongApproximate)).isEqualTo("1 hour") - assertThat(duration(1.hours + 1.minutes, formatStyle = FormatLongApproximate)).isEqualTo("about 1 hour") assertThat( - duration(1.hours + 1.minutes, formatStyle = FormatLongApproximate, parts = Parts(smallestDuration = 1.days)) + duration( + duration = 1.hours, + formatStyle = FormatStyle(indicateApproximation = true) + ) + ).isEqualTo("1 hour") + assertThat( + duration( + duration = 1.hours + 1.minutes, + formatStyle = FormatStyle(indicateApproximation = true) + ) + ).isEqualTo("about 1 hour") + assertThat( + duration( + duration = 1.hours + 1.minutes, + formatStyle = FormatStyle(indicateApproximation = true), + parts = Parts(smallestDuration = 1.days) + ) ).isEqualTo("less than 1 day") } + + @Test + fun formatStyleDigitalTime() { + assertThat( + duration( + duration = 1.hours, + formatStyle = FormatStyle(time = FormatStyle.Time.Digital), + parts = Parts(max = 5) + ) + ).isEqualTo("01:00:00") + + assertThat( + duration( + duration = 1.hours + 10.minutes + 5.seconds, + formatStyle = FormatStyle(time = FormatStyle.Time.Digital), + parts = Parts(max = 5) + ) + ).isEqualTo("01:10:05") + + assertThat( + duration( + duration = 5.days + 1.hours, + formatStyle = FormatStyle(time = FormatStyle.Time.Digital), + parts = Parts(max = 5) + ) + ).isEqualTo("5 days, 01:00:00") + + assertThat( + duration( + duration = 21.days + 2.days + 1.hours, + formatStyle = FormatStyle(time = FormatStyle.Time.Digital), + parts = Parts(max = 5) + ) + ).isEqualTo("3 weeks, 2 days, 01:00:00") + } } \ No newline at end of file From 6766688cb04ebb53413e78de1439ccba4ab9c789 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 1 Jul 2026 08:38:18 +0200 Subject: [PATCH 28/57] Shorten approximation word --- .../time/HumanReadableDuration.kt | 9 +++++++-- .../time/HumanReadableDurationTests.kt | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index 3b978d8..da5ded2 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -49,8 +49,13 @@ internal fun formatDuration( } } else if (format.indicateApproximation && duration != parts.totalDuration) { // Large enough, somewhere rounding occurred? Add "about ..." - append(strings.dateTime.about) - append(' ') + when (format.date) { + FormatStyle.Date.Long -> { + append(strings.dateTime.about) + append(' ') + } + FormatStyle.Date.Short, FormatStyle.Date.Narrow -> append('~') + } } // Format all parts diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 3bca504..156d64b 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -235,19 +235,31 @@ class HumanReadableDurationTests { assertThat( duration( duration = 1.hours, - formatStyle = FormatStyle(indicateApproximation = true) + formatStyle = longStyle.copy(indicateApproximation = true) ) ).isEqualTo("1 hour") assertThat( duration( duration = 1.hours + 1.minutes, - formatStyle = FormatStyle(indicateApproximation = true) + formatStyle = longStyle.copy(indicateApproximation = true) ) ).isEqualTo("about 1 hour") assertThat( duration( duration = 1.hours + 1.minutes, - formatStyle = FormatStyle(indicateApproximation = true), + formatStyle = shortStyle.copy(indicateApproximation = true) + ) + ).isEqualTo("~1 hr") + assertThat( + duration( + duration = 1.hours + 1.minutes, + formatStyle = narrowStyle.copy(indicateApproximation = true) + ) + ).isEqualTo("~1h") + assertThat( + duration( + duration = 1.hours + 1.minutes, + formatStyle = longStyle.copy(indicateApproximation = true), parts = Parts(smallestDuration = 1.days) ) ).isEqualTo("less than 1 day") From c8182e57304f10347e4740557069bb4a62e03380 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Wed, 1 Jul 2026 22:49:45 +0200 Subject: [PATCH 29/57] Rework demo UI --- demo/build.gradle.kts | 3 + demo/src/webMain/kotlin/App.kt | 62 ++-- demo/src/webMain/kotlin/Feature.kt | 6 + .../webMain/kotlin/feature/DistanceDemo.kt | 100 ++++++ .../kotlin/{ui => feature}/FileSizeDemo.kt | 75 ++--- .../{ui => feature}/NumberFormatDemo.kt | 83 +++-- demo/src/webMain/kotlin/feature/TimeDemo.kt | 288 ++++++++++++++++++ .../src/webMain/kotlin/ui/AbbreviationDemo.kt | 97 ------ demo/src/webMain/kotlin/ui/CodeExample.kt | 19 ++ demo/src/webMain/kotlin/ui/DecimalsInput.kt | 69 +++++ demo/src/webMain/kotlin/ui/DistanceDemo.kt | 114 ------- demo/src/webMain/kotlin/ui/FeaturesList.kt | 35 +++ demo/src/webMain/kotlin/ui/FlexibleLayout.kt | 33 -- demo/src/webMain/kotlin/ui/TimeDemo.kt | 142 --------- .../webMain/kotlin/ui/highlightKotlinCode.kt | 44 +++ gradle/libs.versions.toml | 8 +- 16 files changed, 683 insertions(+), 495 deletions(-) create mode 100644 demo/src/webMain/kotlin/Feature.kt create mode 100644 demo/src/webMain/kotlin/feature/DistanceDemo.kt rename demo/src/webMain/kotlin/{ui => feature}/FileSizeDemo.kt (53%) rename demo/src/webMain/kotlin/{ui => feature}/NumberFormatDemo.kt (52%) create mode 100644 demo/src/webMain/kotlin/feature/TimeDemo.kt delete mode 100644 demo/src/webMain/kotlin/ui/AbbreviationDemo.kt create mode 100644 demo/src/webMain/kotlin/ui/CodeExample.kt create mode 100644 demo/src/webMain/kotlin/ui/DecimalsInput.kt delete mode 100644 demo/src/webMain/kotlin/ui/DistanceDemo.kt create mode 100644 demo/src/webMain/kotlin/ui/FeaturesList.kt delete mode 100644 demo/src/webMain/kotlin/ui/FlexibleLayout.kt delete mode 100644 demo/src/webMain/kotlin/ui/TimeDemo.kt create mode 100644 demo/src/webMain/kotlin/ui/highlightKotlinCode.kt diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 875d07f..64bbca1 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -28,6 +28,9 @@ kotlin { implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) + implementation(libs.compose.adaptive) + implementation(libs.compose.adaptive.layout) + implementation(libs.compose.adaptive.navigation) implementation(libs.kotlinX.datetime) implementation(rootProject) diff --git a/demo/src/webMain/kotlin/App.kt b/demo/src/webMain/kotlin/App.kt index 1a0baaf..519c6d5 100644 --- a/demo/src/webMain/kotlin/App.kt +++ b/demo/src/webMain/kotlin/App.kt @@ -4,22 +4,25 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.VerticalDivider +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -31,19 +34,20 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import feature.DistanceDemo +import feature.FileSizeDemo +import feature.NumberFormatDemo +import feature.TimeDemo +import kotlinx.coroutines.launch import nl.jacobras.humanreadable.HumanReadable -import ui.AbbreviationDemo -import ui.DistanceDemo -import ui.FileSizeDemo -import ui.FlexibleLayout +import ui.FeaturesList import ui.LanguageChip -import ui.NumberFormatDemo -import ui.TimeDemo -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun App() { var selectedLanguageCode by remember { mutableStateOf(HumanReadable.languageTag) } + var selectedFeature by remember { mutableStateOf(Feature.Time) } val layoutDirection = if (selectedLanguageCode == "ar") { LayoutDirection.Rtl } else { @@ -55,14 +59,19 @@ internal fun App() { HumanReadable.languageTag = code } + val navigator = rememberListDetailPaneScaffoldNavigator() + val scope = rememberCoroutineScope() + MaterialTheme { CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) { val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace) + Scaffold( topBar = { TopAppBar(title = { Text("Human-Readable web demo") }) } ) { paddingValues -> + BoxWithConstraints( modifier = Modifier .fillMaxWidth() @@ -97,16 +106,29 @@ internal fun App() { } Spacer(modifier = Modifier.height(32.dp)) - FlexibleLayout( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(bottom = 32.dp), - timeDemo = { TimeDemo(selectedLanguageCode, it) }, - fileSizeDemo = { FileSizeDemo(selectedLanguageCode, it) }, - abbreviationDemo = { AbbreviationDemo(selectedLanguageCode, it) }, - numberFormatDemo = { NumberFormatDemo(selectedLanguageCode, it) }, - distanceDemo = { DistanceDemo(selectedLanguageCode, it) } + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + FeaturesList( + selected = selectedFeature, + onSelect = { + selectedFeature = it + scope.launch { + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail) + } + } + ) + }, + detailPane = { + when (selectedFeature) { + Feature.Time -> TimeDemo(selectedLanguageCode) + Feature.FileSize -> FileSizeDemo(selectedLanguageCode) + Feature.Number -> NumberFormatDemo(selectedLanguageCode) + Feature.Distance -> DistanceDemo(selectedLanguageCode) + } + }, + paneExpansionDragHandle = { _ -> VerticalDivider() } ) } } diff --git a/demo/src/webMain/kotlin/Feature.kt b/demo/src/webMain/kotlin/Feature.kt new file mode 100644 index 0000000..8fc0ecd --- /dev/null +++ b/demo/src/webMain/kotlin/Feature.kt @@ -0,0 +1,6 @@ +enum class Feature(val title: String) { + Time("Time"), + Number("Number"), + FileSize("File size"), + Distance("Distance") +} \ No newline at end of file diff --git a/demo/src/webMain/kotlin/feature/DistanceDemo.kt b/demo/src/webMain/kotlin/feature/DistanceDemo.kt new file mode 100644 index 0000000..f2fe6cd --- /dev/null +++ b/demo/src/webMain/kotlin/feature/DistanceDemo.kt @@ -0,0 +1,100 @@ +package feature + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import monoBodyOrange +import nl.jacobras.humanreadable.DistanceUnit +import nl.jacobras.humanreadable.HumanReadable +import ui.CodeExample +import ui.DecimalsInput + +@Composable +internal fun DistanceDemo( + selectedLanguageCode: String, + modifier: Modifier = Modifier +) { + val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace) + + Column(modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + Text( + text = "Distance formatting", + style = MaterialTheme.typography.headlineLarge + ) + Spacer(Modifier.height(16.dp)) + var myNumber by remember { mutableStateOf("3000") } + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = buildAnnotatedString { + withStyle(monoBodyOrange) { append("val ") } + append("myDistance = ") + }, + style = monoBody + ) + TextField( + modifier = Modifier.sizeIn(minWidth = 20.dp), + value = myNumber, + onValueChange = { + if (it.length < 16) { + myNumber = it + } + }, + isError = myNumber.toLongOrNull() == null, + supportingText = if (myNumber.toLongOrNull() == null) { + { Text("Invalid number") } + } else { + null + } + ) + } + Spacer(Modifier.height(8.dp)) + var decimals by remember { mutableIntStateOf(2) } + DecimalsInput( + decimals = decimals, + onChange = { decimals = it } + ) + Spacer(Modifier.height(16.dp)) + + CodeExample( + code = "HumanReadable.distance(myDistance, DistanceUnit.Meter, decimals)", + res = remember(selectedLanguageCode, myNumber, decimals) { + HumanReadable.distance( + value = myNumber.toLongOrNull() ?: 0L, + unit = DistanceUnit.Meter, + decimals = decimals + ) + } + ) + Spacer(Modifier.height(8.dp)) + CodeExample( + code = "HumanReadable.distance(myDistance, DistanceUnit.Foot, decimals)", + res = remember(selectedLanguageCode, myNumber, decimals) { + HumanReadable.distance( + value = myNumber.toLongOrNull() ?: 0L, + unit = DistanceUnit.Foot, + decimals = decimals + ) + } + ) + } +} \ No newline at end of file diff --git a/demo/src/webMain/kotlin/ui/FileSizeDemo.kt b/demo/src/webMain/kotlin/feature/FileSizeDemo.kt similarity index 53% rename from demo/src/webMain/kotlin/ui/FileSizeDemo.kt rename to demo/src/webMain/kotlin/feature/FileSizeDemo.kt index 1173fca..75cbd88 100644 --- a/demo/src/webMain/kotlin/ui/FileSizeDemo.kt +++ b/demo/src/webMain/kotlin/feature/FileSizeDemo.kt @@ -1,10 +1,20 @@ -package ui +package feature -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.sizeIn import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.buildAnnotatedString @@ -12,9 +22,9 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import monoBodyOrange -import monoBodyString -import monoBodyStringBold import nl.jacobras.humanreadable.HumanReadable +import ui.CodeExample +import ui.DecimalsInput @Composable internal fun FileSizeDemo( @@ -25,7 +35,7 @@ internal fun FileSizeDemo( Column(modifier.fillMaxWidth()) { Text( - text = "File size", + text = "File size formatting", style = MaterialTheme.typography.headlineLarge ) Text("File size formatting uses base 1024.") @@ -60,44 +70,21 @@ internal fun FileSizeDemo( ) } Spacer(Modifier.height(8.dp)) - var decimals by remember { mutableStateOf("2") } - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = buildAnnotatedString { - withStyle(monoBodyOrange) { append("val ") } - append("decimals = ") - }, - style = monoBody - ) - TextField( - modifier = Modifier.sizeIn(minWidth = 20.dp), - value = decimals, - onValueChange = { decimals = it }, - isError = decimals.toIntOrNull() == null, - supportingText = if (decimals.toIntOrNull() == null) { - { Text("Invalid number") } - } else { - null - } - ) - } - Spacer(Modifier.height(8.dp)) - Text( - text = buildAnnotatedString { - appendLine("HumanReadable.fileSize(myFile, decimals)") - withStyle(monoBodyStringBold) { - append("// \"") - append( - remember(selectedLanguageCode, myFile, decimals) { - HumanReadable.fileSize( - bytes = myFile.toLongOrNull() ?: 0L, - decimals = decimals.toIntOrNull() ?: 0 - ) - }) - append("\"") - } - }, - style = monoBody + var decimals by remember { mutableIntStateOf(2) } + DecimalsInput( + decimals = decimals, + onChange = { decimals = it } + ) + Spacer(Modifier.height(16.dp)) + + CodeExample( + code = "HumanReadable.fileSize(myFile, decimals)", + res = remember(selectedLanguageCode, myFile, decimals) { + HumanReadable.fileSize( + bytes = myFile.toLongOrNull() ?: 0L, + decimals = decimals + ) + } ) } } \ No newline at end of file diff --git a/demo/src/webMain/kotlin/ui/NumberFormatDemo.kt b/demo/src/webMain/kotlin/feature/NumberFormatDemo.kt similarity index 52% rename from demo/src/webMain/kotlin/ui/NumberFormatDemo.kt rename to demo/src/webMain/kotlin/feature/NumberFormatDemo.kt index 2b6bac3..3329354 100644 --- a/demo/src/webMain/kotlin/ui/NumberFormatDemo.kt +++ b/demo/src/webMain/kotlin/feature/NumberFormatDemo.kt @@ -1,10 +1,20 @@ -package ui +package feature -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.sizeIn import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.buildAnnotatedString @@ -12,9 +22,9 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import monoBodyOrange -import monoBodyString -import monoBodyStringBold import nl.jacobras.humanreadable.HumanReadable +import ui.CodeExample +import ui.DecimalsInput @Composable internal fun NumberFormatDemo( @@ -29,7 +39,7 @@ internal fun NumberFormatDemo( style = MaterialTheme.typography.headlineLarge ) Spacer(Modifier.height(16.dp)) - var myNumber by remember { mutableStateOf("1000000.34") } + var myNumber by remember { mutableStateOf("105122.34") } Row(verticalAlignment = Alignment.CenterVertically) { Text( text = buildAnnotatedString { @@ -55,44 +65,31 @@ internal fun NumberFormatDemo( ) } Spacer(Modifier.height(8.dp)) - var decimals by remember { mutableStateOf("2") } - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = buildAnnotatedString { - withStyle(monoBodyOrange) { append("val ") } - append("decimals = ") - }, - style = monoBody - ) - TextField( - modifier = Modifier.sizeIn(minWidth = 20.dp), - value = decimals, - onValueChange = { decimals = it }, - isError = decimals.toIntOrNull() == null, - supportingText = if (decimals.toIntOrNull() == null) { - { Text("Invalid number") } - } else { - null - } - ) - } + var decimals by remember { mutableIntStateOf(1) } + DecimalsInput( + decimals = decimals, + onChange = { decimals = it } + ) + Spacer(Modifier.height(16.dp)) + + CodeExample( + code = "HumanReadable.number(myNumber, decimals)", + res = remember(selectedLanguageCode, myNumber, decimals) { + HumanReadable.number( + number = myNumber.toDoubleOrNull() ?: 0L, + decimals = decimals + ) + } + ) Spacer(Modifier.height(8.dp)) - Text( - text = buildAnnotatedString { - appendLine("HumanReadable.number(myNumber, decimals)") - withStyle(monoBodyStringBold) { - append("// \"") - append( - remember(selectedLanguageCode, myNumber, decimals) { - HumanReadable.number( - number = myNumber.toDoubleOrNull() ?: 0L, - decimals = decimals.toIntOrNull() ?: 0 - ) - }) - append("\"") - } - }, - style = monoBody + CodeExample( + code = "HumanReadable.abbreviation(myNumber, decimals)", + res = remember(selectedLanguageCode, myNumber, decimals) { + HumanReadable.abbreviation( + number = myNumber.toDoubleOrNull() ?: 0L, + decimals = decimals + ) + } ) } } \ No newline at end of file diff --git a/demo/src/webMain/kotlin/feature/TimeDemo.kt b/demo/src/webMain/kotlin/feature/TimeDemo.kt new file mode 100644 index 0000000..022aad3 --- /dev/null +++ b/demo/src/webMain/kotlin/feature/TimeDemo.kt @@ -0,0 +1,288 @@ +package feature + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.TimeZone +import kotlinx.datetime.minus +import kotlinx.datetime.toLocalDateTime +import monoBodyOrange +import nl.jacobras.humanreadable.HumanReadable +import nl.jacobras.humanreadable.time.FormatStyle +import nl.jacobras.humanreadable.time.Parts +import nl.jacobras.humanreadable.time.Rounding +import nl.jacobras.humanreadable.time.TimeUnit +import ui.CodeExample +import ui.appendKotlinCode +import kotlin.time.Clock +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +@OptIn(ExperimentalLayoutApi::class, ExperimentalTime::class) +@Composable +internal fun TimeDemo( + selectedLanguageCode: String, + modifier: Modifier = Modifier +) { + val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace) + val baseInstant = remember { Clock.System.now() } + var myInstant by remember { mutableStateOf(baseInstant.minus(90, DateTimeUnit.SECOND)) } + + Column(modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + Text( + text = "Date/Time formatting", + style = MaterialTheme.typography.headlineLarge + ) + Spacer(Modifier.height(16.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = buildAnnotatedString { + withStyle(monoBodyOrange) { append("val ") } + append("baseInstant =") + }, + style = monoBody + ) + DateTimeField(baseInstant) {} + } + Spacer(Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = buildAnnotatedString { + withStyle(monoBodyOrange) { append("val ") } + append("myInstant =") + }, + style = monoBody + ) + DateTimeField(myInstant) { myInstant = it } + } + Spacer(Modifier.height(4.dp)) + + FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Button(onClick = { myInstant = baseInstant }) { Text("[NOW]") } + Button(onClick = { myInstant = myInstant.minus(10.seconds) }) { Text("-10 sec") } + Button(onClick = { myInstant = myInstant.minus(1.minutes) }) { Text("-1 min") } + Button(onClick = { myInstant = myInstant.minus(1.hours) }) { Text("-1 hr") } + Button(onClick = { myInstant = myInstant.minus(1.days) }) { Text("-1 day") } + Button(onClick = { myInstant = myInstant.minus(1.days) }) { Text("-7 days") } + Button(onClick = { myInstant = myInstant.minus(30.days) }) { Text("-30 days") } + Button(onClick = { myInstant = myInstant.minus(365.days) }) { Text("-365 days") } + } + Spacer(Modifier.height(16.dp)) + + CodeExample( + code = "HumanReadable.duration(now - myInstant)", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.duration(baseInstant - myInstant) + } + ) + Spacer(Modifier.height(12.dp)) + + CodeExample( + code = "HumanReadable.timeAgo(myInstant)", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo(myInstant) + } + ) + Spacer(Modifier.height(12.dp)) + + CodeExample( + code = "HumanReadable.timeAgo(myLocalDate) // overloaded method that takes a LocalDate", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo( + date = myInstant.toLocalDateTime(TimeZone.currentSystemDefault()).date, + baseDate = baseInstant.toLocalDateTime(TimeZone.currentSystemDefault()).date + ) + } + ) + Spacer(Modifier.height(32.dp)) + + Text( + text = "Configuration parameters", + style = MaterialTheme.typography.titleLarge + ) + Text(buildAnnotatedString { + append("The examples below call ") + appendKotlinCode("HumanReadable.timeAgo( parameter1, parameter2, ... )") + append(". All parameters are optional and also work with ") + appendKotlinCode("HumanReadable.duration()") + append(".") + }) + Spacer(Modifier.height(16.dp)) + + CodeExample( + code = "formatStyle = FormatStyle(date = Date.Short)", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo( + myInstant, baseInstant, + formatStyle = FormatStyle(date = FormatStyle.Date.Short) + ) + }, + inline = true + ) + Spacer(Modifier.height(8.dp)) + CodeExample( + code = "formatStyle = FormatStyle(date = Date.Narrow)", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo( + myInstant, baseInstant, + formatStyle = FormatStyle(date = FormatStyle.Date.Narrow) + ) + }, + inline = true + ) + Spacer(Modifier.height(8.dp)) + CodeExample( + code = "formatStyle = FormatStyle(time = FormatStyle.Time.Digital)", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo( + myInstant, baseInstant, + formatStyle = FormatStyle(time = FormatStyle.Time.Digital) + ) + }, + inline = true + ) + Spacer(Modifier.height(8.dp)) + CodeExample( + code = "formatStyle = FormatStyle(indicateApproximation = true)", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo( + myInstant, baseInstant, + formatStyle = FormatStyle(indicateApproximation = true) + ) + }, + inline = true + ) + Spacer(Modifier.height(16.dp)) + + CodeExample( + code = "rounding = Rounding.Floor", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo( + myInstant, baseInstant, + rounding = Rounding.Floor + ) + }, + inline = true + ) + Spacer(Modifier.height(8.dp)) + CodeExample( + code = "rounding = Rounding.UpIfClose", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo( + myInstant, baseInstant, + rounding = Rounding.UpIfClose + ) + }, + inline = true + ) + Spacer(Modifier.height(16.dp)) + + CodeExample( + code = "parts = Parts(max = 3)", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo( + myInstant, baseInstant, + parts = Parts(max = 3) + ) + }, + inline = true + ) + Spacer(Modifier.height(8.dp)) + CodeExample( + code = "parts = Parts(max = 3, subpartCutOffs = mapOf(TimeUnit.Minutes to 10, TimeUnit.Hours to 12))", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo( + myInstant, baseInstant, + parts = Parts( + max = 3, + subpartCutOffs = mapOf(TimeUnit.Minutes to 10, TimeUnit.Hours to 12) + ) + ) + }, + inline = true + ) + Spacer(Modifier.height(8.dp)) + CodeExample( + code = "parts = Parts(smallestDuration = 10.minutes)", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo( + myInstant, baseInstant, + parts = Parts(smallestDuration = 10.minutes) + ) + }, + inline = true + ) + Spacer(Modifier.height(16.dp)) + + CodeExample( + code = "units = setOf(TimeUnit.Hours, TimeUnit.Days)", + res = remember(selectedLanguageCode, baseInstant, myInstant) { + HumanReadable.timeAgo( + myInstant, baseInstant, + units = setOf(TimeUnit.Hours, TimeUnit.Days) + ) + }, + inline = true + ) + Spacer(Modifier.height(8.dp)) + } +} + +@OptIn(ExperimentalTime::class) +@Composable +private fun DateTimeField(instant: Instant, onUpdate: (Instant) -> Unit) { + var error by remember { mutableStateOf(false) } + var value by remember(instant) { mutableStateOf(instant.toString()) } + + TextField( + modifier = Modifier.widthIn(min = 0.dp), + value = value, + onValueChange = { + value = it + try { + onUpdate(Instant.parse(it)) + error = false + } catch (e: IllegalArgumentException) { + error = true + } + }, + isError = error, + supportingText = if (error) { + { Text("Invalid date format. Please use ISO-8601 ;-)") } + } else null + ) +} \ No newline at end of file diff --git a/demo/src/webMain/kotlin/ui/AbbreviationDemo.kt b/demo/src/webMain/kotlin/ui/AbbreviationDemo.kt deleted file mode 100644 index a583211..0000000 --- a/demo/src/webMain/kotlin/ui/AbbreviationDemo.kt +++ /dev/null @@ -1,97 +0,0 @@ -package ui - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp -import monoBodyOrange -import monoBodyStringBold -import nl.jacobras.humanreadable.HumanReadable - -@Composable -internal fun AbbreviationDemo( - selectedLanguageCode: String, - modifier: Modifier = Modifier -) { - val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace) - - Column(modifier.fillMaxWidth()) { - Text( - text = "Abbreviation", - style = MaterialTheme.typography.headlineLarge - ) - Spacer(Modifier.height(16.dp)) - var myNumber by remember { mutableStateOf("3000") } - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = buildAnnotatedString { - withStyle(monoBodyOrange) { append("val ") } - append("myNumber = ") - }, - style = monoBody - ) - TextField( - modifier = Modifier.sizeIn(minWidth = 20.dp), - value = myNumber, - onValueChange = { - if (it.length < 16) { - myNumber = it - } - }, - isError = myNumber.toLongOrNull() == null, - supportingText = if (myNumber.toLongOrNull() == null) { - { Text("Invalid number") } - } else { - null - } - ) - } - Spacer(Modifier.height(8.dp)) - var decimals by remember { mutableStateOf("2") } - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = buildAnnotatedString { - withStyle(monoBodyOrange) { append("val ") } - append("decimals = ") - }, - style = monoBody - ) - TextField( - modifier = Modifier.sizeIn(minWidth = 20.dp), - value = decimals, - onValueChange = { decimals = it }, - isError = decimals.toIntOrNull() == null, - supportingText = if (decimals.toIntOrNull() == null) { - { Text("Invalid number") } - } else { - null - } - ) - } - Spacer(Modifier.height(8.dp)) - Text( - text = buildAnnotatedString { - appendLine("HumanReadable.abbreviation(myNumber, decimals)") - withStyle(monoBodyStringBold) { - append("// \"") - append( - remember(selectedLanguageCode, myNumber, decimals) { - HumanReadable.abbreviation( - number = myNumber.toLongOrNull() ?: 0L, - decimals = decimals.toIntOrNull() ?: 0 - ) - }) - append("\"") - } - }, - style = monoBody - ) - } -} \ No newline at end of file diff --git a/demo/src/webMain/kotlin/ui/CodeExample.kt b/demo/src/webMain/kotlin/ui/CodeExample.kt new file mode 100644 index 0000000..6b0480e --- /dev/null +++ b/demo/src/webMain/kotlin/ui/CodeExample.kt @@ -0,0 +1,19 @@ +package ui + +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily + +@Composable +internal fun CodeExample(code: String, res: String, inline: Boolean = false) { + SelectionContainer { + val separator = if (inline) " " else "\n" + Text( + text = buildAnnotatedString { appendKotlinCode("$code$separator// \"$res\"") }, + style = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace) + ) + } +} \ No newline at end of file diff --git a/demo/src/webMain/kotlin/ui/DecimalsInput.kt b/demo/src/webMain/kotlin/ui/DecimalsInput.kt new file mode 100644 index 0000000..4dda3a9 --- /dev/null +++ b/demo/src/webMain/kotlin/ui/DecimalsInput.kt @@ -0,0 +1,69 @@ +package ui + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import monoBodyOrange + +@Composable +internal fun DecimalsInput( + decimals: Int, + onChange: (Int) -> Unit, +) { + var input by remember { mutableStateOf(decimals.toString()) } + val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = buildAnnotatedString { + withStyle(monoBodyOrange) { append("val ") } + append("decimals = ") + }, + style = monoBody + ) + TextField( + modifier = Modifier.sizeIn(minWidth = 20.dp), + value = input, + onValueChange = { + input = it + val new = it.toIntOrNull() + if (new != null) { + onChange(new) + } + }, + isError = input.toIntOrNull() == null, + supportingText = if (input.toIntOrNull() == null) { + { Text("Invalid number") } + } else { + null + } + ) + Spacer(Modifier.width(8.dp)) + Button(onClick = { + val new = (decimals - 1).coerceAtLeast(0) + input = new.toString() + onChange(new) + }) { Text("-") } + Spacer(Modifier.width(4.dp)) + Button(onClick = { + val new = decimals + 1 + input = new.toString() + onChange(new) + }) { Text("+") } + } +} \ No newline at end of file diff --git a/demo/src/webMain/kotlin/ui/DistanceDemo.kt b/demo/src/webMain/kotlin/ui/DistanceDemo.kt deleted file mode 100644 index 5461584..0000000 --- a/demo/src/webMain/kotlin/ui/DistanceDemo.kt +++ /dev/null @@ -1,114 +0,0 @@ -package ui - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp -import monoBodyOrange -import monoBodyStringBold -import nl.jacobras.humanreadable.DistanceUnit -import nl.jacobras.humanreadable.HumanReadable - -@Composable -internal fun DistanceDemo( - selectedLanguageCode: String, - modifier: Modifier = Modifier -) { - val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace) - - Column(modifier.fillMaxWidth()) { - Text( - text = "Distance", - style = MaterialTheme.typography.headlineLarge - ) - Spacer(Modifier.height(16.dp)) - var myNumber by remember { mutableStateOf("3000") } - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = buildAnnotatedString { - withStyle(monoBodyOrange) { append("val ") } - append("myDistance = ") - }, - style = monoBody - ) - TextField( - modifier = Modifier.sizeIn(minWidth = 20.dp), - value = myNumber, - onValueChange = { - if (it.length < 16) { - myNumber = it - } - }, - isError = myNumber.toLongOrNull() == null, - supportingText = if (myNumber.toLongOrNull() == null) { - { Text("Invalid number") } - } else { - null - } - ) - } - Spacer(Modifier.height(8.dp)) - var decimals by remember { mutableStateOf("2") } - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = buildAnnotatedString { - withStyle(monoBodyOrange) { append("val ") } - append("decimals = ") - }, - style = monoBody - ) - TextField( - modifier = Modifier.sizeIn(minWidth = 20.dp), - value = decimals, - onValueChange = { decimals = it }, - isError = decimals.toIntOrNull() == null, - supportingText = if (decimals.toIntOrNull() == null) { - { Text("Invalid number") } - } else { - null - } - ) - } - Spacer(Modifier.height(8.dp)) - Text( - text = buildAnnotatedString { - appendLine("HumanReadable.distance(myDistance, DistanceUnit.Meter, decimals)") - withStyle(monoBodyStringBold) { - append("// \"") - append( - remember(selectedLanguageCode, myNumber, decimals) { - HumanReadable.distance( - value = myNumber.toLongOrNull() ?: 0L, - unit = DistanceUnit.Meter, - decimals = decimals.toIntOrNull() ?: 0 - ) - }) - append("\"") - } - appendLine() - - appendLine("HumanReadable.distance(myDistance, DistanceUnit.Foot, decimals)") - withStyle(monoBodyStringBold) { - append("// \"") - append( - remember(selectedLanguageCode, myNumber, decimals) { - HumanReadable.distance( - value = myNumber.toLongOrNull() ?: 0L, - unit = DistanceUnit.Foot, - decimals = decimals.toIntOrNull() ?: 0 - ) - }) - append("\"") - } - }, - style = monoBody - ) - } -} \ No newline at end of file diff --git a/demo/src/webMain/kotlin/ui/FeaturesList.kt b/demo/src/webMain/kotlin/ui/FeaturesList.kt new file mode 100644 index 0000000..9265769 --- /dev/null +++ b/demo/src/webMain/kotlin/ui/FeaturesList.kt @@ -0,0 +1,35 @@ +package ui + +import Feature +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun FeaturesList( + selected: Feature, + onSelect: (Feature) -> Unit +) { + Column { + for (feature in Feature.entries) { + Text( + text = feature.title, + fontWeight = if (selected == feature) { + FontWeight.Bold + } else { + FontWeight.Normal + }, + modifier = Modifier + .fillMaxWidth() + .clickable { onSelect(feature) } + .padding(8.dp, 4.dp) + ) + } + } +} \ No newline at end of file diff --git a/demo/src/webMain/kotlin/ui/FlexibleLayout.kt b/demo/src/webMain/kotlin/ui/FlexibleLayout.kt deleted file mode 100644 index 1b3f54f..0000000 --- a/demo/src/webMain/kotlin/ui/FlexibleLayout.kt +++ /dev/null @@ -1,33 +0,0 @@ -package ui - -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@OptIn(ExperimentalLayoutApi::class) -@Composable -internal fun FlexibleLayout( - modifier: Modifier = Modifier, - timeDemo: @Composable (Modifier) -> Unit, - fileSizeDemo: @Composable (Modifier) -> Unit, - abbreviationDemo: @Composable (Modifier) -> Unit, - numberFormatDemo: @Composable (Modifier) -> Unit, - distanceDemo: @Composable (Modifier) -> Unit, -) { - val colModifier = Modifier - .widthIn(max = 500.dp) - .padding(horizontal = 16.dp) - - FlowRow( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceEvenly, - verticalArrangement = Arrangement.spacedBy(48.dp) - ) { - timeDemo(colModifier) - fileSizeDemo(colModifier) - abbreviationDemo(colModifier) - numberFormatDemo(colModifier) - distanceDemo(colModifier) - } -} \ No newline at end of file diff --git a/demo/src/webMain/kotlin/ui/TimeDemo.kt b/demo/src/webMain/kotlin/ui/TimeDemo.kt deleted file mode 100644 index 96aa315..0000000 --- a/demo/src/webMain/kotlin/ui/TimeDemo.kt +++ /dev/null @@ -1,142 +0,0 @@ -package ui - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp -import kotlinx.datetime.* -import monoBodyOrange -import monoBodyStringBold -import nl.jacobras.humanreadable.HumanReadable -import kotlin.time.Clock -import kotlin.time.Duration.Companion.days -import kotlin.time.Duration.Companion.hours -import kotlin.time.ExperimentalTime -import kotlin.time.Instant - -@OptIn(ExperimentalLayoutApi::class, ExperimentalTime::class) -@Composable -internal fun TimeDemo( - selectedLanguageCode: String, - modifier: Modifier = Modifier -) { - val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace) - val now = remember { Clock.System.now() } - var instant1 by remember { mutableStateOf(now.minus(1337, DateTimeUnit.HOUR)) } - var instant2 by remember { mutableStateOf(now.plus(2, DateTimeUnit.HOUR)) } - - Column(modifier.fillMaxWidth()) { - Text( - text = "Date/Time", - style = MaterialTheme.typography.headlineLarge - ) - Spacer(Modifier.height(16.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = buildAnnotatedString { - withStyle(monoBodyOrange) { append("val ") } - append("instant1 =") - }, - style = monoBody - ) - DateTimeField(instant1) { instant1 = it } - } - Spacer(Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = buildAnnotatedString { - withStyle(monoBodyOrange) { append("val ") } - append("instant2 =") - }, - style = monoBody - ) - DateTimeField(instant2) { instant2 = it } - } - Spacer(Modifier.height(4.dp)) - - FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - Button(onClick = { instant2 = instant2.plus(1.hours) }) { Text("+ hour") } - Button(onClick = { instant2 = instant2.plus(1.days) }) { Text("+ day") } - Button(onClick = { instant2 = instant2.plus(30.days) }) { Text("+ ~month") } - Button(onClick = { instant2 = instant2.plus(365.days) }) { Text("+ ~year") } - } - Spacer(Modifier.height(16.dp)) - - Text( - text = buildAnnotatedString { - appendLine("HumanReadable.timeAgo(instant1)") - withStyle(monoBodyStringBold) { - append("// \"") - append(remember(selectedLanguageCode, instant1) { HumanReadable.timeAgo(instant1) }) - appendLine("\"") - } - }, - style = monoBody - ) - Text( - text = buildAnnotatedString { - appendLine("HumanReadable.timeAgo(instant2)") - withStyle(monoBodyStringBold) { - append("// \"") - append(remember(selectedLanguageCode, instant2) { HumanReadable.timeAgo(instant2) }) - append("\"") - } - }, - style = monoBody - ) - Spacer(Modifier.height(16.dp)) - - Text( - text = buildAnnotatedString { - appendLine("HumanReadable.duration(instant2 - instant1)") - withStyle(monoBodyStringBold) { - append("// \"") - append(remember(selectedLanguageCode, instant1, instant2) { - HumanReadable.duration(instant2 - instant1) - }) - append("\"") - } - }, - style = monoBody - ) - } -} - -@OptIn(ExperimentalTime::class) -@Composable -private fun DateTimeField(instant: Instant, onUpdate: (Instant) -> Unit) { - var error by remember { mutableStateOf(false) } - var value by remember(instant) { mutableStateOf(instant.toString()) } - - TextField( - modifier = Modifier.widthIn(min = 0.dp), - value = value, - onValueChange = { - value = it - try { - onUpdate(Instant.parse(it)) - error = false - } catch (e: IllegalArgumentException) { - error = true - } - }, - isError = error, - supportingText = if (error) { - { Text("Invalid date format. Please use ISO-8601 ;-)") } - } else null - ) -} \ No newline at end of file diff --git a/demo/src/webMain/kotlin/ui/highlightKotlinCode.kt b/demo/src/webMain/kotlin/ui/highlightKotlinCode.kt new file mode 100644 index 0000000..5cea35a --- /dev/null +++ b/demo/src/webMain/kotlin/ui/highlightKotlinCode.kt @@ -0,0 +1,44 @@ +package ui + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle + +fun AnnotatedString.Builder.appendKotlinCode(code: String) { + var lastIndex = 0 + + for (match in TOKEN_REGEX.findAll(code)) { + if (match.range.first > lastIndex) { + append(code.substring(lastIndex, match.range.first)) + } + + val groups = match.groupValues + when { + groups[1].isNotEmpty() -> withStyle( + SpanStyle(color = Color(0xFF6aab73), fontWeight = FontWeight.Bold) + ) { append(match.value) } + groups[2].isNotEmpty() || groups[3].isNotEmpty() -> withStyle( + SpanStyle(color = Color(0xFF00627A)) + ) { append(match.value) } + groups[4].isNotEmpty() -> withStyle( + SpanStyle(color = Color(0xFF999999)) + ) { append(match.value) } + else -> append(match.value) + } + + lastIndex = match.range.last + 1 + } + + if (lastIndex < code.length) { + append(code.substring(lastIndex)) + } +} + +private val TOKEN_REGEX = Regex( + """(//[^\n]*)""" + // 1: comments + """|(\b[A-Z][A-Za-z0-9_]*\b)""" + // 2: class names + """|(\b[a-z_][A-Za-z0-9_]*)(?=\s*\()""" + // 3: method names + """|([(),.+*-])""" // 4: punctuation/operators +) \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c7bdda2..4a073d6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,17 @@ [versions] kotlin = "2.4.0" -compose-plugin = "1.12.0-alpha02" +compose = "1.12.0-alpha02" +compose-adaptive = "1.3.0-beta02" [libraries] assertK = { module = "com.willowtreeapps.assertk:assertk", version = "0.28.1" } +compose-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "compose-adaptive" } +compose-adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "compose-adaptive" } +compose-adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "compose-adaptive" } kotlinX-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version = "0.5.0" } kotlinX-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.8.0" } [plugins] compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } +jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } \ No newline at end of file From 0fa1dbcbbf09571cc09651638d5ef52d94662328 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 2 Jul 2026 08:29:28 +0200 Subject: [PATCH 30/57] Fix timeAgo() with LocalDate returning "now" incorrectly --- .../humanreadable/time/HumanReadableRelativeTime.kt | 4 ++-- .../time/HumanReadableRelativeTimeTests.kt | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt index 04906db..1208bf8 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt @@ -2,6 +2,7 @@ package nl.jacobras.humanreadable.time import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate +import kotlinx.datetime.daysUntil import kotlinx.datetime.minus import kotlinx.datetime.plus import nl.jacobras.humanreadable.HumanReadable.strings @@ -84,7 +85,7 @@ internal fun formatTimeAgo( baseDate -> return strings.dateTime.today baseDate.plus(1, DateTimeUnit.DAY) -> return strings.dateTime.tomorrow } - val secondsAgo = (baseDate - date).seconds + val secondsAgo = date.daysUntil(baseDate) * 86_400 return when { secondsAgo < 0 -> strings.dateTime.timeInFuture( @@ -97,7 +98,6 @@ internal fun formatTimeAgo( rounding = rounding ) ) - secondsAgo <= 1 -> strings.dateTime.now else -> strings.dateTime.timeAgo( formatDuration( duration = secondsAgo.absoluteValue.seconds, diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt index 4b3112b..d46b916 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt @@ -67,5 +67,18 @@ class HumanReadableRelativeTimeTests { baseDate = today ) ).isEqualTo("tomorrow") + + assertThat( + HumanReadable.timeAgo( + date = today.minus(2, DateTimeUnit.DAY), + baseDate = today + ) + ).isEqualTo("2 days ago") + assertThat( + HumanReadable.timeAgo( + date = today.plus(2, DateTimeUnit.DAY), + baseDate = today + ) + ).isEqualTo("in 2 days") } } \ No newline at end of file From f35481beacecf481df5c607935efd586c71347a6 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 2 Jul 2026 08:32:54 +0200 Subject: [PATCH 31/57] Rename parameters --- demo/src/webMain/kotlin/feature/TimeDemo.kt | 30 +++--- .../jacobras/humanreadable/HumanReadable.kt | 26 +++--- .../time/HumanReadableDuration.kt | 4 +- .../time/HumanReadableRelativeTime.kt | 20 ++-- .../time/{Parts.kt => PartsConfig.kt} | 2 +- .../jacobras/humanreadable/time/Rounding.kt | 4 +- .../time/HumanReadableDurationTests.kt | 92 +++++++++---------- 7 files changed, 89 insertions(+), 89 deletions(-) rename src/commonMain/kotlin/nl/jacobras/humanreadable/time/{Parts.kt => PartsConfig.kt} (98%) diff --git a/demo/src/webMain/kotlin/feature/TimeDemo.kt b/demo/src/webMain/kotlin/feature/TimeDemo.kt index 022aad3..2a28033 100644 --- a/demo/src/webMain/kotlin/feature/TimeDemo.kt +++ b/demo/src/webMain/kotlin/feature/TimeDemo.kt @@ -33,7 +33,7 @@ import kotlinx.datetime.toLocalDateTime import monoBodyOrange import nl.jacobras.humanreadable.HumanReadable import nl.jacobras.humanreadable.time.FormatStyle -import nl.jacobras.humanreadable.time.Parts +import nl.jacobras.humanreadable.time.PartsConfig import nl.jacobras.humanreadable.time.Rounding import nl.jacobras.humanreadable.time.TimeUnit import ui.CodeExample @@ -144,44 +144,44 @@ internal fun TimeDemo( Spacer(Modifier.height(16.dp)) CodeExample( - code = "formatStyle = FormatStyle(date = Date.Short)", + code = "formatting = FormatStyle(date = Date.Short)", res = remember(selectedLanguageCode, baseInstant, myInstant) { HumanReadable.timeAgo( myInstant, baseInstant, - formatStyle = FormatStyle(date = FormatStyle.Date.Short) + formatting = FormatStyle(date = FormatStyle.Date.Short) ) }, inline = true ) Spacer(Modifier.height(8.dp)) CodeExample( - code = "formatStyle = FormatStyle(date = Date.Narrow)", + code = "formatting = FormatStyle(date = Date.Narrow)", res = remember(selectedLanguageCode, baseInstant, myInstant) { HumanReadable.timeAgo( myInstant, baseInstant, - formatStyle = FormatStyle(date = FormatStyle.Date.Narrow) + formatting = FormatStyle(date = FormatStyle.Date.Narrow) ) }, inline = true ) Spacer(Modifier.height(8.dp)) CodeExample( - code = "formatStyle = FormatStyle(time = FormatStyle.Time.Digital)", + code = "formatting = FormatStyle(time = FormatStyle.Time.Digital)", res = remember(selectedLanguageCode, baseInstant, myInstant) { HumanReadable.timeAgo( myInstant, baseInstant, - formatStyle = FormatStyle(time = FormatStyle.Time.Digital) + formatting = FormatStyle(time = FormatStyle.Time.Digital) ) }, inline = true ) Spacer(Modifier.height(8.dp)) CodeExample( - code = "formatStyle = FormatStyle(indicateApproximation = true)", + code = "formatting = FormatStyle(indicateApproximation = true)", res = remember(selectedLanguageCode, baseInstant, myInstant) { HumanReadable.timeAgo( myInstant, baseInstant, - formatStyle = FormatStyle(indicateApproximation = true) + formatting = FormatStyle(indicateApproximation = true) ) }, inline = true @@ -212,22 +212,22 @@ internal fun TimeDemo( Spacer(Modifier.height(16.dp)) CodeExample( - code = "parts = Parts(max = 3)", + code = "parts = PartsConfig(max = 3)", res = remember(selectedLanguageCode, baseInstant, myInstant) { HumanReadable.timeAgo( myInstant, baseInstant, - parts = Parts(max = 3) + parts = PartsConfig(max = 3) ) }, inline = true ) Spacer(Modifier.height(8.dp)) CodeExample( - code = "parts = Parts(max = 3, subpartCutOffs = mapOf(TimeUnit.Minutes to 10, TimeUnit.Hours to 12))", + code = "parts = PartsConfig(max = 3, subpartCutOffs = mapOf(TimeUnit.Minutes to 10, TimeUnit.Hours to 12))", res = remember(selectedLanguageCode, baseInstant, myInstant) { HumanReadable.timeAgo( myInstant, baseInstant, - parts = Parts( + parts = PartsConfig( max = 3, subpartCutOffs = mapOf(TimeUnit.Minutes to 10, TimeUnit.Hours to 12) ) @@ -237,11 +237,11 @@ internal fun TimeDemo( ) Spacer(Modifier.height(8.dp)) CodeExample( - code = "parts = Parts(smallestDuration = 10.minutes)", + code = "parts = PartsConfig(smallestDuration = 10.minutes)", res = remember(selectedLanguageCode, baseInstant, myInstant) { HumanReadable.timeAgo( myInstant, baseInstant, - parts = Parts(smallestDuration = 10.minutes) + parts = PartsConfig(smallestDuration = 10.minutes) ) }, inline = true diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 20ef550..6b14722 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -12,7 +12,7 @@ import nl.jacobras.humanreadable.HumanReadable.number import nl.jacobras.humanreadable.i18n.HumanReadableStrings import nl.jacobras.humanreadable.i18n.Localisation import nl.jacobras.humanreadable.time.FormatStyle -import nl.jacobras.humanreadable.time.Parts +import nl.jacobras.humanreadable.time.PartsConfig import nl.jacobras.humanreadable.time.RelativeTime import nl.jacobras.humanreadable.time.Rounding import nl.jacobras.humanreadable.time.TimeUnit @@ -60,7 +60,7 @@ public object HumanReadable { * * @param instant The [Instant] to format. * @param baseInstant The base/starting [Instant], defaulting to "now". - * @param formatStyle The [FormatStyle] to use, defaulting to [FormatStyle.Long]. + * @param formatting The [FormatStyle] to use, defaulting to [FormatStyle.Long]. * @param parts Configures the formatting of multiple parts, defaulting to 1 part. * @param units The [TimeUnit]s to limit to during formatting, not limited by default. * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp]. @@ -70,15 +70,15 @@ public object HumanReadable { public fun timeAgo( instant: Instant, baseInstant: Instant = Clock.System.now(), - formatStyle: FormatStyle = FormatStyle(), - parts: Parts = Parts(), + formatting: FormatStyle = FormatStyle(), + parts: PartsConfig = PartsConfig(), units: Set = TimeUnit.all, rounding: Rounding = Rounding.HalfUp ): String { return formatTimeAgo( instant = instant, baseInstant = baseInstant, - formatStyle = formatStyle, + formatting = formatting, parts = parts, units = units, rounding = rounding @@ -91,7 +91,7 @@ public object HumanReadable { * * @param date The [LocalDate] to format. * @param baseDate The base/starting [LocalDate], defaulting to "today". - * @param formatStyle The [FormatStyle] to use, defaulting to "long". + * @param formatting The [FormatStyle] to use, defaulting to "long". * @param parts Configures the formatting of multiple parts, defaulting to 1 part. * @param units The [TimeUnit]s to limit to during formatting, not limited by default. * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp]. @@ -101,15 +101,15 @@ public object HumanReadable { public fun timeAgo( date: LocalDate, baseDate: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), - formatStyle: FormatStyle = FormatStyle(), - parts: Parts = Parts(), + formatting: FormatStyle = FormatStyle(), + parts: PartsConfig = PartsConfig(), units: Set = TimeUnit.all, rounding: Rounding = Rounding.HalfUp ): String { return formatTimeAgo( date = date, baseDate = baseDate, - formatStyle = formatStyle, + formatting = formatting, parts = parts, units = units, rounding = rounding @@ -121,22 +121,22 @@ public object HumanReadable { * For example, a duration of 3 seconds returns "3 seconds". * * @param duration The [Duration] to format. - * @param formatStyle The [FormatStyle] to use, defaulting to "long". + * @param formatting The [FormatStyle] to use, defaulting to "long". * @param parts Configures the formatting of multiple parts, defaulting to 1 part. * @param units The [TimeUnit]s to limit to during formatting, not limited by default. * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp]. */ public fun duration( duration: Duration, - formatStyle: FormatStyle = FormatStyle(), - parts: Parts = Parts(), + formatting: FormatStyle = FormatStyle(), + parts: PartsConfig = PartsConfig(), units: Set = TimeUnit.all, rounding: Rounding = Rounding.HalfUp ): String { return formatDuration( duration = duration, relativeTime = RelativeTime.Present, - format = formatStyle, + format = formatting, parts = parts, units = units, rounding = rounding diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index da5ded2..415e264 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -19,7 +19,7 @@ internal fun formatDuration( duration: Duration, relativeTime: RelativeTime, format: FormatStyle, - parts: Parts, + parts: PartsConfig, units: Set, rounding: Rounding ): String { @@ -91,7 +91,7 @@ private fun getNeededParts( units: Set, duration: Duration, rounding: Rounding, - parts: Parts + parts: PartsConfig ): Map { val unitsDescending = units.sorted().reversed() val res = mutableMapOf() diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt index 1208bf8..20edaca 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt @@ -17,7 +17,7 @@ import kotlin.time.Instant * * @param instant The [Instant] to compare with [baseInstant]. * @param baseInstant The base/starting [Instant], usually "now". - * @param formatStyle The [FormatStyle] to use. + * @param formatting The [FormatStyle] to use. * @param parts Configures the formatting of multiple parts (defaults to 1 part). * @param units The [TimeUnit]s to limit to during formatting. * @param rounding The [Rounding] strategy to use. @@ -26,8 +26,8 @@ import kotlin.time.Instant internal fun formatTimeAgo( instant: Instant, baseInstant: Instant, - formatStyle: FormatStyle, - parts: Parts, + formatting: FormatStyle, + parts: PartsConfig, units: Set, rounding: Rounding ): String { @@ -39,7 +39,7 @@ internal fun formatTimeAgo( formatDuration( duration = diff.absoluteValue, relativeTime = RelativeTime.Future, - format = formatStyle, + format = formatting, parts = parts, units = units, rounding = rounding @@ -50,7 +50,7 @@ internal fun formatTimeAgo( formatDuration( duration = diff.absoluteValue, relativeTime = RelativeTime.Past, - format = formatStyle, + format = formatting, parts = parts, units = units, rounding = rounding @@ -67,7 +67,7 @@ internal fun formatTimeAgo( * * @param date The [LocalDate] to compare with [baseDate]. * @param baseDate The base/starting [LocalDate], usually "today". - * @param formatStyle The [FormatStyle] to use. + * @param formatting The [FormatStyle] to use. * @param parts Configures the formatting of multiple parts (defaults to 1 part). * @param units The [TimeUnit]s to limit to during formatting. * @param rounding The [Rounding] strategy to use. @@ -75,8 +75,8 @@ internal fun formatTimeAgo( internal fun formatTimeAgo( date: LocalDate, baseDate: LocalDate, - formatStyle: FormatStyle, - parts: Parts, + formatting: FormatStyle, + parts: PartsConfig, units: Set, rounding: Rounding ): String { @@ -92,7 +92,7 @@ internal fun formatTimeAgo( formatDuration( duration = secondsAgo.absoluteValue.seconds, relativeTime = RelativeTime.Future, - format = formatStyle, + format = formatting, parts = parts, units = units, rounding = rounding @@ -102,7 +102,7 @@ internal fun formatTimeAgo( formatDuration( duration = secondsAgo.absoluteValue.seconds, relativeTime = RelativeTime.Past, - format = formatStyle, + format = formatting, parts = parts, units = units, rounding = rounding diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Parts.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt similarity index 98% rename from src/commonMain/kotlin/nl/jacobras/humanreadable/time/Parts.kt rename to src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt index 87f03b5..d49386a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Parts.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt @@ -9,7 +9,7 @@ import kotlin.time.Duration * - "1 day, 20 hours" could be output; * - "1 day, 20 minutes" will never be output (because minutes are not next to days). */ -public data class Parts( +public data class PartsConfig( /** * Maximum number of parts to show. diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt index a4c10bf..39f577f 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt @@ -3,7 +3,7 @@ package nl.jacobras.humanreadable.time /** * Rounding method. * - * Note about multi-parts (when [Parts.max] > 1): rounding is done on the smallest included unit. + * Note about multi-parts (when [PartsConfig.max] > 1): rounding is done on the smallest included unit. */ public sealed interface Rounding { @@ -23,7 +23,7 @@ public sealed interface Rounding { * - `12.days` results in "2 weeks". * - `46.days` results in "2 months". * - * Note that when [Parts.max] is larger than 1, the rounding is done on the smallest unit. + * Note that when [PartsConfig.max] is larger than 1, the rounding is done on the smallest unit. */ public data object HalfUp : Rounding diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 156d64b..b081de7 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -134,100 +134,100 @@ class HumanReadableDurationTests { @Test fun multipleParts() { assertThat( - duration(70.seconds, parts = Parts(max = 2)) + duration(70.seconds, parts = PartsConfig(max = 2)) ).isEqualTo("1 minute, 10 seconds") assertThat( - duration(90.minutes + 10.seconds, parts = Parts(max = 3, subpartCutOffs = emptyMap())) + duration(90.minutes + 10.seconds, parts = PartsConfig(max = 3, subpartCutOffs = emptyMap())) ).isEqualTo("1 hour, 30 minutes, 10 seconds") assertThat( - duration(3.seconds, parts = Parts(max = 2)) + duration(3.seconds, parts = PartsConfig(max = 2)) ).isEqualTo("3 seconds") assertThat( - duration(1.hours + 10.seconds, parts = Parts(max = 2)) + duration(1.hours + 10.seconds, parts = PartsConfig(max = 2)) ).isEqualTo("1 hour, 10 seconds") assertThat( - duration(1.minutes + 55.seconds, rounding = HalfUp, parts = Parts(max = 2)) + duration(1.minutes + 55.seconds, rounding = HalfUp, parts = PartsConfig(max = 2)) ).isEqualTo("1 minute, 55 seconds") assertThat( - duration(1.minutes + 55.seconds, rounding = UpIfClose, parts = Parts(max = 2)) + duration(1.minutes + 55.seconds, rounding = UpIfClose, parts = PartsConfig(max = 2)) ).isEqualTo("2 minutes") // Edge cases assertThat( - duration(59.minutes + 55.seconds, rounding = UpIfClose, parts = Parts(max = 2)) + duration(59.minutes + 55.seconds, rounding = UpIfClose, parts = PartsConfig(max = 2)) ).isEqualTo("1 hour") assertThat( - duration(23.hours + 55.minutes, rounding = UpIfClose, parts = Parts(max = 2)) + duration(23.hours + 55.minutes, rounding = UpIfClose, parts = PartsConfig(max = 2)) ).isEqualTo("1 day") assertThat( - duration(6.days + 23.hours, rounding = UpIfClose, parts = Parts(max = 2)) + duration(6.days + 23.hours, rounding = UpIfClose, parts = PartsConfig(max = 2)) ).isEqualTo("7 days") } @Test fun smallestDuration() { assertThat( - duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = longStyle) + duration(44.seconds, parts = PartsConfig(smallestDuration = 45.seconds), formatting = longStyle) ).isEqualTo("less than 45 seconds") assertThat( - duration(10.minutes, parts = Parts(smallestDuration = 15.minutes), formatStyle = longStyle) + duration(10.minutes, parts = PartsConfig(smallestDuration = 15.minutes), formatting = longStyle) ).isEqualTo("less than 15 minutes") assertThat( - duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = shortStyle) + duration(44.seconds, parts = PartsConfig(smallestDuration = 45.seconds), formatting = shortStyle) ).isEqualTo("<45 sec") assertThat( - duration(44.seconds, parts = Parts(smallestDuration = 45.seconds), formatStyle = narrowStyle) + duration(44.seconds, parts = PartsConfig(smallestDuration = 45.seconds), formatting = narrowStyle) ).isEqualTo("<45s") - assertThat(duration(45.seconds, parts = Parts(smallestDuration = 45.seconds))).isEqualTo("45 seconds") + assertThat(duration(45.seconds, parts = PartsConfig(smallestDuration = 45.seconds))).isEqualTo("45 seconds") } @Test fun subpartsCutOffs() { assertThat( - duration(1.minutes + 4.seconds, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Minutes to 2))) + duration(1.minutes + 4.seconds, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Minutes to 2))) ).isEqualTo("1 minute, 4 seconds") assertThat( - duration(2.minutes + 4.seconds, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Minutes to 2))) + duration(2.minutes + 4.seconds, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Minutes to 2))) ).isEqualTo("2 minutes") assertThat( - duration(19.hours + 4.minutes, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Hours to 2))) + duration(19.hours + 4.minutes, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Hours to 2))) ).isEqualTo("19 hours") assertThat( - duration(20.hours + 4.minutes, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Hours to 2))) + duration(20.hours + 4.minutes, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Hours to 2))) ).isEqualTo("20 hours") assertThat( - duration(1.days + 5.hours, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Days to 2))) + duration(1.days + 5.hours, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Days to 2))) ).isEqualTo("1 day, 5 hours") assertThat( - duration(2.days + 5.hours, parts = Parts(max = 2, subpartCutOffs = mapOf(TimeUnit.Days to 2))) + duration(2.days + 5.hours, parts = PartsConfig(max = 2, subpartCutOffs = mapOf(TimeUnit.Days to 2))) ).isEqualTo("2 days") } @Test fun formatStyle() { assertThat( - duration(1.hours + 50.minutes, formatStyle = longStyle, parts = Parts(max = 2)) + duration(1.hours + 50.minutes, formatting = longStyle, parts = PartsConfig(max = 2)) ).isEqualTo("1 hour, 50 minutes") assertThat( - duration(1.hours + 50.minutes, formatStyle = shortStyle, parts = Parts(max = 2)) + duration(1.hours + 50.minutes, formatting = shortStyle, parts = PartsConfig(max = 2)) ).isEqualTo("1 hr, 50 min") assertThat( - duration(1.hours + 50.minutes, formatStyle = narrowStyle, parts = Parts(max = 2)) + duration(1.hours + 50.minutes, formatting = narrowStyle, parts = PartsConfig(max = 2)) ).isEqualTo("1h 50m") - assertThat(duration(1.hours, formatStyle = longStyle)).isEqualTo("1 hour") - assertThat(duration(1.hours, formatStyle = shortStyle)).isEqualTo("1 hr") - assertThat(duration(1.hours, formatStyle = narrowStyle)).isEqualTo("1h") - assertThat(duration(14.days, formatStyle = longStyle)).isEqualTo("2 weeks") - assertThat(duration(14.days, formatStyle = shortStyle)).isEqualTo("2 wks") - assertThat(duration(14.days, formatStyle = narrowStyle)).isEqualTo("2w") - assertThat(duration(180.days, formatStyle = longStyle)).isEqualTo("6 months") - assertThat(duration(180.days, formatStyle = shortStyle)).isEqualTo("6 mths") - assertThat(duration(180.days, formatStyle = narrowStyle)).isEqualTo("6m") + assertThat(duration(1.hours, formatting = longStyle)).isEqualTo("1 hour") + assertThat(duration(1.hours, formatting = shortStyle)).isEqualTo("1 hr") + assertThat(duration(1.hours, formatting = narrowStyle)).isEqualTo("1h") + assertThat(duration(14.days, formatting = longStyle)).isEqualTo("2 weeks") + assertThat(duration(14.days, formatting = shortStyle)).isEqualTo("2 wks") + assertThat(duration(14.days, formatting = narrowStyle)).isEqualTo("2w") + assertThat(duration(180.days, formatting = longStyle)).isEqualTo("6 months") + assertThat(duration(180.days, formatting = shortStyle)).isEqualTo("6 mths") + assertThat(duration(180.days, formatting = narrowStyle)).isEqualTo("6m") } @Test @@ -235,32 +235,32 @@ class HumanReadableDurationTests { assertThat( duration( duration = 1.hours, - formatStyle = longStyle.copy(indicateApproximation = true) + formatting = longStyle.copy(indicateApproximation = true) ) ).isEqualTo("1 hour") assertThat( duration( duration = 1.hours + 1.minutes, - formatStyle = longStyle.copy(indicateApproximation = true) + formatting = longStyle.copy(indicateApproximation = true) ) ).isEqualTo("about 1 hour") assertThat( duration( duration = 1.hours + 1.minutes, - formatStyle = shortStyle.copy(indicateApproximation = true) + formatting = shortStyle.copy(indicateApproximation = true) ) ).isEqualTo("~1 hr") assertThat( duration( duration = 1.hours + 1.minutes, - formatStyle = narrowStyle.copy(indicateApproximation = true) + formatting = narrowStyle.copy(indicateApproximation = true) ) ).isEqualTo("~1h") assertThat( duration( duration = 1.hours + 1.minutes, - formatStyle = longStyle.copy(indicateApproximation = true), - parts = Parts(smallestDuration = 1.days) + formatting = longStyle.copy(indicateApproximation = true), + parts = PartsConfig(smallestDuration = 1.days) ) ).isEqualTo("less than 1 day") } @@ -270,32 +270,32 @@ class HumanReadableDurationTests { assertThat( duration( duration = 1.hours, - formatStyle = FormatStyle(time = FormatStyle.Time.Digital), - parts = Parts(max = 5) + formatting = FormatStyle(time = FormatStyle.Time.Digital), + parts = PartsConfig(max = 5) ) ).isEqualTo("01:00:00") assertThat( duration( duration = 1.hours + 10.minutes + 5.seconds, - formatStyle = FormatStyle(time = FormatStyle.Time.Digital), - parts = Parts(max = 5) + formatting = FormatStyle(time = FormatStyle.Time.Digital), + parts = PartsConfig(max = 5) ) ).isEqualTo("01:10:05") assertThat( duration( duration = 5.days + 1.hours, - formatStyle = FormatStyle(time = FormatStyle.Time.Digital), - parts = Parts(max = 5) + formatting = FormatStyle(time = FormatStyle.Time.Digital), + parts = PartsConfig(max = 5) ) ).isEqualTo("5 days, 01:00:00") assertThat( duration( duration = 21.days + 2.days + 1.hours, - formatStyle = FormatStyle(time = FormatStyle.Time.Digital), - parts = Parts(max = 5) + formatting = FormatStyle(time = FormatStyle.Time.Digital), + parts = PartsConfig(max = 5) ) ).isEqualTo("3 weeks, 2 days, 01:00:00") } From 4eda16d7f1d9948acefaddd828c149fc6dc1cbda Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 2 Jul 2026 08:45:19 +0200 Subject: [PATCH 32/57] Update README --- README.md | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 39177e9..74eb482 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,33 @@ HumanReadable.duration(7.days) // "1 week" HumanReadable.duration(544.hours) // "3 weeks" ``` -**Note**: The formatter switches to a bigger unit (minute, hour, day, ...) as soon as it can. -See [Precision](#datetime-precision). +### ✍️ Formatting options + +The formatter switches to a bigger unit (minute, hour, day, ...) as soon as it can. + +There are a number of configuration options available for both `timeAgo()` and `duration()`. + +```kotlin +HumanReadable.timeAgo( + instant = now - 134.minutes, + formatting = FormatStyle( + date = FormatStyle.Date.Long, // or Short: "1 hr, 50 min" or Narrow: "1h 50m" + time = FormatStyle.Time.Regular, // or Digital: "01:50:00" + indicateApproximation = true // will prefix "about" if the formatted time is not exact (i.e. a part was dropped or rounded) + ), + parts = PartsConfig( + max = 2, // "1 hour, 50 minutes" + smallestDuration = 10.minutes, // anything smaller will return "less than 10 minutes" + subpartCutOffs = mapOf(TimeUnit.Hours to 12.hours) // drops subparts, e.g. "11 hours, 40 minutes" and then "12 hours" + ), + units = setOf(TimeUnit.Hours), // limits the output to these units, e.g. "391 days" + rounding = Rounding.HalfUp // or Floor to round down, or UpIfClose to round up on 55 seconds/55 minutes/23 hours/13 days +) +``` + +// TODO: global configuration + +Visit the [interactive demo](#features) to see more examples in action. ### 📂 File size @@ -99,23 +124,6 @@ HumanReadable.distance(value = 28512, unit = DistanceUnit.Foot, decimals = 2) // **Note:** numbers in meters and feet are always formatted with zero decimals. The passed in number of decimals is only used for the larger units kilometers and miles. -## Date/time precision - -// TODO global config options - -The formatter switches to a bigger unit (minute, hour, day, ...) as soon as it can. -For example: - -* `59.seconds` is "59 seconds" but `60.seconds` becomes "1 minute" -* `6.days` is "6 days" but `7.days` becomes "1 week" -* `29.days` is "29 days" but `30.days` becomes "1 month" - -There's also some rounding involved: - -* `8.days` and `10.days` are "1 week", but `11.days` already becomes "2 weeks" - -This behaviour may become configurable in future releases. - ## Localisation The library uses an internal i18n mechanism. It detects the current locale by default, but it's changeable at From a6c8b79880045b8a8233e5542a60b6c8051df4cb Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 2 Jul 2026 09:12:11 +0200 Subject: [PATCH 33/57] Only format consecutive parts by default --- README.md | 3 ++- .../time/HumanReadableDuration.kt | 4 ++++ .../humanreadable/time/PartsConfig.kt | 11 +++++++++- .../time/HumanReadableDurationTests.kt | 20 ++++++++++++++++--- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 74eb482..03c7ccf 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,8 @@ HumanReadable.timeAgo( parts = PartsConfig( max = 2, // "1 hour, 50 minutes" smallestDuration = 10.minutes, // anything smaller will return "less than 10 minutes" - subpartCutOffs = mapOf(TimeUnit.Hours to 12.hours) // drops subparts, e.g. "11 hours, 40 minutes" and then "12 hours" + subpartCutOffs = mapOf(TimeUnit.Hours to 12.hours), // drops subparts, e.g. "11 hours, 40 minutes" and then "12 hours" + onlyConsecutiveParts = true // whether "1 hour, 5 seconds" can be returned (as seconds are not the next unit after hours) ), units = setOf(TimeUnit.Hours), // limits the output to these units, e.g. "391 days" rounding = Rounding.HalfUp // or Floor to round down, or UpIfClose to round up on 55 seconds/55 minutes/23 hours/13 days diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index 415e264..de448c1 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -112,6 +112,10 @@ private fun getNeededParts( remainingBeforeLast = remainingDuration res[unit] = value remainingDuration -= unit.valueToDuration(value) + } else if (res.isNotEmpty() && parts.onlyConsecutiveParts) { + // Started formatting, but the next part is zero, so we're done because + // anything after this would be non-consecutive. + break } } diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt index d49386a..e0273bc 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt @@ -45,5 +45,14 @@ public data class PartsConfig( TimeUnit.Minutes to 2, TimeUnit.Hours to 2, TimeUnit.Days to 2 - ) + ), + + /** + * If `true`, only consecutive [TimeUnit]s will be formatted. + * + * For example: + * - "1 hour, 5 minutes" will be possible (because minutes are the next unit after hours) + * - "1 hour, 5 seconds" will not be returned (because seconds are not the next unit after hours) + */ + val onlyConsecutiveParts: Boolean = true ) \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index b081de7..0dc0ce0 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -10,6 +10,7 @@ import nl.jacobras.humanreadable.time.Rounding.Floor import nl.jacobras.humanreadable.time.Rounding.HalfUp import nl.jacobras.humanreadable.time.Rounding.UpIfClose import kotlin.test.Test +import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes @@ -142,9 +143,6 @@ class HumanReadableDurationTests { assertThat( duration(3.seconds, parts = PartsConfig(max = 2)) ).isEqualTo("3 seconds") - assertThat( - duration(1.hours + 10.seconds, parts = PartsConfig(max = 2)) - ).isEqualTo("1 hour, 10 seconds") assertThat( duration(1.minutes + 55.seconds, rounding = HalfUp, parts = PartsConfig(max = 2)) ).isEqualTo("1 minute, 55 seconds") @@ -164,6 +162,22 @@ class HumanReadableDurationTests { ).isEqualTo("7 days") } + @Test + fun onlyConnectedSubparts() { + assertThat( + duration(1.hours + 10.seconds, parts = PartsConfig(max = 2, onlyConsecutiveParts = false)) + ).isEqualTo("1 hour, 10 seconds") + assertThat( + duration(1.hours + 10.seconds, parts = PartsConfig(max = 2, onlyConsecutiveParts = true)) + ).isEqualTo("1 hour") + assertThat( + duration(23.hours + 5.minutes, parts = PartsConfig(max = 2, subpartCutOffs = mapOf())) + ).isEqualTo("23 hours, 5 minutes") + assertThat( + duration(24.hours + 5.minutes, parts = PartsConfig(max = 2, subpartCutOffs = mapOf())) + ).isEqualTo("1 day") + } + @Test fun smallestDuration() { assertThat( From 37d48d42d36f312b2f058a848e20853ed9a899ca Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 2 Jul 2026 09:13:44 +0200 Subject: [PATCH 34/57] Support setting global time formatting options --- README.md | 13 +++-- demo/src/webMain/kotlin/App.kt | 6 +-- demo/src/webMain/kotlin/feature/TimeDemo.kt | 2 +- .../nl/jacobras/humanreadable/Config.kt | 46 +++++++++++++++++ .../jacobras/humanreadable/HumanReadable.kt | 51 +++++-------------- .../time/HumanReadableDuration.kt | 9 ++-- .../HumanReadableAbbreviationTests.kt | 2 +- .../HumanReadableFileSizeTests.kt | 2 +- .../HumanReadableNumberEdgeCaseTests.kt | 2 +- .../humanreadable/localized/LocalisedTests.kt | 44 ++++++++-------- .../time/HumanReadableDurationTests.kt | 12 ++++- .../time/HumanReadableRelativeTimeTests.kt | 2 +- 12 files changed, 116 insertions(+), 75 deletions(-) create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt diff --git a/README.md b/README.md index 03c7ccf..b825e17 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,12 @@ HumanReadable.timeAgo( ) ``` -// TODO: global configuration +The above parameters can be set globally via `HumanReadable.config.time`. + +```kotlin +HumanReadable.config.time.units = setOf(TimeUnit.Hours) +HumanReadable.timeAgo(now - 2.days) // "48 hours ago" +``` Visit the [interactive demo](#features) to see more examples in action. @@ -128,15 +133,15 @@ number of decimals is only used for the larger units kilometers and miles. ## Localisation The library uses an internal i18n mechanism. It detects the current locale by default, but it's changeable at -runtime via `HumanReadable.languageTag`: +runtime via `HumanReadable.config.languageTag`: ```kotlin HumanReadable.timeAgo(instant) // "3 days ago" -HumanReadable.languageTag = "nl" +HumanReadable.config.languageTag = "nl" HumanReadable.timeAgo(instant) // "3 dagen geleden" -HumanReadable.languageTag = "fr" +HumanReadable.config.languageTag = "fr" HumanReadable.timeAgo(instant) // "il y a 3 jours" ``` diff --git a/demo/src/webMain/kotlin/App.kt b/demo/src/webMain/kotlin/App.kt index 519c6d5..4ab445b 100644 --- a/demo/src/webMain/kotlin/App.kt +++ b/demo/src/webMain/kotlin/App.kt @@ -46,7 +46,7 @@ import ui.LanguageChip @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, ExperimentalMaterial3AdaptiveApi::class) @Composable internal fun App() { - var selectedLanguageCode by remember { mutableStateOf(HumanReadable.languageTag) } + var selectedLanguageCode by remember { mutableStateOf(HumanReadable.config.languageTag) } var selectedFeature by remember { mutableStateOf(Feature.Time) } val layoutDirection = if (selectedLanguageCode == "ar") { LayoutDirection.Rtl @@ -56,7 +56,7 @@ internal fun App() { fun onSelectLanguage(code: String) { selectedLanguageCode = code - HumanReadable.languageTag = code + HumanReadable.config.languageTag = code } val navigator = rememberListDetailPaneScaffoldNavigator() @@ -83,7 +83,7 @@ internal fun App() { ) { Text( text = buildAnnotatedString { - append("HumanReadable.languageTag = \"") + append("HumanReadable.config.languageTag = \"") withStyle(monoBodyStringBold) { append(selectedLanguageCode) } diff --git a/demo/src/webMain/kotlin/feature/TimeDemo.kt b/demo/src/webMain/kotlin/feature/TimeDemo.kt index 2a28033..06d6fd3 100644 --- a/demo/src/webMain/kotlin/feature/TimeDemo.kt +++ b/demo/src/webMain/kotlin/feature/TimeDemo.kt @@ -136,7 +136,7 @@ internal fun TimeDemo( ) Text(buildAnnotatedString { append("The examples below call ") - appendKotlinCode("HumanReadable.timeAgo( parameter1, parameter2, ... )") + appendKotlinCode("HumanReadable.timeAgo(option1, option2, ...)") append(". All parameters are optional and also work with ") appendKotlinCode("HumanReadable.duration()") append(".") diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt new file mode 100644 index 0000000..cbde61c --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt @@ -0,0 +1,46 @@ +package nl.jacobras.humanreadable + +import nl.jacobras.humanreadable.HumanReadable.localisation +import nl.jacobras.humanreadable.time.FormatStyle +import nl.jacobras.humanreadable.time.PartsConfig +import nl.jacobras.humanreadable.time.Rounding +import nl.jacobras.humanreadable.time.TimeUnit + +public class Config { + /** + * The language tag (e.g. `"en"`, `"fr"`) to use when formatting. Defaults to the detected system + * language when it is supported, otherwise [fallbackLanguageTag]. + */ + public var languageTag: String + get() = localisation.languageTag + set(value) { + localisation.requestedLanguageTag = value + } + + /** + * The fallback language tag (e.g. `"en"`, `"fr"`) to use when [languageTag] is not supported. + * If an unsupported language is passed in, nothing changes. + * + * Defaults to English. + */ + public var fallbackLanguageTag: String + get() = localisation.fallbackLanguageTag + set(value) { + localisation.fallbackLanguageTag = value + } + + /** + * Holds default parameters for time formatting of `duration()` and `timeAgo()`. + */ + public val time: Time = Time() +} + +/** + * Default parameters for time formatting of `duration()` and `timeAgo()`. + */ +public class Time { + public var formatting: FormatStyle = FormatStyle() + public var parts: PartsConfig = PartsConfig() + public var units: Set = TimeUnit.all + public var rounding: Rounding = Rounding.HalfUp +} \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt index 6b14722..f8ba41b 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/HumanReadable.kt @@ -6,8 +6,6 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.todayIn import nl.jacobras.humanreadable.HumanReadable.duration -import nl.jacobras.humanreadable.HumanReadable.fallbackLanguageTag -import nl.jacobras.humanreadable.HumanReadable.languageTag import nl.jacobras.humanreadable.HumanReadable.number import nl.jacobras.humanreadable.i18n.HumanReadableStrings import nl.jacobras.humanreadable.i18n.Localisation @@ -31,28 +29,7 @@ public object HumanReadable { internal val localisation = Localisation() internal val strings: HumanReadableStrings get() = localisation.currentStrings - - /** - * The language tag (e.g. `"en"`, `"fr"`) to use when formatting. Defaults to the detected system - * language when it is supported, otherwise [fallbackLanguageTag]. - */ - public var languageTag: String - get() = localisation.languageTag - set(value) { - localisation.requestedLanguageTag = value - } - - /** - * The fallback language tag (e.g. `"en"`, `"fr"`) to use when [languageTag] is not supported. - * If an unsupported language is passed in, nothing changes. - * - * Defaults to English. - */ - public var fallbackLanguageTag: String - get() = localisation.fallbackLanguageTag - set(value) { - localisation.fallbackLanguageTag = value - } + public val config: Config = Config() /** * Returns the difference between [baseInstant] and [instant], in human-readable format. Also supports @@ -60,7 +37,7 @@ public object HumanReadable { * * @param instant The [Instant] to format. * @param baseInstant The base/starting [Instant], defaulting to "now". - * @param formatting The [FormatStyle] to use, defaulting to [FormatStyle.Long]. + * @param formatting The [FormatStyle] to use, defaulting to "long". * @param parts Configures the formatting of multiple parts, defaulting to 1 part. * @param units The [TimeUnit]s to limit to during formatting, not limited by default. * @param rounding The [Rounding] strategy to use, defaulting to [Rounding.HalfUp]. @@ -70,10 +47,10 @@ public object HumanReadable { public fun timeAgo( instant: Instant, baseInstant: Instant = Clock.System.now(), - formatting: FormatStyle = FormatStyle(), - parts: PartsConfig = PartsConfig(), - units: Set = TimeUnit.all, - rounding: Rounding = Rounding.HalfUp + formatting: FormatStyle = config.time.formatting, + parts: PartsConfig = config.time.parts, + units: Set = config.time.units, + rounding: Rounding = config.time.rounding ): String { return formatTimeAgo( instant = instant, @@ -101,10 +78,10 @@ public object HumanReadable { public fun timeAgo( date: LocalDate, baseDate: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), - formatting: FormatStyle = FormatStyle(), - parts: PartsConfig = PartsConfig(), - units: Set = TimeUnit.all, - rounding: Rounding = Rounding.HalfUp + formatting: FormatStyle = config.time.formatting, + parts: PartsConfig = config.time.parts, + units: Set = config.time.units, + rounding: Rounding = config.time.rounding ): String { return formatTimeAgo( date = date, @@ -128,10 +105,10 @@ public object HumanReadable { */ public fun duration( duration: Duration, - formatting: FormatStyle = FormatStyle(), - parts: PartsConfig = PartsConfig(), - units: Set = TimeUnit.all, - rounding: Rounding = Rounding.HalfUp + formatting: FormatStyle = config.time.formatting, + parts: PartsConfig = config.time.parts, + units: Set = config.time.units, + rounding: Rounding = config.time.rounding ): String { return formatDuration( duration = duration, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index de448c1..8ddbcf4 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -47,7 +47,10 @@ internal fun formatDuration( FormatStyle.Date.Short -> append('<') FormatStyle.Date.Narrow -> append('<') } - } else if (format.indicateApproximation && duration != parts.totalDuration) { + } else if (format.indicateApproximation + && duration != parts.totalDuration + && format.time != FormatStyle.Time.Digital + ) { // Large enough, somewhere rounding occurred? Add "about ..." when (format.date) { FormatStyle.Date.Long -> { @@ -154,9 +157,9 @@ private fun getNeededParts( } private val Map.totalDuration: Duration - get() = entries.maxOf { (unit, value) -> + get() = entries.maxOfOrNull { (unit, value) -> unit.valueToDuration(value) - } + } ?: Duration.ZERO /** * Formats a [count] with its [unit]. diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt index fdcab9d..99f7b61 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableAbbreviationTests.kt @@ -7,7 +7,7 @@ import kotlin.test.Test class HumanReadableAbbreviationTests { init { - HumanReadable.languageTag = "en" + HumanReadable.config.languageTag = "en" } @Test diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt index 5ec9d71..32a4130 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableFileSizeTests.kt @@ -7,7 +7,7 @@ import kotlin.test.Test class HumanReadableFileSizeTests { init { - HumanReadable.languageTag = "en" + HumanReadable.config.languageTag = "en" } @Test diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt index b70a932..dd05573 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/HumanReadableNumberEdgeCaseTests.kt @@ -6,7 +6,7 @@ import kotlin.test.Test class HumanReadableNumberEdgeCaseTests { init { - HumanReadable.languageTag = "en" + HumanReadable.config.languageTag = "en" } @Test diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt index e62f74f..050c082 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/localized/LocalisedTests.kt @@ -62,7 +62,7 @@ class LocalisedTests { @Test fun ar_arabic() { - HumanReadable.languageTag = "ar" + HumanReadable.config.languageTag = "ar" assertThat(HumanReadable.duration(zeroSeconds)).isEqualTo("0 ثانية") assertThat(HumanReadable.duration(oneSecond)).isEqualTo("ثانية") assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("ثانيتان") @@ -88,7 +88,7 @@ class LocalisedTests { @Test fun cs_czech() { - HumanReadable.languageTag = "cs" + HumanReadable.config.languageTag = "cs" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 sekundy") assertThat(HumanReadable.duration(oneDay)).isEqualTo("1 den") assertThat(HumanReadable.duration(twoDays)).isEqualTo("2 dny") @@ -117,7 +117,7 @@ class LocalisedTests { @Test fun de_german() { - HumanReadable.languageTag = "de" + HumanReadable.config.languageTag = "de" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 Sekunden") assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2 Monate") @@ -130,7 +130,7 @@ class LocalisedTests { @Test fun el_greek() { - HumanReadable.languageTag = "el" + HumanReadable.config.languageTag = "el" assertThat(HumanReadable.timeAgo(oneWeekAgo, baseInstant = now)).isEqualTo("1 εβδομάδα πριν") assertThat(HumanReadable.timeAgo(oneYearFromNow, baseInstant = now)).isEqualTo("σε 1 έτος") assertThat(HumanReadable.timeAgo(oneMonthAgo, baseInstant = now)).isEqualTo("1 μήνα πριν") @@ -152,7 +152,7 @@ class LocalisedTests { @Test fun en_english() { - HumanReadable.languageTag = "en" + HumanReadable.config.languageTag = "en" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 seconds") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") @@ -167,7 +167,7 @@ class LocalisedTests { @Test fun es_spanish() { - HumanReadable.languageTag = "es" + HumanReadable.config.languageTag = "es" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 segundos") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -176,7 +176,7 @@ class LocalisedTests { @Test fun fi_finnish() { - HumanReadable.languageTag = "fi" + HumanReadable.config.languageTag = "fi" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 sekuntia") assertThat(HumanReadable.timeAgo(twoMinutesAgo, baseInstant = now)).isEqualTo("2 minuuttia sitten") @@ -189,7 +189,7 @@ class LocalisedTests { @Test fun fr_french() { - HumanReadable.languageTag = "fr" + HumanReadable.config.languageTag = "fr" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 secondes") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1 000 000,34") @@ -200,7 +200,7 @@ class LocalisedTests { @Test fun id_indonesian() { - HumanReadable.languageTag = "id" + HumanReadable.config.languageTag = "id" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 detik") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -209,7 +209,7 @@ class LocalisedTests { @Test fun it_italian() { - HumanReadable.languageTag = "it" + HumanReadable.config.languageTag = "it" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 secondi") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -218,7 +218,7 @@ class LocalisedTests { @Test fun ja_japanese() { - HumanReadable.languageTag = "ja" + HumanReadable.config.languageTag = "ja" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 秒") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") @@ -227,7 +227,7 @@ class LocalisedTests { @Test fun kk_kazakh() { - HumanReadable.languageTag = "kk" + HumanReadable.config.languageTag = "kk" assertThat(HumanReadable.timeAgo(now)).isEqualTo("қазір") assertThat(HumanReadable.timeAgo(twoSecondsAgo, baseInstant = now)).isEqualTo("2 секунд бұрын") assertThat(HumanReadable.timeAgo(twoSecondsFromNow, baseInstant = now)).isEqualTo("2 секундтан кейін") @@ -255,7 +255,7 @@ class LocalisedTests { @Test fun ko_korean() { - HumanReadable.languageTag = "ko" + HumanReadable.config.languageTag = "ko" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2초") assertThat(HumanReadable.duration(twoMonths)).isEqualTo("2개월") @@ -269,7 +269,7 @@ class LocalisedTests { @Test fun nl_dutch() { - HumanReadable.languageTag = "nl" + HumanReadable.config.languageTag = "nl" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 seconden") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -284,7 +284,7 @@ class LocalisedTests { @Test fun pl_polish() { - HumanReadable.languageTag = "pl" + HumanReadable.config.languageTag = "pl" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 sekundy") assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 minuta") @@ -297,7 +297,7 @@ class LocalisedTests { @Test fun pt_portuguese() { - HumanReadable.languageTag = "pt" + HumanReadable.config.languageTag = "pt" assertThat(HumanReadable.duration(zeroSeconds)).isEqualTo("0 segundo") assertThat(HumanReadable.duration(oneSecond)).isEqualTo("1 segundo") assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 segundos") @@ -308,7 +308,7 @@ class LocalisedTests { @Test fun ru_russian() { - HumanReadable.languageTag = "ru" + HumanReadable.config.languageTag = "ru" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 секунды") assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 минута") @@ -321,7 +321,7 @@ class LocalisedTests { @Test fun tr_turkish() { - HumanReadable.languageTag = "tr" + HumanReadable.config.languageTag = "tr" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 saniye") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1.000.000,34") @@ -330,7 +330,7 @@ class LocalisedTests { @Test fun uk_ukrainian() { - HumanReadable.languageTag = "uk" + HumanReadable.config.languageTag = "uk" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 секунди") assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 хвилина") assertThat(HumanReadable.duration(oneHour)).isEqualTo("1 година") @@ -345,7 +345,7 @@ class LocalisedTests { @Test fun uz_uzbek() { - HumanReadable.languageTag = "uz" + HumanReadable.config.languageTag = "uz" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 soniya") assertThat(HumanReadable.duration(oneMinute)).isEqualTo("1 daqiqa") @@ -359,7 +359,7 @@ class LocalisedTests { @Test fun vi_vietnamese() { - HumanReadable.languageTag = "vi" + HumanReadable.config.languageTag = "vi" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 giây") assertThat(HumanReadable.timeAgo(twoSecondsAgo, baseInstant = now)).isEqualTo("2 giây trước") @@ -371,7 +371,7 @@ class LocalisedTests { @Test fun zh_chinese() { - HumanReadable.languageTag = "zh" + HumanReadable.config.languageTag = "zh" assertThat(HumanReadable.duration(twoSeconds)).isEqualTo("2 秒") assertThat(HumanReadable.number(1_000_000.34, decimals = 2)).isEqualTo("1,000,000.34") diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 0dc0ce0..a636a37 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -28,7 +28,7 @@ class HumanReadableDurationTests { private val narrowStyle = FormatStyle(date = FormatStyle.Date.Narrow) init { - HumanReadable.languageTag = "en" + HumanReadable.config.languageTag = "en" } @Test @@ -281,6 +281,16 @@ class HumanReadableDurationTests { @Test fun formatStyleDigitalTime() { + assertThat( + duration( + duration = 5.minutes, + formatting = FormatStyle( + time = FormatStyle.Time.Digital, + indicateApproximation = true + ) + ) + ).isEqualTo("00:05:00") + assertThat( duration( duration = 1.hours, diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt index d46b916..9193ea9 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt @@ -24,7 +24,7 @@ import kotlin.time.Instant class HumanReadableRelativeTimeTests { init { - HumanReadable.languageTag = "en" + HumanReadable.config.languageTag = "en" } private val now: Instant From 5e61da00e4fcb525bce6e7757efa2924b9b5fbe9 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 2 Jul 2026 17:42:21 +0200 Subject: [PATCH 35/57] Show version number in web demo --- build.gradle.kts | 2 +- demo/build.gradle.kts | 5 +++++ demo/src/webMain/kotlin/App.kt | 3 ++- gradle.properties | 3 ++- gradle/libs.versions.toml | 1 + 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 71f5dae..48d7b37 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "nl.jacobras" -version = "2.0.0" +version = project.property("nl.jacobras.humanreadable.version") as String mavenPublishing { publishToMavenCentral() diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 64bbca1..d0d70e5 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.jetbrainsCompose) alias(libs.plugins.compose.compiler) + alias(libs.plugins.buildconfig) } kotlin { @@ -40,4 +41,8 @@ kotlin { rootProject.plugins.withType { rootProject.the().yarnLockMismatchReport = YarnLockMismatchReport.NONE +} + +buildConfig { + buildConfigField("VERSION", project.property("nl.jacobras.humanreadable.version") as String) } \ No newline at end of file diff --git a/demo/src/webMain/kotlin/App.kt b/demo/src/webMain/kotlin/App.kt index 4ab445b..4138d9f 100644 --- a/demo/src/webMain/kotlin/App.kt +++ b/demo/src/webMain/kotlin/App.kt @@ -1,3 +1,4 @@ +import Human_Readable.demo.BuildConfig import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column @@ -68,7 +69,7 @@ internal fun App() { Scaffold( topBar = { - TopAppBar(title = { Text("Human-Readable web demo") }) + TopAppBar(title = { Text("Human-Readable ${BuildConfig.VERSION} web demo") }) } ) { paddingValues -> diff --git a/gradle.properties b/gradle.properties index 61bedfd..40789ea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,5 @@ org.gradle.jvmargs=-Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerM kotlin.daemon.jvmargs=-Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=1 -XX:ReservedCodeCacheSize=320m -XX:+HeapDumpOnOutOfMemoryError -Xmx4g -Xms4g kotlin.code.style=official -org.jetbrains.compose.experimental.jscanvas.enabled=true \ No newline at end of file +org.jetbrains.compose.experimental.jscanvas.enabled=true +nl.jacobras.humanreadable.version=2.0.0 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a073d6..c29b7b3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ kotlinX-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version = kotlinX-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.8.0" } [plugins] +buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.9" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } \ No newline at end of file From 641a0bc1c293445b5e9546c315d19f2fb9960d09 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 2 Jul 2026 17:44:10 +0200 Subject: [PATCH 36/57] Remove outdated workaround --- karma.config.d/index.js | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 karma.config.d/index.js diff --git a/karma.config.d/index.js b/karma.config.d/index.js deleted file mode 100644 index bfc5ab9..0000000 --- a/karma.config.d/index.js +++ /dev/null @@ -1,30 +0,0 @@ -// TODO: remove the file it after 2.0.20 -// https://youtrack.jetbrains.com/issue/KT-67468 - -function KarmaWebpackOutputFramework(config) { - // This controller is instantiated and set during the preprocessor phase. - const controller = config.__karmaWebpackController; - - // only if webpack has instantiated its controller - if (!controller) { - console.warn( - "Webpack has not instantiated controller yet.\n" + - "Check if you have enabled webpack preprocessor and framework before this framework" - ) - return - } - - config.files.push({ - pattern: `${controller.outputPath}/**/*`, - included: false, - served: true, - watched: false - }) -} - -const KarmaWebpackOutputPlugin = { - 'framework:webpack-output': ['factory', KarmaWebpackOutputFramework], -}; - -config.plugins.push(KarmaWebpackOutputPlugin); -config.frameworks.push("webpack-output"); \ No newline at end of file From 8f19f8b8135a79f77f832938eb438b98900c4155 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 2 Jul 2026 17:44:16 +0200 Subject: [PATCH 37/57] Remove outdated TODO --- src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt index b7e670d..1ec28e0 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt @@ -12,7 +12,6 @@ import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -// TODO: extensions instead of parameters? Cleaner public API public enum class TimeUnit( internal val calculateValue: (Duration, Rounding) -> Int, internal val valueToDuration: (Int) -> Duration, From 404721382d5733ab36da965ed2c998727c4a9bb8 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 2 Jul 2026 17:45:54 +0200 Subject: [PATCH 38/57] Update Compose to 1.12.0-beta01 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c29b7b3..9bc7d2f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin = "2.4.0" -compose = "1.12.0-alpha02" +compose = "1.12.0-beta01" compose-adaptive = "1.3.0-beta02" [libraries] From 4bed65ec0242c7ca371e516877f9285ba33fa30c Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 2 Jul 2026 17:46:06 +0200 Subject: [PATCH 39/57] Update Gradle to 9.6.1 --- gradle/wrapper/gradle-wrapper.properties | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7e7d24f..a9db115 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 3ee81c0e8a39e9830e6b045ecb326f6b1b78bae6 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Mon, 6 Jul 2026 20:33:06 +0200 Subject: [PATCH 40/57] Fix "about" prefix not correctly added when formatting multiple parts --- .../jacobras/humanreadable/time/HumanReadableDuration.kt | 6 +++--- .../humanreadable/time/HumanReadableDurationTests.kt | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index 8ddbcf4..cc9fe7a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -157,9 +157,9 @@ private fun getNeededParts( } private val Map.totalDuration: Duration - get() = entries.maxOfOrNull { (unit, value) -> - unit.valueToDuration(value) - } ?: Duration.ZERO + get() = entries.fold(Duration.ZERO) { acc, (unit, value) -> + acc + unit.valueToDuration(value) + } /** * Formats a [count] with its [unit]. diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index a636a37..769eaff 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -277,6 +277,13 @@ class HumanReadableDurationTests { parts = PartsConfig(smallestDuration = 1.days) ) ).isEqualTo("less than 1 day") + assertThat( + duration( + duration = 1.days + 1.hours + 4.minutes, + formatting = longStyle.copy(indicateApproximation = true), + parts = PartsConfig(max = 5) + ) + ).isEqualTo("1 day, 1 hour, 4 minutes") } @Test From 3d39814261f88815f4f0c13e5dc2bd101ad273aa Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Tue, 7 Jul 2026 18:14:22 +0200 Subject: [PATCH 41/57] Make `IfClose` rounding configurable --- demo/src/webMain/kotlin/feature/TimeDemo.kt | 72 ++++----- .../nl/jacobras/humanreadable/Config.kt | 3 +- .../time/HumanReadableDuration.kt | 27 +++- .../humanreadable/time/PartsConfig.kt | 2 +- .../jacobras/humanreadable/time/Rounding.kt | 33 ++-- .../jacobras/humanreadable/time/TimeUnit.kt | 16 +- .../time/HumanReadableDurationTests.kt | 144 +++++++++++++----- 7 files changed, 192 insertions(+), 105 deletions(-) diff --git a/demo/src/webMain/kotlin/feature/TimeDemo.kt b/demo/src/webMain/kotlin/feature/TimeDemo.kt index 06d6fd3..b77065f 100644 --- a/demo/src/webMain/kotlin/feature/TimeDemo.kt +++ b/demo/src/webMain/kotlin/feature/TimeDemo.kt @@ -53,8 +53,8 @@ internal fun TimeDemo( modifier: Modifier = Modifier ) { val monoBody = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace) - val baseInstant = remember { Clock.System.now() } - var myInstant by remember { mutableStateOf(baseInstant.minus(90, DateTimeUnit.SECOND)) } + val now = remember { Clock.System.now() } + var myInstant by remember { mutableStateOf(now.minus(90, DateTimeUnit.SECOND)) } Column(modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { Text( @@ -69,11 +69,11 @@ internal fun TimeDemo( Text( text = buildAnnotatedString { withStyle(monoBodyOrange) { append("val ") } - append("baseInstant =") + append("now =") }, style = monoBody ) - DateTimeField(baseInstant) {} + DateTimeField(now) {} } Spacer(Modifier.height(8.dp)) Row( @@ -92,12 +92,12 @@ internal fun TimeDemo( Spacer(Modifier.height(4.dp)) FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - Button(onClick = { myInstant = baseInstant }) { Text("[NOW]") } + Button(onClick = { myInstant = now }) { Text("[NOW]") } Button(onClick = { myInstant = myInstant.minus(10.seconds) }) { Text("-10 sec") } Button(onClick = { myInstant = myInstant.minus(1.minutes) }) { Text("-1 min") } Button(onClick = { myInstant = myInstant.minus(1.hours) }) { Text("-1 hr") } Button(onClick = { myInstant = myInstant.minus(1.days) }) { Text("-1 day") } - Button(onClick = { myInstant = myInstant.minus(1.days) }) { Text("-7 days") } + Button(onClick = { myInstant = myInstant.minus(7.days) }) { Text("-7 days") } Button(onClick = { myInstant = myInstant.minus(30.days) }) { Text("-30 days") } Button(onClick = { myInstant = myInstant.minus(365.days) }) { Text("-365 days") } } @@ -105,26 +105,26 @@ internal fun TimeDemo( CodeExample( code = "HumanReadable.duration(now - myInstant)", - res = remember(selectedLanguageCode, baseInstant, myInstant) { - HumanReadable.duration(baseInstant - myInstant) + res = remember(selectedLanguageCode, now, myInstant) { + HumanReadable.duration(now - myInstant) } ) Spacer(Modifier.height(12.dp)) CodeExample( - code = "HumanReadable.timeAgo(myInstant)", - res = remember(selectedLanguageCode, baseInstant, myInstant) { - HumanReadable.timeAgo(myInstant) + code = "HumanReadable.timeAgo(myInstant) // compared to now", + res = remember(selectedLanguageCode, now, myInstant) { + HumanReadable.timeAgo(myInstant, now) } ) Spacer(Modifier.height(12.dp)) CodeExample( code = "HumanReadable.timeAgo(myLocalDate) // overloaded method that takes a LocalDate", - res = remember(selectedLanguageCode, baseInstant, myInstant) { + res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( date = myInstant.toLocalDateTime(TimeZone.currentSystemDefault()).date, - baseDate = baseInstant.toLocalDateTime(TimeZone.currentSystemDefault()).date + baseDate = now.toLocalDateTime(TimeZone.currentSystemDefault()).date ) } ) @@ -136,8 +136,8 @@ internal fun TimeDemo( ) Text(buildAnnotatedString { append("The examples below call ") - appendKotlinCode("HumanReadable.timeAgo(option1, option2, ...)") - append(". All parameters are optional and also work with ") + appendKotlinCode("HumanReadable.timeAgo(myInstant, option1, option2, ...)") + append(". All configuration parameters are optional and also work with ") appendKotlinCode("HumanReadable.duration()") append(".") }) @@ -145,9 +145,9 @@ internal fun TimeDemo( CodeExample( code = "formatting = FormatStyle(date = Date.Short)", - res = remember(selectedLanguageCode, baseInstant, myInstant) { + res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( - myInstant, baseInstant, + myInstant, now, formatting = FormatStyle(date = FormatStyle.Date.Short) ) }, @@ -156,9 +156,9 @@ internal fun TimeDemo( Spacer(Modifier.height(8.dp)) CodeExample( code = "formatting = FormatStyle(date = Date.Narrow)", - res = remember(selectedLanguageCode, baseInstant, myInstant) { + res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( - myInstant, baseInstant, + myInstant, now, formatting = FormatStyle(date = FormatStyle.Date.Narrow) ) }, @@ -167,9 +167,9 @@ internal fun TimeDemo( Spacer(Modifier.height(8.dp)) CodeExample( code = "formatting = FormatStyle(time = FormatStyle.Time.Digital)", - res = remember(selectedLanguageCode, baseInstant, myInstant) { + res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( - myInstant, baseInstant, + myInstant, now, formatting = FormatStyle(time = FormatStyle.Time.Digital) ) }, @@ -178,9 +178,9 @@ internal fun TimeDemo( Spacer(Modifier.height(8.dp)) CodeExample( code = "formatting = FormatStyle(indicateApproximation = true)", - res = remember(selectedLanguageCode, baseInstant, myInstant) { + res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( - myInstant, baseInstant, + myInstant, now, formatting = FormatStyle(indicateApproximation = true) ) }, @@ -190,9 +190,9 @@ internal fun TimeDemo( CodeExample( code = "rounding = Rounding.Floor", - res = remember(selectedLanguageCode, baseInstant, myInstant) { + res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( - myInstant, baseInstant, + myInstant, now, rounding = Rounding.Floor ) }, @@ -201,10 +201,10 @@ internal fun TimeDemo( Spacer(Modifier.height(8.dp)) CodeExample( code = "rounding = Rounding.UpIfClose", - res = remember(selectedLanguageCode, baseInstant, myInstant) { + res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( - myInstant, baseInstant, - rounding = Rounding.UpIfClose + myInstant, now, + rounding = Rounding.IfClose() ) }, inline = true @@ -213,9 +213,9 @@ internal fun TimeDemo( CodeExample( code = "parts = PartsConfig(max = 3)", - res = remember(selectedLanguageCode, baseInstant, myInstant) { + res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( - myInstant, baseInstant, + myInstant, now, parts = PartsConfig(max = 3) ) }, @@ -224,9 +224,9 @@ internal fun TimeDemo( Spacer(Modifier.height(8.dp)) CodeExample( code = "parts = PartsConfig(max = 3, subpartCutOffs = mapOf(TimeUnit.Minutes to 10, TimeUnit.Hours to 12))", - res = remember(selectedLanguageCode, baseInstant, myInstant) { + res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( - myInstant, baseInstant, + myInstant, now, parts = PartsConfig( max = 3, subpartCutOffs = mapOf(TimeUnit.Minutes to 10, TimeUnit.Hours to 12) @@ -238,9 +238,9 @@ internal fun TimeDemo( Spacer(Modifier.height(8.dp)) CodeExample( code = "parts = PartsConfig(smallestDuration = 10.minutes)", - res = remember(selectedLanguageCode, baseInstant, myInstant) { + res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( - myInstant, baseInstant, + myInstant, now, parts = PartsConfig(smallestDuration = 10.minutes) ) }, @@ -250,9 +250,9 @@ internal fun TimeDemo( CodeExample( code = "units = setOf(TimeUnit.Hours, TimeUnit.Days)", - res = remember(selectedLanguageCode, baseInstant, myInstant) { + res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( - myInstant, baseInstant, + myInstant, now, units = setOf(TimeUnit.Hours, TimeUnit.Days) ) }, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt index cbde61c..bf08b3c 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt @@ -32,7 +32,8 @@ public class Config { /** * Holds default parameters for time formatting of `duration()` and `timeAgo()`. */ - public val time: Time = Time() + public var time: Time = Time() + internal set } /** diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index cc9fe7a..7b92ac4 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -124,23 +124,34 @@ private fun getNeededParts( // Round the last part. Only needed for HalfUp, because Floor is already done above // and UpIfClose is done below in the roll-overs section. - if (lastUnit != null && rounding == Rounding.HalfUp) { - res[lastUnit] = lastUnit.calculateValue(remainingBeforeLast, rounding) + val roundMethod = if (rounding is Rounding.IfClose) rounding.default else rounding + if (lastUnit != null && roundMethod == Rounding.HalfUp) { + res[lastUnit] = lastUnit.calculateValue(remainingBeforeLast, Rounding.HalfUp) } // Roll-overs - if (rounding == Rounding.UpIfClose) { - val unitsAscending = res.keys.sorted() + if (rounding is Rounding.IfClose) { + val thresholdsAscending = rounding.thresholds.entries.sortedBy { it.key } - for (unit in unitsAscending) { + for ((unit, threshold) in thresholdsAscending) { val value = res[unit] ?: continue - if (value >= unit.upIfCloseRollover) { - val largerUnit = TimeUnit.entries.getOrNull(unit.ordinal + 1) - if (largerUnit != null) { + val largerUnit = unit.largerUnit + + // Round up if the next unit is nearer than the threshold + if (largerUnit != null) { + val largerUnitSmallestDuration = largerUnit.valueToDuration(1) + val currentUnitSmallestDuration = unit.valueToDuration(1) + val maxValueForThisUnit = (largerUnitSmallestDuration / currentUnitSmallestDuration).toInt() + if (value >= (maxValueForThisUnit - threshold)) { res[largerUnit] = (res[largerUnit] ?: 0) + 1 res.remove(unit) } } + + // Round down if the unit is closer than the threshold to 0 + if (res.size > 1 && unit == res.keys.min() && value < threshold) { + res.remove(unit) + } } } diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt index e0273bc..f42cd04 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt @@ -31,7 +31,7 @@ public data class PartsConfig( val smallestDuration: Duration = Duration.ZERO, /** - * Controls up to how many of [TimeUnit] smaller subparts should be shown. + * Controls how large a [TimeUnit] may be at most to still get subparts after it. * * For example, if this is set to `[TimeUnit.Minutes = 2]`: * - `1.minutes + 4.seconds` will be "1 minute, 4 seconds" diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt index 39f577f..b27f018 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt @@ -7,6 +7,11 @@ package nl.jacobras.humanreadable.time */ public sealed interface Rounding { + /** + * Either [Floor] or [HalfUp]. Marker interface to use with [IfClose]. + */ + public sealed interface Simple : Rounding + /** * Rounds down. * @@ -14,7 +19,7 @@ public sealed interface Rounding { * - `12.days` results in "1 week". * - `46.days` results in "1 month". */ - public data object Floor : Rounding + public data object Floor : Simple /** * Rounds up. @@ -25,18 +30,26 @@ public sealed interface Rounding { * * Note that when [PartsConfig.max] is larger than 1, the rounding is done on the smallest unit. */ - public data object HalfUp : Rounding + public data object HalfUp : Simple /** - * Rounds up if the duration is close to the next unit. + * Eagerly rounds up to the next unit, or down to the previous, if within [thresholds]. * - * "Close" is defined as: - * - `55.seconds` rounds up to a minute. - * - `55.minutes` rounds up to an hour. - * - `23.hours` rounds up to a day. - * - `13.days` rounds up to two weeks. + * For example, with [PartsConfig.max] set to 2 and [thresholds] set to [Seconds = 5]: + * - `1.minutes + 5.seconds` remains "1 minute, 5 seconds"; + * - `1.minutes + 4.seconds` gets rounded to "1 minute"; + * - `1.minutes + 54 seconds` remains "1 minute, 54 seconds"; + * - `1.minutes + 55 seconds` gets rounded to "2 minutes". * - * Anything else is rounded **down** (see [Floor]). + * Anything else is rounded according to [default], which defaults to [Floor]. */ - public data object UpIfClose : Rounding + public data class IfClose( + val thresholds: Map = mapOf( + TimeUnit.Seconds to 5, + TimeUnit.Minutes to 5, + TimeUnit.Hours to 1, + TimeUnit.Days to 1 + ), + val default: Simple = Floor + ) : Rounding } \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt index 1ec28e0..bf9188c 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt @@ -4,7 +4,7 @@ import nl.jacobras.humanreadable.HumanReadable.localisation import nl.jacobras.humanreadable.HumanReadable.strings import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.TenseForms -import nl.jacobras.humanreadable.time.Rounding.UpIfClose +import nl.jacobras.humanreadable.time.Rounding.IfClose import kotlin.math.roundToInt import kotlin.time.Duration import kotlin.time.Duration.Companion.days @@ -69,15 +69,8 @@ public enum class TimeUnit( narrowForms = { it.yearsNarrow } ); - /** - * When to roll over to the next larger unit for [UpIfClose] rounding. - */ - internal val upIfCloseRollover: Int - get() = when (this) { - Seconds, Minutes -> 55 - Hours -> 23 - else -> Int.MAX_VALUE - } + internal val largerUnit: TimeUnit? + get() = entries.getOrNull(ordinal + 1) internal fun format(value: Int, relativeTime: RelativeTime, formatStyle: FormatStyle.Date): String { val dateTimeStrings = strings.dateTime @@ -105,7 +98,8 @@ public enum class TimeUnit( private fun Float.round(rounding: Rounding): Int { return when (rounding) { - Rounding.Floor, UpIfClose -> toInt() + is IfClose -> toInt() + Rounding.Floor -> toInt() Rounding.HalfUp -> roundToInt() } } \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 769eaff..01c576b 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -5,12 +5,13 @@ import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import nl.jacobras.humanreadable.HumanReadable import nl.jacobras.humanreadable.HumanReadable.duration +import nl.jacobras.humanreadable.Time import nl.jacobras.humanreadable.localized.LocalisedTests import nl.jacobras.humanreadable.time.Rounding.Floor import nl.jacobras.humanreadable.time.Rounding.HalfUp -import nl.jacobras.humanreadable.time.Rounding.UpIfClose +import nl.jacobras.humanreadable.time.Rounding.IfClose +import kotlin.test.BeforeTest import kotlin.test.Test -import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes @@ -31,6 +32,11 @@ class HumanReadableDurationTests { HumanReadable.config.languageTag = "en" } + @BeforeTest + fun resetConfig() { + HumanReadable.config.time = Time() + } + @Test fun halfUpRounding() { assertThat(duration(1.seconds, rounding = HalfUp)).isEqualTo("1 second") @@ -86,37 +92,43 @@ class HumanReadableDurationTests { } @Test - fun upIfCloseRounding() { - assertThat(duration(1.seconds, rounding = UpIfClose)).isEqualTo("1 second") - assertThat(duration(3.seconds, rounding = UpIfClose)).isEqualTo("3 seconds") - - assertThat(duration(55.seconds, rounding = UpIfClose)).isEqualTo("1 minute") - assertThat(duration(1.minutes, rounding = UpIfClose)).isEqualTo("1 minute") - assertThat(duration(3.minutes, rounding = UpIfClose)).isEqualTo("3 minutes") - - assertThat(duration(55.minutes, rounding = UpIfClose)).isEqualTo("1 hour") - assertThat(duration(1.hours, rounding = UpIfClose)).isEqualTo("1 hour") - assertThat(duration(3.hours, rounding = UpIfClose)).isEqualTo("3 hours") - - assertThat(duration(23.hours, rounding = UpIfClose)).isEqualTo("1 day") - assertThat(duration(1.days, rounding = UpIfClose)).isEqualTo("1 day") - assertThat(duration(3.days, rounding = UpIfClose)).isEqualTo("3 days") - - assertThat(duration(7.days, rounding = UpIfClose)).isEqualTo("1 week") - assertThat(duration(10.days, rounding = UpIfClose)).isEqualTo("1 week") - assertThat(duration(11.days, rounding = UpIfClose)).isEqualTo("1 week") - assertThat(duration(12.days, rounding = UpIfClose)).isEqualTo("1 week") - - assertThat(duration(21.days, rounding = UpIfClose)).isEqualTo("3 weeks") - assertThat(duration(29.days, rounding = UpIfClose)).isEqualTo("4 weeks") - assertThat(duration(30.days, rounding = UpIfClose)).isEqualTo("4 weeks") - assertThat(duration(31.days, rounding = UpIfClose)).isEqualTo("1 month") - assertThat(duration(90.days, rounding = UpIfClose)).isEqualTo("2 months") - - assertThat(duration(360.days, rounding = UpIfClose)).isEqualTo("11 months") - assertThat(duration(365.days, rounding = UpIfClose)).isEqualTo("1 year") - assertThat(duration(555.days, rounding = UpIfClose)).isEqualTo("1 year") - assertThat(duration(1095.days, rounding = UpIfClose)).isEqualTo("3 years") + fun ifCloseRounding() { + assertThat(duration(1.seconds, rounding = IfClose())).isEqualTo("1 second") + assertThat(duration(3.seconds, rounding = IfClose())).isEqualTo("3 seconds") + + assertThat(duration(55.seconds, rounding = IfClose())).isEqualTo("1 minute") + assertThat(duration(1.minutes, rounding = IfClose())).isEqualTo("1 minute") + assertThat(duration(3.minutes, rounding = IfClose())).isEqualTo("3 minutes") + + assertThat(duration(55.minutes, rounding = IfClose())).isEqualTo("1 hour") + assertThat(duration(1.hours, rounding = IfClose())).isEqualTo("1 hour") + assertThat(duration(3.hours, rounding = IfClose())).isEqualTo("3 hours") + + assertThat(duration(23.hours, rounding = IfClose())).isEqualTo("1 day") + assertThat(duration(1.days, rounding = IfClose())).isEqualTo("1 day") + assertThat(duration(3.days, rounding = IfClose())).isEqualTo("3 days") + + assertThat(duration(7.days, rounding = IfClose())).isEqualTo("1 week") + assertThat(duration(10.days, rounding = IfClose())).isEqualTo("1 week") + assertThat(duration(11.days, rounding = IfClose())).isEqualTo("1 week") + assertThat(duration(12.days, rounding = IfClose())).isEqualTo("1 week") + + assertThat(duration(21.days, rounding = IfClose())).isEqualTo("3 weeks") + assertThat(duration(29.days, rounding = IfClose())).isEqualTo("4 weeks") + assertThat(duration(30.days, rounding = IfClose())).isEqualTo("4 weeks") + assertThat(duration(31.days, rounding = IfClose())).isEqualTo("1 month") + assertThat(duration(90.days, rounding = IfClose())).isEqualTo("2 months") + + assertThat(duration(360.days, rounding = IfClose())).isEqualTo("11 months") + assertThat(duration(365.days, rounding = IfClose())).isEqualTo("1 year") + assertThat(duration(555.days, rounding = IfClose())).isEqualTo("1 year") + assertThat(duration(1095.days, rounding = IfClose())).isEqualTo("3 years") + } + + @Test + fun ifCloseDefault() { + assertThat(duration(1.minutes + 30.seconds, rounding = IfClose(default = Floor))).isEqualTo("1 minute") + assertThat(duration(1.minutes + 30.seconds, rounding = IfClose(default = HalfUp))).isEqualTo("2 minutes") } @Test @@ -147,19 +159,19 @@ class HumanReadableDurationTests { duration(1.minutes + 55.seconds, rounding = HalfUp, parts = PartsConfig(max = 2)) ).isEqualTo("1 minute, 55 seconds") assertThat( - duration(1.minutes + 55.seconds, rounding = UpIfClose, parts = PartsConfig(max = 2)) + duration(1.minutes + 55.seconds, rounding = IfClose(), parts = PartsConfig(max = 2)) ).isEqualTo("2 minutes") // Edge cases assertThat( - duration(59.minutes + 55.seconds, rounding = UpIfClose, parts = PartsConfig(max = 2)) + duration(59.minutes + 55.seconds, rounding = IfClose(), parts = PartsConfig(max = 2)) ).isEqualTo("1 hour") assertThat( - duration(23.hours + 55.minutes, rounding = UpIfClose, parts = PartsConfig(max = 2)) + duration(23.hours + 55.minutes, rounding = IfClose(), parts = PartsConfig(max = 2)) ).isEqualTo("1 day") assertThat( - duration(6.days + 23.hours, rounding = UpIfClose, parts = PartsConfig(max = 2)) - ).isEqualTo("7 days") + duration(6.days + 23.hours, rounding = IfClose(), parts = PartsConfig(max = 2)) + ).isEqualTo("1 week") } @Test @@ -330,4 +342,60 @@ class HumanReadableDurationTests { ) ).isEqualTo("3 weeks, 2 days, 01:00:00") } + + @Test + fun globalConfig() { + HumanReadable.config.time.formatting = FormatStyle( + time = FormatStyle.Time.Digital, + indicateApproximation = true + ) + assertThat(duration(5.minutes)).isEqualTo("00:05:00") + } + + @Test + fun advancedConfig() { + HumanReadable.config.time.formatting = FormatStyle(indicateApproximation = true) + HumanReadable.config.time.parts = PartsConfig( + max = 2, + smallestDuration = 30.seconds, + subpartCutOffs = mapOf( + TimeUnit.Minutes to 1, + TimeUnit.Hours to 3 + ) + ) + HumanReadable.config.time.rounding = IfClose( + thresholds = mapOf(TimeUnit.Minutes to 5), + default = Floor + ) + + assertThat(duration(15.seconds)).isEqualTo("less than 30 seconds") + assertThat(duration(45.seconds)).isEqualTo("45 seconds") + assertThat(duration(1.minutes + 45.seconds)).isEqualTo("about 1 minute") + assertThat(duration(2.minutes + 15.seconds)).isEqualTo("about 2 minutes") + assertThat(duration(1.hours + 4.minutes)).isEqualTo("about 1 hour") + assertThat(duration(1.hours + 5.minutes)).isEqualTo("1 hour, 5 minutes") + assertThat(duration(1.hours + 55.minutes)).isEqualTo("about 2 hours") + assertThat(duration(2.hours + 4.minutes)).isEqualTo("about 2 hours") + assertThat(duration(2.hours + 5.minutes)).isEqualTo("2 hours, 5 minutes") + assertThat(duration(2.hours + 54.minutes)).isEqualTo("2 hours, 54 minutes") + assertThat(duration(2.hours + 55.minutes)).isEqualTo("about 3 hours") + assertThat(duration(24.hours + 4.minutes)).isEqualTo("about 1 day") + assertThat(duration(24.hours + 5.minutes)).isEqualTo("about 1 day") + assertThat(duration(32.hours + 55.minutes)).isEqualTo("about 1 day, 8 hours") + assertThat(duration(2.days)).isEqualTo("2 days") + assertThat(duration(2.days + 4.minutes)).isEqualTo("about 2 days") + assertThat(duration(2.days + 5.minutes)).isEqualTo("about 2 days") + assertThat(duration(2.days + 1.hours + 4.minutes)).isEqualTo("about 2 days, 1 hour") + assertThat(duration(2.days + 2.hours + 4.minutes)).isEqualTo("about 2 days, 2 hours") + assertThat(duration(3.days)).isEqualTo("3 days") + assertThat(duration(3.days + 4.minutes)).isEqualTo("about 3 days") + assertThat(duration(3.days + 5.minutes)).isEqualTo("about 3 days") + assertThat(duration(3.days + 1.hours + 4.minutes)).isEqualTo("about 3 days, 1 hour") + assertThat(duration(3.days + 2.hours + 4.minutes)).isEqualTo("about 3 days, 2 hours") + assertThat(duration(4.days)).isEqualTo("4 days") + assertThat(duration(4.days + 4.minutes)).isEqualTo("about 4 days") + assertThat(duration(4.days + 5.minutes)).isEqualTo("about 4 days") + assertThat(duration(4.days + 1.hours + 4.minutes)).isEqualTo("about 4 days, 1 hour") + assertThat(duration(4.days + 2.hours + 4.minutes)).isEqualTo("about 4 days, 2 hours") + } } \ No newline at end of file From 7c78db9ae1761af0f3f24547e3cba3206fa90cf5 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Tue, 7 Jul 2026 18:17:07 +0200 Subject: [PATCH 42/57] Reorder list of targets --- build.gradle.kts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 48d7b37..9152c42 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,14 +48,10 @@ kotlin { explicitApi() applyDefaultHierarchyTemplate() + // Targets, alphabetically sorted iosX64() iosArm64() iosSimulatorArm64() - tvosArm64() - tvosSimulatorArm64() - watchosArm32() - watchosArm64() - watchosSimulatorArm64() js { browser { testTask { @@ -65,15 +61,6 @@ kotlin { } } } - wasmJs { - browser { - testTask { - useKarma { - useChromeHeadless() - } - } - } - } jvm { testRuns.named("test") { executionTask.configure { @@ -88,6 +75,20 @@ kotlin { } } } + tvosArm64() + tvosSimulatorArm64() + wasmJs { + browser { + testTask { + useKarma { + useChromeHeadless() + } + } + } + } + watchosArm32() + watchosArm64() + watchosSimulatorArm64() sourceSets { commonMain.dependencies { From 252eb18d1cd63f8600badfa54ec420cbb4cddb8f Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Tue, 7 Jul 2026 18:17:50 +0200 Subject: [PATCH 43/57] Add native targets --- build.gradle.kts | 4 ++++ ...ag.apple.kt => systemLanguageTag.apple.kt} | 0 ...temLanguageTag.kt => systemLanguageTag.kt} | 0 ...ageTag.jvm.kt => systemLanguageTag.jvm.kt} | 0 .../i18n/systemLanguageTag.linux.kt | 20 +++++++++++++++++++ .../i18n/systemLanguageTag.mingw.kt | 20 +++++++++++++++++++ ...ageTag.web.kt => systemLanguageTag.web.kt} | 0 7 files changed, 44 insertions(+) rename src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/{SystemLanguageTag.apple.kt => systemLanguageTag.apple.kt} (100%) rename src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/{SystemLanguageTag.kt => systemLanguageTag.kt} (100%) rename src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/{SystemLanguageTag.jvm.kt => systemLanguageTag.jvm.kt} (100%) create mode 100644 src/linuxMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.linux.kt create mode 100644 src/mingwMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.mingw.kt rename src/webMain/kotlin/nl/jacobras/humanreadable/i18n/{SystemLanguageTag.web.kt => systemLanguageTag.web.kt} (100%) diff --git a/build.gradle.kts b/build.gradle.kts index 9152c42..6c119cd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -75,6 +75,10 @@ kotlin { } } } + linuxArm64() + linuxX64() + macosArm64() + mingwX64() tvosArm64() tvosSimulatorArm64() wasmJs { diff --git a/src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.apple.kt b/src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.apple.kt similarity index 100% rename from src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.apple.kt rename to src/appleMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.apple.kt diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.kt similarity index 100% rename from src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.kt rename to src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.kt diff --git a/src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.jvm.kt b/src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.jvm.kt similarity index 100% rename from src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.jvm.kt rename to src/jvmMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.jvm.kt diff --git a/src/linuxMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.linux.kt b/src/linuxMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.linux.kt new file mode 100644 index 0000000..550b430 --- /dev/null +++ b/src/linuxMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.linux.kt @@ -0,0 +1,20 @@ +package nl.jacobras.humanreadable.i18n + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import platform.posix.getenv + +@OptIn(ExperimentalForeignApi::class) +internal actual fun systemLanguageTag(): String { + val envValue = getenv("LANGUAGE")?.toKString()?.substringBefore(':') + ?: getenv("LC_ALL")?.toKString() + ?: getenv("LANG")?.toKString() + return envValue + ?.substringBefore('.') // Remove charset: "en_US.UTF-8" » "en_US" + ?.substringBefore('@') // Remove modifier: "sr_RS@latin" » "sr_RS" + ?.takeIf { + // Apparently some distros may return "C" or "POSIX" if there is no locale data + it.isNotBlank() && !it.equals("C", true) && !it.equals("POSIX", true) + } + ?.replace('_', '-') ?: "en" +} \ No newline at end of file diff --git a/src/mingwMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.mingw.kt b/src/mingwMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.mingw.kt new file mode 100644 index 0000000..b693a26 --- /dev/null +++ b/src/mingwMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.mingw.kt @@ -0,0 +1,20 @@ +package nl.jacobras.humanreadable.i18n + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.toKStringFromUtf16 +import platform.windows.GetUserDefaultLocaleName +import platform.windows.LOCALE_NAME_MAX_LENGTH +import platform.windows.WCHARVar + +@OptIn(ExperimentalForeignApi::class) +internal actual fun systemLanguageTag(): String = memScoped { + val buffer = allocArray(LOCALE_NAME_MAX_LENGTH) + val length = GetUserDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH) + return if (length > 0) { + buffer.toKStringFromUtf16() + } else { + "en" + } +} \ No newline at end of file diff --git a/src/webMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.web.kt b/src/webMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.web.kt similarity index 100% rename from src/webMain/kotlin/nl/jacobras/humanreadable/i18n/SystemLanguageTag.web.kt rename to src/webMain/kotlin/nl/jacobras/humanreadable/i18n/systemLanguageTag.web.kt From 790e5b89de03a0ef98df7022823d77d74d5fd4f6 Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Tue, 7 Jul 2026 19:09:09 +0200 Subject: [PATCH 44/57] Update translations --- .../i18n/HumanReadableStrings.kt | 34 +++++++++---------- .../i18n/translations/ArStrings.kt | 19 ++++++++++- .../i18n/translations/CsStrings.kt | 19 ++++++++++- .../i18n/translations/DeStrings.kt | 18 +++++++++- .../i18n/translations/ElStrings.kt | 18 +++++++++- .../i18n/translations/EsStrings.kt | 18 +++++++++- .../i18n/translations/FiStrings.kt | 19 ++++++++++- .../i18n/translations/FrStrings.kt | 18 +++++++++- .../i18n/translations/IdStrings.kt | 18 +++++++++- .../i18n/translations/ItStrings.kt | 18 +++++++++- .../i18n/translations/JaStrings.kt | 18 +++++++++- .../i18n/translations/KkStrings.kt | 19 ++++++++++- .../i18n/translations/KoStrings.kt | 18 +++++++++- .../i18n/translations/NlStrings.kt | 18 +++++++++- .../i18n/translations/PlStrings.kt | 18 +++++++++- .../i18n/translations/PtStrings.kt | 18 +++++++++- .../i18n/translations/RuStrings.kt | 18 +++++++++- .../i18n/translations/TrStrings.kt | 18 +++++++++- .../i18n/translations/UkStrings.kt | 18 +++++++++- .../i18n/translations/UzStrings.kt | 19 ++++++++++- .../i18n/translations/ViStrings.kt | 18 +++++++++- .../i18n/translations/ZhStrings.kt | 18 +++++++++- 22 files changed, 378 insertions(+), 39 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt index 0c84091..5500c1c 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/HumanReadableStrings.kt @@ -29,23 +29,21 @@ internal data class DateTimeStrings( val monthsLong: TenseForms, val yearsLong: TenseForms, - // TODO: remove defaults - val secondsShort: TenseForms = presentTense(one = "hr"), - val minutesShort: TenseForms = presentTense(one = "h"), - val hoursShort: TenseForms = presentTense(one = "day", other = "days"), - val daysShort: TenseForms = presentTense(one = "day", other = "days"), - val weeksShort: TenseForms = presentTense(one = "d"), - val monthsShort: TenseForms = presentTense(one = "week", other = "weeks"), - val yearsShort: TenseForms = presentTense(one = "wk", other = "wks"), + val secondsShort: TenseForms, + val minutesShort: TenseForms, + val hoursShort: TenseForms, + val daysShort: TenseForms, + val weeksShort: TenseForms, + val monthsShort: TenseForms, + val yearsShort: TenseForms, - // TODO: remove defaults - val secondsNarrow: TenseForms = presentTense(one = "w"), - val minutesNarrow: TenseForms = presentTense(one = "month", other = "months"), - val hoursNarrow: TenseForms = presentTense(one = "mth", other = "mths"), - val daysNarrow: TenseForms = presentTense(one = "m"), - val weeksNarrow: TenseForms = presentTense(one = "year", other = "years"), - val monthsNarrow: TenseForms = presentTense(one = "yr", other = "yrs"), - val yearsNarrow: TenseForms = presentTense(one = "y"), + val secondsNarrow: TenseForms, + val minutesNarrow: TenseForms, + val hoursNarrow: TenseForms, + val daysNarrow: TenseForms, + val weeksNarrow: TenseForms, + val monthsNarrow: TenseForms, + val yearsNarrow: TenseForms, /** * Wraps a formatted duration as past relative time, e.g. "3 days ago". @@ -61,8 +59,8 @@ internal data class DateTimeStrings( val today: String, val yesterday: String, val tomorrow: String, - val lessThan: String = "!!TODO", - val about: String = "!!TODO" + val lessThan: String, + val about: String ) /** diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt index f40753a..10a94b1 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt @@ -1,6 +1,7 @@ package nl.jacobras.humanreadable.i18n.translations import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense import nl.jacobras.humanreadable.i18n.multipleTenses import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.DistanceStrings @@ -47,12 +48,28 @@ internal val ArStrings = HumanReadableStrings( present(zero = "سنة", one = "سنة", two = "سنتان", few = "سنوات", many = "سنة", other = "سنة") pastOrFuture(two = "سنتين") }, + secondsShort = presentTense(one = "ثانية"), + minutesShort = presentTense(one = "دقيقة"), + hoursShort = presentTense(one = "ساعة"), + daysShort = presentTense(one = "يوم"), + weeksShort = presentTense(one = "أسبوع"), + monthsShort = presentTense(one = "شهر"), + yearsShort = presentTense(one = "سنة"), + secondsNarrow = presentTense(one = "ث"), + minutesNarrow = presentTense(one = "د"), + hoursNarrow = presentTense(one = "س"), + daysNarrow = presentTense(one = "ي"), + weeksNarrow = presentTense(one = "أ"), + monthsNarrow = presentTense(one = "شهر"), + yearsNarrow = presentTense(one = "سنة"), timeAgo = { "قبل $it" }, timeInFuture = { "بعد $it" }, now = "الآن", today = "اليوم", tomorrow = "أمس", - yesterday = "غداً" + yesterday = "غداً", + lessThan = "أقل من", + about = "حوالي" ), fileSize = FileSizeStrings( byteSymbol = "ب", diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt index 658a30c..a049530 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/CsStrings.kt @@ -1,6 +1,7 @@ package nl.jacobras.humanreadable.i18n.translations import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense import nl.jacobras.humanreadable.i18n.multipleTenses import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.HumanReadableStrings @@ -46,12 +47,28 @@ internal val CsStrings = HumanReadableStrings( present(one = "rok", few = "roky", other = "let") past(one = "rokem", few = "roky", other = "lety") }, + secondsShort = presentTense(one = "s"), + minutesShort = presentTense(one = "min"), + hoursShort = presentTense(one = "h"), + daysShort = presentTense(one = "d"), + weeksShort = presentTense(one = "týd."), + monthsShort = presentTense(one = "měs."), + yearsShort = presentTense(one = "r."), + secondsNarrow = presentTense(one = "s"), + minutesNarrow = presentTense(one = "min"), + hoursNarrow = presentTense(one = "h"), + daysNarrow = presentTense(one = "d"), + weeksNarrow = presentTense(one = "t"), + monthsNarrow = presentTense(one = "m"), + yearsNarrow = presentTense(one = "r"), timeAgo = { "před $it" }, timeInFuture = { "za $it" }, now = "nyní", today = "dnes", yesterday = "včera", - tomorrow = "zítra" + tomorrow = "zítra", + lessThan = "méně než", + about = "přibližně" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt index 8ec251a..e99b9d3 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/DeStrings.kt @@ -26,12 +26,28 @@ internal val DeStrings = HumanReadableStrings( present(one = "Jahr", other = "Jahre") pastOrFuture(other = "Jahren") }, + secondsShort = presentTense(one = "Sek."), + minutesShort = presentTense(one = "Min."), + hoursShort = presentTense(one = "Std."), + daysShort = presentTense(one = "Tg."), + weeksShort = presentTense(one = "Wo."), + monthsShort = presentTense(one = "Mon."), + yearsShort = presentTense(one = "J."), + secondsNarrow = presentTense(one = "Sek."), + minutesNarrow = presentTense(one = "Min."), + hoursNarrow = presentTense(one = "Std."), + daysNarrow = presentTense(one = "T"), + weeksNarrow = presentTense(one = "Wo."), + monthsNarrow = presentTense(one = "M"), + yearsNarrow = presentTense(one = "J"), timeAgo = { "vor $it" }, timeInFuture = { "in $it" }, now = "jetzt", today = "heute", yesterday = "gestern", - tomorrow = "morgen" + tomorrow = "morgen", + lessThan = "weniger als", + about = "etwa" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt index 3c35f57..db92acf 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ElStrings.kt @@ -20,12 +20,28 @@ internal val ElStrings = HumanReadableStrings( pastOrFuture(one = "μήνα") }, yearsLong = presentTense(one = "έτος", other = "έτη"), + secondsShort = presentTense(one = "δευτ."), + minutesShort = presentTense(one = "λεπ."), + hoursShort = presentTense(one = "ώ."), + daysShort = presentTense(one = "ημ."), + weeksShort = presentTense(one = "εβδ."), + monthsShort = presentTense(one = "μήν."), + yearsShort = presentTense(one = "έτ."), + secondsNarrow = presentTense(one = "δ"), + minutesNarrow = presentTense(one = "λ"), + hoursNarrow = presentTense(one = "ώ"), + daysNarrow = presentTense(one = "η"), + weeksNarrow = presentTense(one = "ε"), + monthsNarrow = presentTense(one = "μ"), + yearsNarrow = presentTense(one = "έ"), timeAgo = { "$it πριν" }, timeInFuture = { "σε $it" }, now = "τώρα", today = "σήμερα", yesterday = "χθες", - tomorrow = "αύριο" + tomorrow = "αύριο", + lessThan = "λιγότερο από", + about = "περίπου" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt index 354735b..269d46f 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/EsStrings.kt @@ -16,12 +16,28 @@ internal val EsStrings = HumanReadableStrings( weeksLong = presentTense(one = "semana", other = "semanas"), monthsLong = presentTense(one = "mes", other = "meses"), yearsLong = presentTense(one = "año", other = "años"), + secondsShort = presentTense(one = "s"), + minutesShort = presentTense(one = "min"), + hoursShort = presentTense(one = "h"), + daysShort = presentTense(one = "d"), + weeksShort = presentTense(one = "sem."), + monthsShort = presentTense(one = "m."), + yearsShort = presentTense(one = "a"), + secondsNarrow = presentTense(one = "s"), + minutesNarrow = presentTense(one = "min"), + hoursNarrow = presentTense(one = "h"), + daysNarrow = presentTense(one = "d"), + weeksNarrow = presentTense(one = "sem."), + monthsNarrow = presentTense(one = "m."), + yearsNarrow = presentTense(one = "a"), timeAgo = { "hace $it" }, timeInFuture = { "en $it" }, now = "ahora", today = "hoy", yesterday = "ayer", - tomorrow = "mañana" + tomorrow = "mañana", + lessThan = "menos de", + about = "aprox." ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt index 691643a..0db9625 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FiStrings.kt @@ -1,6 +1,7 @@ package nl.jacobras.humanreadable.i18n.translations import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense import nl.jacobras.humanreadable.i18n.multipleTenses import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.FileSizeStrings @@ -38,12 +39,28 @@ internal val FiStrings = HumanReadableStrings( present(one = "vuosi", other = "vuotta") future(one = "vuoden", other = "vuoden") }, + secondsShort = presentTense(one = "s"), + minutesShort = presentTense(one = "min"), + hoursShort = presentTense(one = "t"), + daysShort = presentTense(one = "pv"), + weeksShort = presentTense(one = "vk"), + monthsShort = presentTense(one = "kk"), + yearsShort = presentTense(one = "v"), + secondsNarrow = presentTense(one = "s"), + minutesNarrow = presentTense(one = "min"), + hoursNarrow = presentTense(one = "t"), + daysNarrow = presentTense(one = "pv"), + weeksNarrow = presentTense(one = "vk"), + monthsNarrow = presentTense(one = "kk"), + yearsNarrow = presentTense(one = "v"), timeAgo = { "$it sitten" }, timeInFuture = { "$it kuluttua" }, now = "nyt", today = "tänään", yesterday = "eilen", - tomorrow = "huomenna" + tomorrow = "huomenna", + lessThan = "alle", + about = "noin" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ","), fileSize = FileSizeStrings( diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt index 363d04f..bc2a6cc 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/FrStrings.kt @@ -17,12 +17,28 @@ internal val FrStrings = HumanReadableStrings( weeksLong = presentTense(one = "semaine", other = "semaines"), monthsLong = presentTense(one = "mois", other = "mois"), yearsLong = presentTense(one = "an", other = "ans"), + secondsShort = presentTense(one = "s"), + minutesShort = presentTense(one = "min"), + hoursShort = presentTense(one = "h"), + daysShort = presentTense(one = "j"), + weeksShort = presentTense(one = "sem."), + monthsShort = presentTense(one = "m."), + yearsShort = presentTense(one = "an"), + secondsNarrow = presentTense(one = "s"), + minutesNarrow = presentTense(one = "min"), + hoursNarrow = presentTense(one = "h"), + daysNarrow = presentTense(one = "j"), + weeksNarrow = presentTense(one = "sem."), + monthsNarrow = presentTense(one = "m."), + yearsNarrow = presentTense(one = "an"), timeAgo = { "il y a $it" }, timeInFuture = { "dans $it" }, now = "maintenant", today = "aujourd'hui", yesterday = "hier", - tomorrow = "demain" + tomorrow = "demain", + lessThan = "moins de", + about = "environ" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ","), fileSize = FileSizeStrings( diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt index a1fecb8..2d8e8b7 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/IdStrings.kt @@ -16,12 +16,28 @@ internal val IdStrings = HumanReadableStrings( weeksLong = presentTense(other = "minggu"), monthsLong = presentTense(other = "bulan"), yearsLong = presentTense(other = "tahun"), + secondsShort = presentTense(other = "dtk"), + minutesShort = presentTense(other = "mnt"), + hoursShort = presentTense(other = "jam"), + daysShort = presentTense(other = "h"), + weeksShort = presentTense(other = "mgg"), + monthsShort = presentTense(other = "bln"), + yearsShort = presentTense(other = "thn"), + secondsNarrow = presentTense(other = "dtk"), + minutesNarrow = presentTense(other = "mnt"), + hoursNarrow = presentTense(other = "jam"), + daysNarrow = presentTense(other = "h"), + weeksNarrow = presentTense(other = "mg"), + monthsNarrow = presentTense(other = "bln"), + yearsNarrow = presentTense(other = "thn"), timeAgo = { "$it yang lalu" }, timeInFuture = { "dalam $it" }, now = "sekarang", today = "hari ini", yesterday = "kemarin", - tomorrow = "besok" + tomorrow = "besok", + lessThan = "kurang dari", + about = "sekitar" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt index 3e023e8..4028713 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ItStrings.kt @@ -16,12 +16,28 @@ internal val ItStrings = HumanReadableStrings( weeksLong = presentTense(one = "settimana", other = "settimane"), monthsLong = presentTense(one = "mese", other = "mesi"), yearsLong = presentTense(one = "anno", other = "anni"), + secondsShort = presentTense(one = "sec"), + minutesShort = presentTense(one = "min"), + hoursShort = presentTense(one = "h"), + daysShort = presentTense(one = "g"), + weeksShort = presentTense(one = "sett."), + monthsShort = presentTense(one = "mese"), + yearsShort = presentTense(one = "anno"), + secondsNarrow = presentTense(one = "s"), + minutesNarrow = presentTense(one = "min"), + hoursNarrow = presentTense(one = "h"), + daysNarrow = presentTense(one = "g"), + weeksNarrow = presentTense(one = "sett."), + monthsNarrow = presentTense(one = "m."), + yearsNarrow = presentTense(one = "a"), timeAgo = { "$it fa" }, timeInFuture = { "tra $it" }, now = "adesso", today = "oggi", yesterday = "ieri", - tomorrow = "domani" + tomorrow = "domani", + lessThan = "meno di", + about = "circa" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt index b1918a2..75b83c5 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/JaStrings.kt @@ -15,11 +15,27 @@ internal val JaStrings = HumanReadableStrings( weeksLong = presentTense(other = "週"), monthsLong = presentTense(other = "ヶ月"), yearsLong = presentTense(other = "年"), + secondsShort = presentTense(other = "秒"), + minutesShort = presentTense(other = "分"), + hoursShort = presentTense(other = "時間"), + daysShort = presentTense(other = "日"), + weeksShort = presentTense(other = "週間"), + monthsShort = presentTense(other = "か月"), + yearsShort = presentTense(other = "年"), + secondsNarrow = presentTense(other = "秒"), + minutesNarrow = presentTense(other = "分"), + hoursNarrow = presentTense(other = "時"), + daysNarrow = presentTense(other = "日"), + weeksNarrow = presentTense(other = "週"), + monthsNarrow = presentTense(other = "月"), + yearsNarrow = presentTense(other = "年"), timeAgo = { "${it}前" }, timeInFuture = { "${it}後" }, now = "今", today = "今日", yesterday = "昨日", - tomorrow = "明日" + tomorrow = "明日", + lessThan = "未満", + about = "約" ) ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt index 994dddc..c59f69c 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KkStrings.kt @@ -1,6 +1,7 @@ package nl.jacobras.humanreadable.i18n.translations import nl.jacobras.humanreadable.i18n.Plural +import nl.jacobras.humanreadable.i18n.presentTense import nl.jacobras.humanreadable.i18n.multipleTenses import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.HumanReadableStrings @@ -24,11 +25,27 @@ internal val KkStrings = HumanReadableStrings( weeksLong = multipleTenses { present(one = "апта", other = "апта"); future(one = "аптадан", other = "аптадан") }, monthsLong = multipleTenses { present(one = "ай", other = "ай"); future(one = "айдан", other = "айдан") }, yearsLong = multipleTenses { present(one = "жыл", other = "жыл"); future(one = "жылдан", other = "жылдан") }, + secondsShort = presentTense(one = "сек"), + minutesShort = presentTense(one = "мин"), + hoursShort = presentTense(one = "сағ"), + daysShort = presentTense(one = "күн"), + weeksShort = presentTense(one = "ап."), + monthsShort = presentTense(one = "ай"), + yearsShort = presentTense(one = "ж."), + secondsNarrow = presentTense(one = "с"), + minutesNarrow = presentTense(one = "мин"), + hoursNarrow = presentTense(one = "сағ"), + daysNarrow = presentTense(one = "к"), + weeksNarrow = presentTense(one = "ап"), + monthsNarrow = presentTense(one = "ай"), + yearsNarrow = presentTense(one = "ж"), timeAgo = { "$it бұрын" }, timeInFuture = { "$it кейін" }, now = "қазір", today = "бүгін", yesterday = "кеше", - tomorrow = "ертең" + tomorrow = "ертең", + lessThan = "кем", + about = "шамамен" ) ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt index 067d4dc..5c0e644 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/KoStrings.kt @@ -15,11 +15,27 @@ internal val KoStrings = HumanReadableStrings( weeksLong = presentTense(other = "주"), monthsLong = presentTense(other = "개월"), yearsLong = presentTense(other = "년"), + secondsShort = presentTense(other = "초"), + minutesShort = presentTense(other = "분"), + hoursShort = presentTense(other = "시간"), + daysShort = presentTense(other = "일"), + weeksShort = presentTense(other = "주"), + monthsShort = presentTense(other = "개월"), + yearsShort = presentTense(other = "년"), + secondsNarrow = presentTense(other = "초"), + minutesNarrow = presentTense(other = "분"), + hoursNarrow = presentTense(other = "시"), + daysNarrow = presentTense(other = "일"), + weeksNarrow = presentTense(other = "주"), + monthsNarrow = presentTense(other = "개월"), + yearsNarrow = presentTense(other = "년"), timeAgo = { "$it 전" }, timeInFuture = { "$it 후" }, now = "지금", today = "오늘", yesterday = "어제", - tomorrow = "내일" + tomorrow = "내일", + lessThan = "미만", + about = "약" ) ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt index febacd2..237e6a1 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/NlStrings.kt @@ -16,12 +16,28 @@ internal val NlStrings = HumanReadableStrings( weeksLong = presentTense(one = "week", other = "weken"), monthsLong = presentTense(one = "maand", other = "maanden"), yearsLong = presentTense(one = "jaar", other = "jaar"), + secondsShort = presentTense(one = "sec"), + minutesShort = presentTense(one = "min"), + hoursShort = presentTense(one = "uur"), + daysShort = presentTense(one = "d"), + weeksShort = presentTense(one = "wk"), + monthsShort = presentTense(one = "mnd"), + yearsShort = presentTense(one = "jr"), + secondsNarrow = presentTense(one = "s"), + minutesNarrow = presentTense(one = "m"), + hoursNarrow = presentTense(one = "u"), + daysNarrow = presentTense(one = "d"), + weeksNarrow = presentTense(one = "w"), + monthsNarrow = presentTense(one = "mnd"), + yearsNarrow = presentTense(one = "j"), timeAgo = { "$it geleden" }, timeInFuture = { "over $it" }, now = "nu", today = "vandaag", yesterday = "gisteren", - tomorrow = "morgen" + tomorrow = "morgen", + lessThan = "minder dan", + about = "ongeveer" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt index e7d0f4f..34e173a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PlStrings.kt @@ -32,12 +32,28 @@ internal val PlStrings = HumanReadableStrings( weeksLong = presentTense(one = "tydzień", few = "tygodnie", many = "tygodni"), monthsLong = presentTense(one = "miesiąc", few = "miesiące", many = "miesięcy"), yearsLong = presentTense(one = "rok", few = "lata", many = "lat"), + secondsShort = presentTense(one = "sek."), + minutesShort = presentTense(one = "min"), + hoursShort = presentTense(one = "godz."), + daysShort = presentTense(one = "dz."), + weeksShort = presentTense(one = "tydz."), + monthsShort = presentTense(one = "mies."), + yearsShort = presentTense(one = "r."), + secondsNarrow = presentTense(one = "s"), + minutesNarrow = presentTense(one = "min"), + hoursNarrow = presentTense(one = "g"), + daysNarrow = presentTense(one = "d"), + weeksNarrow = presentTense(one = "t"), + monthsNarrow = presentTense(one = "m"), + yearsNarrow = presentTense(one = "r"), timeAgo = { "$it temu" }, timeInFuture = { "za $it" }, now = "teraz", today = "dzisiaj", yesterday = "wczoraj", - tomorrow = "jutro" + tomorrow = "jutro", + lessThan = "mniej niż", + about = "około" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt index e0f68bf..bf50290 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/PtStrings.kt @@ -22,12 +22,28 @@ internal val PtStrings = HumanReadableStrings( weeksLong = presentTense(one = "semana", many = "de semanas", other = "semanas"), monthsLong = presentTense(one = "mês", many = "de meses", other = "meses"), yearsLong = presentTense(one = "ano", many = "de anos", other = "anos"), + secondsShort = presentTense(one = "s"), + minutesShort = presentTense(one = "min"), + hoursShort = presentTense(one = "h"), + daysShort = presentTense(one = "dia"), + weeksShort = presentTense(one = "sem."), + monthsShort = presentTense(one = "mês"), + yearsShort = presentTense(one = "ano"), + secondsNarrow = presentTense(one = "s"), + minutesNarrow = presentTense(one = "min"), + hoursNarrow = presentTense(one = "h"), + daysNarrow = presentTense(one = "d"), + weeksNarrow = presentTense(one = "sem."), + monthsNarrow = presentTense(one = "m."), + yearsNarrow = presentTense(one = "a"), timeAgo = { "há $it" }, timeInFuture = { "em $it" }, now = "agora", today = "hoje", yesterday = "ontem", - tomorrow = "amanhã" + tomorrow = "amanhã", + lessThan = "menos de", + about = "cerca de" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt index 2874562..4deb522 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/RuStrings.kt @@ -32,12 +32,28 @@ internal val RuStrings = HumanReadableStrings( }, monthsLong = presentTense(one = "месяц", few = "месяца", many = "месяцев"), yearsLong = presentTense(one = "год", few = "года", many = "лет"), + secondsShort = presentTense(one = "с"), + minutesShort = presentTense(one = "мин"), + hoursShort = presentTense(one = "ч"), + daysShort = presentTense(one = "дн"), + weeksShort = presentTense(one = "нед"), + monthsShort = presentTense(one = "мес"), + yearsShort = presentTense(one = "г"), + secondsNarrow = presentTense(one = "с"), + minutesNarrow = presentTense(one = "м"), + hoursNarrow = presentTense(one = "ч"), + daysNarrow = presentTense(one = "д"), + weeksNarrow = presentTense(one = "н"), + monthsNarrow = presentTense(one = "мес"), + yearsNarrow = presentTense(one = "г"), timeAgo = { "$it назад" }, timeInFuture = { "через $it" }, now = "сейчас", today = "сегодня", yesterday = "вчера", - tomorrow = "завтра" + tomorrow = "завтра", + lessThan = "менее", + about = "около" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt index f1c8b64..e6b6d22 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/TrStrings.kt @@ -16,12 +16,28 @@ internal val TrStrings = HumanReadableStrings( weeksLong = presentTense(other = "hafta"), monthsLong = presentTense(other = "ay"), yearsLong = presentTense(other = "yıl"), + secondsShort = presentTense(other = "sn."), + minutesShort = presentTense(other = "dk."), + hoursShort = presentTense(other = "sa."), + daysShort = presentTense(other = "g"), + weeksShort = presentTense(other = "hf."), + monthsShort = presentTense(other = "ay"), + yearsShort = presentTense(other = "yıl"), + secondsNarrow = presentTense(other = "sn"), + minutesNarrow = presentTense(other = "dk"), + hoursNarrow = presentTense(other = "sa"), + daysNarrow = presentTense(other = "g"), + weeksNarrow = presentTense(other = "h"), + monthsNarrow = presentTense(other = "a"), + yearsNarrow = presentTense(other = "y"), timeAgo = { "$it önce" }, timeInFuture = { "$it sonra" }, now = "şimdi", today = "bugün", yesterday = "dün", - tomorrow = "yarın" + tomorrow = "yarın", + lessThan = "daha az", + about = "yaklaşık" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt index 479384f..64c4761 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UkStrings.kt @@ -32,12 +32,28 @@ internal val UkStrings = HumanReadableStrings( weeksLong = presentTense(one = "тиждень", few = "тижні", many = "тижнів"), monthsLong = presentTense(one = "місяць", few = "місяці", many = "місяців"), yearsLong = presentTense(one = "рік", few = "роки", many = "років"), + secondsShort = presentTense(one = "с"), + minutesShort = presentTense(one = "хв"), + hoursShort = presentTense(one = "год"), + daysShort = presentTense(one = "дн"), + weeksShort = presentTense(one = "тиж"), + monthsShort = presentTense(one = "міс"), + yearsShort = presentTense(one = "р"), + secondsNarrow = presentTense(one = "с"), + minutesNarrow = presentTense(one = "хв"), + hoursNarrow = presentTense(one = "год"), + daysNarrow = presentTense(one = "д"), + weeksNarrow = presentTense(one = "т"), + monthsNarrow = presentTense(one = "м"), + yearsNarrow = presentTense(one = "р"), timeAgo = { "$it тому" }, timeInFuture = { "через $it" }, now = "зараз", today = "сьогодні", yesterday = "вчора", - tomorrow = "завтра" + tomorrow = "завтра", + lessThan = "менше", + about = "близько" ), number = NumberStrings(groupSeparator = " ", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt index 7b3137b..7634c0c 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/UzStrings.kt @@ -4,6 +4,7 @@ import nl.jacobras.humanreadable.i18n.Plural import nl.jacobras.humanreadable.i18n.DateTimeStrings import nl.jacobras.humanreadable.i18n.HumanReadableStrings import nl.jacobras.humanreadable.i18n.NumberStrings +import nl.jacobras.humanreadable.i18n.presentTense import nl.jacobras.humanreadable.i18n.multipleTenses internal val UzStrings = HumanReadableStrings( @@ -37,12 +38,28 @@ internal val UzStrings = HumanReadableStrings( present(one = "yil", other = "yil") future(one = "yildan", other = "yildan") }, + secondsShort = presentTense(one = "son."), + minutesShort = presentTense(one = "daq."), + hoursShort = presentTense(one = "soat"), + daysShort = presentTense(one = "kun"), + weeksShort = presentTense(one = "hafta"), + monthsShort = presentTense(one = "oy"), + yearsShort = presentTense(one = "yil"), + secondsNarrow = presentTense(one = "s"), + minutesNarrow = presentTense(one = "d"), + hoursNarrow = presentTense(one = "soat"), + daysNarrow = presentTense(one = "k"), + weeksNarrow = presentTense(one = "h"), + monthsNarrow = presentTense(one = "oy"), + yearsNarrow = presentTense(one = "y"), timeAgo = { "$it oldin" }, timeInFuture = { "$it keyin" }, now = "hozir", today = "bugun", yesterday = "kecha", - tomorrow = "ertaga" + tomorrow = "ertaga", + lessThan = "kamroq", + about = "taxminan" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt index abb06b0..edcc8ce 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ViStrings.kt @@ -16,12 +16,28 @@ internal val ViStrings = HumanReadableStrings( weeksLong = presentTense(other = "tuần"), monthsLong = presentTense(other = "tháng"), yearsLong = presentTense(other = "năm"), + secondsShort = presentTense(other = "giây"), + minutesShort = presentTense(other = "phút"), + hoursShort = presentTense(other = "giờ"), + daysShort = presentTense(other = "ngày"), + weeksShort = presentTense(other = "tuần"), + monthsShort = presentTense(other = "tháng"), + yearsShort = presentTense(other = "năm"), + secondsNarrow = presentTense(other = "giây"), + minutesNarrow = presentTense(other = "phút"), + hoursNarrow = presentTense(other = "giờ"), + daysNarrow = presentTense(other = "ngày"), + weeksNarrow = presentTense(other = "tuần"), + monthsNarrow = presentTense(other = "tháng"), + yearsNarrow = presentTense(other = "năm"), timeAgo = { "$it trước" }, timeInFuture = { "sau $it" }, now = "bây giờ", today = "hôm nay", yesterday = "hôm qua", - tomorrow = "ngày mai" + tomorrow = "ngày mai", + lessThan = "ít hơn", + about = "khoảng" ), number = NumberStrings(groupSeparator = ".", decimalSymbol = ",") ) \ No newline at end of file diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt index ca98cb6..82c9b1a 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ZhStrings.kt @@ -15,11 +15,27 @@ internal val ZhStrings = HumanReadableStrings( weeksLong = presentTense(other = "周"), monthsLong = presentTense(other = "个月"), yearsLong = presentTense(other = "年"), + secondsShort = presentTense(other = "秒"), + minutesShort = presentTense(other = "分钟"), + hoursShort = presentTense(other = "小时"), + daysShort = presentTense(other = "天"), + weeksShort = presentTense(other = "周"), + monthsShort = presentTense(other = "个月"), + yearsShort = presentTense(other = "年"), + secondsNarrow = presentTense(other = "秒"), + minutesNarrow = presentTense(other = "分"), + hoursNarrow = presentTense(other = "时"), + daysNarrow = presentTense(other = "天"), + weeksNarrow = presentTense(other = "周"), + monthsNarrow = presentTense(other = "月"), + yearsNarrow = presentTense(other = "年"), timeAgo = { "${it}之前" }, timeInFuture = { "${it}之后" }, now = "现在", today = "今天", yesterday = "昨天", - tomorrow = "明天" + tomorrow = "明天", + lessThan = "少于", + about = "大约" ) ) \ No newline at end of file From bc139297b8cbae19c0bb92f0d6c1c38852a9683e Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 11:48:06 +0200 Subject: [PATCH 45/57] Fix today/tomorrow swapped in Arabic translation --- .../nl/jacobras/humanreadable/i18n/translations/ArStrings.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt index 10a94b1..b98bec4 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/translations/ArStrings.kt @@ -66,8 +66,8 @@ internal val ArStrings = HumanReadableStrings( timeInFuture = { "بعد $it" }, now = "الآن", today = "اليوم", - tomorrow = "أمس", - yesterday = "غداً", + tomorrow = "غداً", + yesterday = "أمس", lessThan = "أقل من", about = "حوالي" ), From 4133b8415aeec3a996ae2948eefb1609f5991236 Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 11:53:10 +0200 Subject: [PATCH 46/57] Filter out language part on default value --- .../kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt | 4 ++-- .../nl/jacobras/humanreadable/i18n/filterLanguagePart.kt | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/filterLanguagePart.kt diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt index 6f6fc4c..03fd00e 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt @@ -13,9 +13,9 @@ internal class Localisation { * * @see languageTag for the actual language tag used, which may differ if the requested one is not supported. */ - var requestedLanguageTag = systemLanguageTag() + var requestedLanguageTag = systemLanguageTag().filterLanguagePart() set(value) { - field = value.lowercase().substringBefore("-") + field = value.filterLanguagePart() currentTagAndStrings = null } diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/filterLanguagePart.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/filterLanguagePart.kt new file mode 100644 index 0000000..176aef2 --- /dev/null +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/filterLanguagePart.kt @@ -0,0 +1,5 @@ +package nl.jacobras.humanreadable.i18n + +internal fun String.filterLanguagePart(): String { + return lowercase().substringBefore("-") +} \ No newline at end of file From 9ac405dbe3b35ecf08726c62e3aedee1a1c4bf1f Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 12:24:00 +0200 Subject: [PATCH 47/57] Fix docs inconsistencies --- README.md | 6 +++--- demo/src/webMain/kotlin/feature/TimeDemo.kt | 2 +- .../nl/jacobras/humanreadable/time/HumanReadableDuration.kt | 2 +- .../kotlin/nl/jacobras/humanreadable/time/Rounding.kt | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b825e17..2603c50 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,11 @@ HumanReadable.timeAgo( parts = PartsConfig( max = 2, // "1 hour, 50 minutes" smallestDuration = 10.minutes, // anything smaller will return "less than 10 minutes" - subpartCutOffs = mapOf(TimeUnit.Hours to 12.hours), // drops subparts, e.g. "11 hours, 40 minutes" and then "12 hours" + subpartCutOffs = mapOf(TimeUnit.Hours to 12), // drops subparts, e.g. "11 hours, 40 minutes" and then "12 hours" onlyConsecutiveParts = true // whether "1 hour, 5 seconds" can be returned (as seconds are not the next unit after hours) ), - units = setOf(TimeUnit.Hours), // limits the output to these units, e.g. "391 days" - rounding = Rounding.HalfUp // or Floor to round down, or UpIfClose to round up on 55 seconds/55 minutes/23 hours/13 days + units = setOf(TimeUnit.Days), // limits the output to these units, e.g. "391 days" + rounding = Rounding.HalfUp // or Floor to round down, or IfClose to round up or down when close enough (5s/5m/23h/6d, configurable) to the larger unit ) ``` diff --git a/demo/src/webMain/kotlin/feature/TimeDemo.kt b/demo/src/webMain/kotlin/feature/TimeDemo.kt index b77065f..2ac78a6 100644 --- a/demo/src/webMain/kotlin/feature/TimeDemo.kt +++ b/demo/src/webMain/kotlin/feature/TimeDemo.kt @@ -200,7 +200,7 @@ internal fun TimeDemo( ) Spacer(Modifier.height(8.dp)) CodeExample( - code = "rounding = Rounding.UpIfClose", + code = "rounding = Rounding.IfClose()", res = remember(selectedLanguageCode, now, myInstant) { HumanReadable.timeAgo( myInstant, now, diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index 7b92ac4..61998c2 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -123,7 +123,7 @@ private fun getNeededParts( } // Round the last part. Only needed for HalfUp, because Floor is already done above - // and UpIfClose is done below in the roll-overs section. + // and IfClose is done below in the roll-overs section. val roundMethod = if (rounding is Rounding.IfClose) rounding.default else rounding if (lastUnit != null && roundMethod == Rounding.HalfUp) { res[lastUnit] = lastUnit.calculateValue(remainingBeforeLast, Rounding.HalfUp) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt index b27f018..32077de 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt @@ -38,8 +38,8 @@ public sealed interface Rounding { * For example, with [PartsConfig.max] set to 2 and [thresholds] set to [Seconds = 5]: * - `1.minutes + 5.seconds` remains "1 minute, 5 seconds"; * - `1.minutes + 4.seconds` gets rounded to "1 minute"; - * - `1.minutes + 54 seconds` remains "1 minute, 54 seconds"; - * - `1.minutes + 55 seconds` gets rounded to "2 minutes". + * - `1.minutes + 54.seconds` remains "1 minute, 54 seconds"; + * - `1.minutes + 55.seconds` gets rounded to "2 minutes". * * Anything else is rounded according to [default], which defaults to [Floor]. */ From e96cdf5b82aaa1bfc06944bd3cc226b4265e415d Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 12:35:36 +0200 Subject: [PATCH 48/57] Add tests for large durations --- .../humanreadable/time/HumanReadableRelativeTime.kt | 2 +- .../humanreadable/time/HumanReadableDurationTests.kt | 5 +++++ .../humanreadable/time/HumanReadableRelativeTimeTests.kt | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt index 20edaca..f96fec5 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt @@ -85,7 +85,7 @@ internal fun formatTimeAgo( baseDate -> return strings.dateTime.today baseDate.plus(1, DateTimeUnit.DAY) -> return strings.dateTime.tomorrow } - val secondsAgo = date.daysUntil(baseDate) * 86_400 + val secondsAgo = date.daysUntil(baseDate) * 86_400L return when { secondsAgo < 0 -> strings.dateTime.timeInFuture( diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt index 01c576b..6d3209c 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableDurationTests.kt @@ -398,4 +398,9 @@ class HumanReadableDurationTests { assertThat(duration(4.days + 1.hours + 4.minutes)).isEqualTo("about 4 days, 1 hour") assertThat(duration(4.days + 2.hours + 4.minutes)).isEqualTo("about 4 days, 2 hours") } + + @Test + fun longDuration() { + assertThat(duration((4000 * 365).days)).isEqualTo("4,000 years") + } } \ No newline at end of file diff --git a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt index 9193ea9..948a9b5 100644 --- a/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt +++ b/src/commonTest/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTimeTests.kt @@ -45,6 +45,12 @@ class HumanReadableRelativeTimeTests { assertThat(HumanReadable.timeAgo(now - 3.days, baseInstant = now)).isEqualTo("3 days ago") } + @Test + fun longTime() { + assertThat(HumanReadable.timeAgo(now - (4000 * 365).days, baseInstant = now)).isEqualTo("4,000 years ago") + assertThat(HumanReadable.timeAgo(now + (4000 * 365).days, baseInstant = now)).isEqualTo("in 4,000 years") + } + @Test fun todayTomorrowYesterday() { val today = LocalDate.parse("2026-07-01") From 261d39e0e40f5c62879c2b162b63722aa57f1b66 Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 12:39:21 +0200 Subject: [PATCH 49/57] Update docs --- .../kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt | 2 ++ .../nl/jacobras/humanreadable/time/HumanReadableDuration.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt index 51b583a..763e485 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt @@ -40,6 +40,8 @@ public data class FormatStyle( /** * Digital format style, e.g. "1:50:00". + * + * Note that [PartsConfig.smallestDuration] is ignored when using this time style. */ Digital } diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt index 61998c2..48dfa58 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableDuration.kt @@ -9,7 +9,7 @@ import kotlin.time.Duration * Returns the given [duration] in human-readable format. * * @param duration The duration to format. - * @param relativeTime Whether this is in the past, current or future (for grammatical correctness). + * @param relativeTime Whether this is in the past, current, or future (for grammatical correctness). * @param format The [FormatStyle] to use. * @param parts Configures the formatting of multiple parts (defaults to 1 part). * @param units The [TimeUnit]s to limit to during formatting. From e2d5cba169c7e71e1ecc8197008d21fbd666b046 Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 12:58:07 +0200 Subject: [PATCH 50/57] Bump version to 2.0.0-alpha01 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2603c50..daf1b6c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A small set of data formatting utilities for Kotlin Multiplatform (KMP). -This library only supports [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime). +The library depends on [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime). ## Installation @@ -19,7 +19,7 @@ The library is published to Maven Central. ```kotlin dependencies { - implementation("nl.jacobras:Human-Readable:2.0.0") // Not yet published! + implementation("nl.jacobras:Human-Readable:2.0.0-alpha01") // Not yet published! } ``` From de11fdda1ac7cbe91800d2f758e0a302e4a790ea Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 13:01:55 +0200 Subject: [PATCH 51/57] Update examples in `Date` docs --- .../kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt index 763e485..6051861 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/FormatStyle.kt @@ -16,17 +16,17 @@ public data class FormatStyle( public enum class Date { /** - * Long format style, e.g. "1 hour, 50 minutes". + * Long format style, e.g. "3 weeks, 2 days". */ Long, /** - * Shorter format style, e.g. "1 hr, 50 min". + * Shorter format style, e.g. "3 wks, 2 days". */ Short, /** - * Shortest format style, e.g. "1h 50m". + * Shortest format style, e.g. "3w 2d". */ Narrow } From 2e5e44be5aee98bc527d3e566796a3ef8ee60486 Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 13:05:00 +0200 Subject: [PATCH 52/57] Reword docs --- .../jacobras/humanreadable/time/HumanReadableRelativeTime.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt index f96fec5..4b797d6 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/HumanReadableRelativeTime.kt @@ -13,7 +13,7 @@ import kotlin.time.Instant /** * Returns the difference between [baseInstant] and [instant], in human-readable format. - * Also supports instants in the future or past. + * Supports instants in both the future and past. * * @param instant The [Instant] to compare with [baseInstant]. * @param baseInstant The base/starting [Instant], usually "now". @@ -61,7 +61,7 @@ internal fun formatTimeAgo( /** * Returns the difference between [date] and [baseDate], in human-readable format. - * Also supports dates in the future or past. + * Supports dates in both the future and past. * * Will return "today", "tomorrow" or "yesterday" if the date is today, tomorrow, or yesterday. * From 17f64077fb016443a9c129afa0d5deb3be34314a Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 13:06:56 +0200 Subject: [PATCH 53/57] Remove duplicate docs --- .../kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt index f42cd04..cc8c5a6 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/PartsConfig.kt @@ -4,10 +4,6 @@ import kotlin.time.Duration /** * Configures the formatting of multiple parts when formatting a duration. - * - * Note that only adjacent subparts are used: - * - "1 day, 20 hours" could be output; - * - "1 day, 20 minutes" will never be output (because minutes are not next to days). */ public data class PartsConfig( From 926062006a28a4e6efb623f800f8b630d515f616 Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 13:59:09 +0200 Subject: [PATCH 54/57] Set version to 2.0.0-alpha01 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 40789ea..46240f2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,4 @@ kotlin.daemon.jvmargs=-Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:SoftRefLRUPolicyMSP kotlin.code.style=official org.jetbrains.compose.experimental.jscanvas.enabled=true -nl.jacobras.humanreadable.version=2.0.0 \ No newline at end of file +nl.jacobras.humanreadable.version=2.0.0-alpha01 \ No newline at end of file From 3d5b79c5982feba98e05427e2f9cba70680aa2d3 Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 15:29:54 +0200 Subject: [PATCH 55/57] Move `Simple` marker interface down --- .../kotlin/nl/jacobras/humanreadable/time/Rounding.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt index 32077de..4b77255 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/Rounding.kt @@ -7,11 +7,6 @@ package nl.jacobras.humanreadable.time */ public sealed interface Rounding { - /** - * Either [Floor] or [HalfUp]. Marker interface to use with [IfClose]. - */ - public sealed interface Simple : Rounding - /** * Rounds down. * @@ -32,6 +27,11 @@ public sealed interface Rounding { */ public data object HalfUp : Simple + /** + * Either [Floor] or [HalfUp]. Marker interface to use with [IfClose]. + */ + public sealed interface Simple : Rounding + /** * Eagerly rounds up to the next unit, or down to the previous, if within [thresholds]. * From 16d6cb08dd016b25d826715c03efc04fb394285e Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 15:33:17 +0200 Subject: [PATCH 56/57] Add missing kdoc --- .../kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt index bf9188c..ef016a3 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/time/TimeUnit.kt @@ -12,12 +12,15 @@ import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +/** + * All time units the library can format, from [Seconds] to [Years]. + */ public enum class TimeUnit( internal val calculateValue: (Duration, Rounding) -> Int, internal val valueToDuration: (Int) -> Duration, internal val longForms: (DateTimeStrings) -> TenseForms, internal val shortForms: (DateTimeStrings) -> TenseForms, - internal val narrowForms: (DateTimeStrings) -> TenseForms, + internal val narrowForms: (DateTimeStrings) -> TenseForms ) { Seconds( calculateValue = { duration, rounding -> duration.inWholeSeconds.toFloat().round(rounding) }, From a075bcce0a08af1897310dc6581c0cce1aaf49ef Mon Sep 17 00:00:00 2001 From: jacobras Date: Thu, 9 Jul 2026 15:35:48 +0200 Subject: [PATCH 57/57] Simplify language tag examples in kdoc --- src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt | 4 ++-- .../kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt index bf08b3c..2185e66 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/Config.kt @@ -8,7 +8,7 @@ import nl.jacobras.humanreadable.time.TimeUnit public class Config { /** - * The language tag (e.g. `"en"`, `"fr"`) to use when formatting. Defaults to the detected system + * The language tag (e.g. `en`, `fr`) to use when formatting. Defaults to the detected system * language when it is supported, otherwise [fallbackLanguageTag]. */ public var languageTag: String @@ -18,7 +18,7 @@ public class Config { } /** - * The fallback language tag (e.g. `"en"`, `"fr"`) to use when [languageTag] is not supported. + * The fallback language tag (e.g. `en`, `fr`) to use when [languageTag] is not supported. * If an unsupported language is passed in, nothing changes. * * Defaults to English. diff --git a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt index 03fd00e..4910989 100644 --- a/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt +++ b/src/commonMain/kotlin/nl/jacobras/humanreadable/i18n/Localisation.kt @@ -8,7 +8,7 @@ internal class Localisation { private var currentTagAndStrings: Pair? = null /** - * The requested language tag (e.g. `"en"`, `"fr"`). A language tag with a region, e.g. `"en-US"`, may also + * The requested language tag (e.g. `en`, `fr`). A language tag with a region, e.g. `en-US`, may also * be passed in. The region code is ignored. * * @see languageTag for the actual language tag used, which may differ if the requested one is not supported. @@ -33,7 +33,7 @@ internal class Localisation { } /** - * The currently used language tag (e.g. `"en"`, `"fr"`). + * The currently used language tag (e.g. `en`, `fr`). */ val languageTag: String get() {