Repository: kevincianfarini/alchemist Branch: trunk Commit: 9f0101a7c146 Files: 71 Total size: 290.8 KB Directory structure: gitextract_1pez5g1s/ ├── .github/ │ ├── .java-version │ ├── renovate.json5 │ └── workflows/ │ ├── gradle-wrapper.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── LICENSE.txt ├── ModuleDocumentation.md ├── README.md ├── build.gradle.kts ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src/ ├── commonMain/ │ └── kotlin/ │ └── io/ │ └── github/ │ └── kevincianfarini/ │ └── alchemist/ │ ├── internal/ │ │ ├── SaturatingLong.kt │ │ ├── double.kt │ │ ├── duration.kt │ │ └── exception.kt │ ├── scalar/ │ │ ├── acceleration.kt │ │ ├── area.kt │ │ ├── energy.kt │ │ ├── force.kt │ │ ├── length.kt │ │ ├── mass.kt │ │ ├── power.kt │ │ ├── temperature.kt │ │ ├── velocity.kt │ │ └── volume.kt │ ├── type/ │ │ ├── Acceleration.kt │ │ ├── Area.kt │ │ ├── Energy.kt │ │ ├── Force.kt │ │ ├── Length.kt │ │ ├── Mass.kt │ │ ├── Power.kt │ │ ├── Temperature.kt │ │ ├── Velocity.kt │ │ ├── Volume.kt │ │ └── duration.kt │ └── unit/ │ ├── AreaUnit.kt │ ├── EnergyUnit.kt │ ├── ForceUnit.kt │ ├── LengthUnit.kt │ ├── MassUnit.kt │ ├── PowerUnit.kt │ ├── TemperatureUnit.kt │ └── VolumeUnit.kt ├── commonTest/ │ └── kotlin/ │ └── io/ │ └── github/ │ └── kevincianfarini/ │ └── alchemist/ │ ├── Long.kt │ ├── ignore.kt │ ├── internal/ │ │ ├── DoubleTest.kt │ │ └── SaturatingLongTest.kt │ └── type/ │ ├── AccelerationTest.kt │ ├── AreaTest.kt │ ├── EnergyTest.kt │ ├── ForceTest.kt │ ├── LengthTest.kt │ ├── PowerTest.kt │ ├── TemperatureTest.kt │ ├── VelocityTest.kt │ └── VolumeTest.kt ├── jsMain/ │ └── kotlin/ │ └── io/ │ └── github/ │ └── kevincianfarini/ │ └── alchemist/ │ └── internal/ │ └── double.js.kt ├── jsTest/ │ └── kotlin/ │ └── io/ │ └── github/ │ └── kevincianfarini/ │ └── alchemist/ │ └── ignore.js.kt ├── jvmMain/ │ └── kotlin/ │ └── io/ │ └── github/ │ └── kevincianfarini/ │ └── alchemist/ │ └── internal/ │ └── double.jvm.kt ├── nativeMain/ │ └── kotlin/ │ └── io/ │ └── github/ │ └── kevincianfarini/ │ └── alchemist/ │ └── internal/ │ └── double.native.kt ├── wasmJsMain/ │ └── kotlin/ │ └── io/ │ └── github/ │ └── kevincianfarini/ │ └── alchemist/ │ └── internal/ │ └── double.wasmjs.kt ├── wasmJsTest/ │ └── kotlin/ │ └── io/ │ └── github/ │ └── kevincianfarini/ │ └── alchemist/ │ └── ignore.wasmjs.kt ├── wasmWasiMain/ │ └── kotlin/ │ └── io/ │ └── github/ │ └── kevincianfarini/ │ └── alchemist/ │ └── internal/ │ └── double.wasmwasi.kt └── wasmWasiTest/ └── kotlin/ └── io/ └── github/ └── kevincianfarini/ └── alchemist/ └── ignore.wasmwasi.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/.java-version ================================================ 24 ================================================ FILE: .github/renovate.json5 ================================================ { $schema: 'https://docs.renovatebot.com/renovate-schema.json', extends: [ 'config:recommended', ], reviewers: ['kevincianfarini'], ignorePresets: [ // Ensure we get the latest version and are not pinned to old versions. 'workarounds:javaLTSVersions', ], customManagers: [ // Update .java-version file with the latest JDK version. { customType: 'regex', fileMatch: [ '\\.java-version$', ], matchStrings: [ '(?.*)\\n', ], datasourceTemplate: 'java-version', depNameTemplate: 'java', // Only write the major version. extractVersionTemplate: '^(?\\d+)', }, ], } ================================================ FILE: .github/workflows/gradle-wrapper.yaml ================================================ name: gradle-wrapper on: pull_request: paths: - 'gradlew' - 'gradlew.bat' - 'gradle/wrapper/**' jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: gradle/wrapper-validation-action@v3 ================================================ FILE: .github/workflows/release.yaml ================================================ name: release on: push: branches: - 'trunk' env: GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" jobs: publish: runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: 'zulu' java-version-file: .github/.java-version - name: Build and publish artifacts env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGINMEMORYKEY }} run: ./gradlew dokkaGenerate publish - name: Deploy docs to website uses: JamesIves/github-pages-deploy-action@releases/v3 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: site FOLDER: build/dokka/html TARGET_FOLDER: docs/0.x/ CLEAN: true ================================================ FILE: .github/workflows/test.yaml ================================================ name: test on: pull_request: {} push: branches: - 'trunk' env: GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" jobs: test: strategy: matrix: os: [macOS-latest, windows-latest, ubuntu-latest] include: - os: macOS-latest job: iosSimulatorArm64Test iosX64Test macosArm64Test macosX64Test tvosSimulatorArm64Test tvosX64Test watchosSimulatorArm64Test watchosX64Test - os: windows-latest job: mingwX64Test - os: ubuntu-latest job: jsTest jvmTest linuxX64Test wasmJsTest wasmWasiTest runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: 'zulu' java-version-file: .github/.java-version - run: ./gradlew ${{matrix.job}} kmp-missing-targets: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: 'zulu' java-version-file: .github/.java-version - run: ./gradlew kmpMissingTargets ================================================ FILE: .gitignore ================================================ .idea .gradle **/build local.properties .kotlin ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: ModuleDocumentation.md ================================================ # Module alchemist Alchemist allows you to manage physical quanities defined in the [International System of Units](https://en.wikipedia.org/wiki/International_System_of_Units). Like [Duration](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.time/-duration/), alchemist models its quanities as a value class with a single underlying `Long` value. ### Type Safety Alchemist's main goal is to provide type safety for arithmetic on physical quanities. There's no need to pass loosely typed `Long`, `Int`, or `Double` values around anymore! ```kt val energy1 = 10.wattHours val energy2 = 10.millijoules println(energy1 + energy2) // OK: both are type Energy. val power = 10.watts println(energy1 + power) // Compiler Error! ``` ### Scalar Arithmetic Physical quantities expose scalar arithmetic like the following: ```kt val power = 10.watts println(power / 2) // "5W" println(power * 100) // "1kW" println(-power) // "-10W" ``` and each quantity exposes typed arithmetic: ```kt val first = 10.wattHours val second = 2.wattHours println(first + second) // "12Wh" println(first - second) // "8Wh" println(second - first) // "-8Wh" println(first / second) // "5.0" ``` ### Physical Arithmetic Physical quantities expose valid physical arithmetic defined in the International System of Units. For example: ```kt val energy = 10.wattHours val power = 10.watts val duration: Duration = energy / power println(duration) // "1h" val length = 10.nanometers val force: Force = energy / length println(force) // "3.6TN" val velocity = length / 1.seconds println(velocity) // 10 nm/s val acceleration = velocity / 1.seconds println(acceleration) // 10 nm/s² ``` ### Integration with the Kotlin Standard Library Kotlin provides a measure of time, called [`Duration`](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.time/-duration/), in its standard library. Alchemist integrates with Duration for physical arithmetic rather than supplying its own implementation of time. ```kt val time: kotlin.time.Duration = 1.hours val energy = time * 1.watts println(energy) // "1Wh" println(energy / time) // "1W" val velocity = 1.meters / time println(velocity) // "1 m/s" println(velocity * time) // "1m" val acceleration = velocity / time println(velocity) // "1 m/s² println(acceleration * time) // "1 m/s" ``` ### Custom Quantity Unit Implementation Every unit alchemist exposes is an interface, thus allowing you to implement non-standard or uncommonly used units: ```kt // Alchemist does not provide horsepower out of the box. object HorsePower : PowerUnit { override val symbol: String get() = "hp" override val microwattScale: Long get() = 745_699_871 } val Int.horsepower: Power get() = this.toPower(HorsePower) println(1.horsepower) // "745.7W" ``` Note that alchemist does not provide certain units out of the box because they're impossible to represent without losing precision. In the above example, 1 horsepower is actually 745,699,871.58μW. ### Infinity While Long values can store huge numbers, sometimes they're not enough for the operations you're attempting to perform. Rather than silently over or underflowing during an arithmetic operation, Alchemist will clamp your quantities to a special infinite value. These special infinite values have some restrictions. ```kt // This would overflow. val infiniteEnergy = 100.joules * Long.MAX_VALUE println(infiniteEnergy) // "Infinity" // Infinite values don't behave like other values. println(infiniteEnergy / Long.MAX_VALUE) // "Infinity" println(infiniteEnergy * 10) // "Infinity" println(-infiniteEnergy) // "-Infinity" // Some operations are invalid with infinite values. println(infiniteEnergy / infiniteEnergy) // Oops! This throws IllegalArgumentException. println(infiniteEnergy * -infiniteEnergy) // This throws, too. ``` ### We're alchemists, not physicists! Alchemist is not built with physics engines in mind. Instead, Alchemist aims to provide type safety and reasonable levels of precision that are sufficient for most business use cases. For example, Alchemist aims to help you model how much energy an entire country used for a year with a reasonable level of precision, but it won't help you model how much energy the Sun produces in a day. Additionally, Alchemist values type safety over modeling arbitrary quantities derived from SI base units. Introducing that functionality involves too many compromises and would be better handled by a different library with different goals. If you need to model things like how much energy your air conditioning used in a year, or how far your compact electric vehicle charged with the electricity from your solar panels can drive, great! Alchemist might be a good fit for you. We hope you like it. ================================================ FILE: README.md ================================================ # Alchemist Manage physical units. Inspired by kotlin.time.Duration. Alchemist allow type safe arithmetic between different physical quantities defined in the [International System of Units](https://en.wikipedia.org/wiki/International_System_of_Units#). ```kt val time: Duration = 10.seconds val length: Length = 10.kilometers val velocity: Velocity = length / time val acceleration: Acceleration = velocity / time val mass: Mass = 10.kilograms val force: Force = acceleration * mass val energy: Energy = force * length val power: Power = energy / time val area: Area = length * length val volume: Volume = length * length * length ``` ## Download ```toml [versions] alchemist = "0.2.0" [libraries] alchemist = { module = "io.github.kevincianfarini.alchemist:alchemist", version.ref = "alchemist" } ``` ## Alchemist's Goals 1. Model physical quantities as Kotlin value classes which wrap a single `Long` value. 2. Provide logical arithmetic between different physical quantities, like `power = energy / time`. 3. Allow for the implementation of custom units on physical quanities that Alchemist does not provide, such as horsepower as a unit of `Power` or Rankine degrees for `Temperature`. 4. (In the future) Allow different order of magnitude precision and wider ranges of valid values. 5. Easy extensibility for custom formulas, such as `energyₖ = ½ * mass * velocity²`. ## Alchemist's Non-Goals 1. Using generic or floating point values as the underlying storage mechanism. 2. Representing arbitrary formulaic expressions. 3. Providing as many formulaic conversions as possible out of the box, such as `energyₖ = ½ * mass * velocity²`. 4. Infinitely precise values. 5. Infinitely large ranges of valid values. ## Platform Support | Platform | Compiled | Tested in CI | |----------------------|----------|----------------------------------------------------------------------------------------------------| | androidNativeArm32 | ✅ | ❌ | | androidNativeArm64 | ✅ | ❌ | | androidNativeX64 | ✅ | ❌ | | androidNativeX86 | ✅ | ❌ | | iosArm64 | ✅ | ❌ | | iosSimulatorArm64 | ✅ | ✅ | | iosX64 | ✅ | ✅ | | js | ✅ | ✅ | | jvm | ✅ | ✅ | | linuxArm64 | ✅ | ❌ (Prohibited by [Tier 2](https://kotlinlang.org/docs/native-target-support.html#tier-2) support.) | | linuxX64 | ✅ | ✅ | | macosArm64 | ✅ | ✅ | | macosX64 | ✅ | ✅ | | mingwX64 | ✅ | ✅ | | tvosArm64 | ✅ | ❌ | | tvosSimulatorArm64 | ✅ | ✅ | | tvosX64 | ✅ | ✅ | | wasmJs | ✅ | ✅ | | wasmWasi | ✅ | ⚠️ (Some tests don't run because of [KT-60964](https://youtrack.jetbrains.com/issue/KT-60964).) | | watchosArm32 | ✅ | ❌ | | watchosArm64 | ✅ | ❌ | | watchosDeviceArm64 | ✅ | ❌ | | watchosSimuatorArm64 | ✅ | ✅ | | watchosX64 | ✅ | ✅ | ================================================ FILE: build.gradle.kts ================================================ import com.vanniktech.maven.publish.SonatypeHost import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.dokka) alias(libs.plugins.kmpmt) alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.publish) } @OptIn(ExperimentalWasmDsl::class) kotlin { explicitApi() androidNativeArm32() androidNativeArm64() androidNativeX64() androidNativeX86() iosArm64() iosSimulatorArm64() iosX64() js { nodejs { testTask { useMocha { timeout = "5s" } } } } jvm { compilations.configureEach { compilerOptions.options.apply { jvmTarget = JvmTarget.JVM_1_8 freeCompilerArgs.add("-Xjvm-default=all") } } } linuxArm64() linuxX64() macosArm64() macosX64() mingwX64() tvosArm64() tvosSimulatorArm64() tvosX64() wasmJs { nodejs { testTask { useMocha { timeout = "5s" } } } } wasmWasi { nodejs() } watchosArm32() watchosArm64() watchosDeviceArm64() watchosSimulatorArm64() watchosX64() sourceSets { configureEach { if (name.endsWith("Test")) { compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") } } } commonTest.dependencies { implementation(libs.kotlin.test.core) } } } tasks.withType { testLogging { exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL showStandardStreams = true showStackTraces = true } } mavenPublishing { publishToMavenCentral(host = SonatypeHost.S01, automaticRelease = true) signAllPublications() } dokka { val versionName = project.findProperty("VERSION_NAME") as String val version = if (versionName.contains("-SNAPSHOT")) "trunk" else versionName dokkaSourceSets.commonMain { includes.from("ModuleDocumentation.md") sourceLink { localDirectory.set(projectDir.resolve("src")) remoteUrl("https://github.com/kevincianfarini/alchemist/tree/${version}/src") remoteLineSuffix.set("#L") } } } ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] dokka = "2.0.0" kmpmt = "0.1.1" kotlin = "2.1.21" publish = "0.30.0" [libraries] kotlin-test-core = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } [plugins] dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } kmpmt = { id = "com.jakewharton.kmp-missing-targets", version.ref = "kmpmt" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } publish = { id = "com.vanniktech.maven.publish", version.ref = "publish" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ kotlin.code.style=official GROUP=io.github.kevincianfarini.alchemist POM_ARTIFACT_ID=alchemist VERSION_NAME=0.2.1-SNAPSHOT POM_NAME=Alchemist POM_DESCRIPTION=Type safe management and arithmetic of physical units. Inspired by kotlin.time.Duration. POM_INCEPTION_YEAR=2024 POM_URL=https://github.com/kevincianfarini/alchemist POM_SCM_URL=https://github.com/kevincianfarini/alchemist POM_SCM_CONNECTION=scm:git:git://github.com/kevincianfarini/alchemist.git POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/kevincianfarini/alchemist.git POM_LICENCE_NAME=The Apache Software License, Version 2.0 POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt POM_LICENCE_DIST=repo POM_DEVELOPER_ID=kevincianfarini POM_DEVELOPER_NAME=Kevin Cianfarini POM_DEVELOPER_URL=https://github.com/kevincianfarini SONATYPE_CONNECT_TIMEOUT_SECONDS=120 SONATYPE_CLOSE_TIMEOUT_SECONDS=900 org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH= @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle.kts ================================================ rootProject.name = "alchemist" pluginManagement { repositories { mavenCentral() } } dependencyResolutionManagement { repositories { mavenCentral() } } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/internal/SaturatingLong.kt ================================================ @file:Suppress("NOTHING_TO_INLINE") package io.github.kevincianfarini.alchemist.internal import kotlin.jvm.JvmInline import kotlin.math.absoluteValue import kotlin.math.roundToLong import kotlin.math.sign /** * A [SaturatingLong] is a 64-bit signed integer that protects against overflow during multiplication, addition, and * subtraction. When one of those operations overflows, either [POSITIVE_INFINITY] or [NEGATIVE_INFINITY] is returned. * Performing operations with an infinite value will either return another infinite value if it's a valid operation, * or will error if it's an invalid operation. * * See [Saturation Arithmetic Intrinsics](https://llvm.org/docs/LangRef.html#saturation-arithmetic-intrinsics) from * LLVM for more details. */ @JvmInline internal value class SaturatingLong(val rawValue: Long) { operator fun plus(other: SaturatingLong): SaturatingLong = when { isInfinite() -> when { this == other || other.isFinite() -> this else -> { throwIllegalArgumentException("Summing infinite values of different signs yields an undefined result.") } } else -> { val a = rawValue val b = other.rawValue val result = rawValue + other.rawValue val didOverflow = (((a and b and result.inv()) or (a.inv() and b.inv() and result)) < 0L) when { didOverflow -> if (result > 0) NEGATIVE_INFINITY else POSITIVE_INFINITY else -> SaturatingLong(result) } } } inline operator fun plus(other: Long): SaturatingLong { return this + SaturatingLong(other) } inline operator fun minus(other: SaturatingLong): SaturatingLong = this + (-other) inline operator fun minus(other: Long): SaturatingLong { return this - SaturatingLong(other) } operator fun unaryMinus(): SaturatingLong = when(this) { POSITIVE_INFINITY -> NEGATIVE_INFINITY NEGATIVE_INFINITY -> POSITIVE_INFINITY else -> SaturatingLong(-rawValue) } operator fun times(other: SaturatingLong): SaturatingLong { return when { isInfinite() or other.isInfinite() -> when { rawValue == 0L || other.rawValue == 0L -> { throwIllegalArgumentException("Multiplying an infinite value by zero yields an undefined result.") } rawValue.sign == other.rawValue.sign -> POSITIVE_INFINITY else -> NEGATIVE_INFINITY } else -> { val result = rawValue * other.rawValue val doesOverflow = rawValue != 0L && result / rawValue != other.rawValue when { doesOverflow && rawValue.sign == other.rawValue.sign -> POSITIVE_INFINITY doesOverflow -> NEGATIVE_INFINITY else -> SaturatingLong(result) } } } } val sign: Int inline get() = rawValue.sign inline operator fun times(other: Long): SaturatingLong { return this * SaturatingLong(other) } inline operator fun times(other: Int): SaturatingLong { return times(other.toLong()) } inline operator fun times(other: Double): SaturatingLong { val longScale = other.roundToLong() if (longScale.toDouble() == other) { return times(longScale) } else { val thisDouble = toDouble() val result = thisDouble * other return SaturatingLong(result.roundToLong()) } } operator fun div(other: SaturatingLong): SaturatingLong { val thisInfinite = isInfinite() val otherInfinite = other.isInfinite() return when { thisInfinite && otherInfinite -> { throwIllegalArgumentException("Dividing two infinite values yields an undefined result.") } thisInfinite -> this * other.sign otherInfinite -> SaturatingLong(0) else -> SaturatingLong(rawValue / other.rawValue) } } operator fun div(other: Long): SaturatingLong { return this / SaturatingLong(other) } operator fun div(other: Int): SaturatingLong { return div(other.toLong()) } operator fun div(other: Double): SaturatingLong { val longScale = other.roundToLong() if (longScale.toDouble() == other) { return div(longScale) } else { val thisDouble = toDouble() val result = thisDouble / other return SaturatingLong(result.roundToLong()) } } operator fun rem(other: SaturatingLong): SaturatingLong { val thisInfinite = isInfinite() val otherInfinite = other.isInfinite() return when { thisInfinite && otherInfinite -> { throwIllegalArgumentException("Dividing two infinite values yields an undefined result.") } thisInfinite -> this * other.sign otherInfinite -> SaturatingLong(0) else -> SaturatingLong(rawValue % other.rawValue) } } operator fun rem(other: Long): SaturatingLong { return this % SaturatingLong(other) } operator fun compareTo(other: SaturatingLong): Int { return rawValue.compareTo(other.rawValue) } operator fun compareTo(other: Long): Int { return compareTo(SaturatingLong(other)) } operator fun compareTo(other: Int): Int { return compareTo(SaturatingLong(other.toLong())) } override fun toString(): String = when (this) { POSITIVE_INFINITY -> "Infinity" NEGATIVE_INFINITY -> "-Infinity" else -> rawValue.toString() } inline fun isInfinite(): Boolean { return (this == POSITIVE_INFINITY) or (this == NEGATIVE_INFINITY) } inline fun isFinite(): Boolean = !isInfinite() val absoluteValue: SaturatingLong get() = when { isInfinite() -> POSITIVE_INFINITY else -> SaturatingLong(rawValue.absoluteValue) } fun toDouble(): Double = when (this) { POSITIVE_INFINITY -> Double.POSITIVE_INFINITY NEGATIVE_INFINITY -> Double.NEGATIVE_INFINITY else -> rawValue.toDouble() } } internal inline val Long.saturated get() = SaturatingLong(this) // Inline the constants versus using `MIN_VALUE` and `MAX_VALUE` to avoid accessing Long's companion. internal inline val POSITIVE_INFINITY get() = SaturatingLong(9223372036854775807L) internal inline val NEGATIVE_INFINITY get() = SaturatingLong(-9223372036854775807L - 1L) ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/internal/double.kt ================================================ package io.github.kevincianfarini.alchemist.internal /** * Returns a decimal string that has been rounded to the specified number of [decimals]. */ internal expect fun Double.toDecimalString(decimals: Int): String ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/internal/duration.kt ================================================ package io.github.kevincianfarini.alchemist.internal import io.github.kevincianfarini.alchemist.type.Energy import kotlin.time.Duration import kotlin.time.Duration.Companion.microseconds import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit internal fun Duration.isPreciseToNanosecond(): Boolean { return this == inWholeNanoseconds.nanoseconds } internal val Duration.sign: Int get() = when { isPositive() -> 1 isNegative() -> -1 else -> 0 } internal val DurationUnit.shortName: String get() = when (this) { DurationUnit.NANOSECONDS -> "ns" DurationUnit.MICROSECONDS -> "us" DurationUnit.MILLISECONDS -> "ms" DurationUnit.SECONDS -> "s" DurationUnit.MINUTES -> "m" DurationUnit.HOURS -> "h" DurationUnit.DAYS -> "d" else -> error("Unknown unit: $this") } internal val DurationUnit.shortNameSquared: String get() = when (this) { DurationUnit.NANOSECONDS -> "ns²" DurationUnit.MICROSECONDS -> "us²" DurationUnit.MILLISECONDS -> "ms²" DurationUnit.SECONDS -> "s²" DurationUnit.MINUTES -> "m²" DurationUnit.HOURS -> "h²" DurationUnit.DAYS -> "d²" else -> error("Unknown unit: $this") } internal val DurationUnit.secondScale: Double get() = when (this) { DurationUnit.NANOSECONDS -> 0.000_000_001 DurationUnit.MICROSECONDS -> 0.000_001 DurationUnit.MILLISECONDS -> 0.001 DurationUnit.SECONDS -> 1.0 DurationUnit.MINUTES -> 60.0 DurationUnit.HOURS -> 3_600.0 DurationUnit.DAYS -> 86_400.0 else -> error("Unknown unit: $this") } internal fun Duration.toDecimalComponents( action: ( kiloseconds: Long, seconds: Long, millis: Long, micros: Long, nanos: Long ) -> Energy, ): Energy { val seconds = inWholeSeconds val secondsRemainder = this - seconds.seconds val millis = secondsRemainder.inWholeMilliseconds val millisRemainder = secondsRemainder - millis.milliseconds val micros = millisRemainder.inWholeMicroseconds val nanos = (millisRemainder - micros.microseconds).inWholeNanoseconds return action(seconds / 1_000, seconds % 1_000, millis, micros, nanos) } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/internal/exception.kt ================================================ package io.github.kevincianfarini.alchemist.internal /** * Ensures that we don't inline the exception throwing instructions and instead make the jump * to this function. This helps avoid busting the instruction cache and localizes exception throwing * instructions to one place. * * TODO: Supply a ProGuard rule to keep this method so it's not inlined */ internal fun throwIllegalArgumentException(message: String): Nothing { throw IllegalArgumentException(message) } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/scalar/acceleration.kt ================================================ package io.github.kevincianfarini.alchemist.scalar import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.type.Acceleration internal val Int.nmPerSecond2: Acceleration get() = toLong().nmPerSecond2 internal val Long.nmPerSecond2: Acceleration get() = Acceleration(saturated) internal val SaturatingLong.nmPerSecond2: Acceleration get() = Acceleration(this) ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/scalar/area.kt ================================================ package io.github.kevincianfarini.alchemist.scalar import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.type.Area import io.github.kevincianfarini.alchemist.unit.AreaUnit import kotlin.math.roundToLong /** * Returns an [Area] equal to [Int] number of decimilliares. */ public inline val Int.decimilliares: Area get() = toArea(AreaUnit.Metric.Decimilliare) /** * Returns an [Area] equal to [Long] number of decimilliares. */ public inline val Long.decimilliares: Area get() = toArea(AreaUnit.Metric.Decimilliare) /** * Returns an [Area] equal to [Double] number of decimilliares. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.decimilliares: Area get() = toArea(AreaUnit.Metric.Decimilliare) /** * Returns an [Area] equal to [Int] number of centiares. */ public inline val Int.centiares: Area get() = toArea(AreaUnit.Metric.Centiare) /** * Returns an [Area] equal to [Long] number of centiares. */ public inline val Long.centiares: Area get() = toArea(AreaUnit.Metric.Centiare) /** * Returns an [Area] equal to [Double] number of centiares. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.centiares: Area get() = toArea(AreaUnit.Metric.Centiare) /** * Returns an [Area] equal to [Int] number of deciares. */ public inline val Int.deciares: Area get() = toArea(AreaUnit.Metric.Deciare) /** * Returns an [Area] equal to [Long] number of deciares. */ public inline val Long.deciares: Area get() = toArea(AreaUnit.Metric.Deciare) /** * Returns an [Area] equal to [Double] number of deciares. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.deciares: Area get() = toArea(AreaUnit.Metric.Deciare) /** * Returns an [Area] equal to [Int] number of ares. */ public inline val Int.ares: Area get() = toArea(AreaUnit.Metric.Are) /** * Returns an [Area] equal to [Long] number of ares. */ public inline val Long.ares: Area get() = toArea(AreaUnit.Metric.Are) /** * Returns an [Area] equal to [Double] number of ares. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.ares: Area get() = toArea(AreaUnit.Metric.Are) /** * Returns an [Area] equal to [Int] number of decares. */ public inline val Int.decares: Area get() = toArea(AreaUnit.Metric.Decare) /** * Returns an [Area] equal to [Long] number of decares. */ public inline val Long.decares: Area get() = toArea(AreaUnit.Metric.Decare) /** * Returns an [Area] equal to [Double] number of decares. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.decares: Area get() = toArea(AreaUnit.Metric.Decare) /** * Returns an [Area] equal to [Int] number of hectares. */ public inline val Int.hectares: Area get() = toArea(AreaUnit.Metric.Hectare) /** * Returns an [Area] equal to [Long] number of hectares. */ public inline val Long.hectares: Area get() = toArea(AreaUnit.Metric.Hectare) /** * Returns an [Area] equal to [Double] number of hectares. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.hectares: Area get() = toArea(AreaUnit.Metric.Hectare) /** * Returns an [Area] equal to [Int] number of the specified [unit]. */ public fun Int.toArea(unit: AreaUnit): Area = toLong().toArea(unit) /** * Returns an [Area] equal to [Long] number of the specified [unit]. */ public fun Long.toArea(unit: AreaUnit): Area = Area(saturated * unit.millimetersSquaredScale) /** * Returns an [Area] equal to [Double] number of the specified [unit]. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public fun Double.toArea(unit: AreaUnit): Area { val valueInMillimeters2 = this * unit.millimetersSquaredScale require(!valueInMillimeters2.isNaN()) { "Area value cannot be NaN." } return Area(valueInMillimeters2.roundToLong().saturated) } internal inline val Long.mm2: Area get() = Area(saturated) internal inline val Int.mm2: Area get() = Area(toLong().saturated) internal inline val SaturatingLong.mm2: Area get() = Area(this) ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/scalar/energy.kt ================================================ package io.github.kevincianfarini.alchemist.scalar import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.type.Energy import io.github.kevincianfarini.alchemist.unit.EnergyUnit import kotlin.math.roundToLong /** * Returns an [Energy] equal to [Int] number of millijoules. */ public inline val Int.millijoules: Energy get() = toEnergy(EnergyUnit.International.Millijoule) /** * Returns an [Energy] equal to [Long] number of millijoules. */ public inline val Long.millijoules: Energy get() = toEnergy(EnergyUnit.International.Millijoule) internal inline val SaturatingLong.millijoules get() = Energy(this) /** * Returns an [Energy] equal to [Int] number of joules. */ public inline val Int.joules: Energy get() = toEnergy(EnergyUnit.International.Joule) /** * Returns an [Energy] equal to [Long] number of joules. */ public inline val Long.joules: Energy get() = toEnergy(EnergyUnit.International.Joule) /** * Returns an [Energy] equal to [Double] number of joules. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.joules: Energy get() = toEnergy(EnergyUnit.International.Joule) /** * Returns an [Energy] equal to [Int] number of kilojoules. */ public inline val Int.kilojoules: Energy get() = toEnergy(EnergyUnit.International.Kilojoule) /** * Returns an [Energy] equal to [Long] number of kilojoules. */ public inline val Long.kilojoules: Energy get() = toEnergy(EnergyUnit.International.Kilojoule) /** * Returns an [Energy] equal to [Double] number of kilojoules. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.kilojoules: Energy get() = toEnergy(EnergyUnit.International.Kilojoule) /** * Returns an [Energy] equal to [Int] number of megajoules. */ public inline val Int.megajoules: Energy get() = toEnergy(EnergyUnit.International.Megajoule) /** * Returns an [Energy] equal to [Long] number of megajoules. */ public inline val Long.megajoules: Energy get() = toEnergy(EnergyUnit.International.Megajoule) /** * Returns an [Energy] equal to [Double] number of megajoules. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.megajoules: Energy get() = toEnergy(EnergyUnit.International.Megajoule) /** * Returns an [Energy] equal to [Int] number of gigajoules. */ public inline val Int.gigajoules: Energy get() = toEnergy(EnergyUnit.International.Gigajoule) /** * Returns an [Energy] equal to [Long] number of gigajoules. */ public inline val Long.gigajoules: Energy get() = toEnergy(EnergyUnit.International.Gigajoule) /** * Returns an [Energy] equal to [Double] number of gigajoules. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.gigajoules: Energy get() = toEnergy(EnergyUnit.International.Gigajoule) /** * Returns an [Energy] equal to [Int] number of tetrajoules. */ public inline val Int.tetrajoules: Energy get() = toEnergy(EnergyUnit.International.Tetrajoule) /** * Returns an [Energy] equal to [Long] number of tetrajoules. */ public inline val Long.tetrajoules: Energy get() = toEnergy(EnergyUnit.International.Tetrajoule) /** * Returns an [Energy] equal to [Double] number of tetrajoules. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.tetrajoules: Energy get() = toEnergy(EnergyUnit.International.Tetrajoule) /** * Returns an [Energy] equal to [Int] number of petajoules. */ public inline val Int.petajoules: Energy get() = toEnergy(EnergyUnit.International.Petajoule) /** * Returns an [Energy] equal to [Long] number of petajoules. */ public inline val Long.petajoules: Energy get() = toEnergy(EnergyUnit.International.Petajoule) /** * Returns an [Energy] equal to [Double] number of petajoules. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.petajoules: Energy get() = toEnergy(EnergyUnit.International.Petajoule) /** * Returns an [Energy] equal to [Int] number of milliwatt-hours. */ public inline val Int.milliwattHours: Energy get() = toEnergy(EnergyUnit.Electricity.MilliwattHour) /** * Returns an [Energy] equal to [Long] number of milliwatt-hours. */ public inline val Long.milliwattHours: Energy get() = toEnergy(EnergyUnit.Electricity.MilliwattHour) /** * Returns an [Energy] equal to [Double] number of milliwatt-hours. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.milliwattHours: Energy get() = toEnergy(EnergyUnit.Electricity.MilliwattHour) /** * Returns an [Energy] equal to [Int] number of watt-hours. */ public inline val Int.wattHours: Energy get() = toEnergy(EnergyUnit.Electricity.WattHour) /** * Returns an [Energy] equal to [Long] number of watt-hours. */ public inline val Long.wattHours: Energy get() = toEnergy(EnergyUnit.Electricity.WattHour) /** * Returns an [Energy] equal to [Double] number of watt-hours. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.wattHours: Energy get() = toEnergy(EnergyUnit.Electricity.WattHour) /** * Returns an [Energy] equal to [Int] number of kilowatt-hours. */ public inline val Int.kilowattHours: Energy get() = toEnergy(EnergyUnit.Electricity.KilowattHour) /** * Returns an [Energy] equal to [Long] number of kilowatt-hours. */ public inline val Long.kilowattHours: Energy get() = toEnergy(EnergyUnit.Electricity.KilowattHour) /** * Returns an [Energy] equal to [Double] number of kilowatt-hours. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.kilowattHours: Energy get() = toEnergy(EnergyUnit.Electricity.KilowattHour) /** * Returns an [Energy] equal to [Int] number of megawatt-hours. */ public inline val Int.megawattHours: Energy get() = toEnergy(EnergyUnit.Electricity.MegawattHour) /** * Returns an [Energy] equal to [Long] number of megawatt-hours. */ public inline val Long.megawattHours: Energy get() = toEnergy(EnergyUnit.Electricity.MegawattHour) /** * Returns an [Energy] equal to [Double] number of megawatt-hours. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.megawattHours: Energy get() = toEnergy(EnergyUnit.Electricity.MegawattHour) /** * Returns an [Energy] equal to [Int] number of gigawatt-hours. */ public inline val Int.gigawattHours: Energy get() = toEnergy(EnergyUnit.Electricity.GigawattHour) /** * Returns an [Energy] equal to [Long] number of gigawatt-hours. */ public inline val Long.gigawattHours: Energy get() = toEnergy(EnergyUnit.Electricity.GigawattHour) /** * Returns an [Energy] equal to [Double] number of gigawatt-hours. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.gigawattHours: Energy get() = toEnergy(EnergyUnit.Electricity.GigawattHour) /** * Returns an [Energy] equal to [Int] number of terawatt-hours. */ public inline val Int.terawattHours: Energy get() = toEnergy(EnergyUnit.Electricity.TerawattHour) /** * Returns an [Energy] equal to [Int] number of terawatt-hours. */ public inline val Long.terawattHours: Energy get() = toEnergy(EnergyUnit.Electricity.TerawattHour) /** * Returns an [Energy] equal to [Double] number of terawatt-hours. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.terawattHours: Energy get() = toEnergy(EnergyUnit.Electricity.TerawattHour) /** * Returns an [Energy] equal to [Int] number of the specified [unit]. */ public fun Int.toEnergy(unit: EnergyUnit): Energy { return toLong().toEnergy(unit) } /** * Returns an [Energy] equal to [Long] number of the specified [unit]. */ public fun Long.toEnergy(unit: EnergyUnit): Energy { return Energy(this.saturated * unit.millijouleScale) } /** * Returns an [Energy] equal to [Double] number of the specified [unit]. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public fun Double.toEnergy(unit: EnergyUnit): Energy { val valueInMillijoules = this * unit.millijouleScale require(!valueInMillijoules.isNaN()) { "Energy value cannot be NaN." } return Energy(valueInMillijoules.roundToLong().saturated) } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/scalar/force.kt ================================================ package io.github.kevincianfarini.alchemist.scalar import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.type.Force import io.github.kevincianfarini.alchemist.unit.ForceUnit import kotlin.math.roundToLong /** * Returns an [Force] equal to [Int] number of nanonewtons. */ public inline val Int.nanonewtons: Force get() = toForce(ForceUnit.International.Nanonewton) /** * Returns an [Force] equal to [Long] number of nanonewtons. */ public inline val Long.nanonewtons: Force get() = toForce(ForceUnit.International.Nanonewton) /** * Returns an [Force] equal to [Int] number of micronewtons. */ public inline val Int.micronewtons: Force get() = toForce(ForceUnit.International.Micronewton) /** * Returns an [Force] equal to [Long] number of micronewtons. */ public inline val Long.micronewtons: Force get() = toForce(ForceUnit.International.Micronewton) /** * Returns an [Force] equal to [Double] number of micronewtons. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.micronewtons: Force get() = toForce(ForceUnit.International.Micronewton) /** * Returns an [Force] equal to [Int] number of millinewtons. */ public inline val Int.millinewtons: Force get() = toForce(ForceUnit.International.Millinewton) /** * Returns an [Force] equal to [Long] number of millinewtons. */ public inline val Long.millinewtons: Force get() = toForce(ForceUnit.International.Millinewton) /** * Returns an [Force] equal to [Double] number of millinewtons. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.millinewtons: Force get() = toForce(ForceUnit.International.Millinewton) /** * Returns an [Force] equal to [Int] number of newtons. */ public inline val Int.newtons: Force get() = toForce(ForceUnit.International.Newton) /** * Returns an [Force] equal to [Long] number of newtons. */ public inline val Long.newtons: Force get() = toForce(ForceUnit.International.Newton) /** * Returns an [Force] equal to [Double] number of newtons. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.newtons: Force get() = toForce(ForceUnit.International.Newton) /** * Returns an [Force] equal to [Int] number of kilonewtons. */ public inline val Int.kilonewtons: Force get() = toForce(ForceUnit.International.Kilonewton) /** * Returns an [Force] equal to [Long] number of kilonewtons. */ public inline val Long.kilonewtons: Force get() = toForce(ForceUnit.International.Kilonewton) /** * Returns an [Force] equal to [Double] number of kilonewtons. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.kilonewtons: Force get() = toForce(ForceUnit.International.Kilonewton) /** * Returns an [Force] equal to [Int] number of meganewtons. */ public inline val Int.meganewtons: Force get() = toForce(ForceUnit.International.Meganewton) /** * Returns an [Force] equal to [Long] number of meganewtons. */ public inline val Long.meganewtons: Force get() = toForce(ForceUnit.International.Meganewton) /** * Returns an [Force] equal to [Double] number of meganewtons. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.meganewtons: Force get() = toForce(ForceUnit.International.Meganewton) /** * Returns an [Force] equal to [Int] number of giganewtons. */ public inline val Int.giganewtons: Force get() = toForce(ForceUnit.International.Giganewton) /** * Returns an [Force] equal to [Long] number of giganewtons. */ public inline val Long.giganewtons: Force get() = toForce(ForceUnit.International.Giganewton) /** * Returns an [Force] equal to [Double] number of giganewtons. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.giganewtons: Force get() = toForce(ForceUnit.International.Giganewton) /** * Returns an [Force] equal to [Int] number of the specified [unit]. */ public fun Int.toForce(unit: ForceUnit): Force = toLong().toForce(unit) /** * Returns an [Force] equal to [Long] number of the specified [unit]. */ public fun Long.toForce(unit: ForceUnit): Force = Force(saturated * unit.nanonewtonScale) /** * Returns an [Force] equal to [Double] number of the specified [unit]. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public fun Double.toForce(unit: ForceUnit): Force { val valueInNanonewtons = this * unit.nanonewtonScale require(!valueInNanonewtons.isNaN()) { "Force value cannot be NaN." } return Force(valueInNanonewtons.roundToLong().saturated) } internal inline val SaturatingLong.kilonewtons: Force get() = rawValue.toForce(ForceUnit.International.Kilonewton) ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/scalar/length.kt ================================================ package io.github.kevincianfarini.alchemist.scalar import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.type.Length import io.github.kevincianfarini.alchemist.unit.LengthUnit import kotlin.math.roundToLong /** * Returns a [Length] equal to [Int] number of nanometers. */ public inline val Int.nanometers: Length get() = toLength(LengthUnit.International.Nanometer) /** * Returns a [Length] equal to [Long] number of nanometers. */ public inline val Long.nanometers: Length get() = toLength(LengthUnit.International.Nanometer) internal inline val SaturatingLong.nanometers: Length get() = Length(this) /** * Returns a [Length] equal to [Int] number of micrometers. */ public inline val Int.micrometers: Length get() = toLength(LengthUnit.International.Micrometer) /** * Returns a [Length] equal to [Long] number of micrometers. */ public inline val Long.micrometers: Length get() = toLength(LengthUnit.International.Micrometer) /** * Returns a [Length] equal to [Double] number of micrometers. Depending on its magnitude, some precision may be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.micrometers: Length get() = toLength(LengthUnit.International.Micrometer) /** * Returns a [Length] equal to [Int] number of millimeters. */ public inline val Int.millimeters: Length get() = toLength(LengthUnit.International.Millimeter) /** * Returns a [Length] equal to [Long] number of millimeters. */ public inline val Long.millimeters: Length get() = toLength(LengthUnit.International.Millimeter) /** * Returns a [Length] equal to [Double] number of millimeters. Depending on its magnitude, some precision may be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.millimeters: Length get() = toLength(LengthUnit.International.Millimeter) /** * Returns a [Length] equal to [Int] number of centimeters. */ public inline val Int.centimeters: Length get() = toLength(LengthUnit.International.Centimeter) /** * Returns a [Length] equal to [Long] number of centimeters. */ public inline val Long.centimeters: Length get() = toLength(LengthUnit.International.Centimeter) internal inline val SaturatingLong.centimeters: Length get() = rawValue.toLength(LengthUnit.International.Centimeter) /** * Returns a [Length] equal to [Double] number of centimeters. Depending on its magnitude, some precision may be lost. */ public inline val Double.centimeters: Length get() = toLength(LengthUnit.International.Centimeter) /** * Returns a [Length] equal to [Int] number of meters. */ public inline val Int.meters: Length get() = toLength(LengthUnit.International.Meter) /** * Returns a [Length] equal to [Long] number of meters. */ public inline val Long.meters: Length get() = toLength(LengthUnit.International.Meter) /** * Returns a [Length] equal to [Double] number of meters. Depending on its magnitude, some precision may be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.meters: Length get() = toLength(LengthUnit.International.Meter) /** * Returns a [Length] equal to [Int] number of kilometers. */ public inline val Int.kilometers: Length get() = toLength(LengthUnit.International.Kilometer) /** * Returns a [Length] equal to [Long] number of kilometers. */ public inline val Long.kilometers: Length get() = toLength(LengthUnit.International.Kilometer) /** * Returns a [Length] equal to [Double] number of kilometers. Depending on its magnitude, some precision may be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.kilometers: Length get() = toLength(LengthUnit.International.Kilometer) /** * Returns a [Length] equal to [Int] number of megameters. */ public inline val Int.megameters: Length get() = toLength(LengthUnit.International.Megameter) /** * Returns a [Length] equal to [Long] number of megameters. */ public inline val Long.megameters: Length get() = toLength(LengthUnit.International.Megameter) /** * Returns a [Length] equal to [Double] number of megameters. Depending on its magnitude, some precision may be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.megameters: Length get() = toLength(LengthUnit.International.Megameter) /** * Returns a [Length] equal to [Int] number of gigameters. */ public inline val Int.gigameters: Length get() = toLength(LengthUnit.International.Gigameter) /** * Returns a [Length] equal to [Long] number of gigameters. */ public inline val Long.gigameters: Length get() = toLength(LengthUnit.International.Gigameter) /** * Returns a [Length] equal to [Double] number of gigameters. Depending on its magnitude, some precision may be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.gigameters: Length get() = toLength(LengthUnit.International.Gigameter) /** * Returns a [Length] equal to [Int] number of inches. */ public inline val Int.inches: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Inch) /** * Returns a [Length] equal to [Long] number of inches. */ public inline val Long.inches: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Inch) /** * Returns a [Length] equal to [Double] number of inches. Depending on its magnitude, some precision may be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.inches: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Inch) /** * Returns a [Length] equal to [Int] number of feet. */ public inline val Int.feet: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Foot) /** * Returns a [Length] equal to [Long] number of feet. */ public inline val Long.feet: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Foot) /** * Returns a [Length] equal to [Double] number of feet. Depending on its magnitude, some precision may be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.feet: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Foot) /** * Returns a [Length] equal to [Int] number of yards. */ public inline val Int.yards: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Yard) /** * Returns a [Length] equal to [Long] number of yards. */ public inline val Long.yards: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Yard) /** * Returns a [Length] equal to [Double] number of yards. Depending on its magnitude, some precision may be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.yards: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Yard) /** * Returns a [Length] equal to [Int] number of miles. */ public inline val Int.miles: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Mile) /** * Returns a [Length] equal to [Long] number of miles. */ public inline val Long.miles: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Mile) /** * Returns a [Length] equal to [Double] number of miles. Depending on its magnitude, some precision may be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.miles: Length get() = toLength(LengthUnit.UnitedStatesCustomary.Mile) /** * Returns a [Length] equal to [Int] number of the specified [unit]. */ public fun Int.toLength(unit: LengthUnit): Length { return toLong().toLength(unit) } /** * Returns a [Length] equal to [Long] number of the specified [unit]. */ public fun Long.toLength(unit: LengthUnit): Length { return Length(this.saturated * unit.nanometerScale) } /** * Returns a [Length] equal to [Double] number of the specified [unit]. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public fun Double.toLength(unit: LengthUnit): Length { val valueInNanometers = this * unit.nanometerScale require(!valueInNanometers.isNaN()) { "Length value cannot be NaN." } return Length(valueInNanometers.roundToLong().saturated) } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/scalar/mass.kt ================================================ package io.github.kevincianfarini.alchemist.scalar import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.type.Mass import io.github.kevincianfarini.alchemist.unit.MassUnit import kotlin.math.roundToLong /** * Returns a [Mass] equal to [Int] number of micrograms. */ public inline val Int.micrograms: Mass get() = toMass(MassUnit.International.Microgram) /** * Returns a [Mass] equal to [Long] number of micrograms. */ public inline val Long.micrograms: Mass get() = toMass(MassUnit.International.Microgram) /** * Returns a [Mass] equal to [Int] number of milligrams. */ public inline val Int.milligrams: Mass get() = toMass(MassUnit.International.Milligram) /** * Returns a [Mass] equal to [Long] number of milligrams. */ public inline val Long.milligrams: Mass get() = toMass(MassUnit.International.Milligram) /** * Returns a [Mass] equal to [Double] number of milligrams. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.milligrams: Mass get() = toMass(MassUnit.International.Milligram) /** * Returns a [Mass] equal to [Int] number of grams. */ public inline val Int.grams: Mass get() = toMass(MassUnit.International.Gram) /** * Returns a [Mass] equal to [Long] number of grams. */ public inline val Long.grams: Mass get() = toMass(MassUnit.International.Gram) /** * Returns a [Mass] equal to [Double] number of grams. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.grams: Mass get() = toMass(MassUnit.International.Gram) /** * Returns a [Mass] equal to [Int] number of kilograms. */ public inline val Int.kilograms: Mass get() = toMass(MassUnit.International.Kilogram) /** * Returns a [Mass] equal to [Long] number of kilograms. */ public inline val Long.kilograms: Mass get() = toMass(MassUnit.International.Kilogram) /** * Returns a [Mass] equal to [Double] number of kilograms. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.kilograms: Mass get() = toMass(MassUnit.International.Kilogram) /** * Returns a [Mass] equal to [Int] number of megagrams. */ public inline val Int.megagrams: Mass get() = toMass(MassUnit.International.Megagram) /** * Returns a [Mass] equal to [Long] number of megagrams. */ public inline val Long.megagrams: Mass get() = toMass(MassUnit.International.Megagram) /** * Returns a [Mass] equal to [Double] number of megagrams. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.megagrams: Mass get() = toMass(MassUnit.International.Megagram) /** * Returns a [Mass] equal to [Int] number of metric tonnes. */ public inline val Int.metricTonnes: Mass get() = toMass(MassUnit.International.Megagram) /** * Returns a [Mass] equal to [Long] number of metric tonnes. */ public inline val Long.metricTonnes: Mass get() = toMass(MassUnit.International.Megagram) /** * Returns a [Mass] equal to [Double] number of metric tonnes. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.metricTonnes: Mass get() = toMass(MassUnit.International.Megagram) /** * Returns a [Mass] equal to [Int] number of gigagrams. */ public inline val Int.gigagrams: Mass get() = toMass(MassUnit.International.Gigagram) /** * Returns a [Mass] equal to [Long] number of gigagrams. */ public inline val Long.gigagrams: Mass get() = toMass(MassUnit.International.Gigagram) /** * Returns a [Mass] equal to [Double] number of gigagrams. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.gigagrams: Mass get() = toMass(MassUnit.International.Gigagram) /** * Returns a [Mass] equal to [Int] number of teragrams. */ public inline val Int.teragrams: Mass get() = toMass(MassUnit.International.Teragram) /** * Returns a [Mass] equal to [Long] number of teragrams. */ public inline val Long.teragrams: Mass get() = toMass(MassUnit.International.Teragram) /** * Returns a [Mass] equal to [Double] number of teragrams. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.teragrams: Mass get() = toMass(MassUnit.International.Teragram) /** * Returns a [Mass] equal to [Int] number of the specified [unit]. */ public fun Int.toMass(unit: MassUnit): Mass { return toLong().toMass(unit) } /** * Returns a [Mass] equal to [Long] number of the specified [unit]. */ public fun Long.toMass(unit: MassUnit): Mass { return Mass(saturated * unit.microgramScale) } /** * Returns a [Mass] equal to [Double] number of the specified [unit]. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public fun Double.toMass(unit: MassUnit): Mass { val valueInMicrograms = this * unit.microgramScale require(!valueInMicrograms.isNaN()) { "Mass value cannot be NaN." } return Mass(valueInMicrograms.roundToLong().saturated) } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/scalar/power.kt ================================================ package io.github.kevincianfarini.alchemist.scalar import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.type.Power import io.github.kevincianfarini.alchemist.unit.PowerUnit import kotlin.math.roundToLong /** * Returns a [Power] equal to [Int] number of terawatts. */ public inline val Int.terawatts: Power get() = toPower(PowerUnit.International.Terawatt) /** * Returns a [Power] equal to [Long] number of terawatts. */ public inline val Long.terawatts: Power get() = toPower(PowerUnit.International.Terawatt) /** * Returns a [Power] equal to [Double] number of terawatts. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.terawatts: Power get() = toPower(PowerUnit.International.Terawatt) /** * Returns a [Power] equal to [Int] number of gigawatts. */ public inline val Int.gigawatts: Power get() = toPower(PowerUnit.International.Gigawatt) /** * Returns a [Power] equal to [Long] number of gigawatts. */ public inline val Long.gigawatts: Power get() = toPower(PowerUnit.International.Gigawatt) /** * Returns a [Power] equal to [Double] number of gigawatts. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.gigawatts: Power get() = toPower(PowerUnit.International.Gigawatt) /** * Returns a [Power] equal to [Int] number of megawatts. */ public inline val Int.megawatts: Power get() = toPower(PowerUnit.International.Megawatt) /** * Returns a [Power] equal to [Long] number of megawatts. */ public inline val Long.megawatts: Power get() = toPower(PowerUnit.International.Megawatt) /** * Returns a [Power] equal to [Double] number of megawatts. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.megawatts: Power get() = toPower(PowerUnit.International.Megawatt) /** * Returns a [Power] equal to [Int] number of kilowatts. */ public inline val Int.kilowatts: Power get() = toPower(PowerUnit.International.Kilowatt) /** * Returns a [Power] equal to [Long] number of kilowatts. */ public inline val Long.kilowatts: Power get() = toPower(PowerUnit.International.Kilowatt) /** * Returns a [Power] equal to [Double] number of kilowatts. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.kilowatts: Power get() = toPower(PowerUnit.International.Kilowatt) /** * Returns a [Power] equal to [Int] number of watts. */ public inline val Int.watts: Power get() = toPower(PowerUnit.International.Watt) /** * Returns a [Power] equal to [Long] number of watts. */ public inline val Long.watts: Power get() = toPower(PowerUnit.International.Watt) /** * Returns a [Power] equal to [Double] number of watts. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.watts: Power get() = toPower(PowerUnit.International.Watt) /** * Returns a [Power] equal to [Int] number of milliwatts. */ public inline val Int.milliwatts: Power get() = toPower(PowerUnit.International.Milliwatt) /** * Returns a [Power] equal to [Long] number of milliwatts. */ public inline val Long.milliwatts: Power get() = toPower(PowerUnit.International.Milliwatt) /** * Returns a [Power] equal to [Double] number of milliwatts. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.milliwatts: Power get() = toPower(PowerUnit.International.Milliwatt) /** * Returns a [Power] equal to [Int] number of microwatts. */ public inline val Int.microwatts: Power get() = toPower(PowerUnit.International.Microwatt) /** * Returns a [Power] equal to [Long] number of microwatts. */ public inline val Long.microwatts: Power get() = toPower(PowerUnit.International.Microwatt) internal inline val SaturatingLong.megawatts get() = rawValue.toPower(PowerUnit.International.Megawatt) internal inline val SaturatingLong.kilowatts get() = rawValue.toPower(PowerUnit.International.Kilowatt) internal inline val SaturatingLong.watts get() = rawValue.toPower(PowerUnit.International.Watt) internal inline val SaturatingLong.milliwatts get() = rawValue.toPower(PowerUnit.International.Milliwatt) internal inline val SaturatingLong.microwatts get() = rawValue.toPower(PowerUnit.International.Microwatt) /** * Returns a [Power] equal to [Int] number of the specified [unit]. */ public fun Int.toPower(unit: PowerUnit): Power { return toLong().toPower(unit) } /** * Returns a [Power] equal to [Long] number of the specified [unit]. */ public fun Long.toPower(unit: PowerUnit): Power { return Power(saturated * unit.microwattScale) } /** * Returns a [Power] equal to [Double] number of the specified [unit]. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public fun Double.toPower(unit: PowerUnit): Power { val valueInMicrowatts = this * unit.microwattScale require(!valueInMicrowatts.isNaN()) { "Mass value cannot be NaN." } return Power(valueInMicrowatts.roundToLong().saturated) } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/scalar/temperature.kt ================================================ package io.github.kevincianfarini.alchemist.scalar import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.type.Temperature import io.github.kevincianfarini.alchemist.unit.TemperatureUnit import io.github.kevincianfarini.alchemist.unit.convertToNanokelvin /** * Returns a [Temperature] equal to [Int] number of nanokelvins. */ public val Int.nanokelvins: Temperature get() = toLong().nanokelvins /** * Returns a [Temperature] equal to [Long] number of nanokelvins. */ public val Long.nanokelvins: Temperature get() = saturated.nanokelvins /** * Returns a [Temperature] equal to [Int] number of microkelvins. */ public val Int.microkelvins: Temperature get() = toLong().microkelvins /** * Returns a [Temperature] equal to [Long] number of microkelvins. */ public val Long.microkelvins: Temperature get() = saturated.microkelvins /** * Returns a [Temperature] equal to [Double] number of microkelvins. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public val Double.microkelvins: Temperature get() = Temperature( TemperatureUnit.International.Microkelvin.convertToNanokelvin(this).saturated ) /** * Returns a [Temperature] equal to [Int] number of millikelvins. */ public val Int.millikelvins: Temperature get() = toLong().millikelvins /** * Returns a [Temperature] equal to [Long] number of millikelvins. */ public val Long.millikelvins: Temperature get() = saturated.millikelvins /** * Returns a [Temperature] equal to [Double] number of millikelvins. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public val Double.millikelvins: Temperature get() = Temperature( TemperatureUnit.International.Millikelvin.convertToNanokelvin(this).saturated ) /** * Returns a [Temperature] equal to [Int] number of kelvins. */ public val Int.kelvins: Temperature get() = toLong().kelvins /** * Returns a [Temperature] equal to [Long] number of kelvins. */ public val Long.kelvins: Temperature get() = saturated.kelvins /** * Returns a [Temperature] equal to [Double] number of kelvins. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public val Double.kelvins: Temperature get() = Temperature( TemperatureUnit.International.Kelvin.convertToNanokelvin(this).saturated ) /** * Returns a [Temperature] equal to [Int] number of kilokelvins. */ public val Int.kilokelvins: Temperature get() = toLong().kilokelvins /** * Returns a [Temperature] equal to [Long] number of kilokelvins. */ public val Long.kilokelvins: Temperature get() = saturated.kilokelvins /** * Returns a [Temperature] equal to [Double] number of kilokelvins. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public val Double.kilokelvins: Temperature get() = Temperature( TemperatureUnit.International.Kilokelvin.convertToNanokelvin(this).saturated ) /** * Returns a [Temperature] equal to [Int] number of megakelvins. */ public val Int.megakelvins: Temperature get() = toLong().megakelvins /** * Returns a [Temperature] equal to [Long] number of megakelvins. */ public val Long.megakelvins: Temperature get() = saturated.megakelvins /** * Returns a [Temperature] equal to [Double] number of megakelvins. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public val Double.megakelvins: Temperature get() = Temperature( TemperatureUnit.International.Megakelvin.convertToNanokelvin(this).saturated ) /** * Returns a [Temperature] equal to [Int] number of gigakelvins. */ public val Int.gigakelvins: Temperature get() = toLong().gigakelvins /** * Returns a [Temperature] equal to [Long] number of gigakelvins. */ public val Long.gigakelvins: Temperature get() = saturated.gigakelvins /** * Returns a [Temperature] equal to [Double] number of gigakelvins. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public val Double.gigakelvins: Temperature get() = Temperature( TemperatureUnit.International.Gigakelvin.convertToNanokelvin(this).saturated ) /** * Returns a [Temperature] equal to [Int] number of Celsius. */ public val Int.celsius: Temperature get() = toLong().celsius /** * Returns a [Temperature] equal to [Long] number of Celsius. */ public val Long.celsius: Temperature get() = saturated.celsius /** * Returns a [Temperature] equal to [Double] number of Celsius. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public val Double.celsius: Temperature get() = Temperature( TemperatureUnit.International.Celsius.convertToNanokelvin(this).saturated ) /** * Returns a [Temperature] equal to [Int] number of Fahrenheit. */ public val Int.fahrenheit: Temperature get() = toLong().fahrenheit /** * Returns a [Temperature] equal to [Long] number of Fahrenheit. */ public val Long.fahrenheit: Temperature get() = saturated.fahrenheit /** * Returns a [Temperature] equal to [Double] number of Fahrenheit. Depending on its magnitude, some precision may be * lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public val Double.fahrenheit: Temperature get() = Temperature( TemperatureUnit.Fahrenheit.convertToNanokelvin(this).saturated ) private inline val SaturatingLong.microkelvins get() = Temperature( TemperatureUnit.International.Microkelvin.convertToNanokelvin(this) ) private inline val SaturatingLong.nanokelvins get() = Temperature( TemperatureUnit.International.Nanokelvin.convertToNanokelvin(this) ) private inline val SaturatingLong.millikelvins get() = Temperature( TemperatureUnit.International.Millikelvin.convertToNanokelvin(this) ) private inline val SaturatingLong.kelvins get() = Temperature( TemperatureUnit.International.Kelvin.convertToNanokelvin(this) ) private inline val SaturatingLong.kilokelvins get() = Temperature( TemperatureUnit.International.Kilokelvin.convertToNanokelvin(this) ) private inline val SaturatingLong.megakelvins get() = Temperature( TemperatureUnit.International.Megakelvin.convertToNanokelvin(this) ) private inline val SaturatingLong.gigakelvins get() = Temperature( TemperatureUnit.International.Gigakelvin.convertToNanokelvin(this) ) private inline val SaturatingLong.celsius get() = Temperature( TemperatureUnit.International.Celsius.convertToNanokelvin(this) ) private inline val SaturatingLong.fahrenheit get() = Temperature( TemperatureUnit.Fahrenheit.convertToNanokelvin(this) ) ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/scalar/velocity.kt ================================================ package io.github.kevincianfarini.alchemist.scalar import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.type.Velocity internal val Int.nmPerSecond: Velocity get() = Velocity(toLong().saturated) internal val Long.nmPerSecond: Velocity get() = Velocity(saturated) internal val SaturatingLong.nmPerSecond: Velocity get() = Velocity(this) ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/scalar/volume.kt ================================================ package io.github.kevincianfarini.alchemist.scalar import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.type.Volume import io.github.kevincianfarini.alchemist.unit.VolumeUnit import kotlin.math.roundToLong /** * Returns a [Volume] equal to [Int] number of milliliters. */ public inline val Int.milliliters: Volume get() = toVolume(VolumeUnit.Metric.Milliliter) /** * Returns a [Volume] equal to [Long] number of milliliters. */ public inline val Long.milliliters: Volume get() = toVolume(VolumeUnit.Metric.Milliliter) /** * Returns a [Volume] equal to [Int] number of liters. */ public inline val Int.liters: Volume get() = toVolume(VolumeUnit.Metric.Liter) /** * Returns a [Volume] equal to [Long] number of liters. */ public inline val Long.liters: Volume get() = toVolume(VolumeUnit.Metric.Liter) /** * Returns a [Volume] equal to [Double] number of liters. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.liters: Volume get() = toVolume(VolumeUnit.Metric.Liter) /** * Returns a [Volume] equal to [Int] number of kiloliters. */ public inline val Int.kiloliters: Volume get() = toVolume(VolumeUnit.Metric.Kiloliter) /** * Returns a [Volume] equal to [Long] number of kiloliters. */ public inline val Long.kiloliters: Volume get() = toVolume(VolumeUnit.Metric.Kiloliter) /** * Returns a [Volume] equal to [Double] number of kiloliters. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.kiloliters: Volume get() = toVolume(VolumeUnit.Metric.Kiloliter) /** * Returns a [Volume] equal to [Int] number of megaliters. */ public inline val Int.megaliters: Volume get() = toVolume(VolumeUnit.Metric.Megaliter) /** * Returns a [Volume] equal to [Long] number of megaliters. */ public inline val Long.megaliters: Volume get() = toVolume(VolumeUnit.Metric.Megaliter) /** * Returns a [Volume] equal to [Double] number of megaliters. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.megaliters: Volume get() = toVolume(VolumeUnit.Metric.Megaliter) /** * Returns a [Volume] equal to [Int] number of gigaliters. */ public inline val Int.gigaliters: Volume get() = toVolume(VolumeUnit.Metric.Gigaliter) /** * Returns a [Volume] equal to [Long] number of gigaliters. */ public inline val Long.gigaliters: Volume get() = toVolume(VolumeUnit.Metric.Gigaliter) /** * Returns a [Volume] equal to [Double] number of gigaliters. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.gigaliters: Volume get() = toVolume(VolumeUnit.Metric.Gigaliter) /** * Returns a [Volume] equal to [Int] number of teraliters. */ public inline val Int.teraliters: Volume get() = toVolume(VolumeUnit.Metric.Teraliter) /** * Returns a [Volume] equal to [Long] number of teraliters. */ public inline val Long.teraliters: Volume get() = toVolume(VolumeUnit.Metric.Teraliter) /** * Returns a [Volume] equal to [Double] number of teraliters. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.teraliters: Volume get() = toVolume(VolumeUnit.Metric.Teraliter) /** * Returns a [Volume] equal to [Int] number of petaliters. */ public inline val Int.petaliters: Volume get() = toVolume(VolumeUnit.Metric.Petaliter) /** * Returns a [Volume] equal to [Long] number of petaliters. */ public inline val Long.petaliters: Volume get() = toVolume(VolumeUnit.Metric.Petaliter) /** * Returns a [Volume] equal to [Double] number of petaliters. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public inline val Double.petaliters: Volume get() = toVolume(VolumeUnit.Metric.Petaliter) /** * Returns a [Volume] equal to [Int] number of the specified [unit]. */ public fun Int.toVolume(unit: VolumeUnit): Volume = toLong().toVolume(unit) /** * Returns a [Volume] equal to [Long] number of the specified [unit]. */ public fun Long.toVolume(unit: VolumeUnit): Volume { return Volume(saturated * unit.cubicCentimetersScale) } /** * Returns a [Volume] equal to [Double] number of the specified [unit]. Depending on its magnitude, some precision may * be lost. * * @throws IllegalArgumentException is this [Double] is [Double.NaN]. */ public fun Double.toVolume(unit: VolumeUnit): Volume { val valueInCubicCentis = this * unit.cubicCentimetersScale require(!valueInCubicCentis.isNaN()) { "Volume value cannot be NaN." } return Volume(valueInCubicCentis.roundToLong().saturated) } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/type/Acceleration.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.secondScale import io.github.kevincianfarini.alchemist.internal.shortNameSquared import io.github.kevincianfarini.alchemist.internal.toDecimalString import io.github.kevincianfarini.alchemist.scalar.nmPerSecond import io.github.kevincianfarini.alchemist.unit.LengthUnit import kotlin.jvm.JvmInline import kotlin.math.roundToLong import kotlin.text.Typography.nbsp import kotlin.time.Duration import kotlin.time.DurationUnit /** * Represents a measure of acceleration and is capable of storing ±9.2 billion m/s² at nm/s² precision. */ @JvmInline public value class Acceleration internal constructor( private val rawNanometersPerSecondSquared: SaturatingLong ) : Comparable { // region SI Arithmetic /** * Returns the resulting [Velocity] after multiplying this acceleration by the specified [duration]. * * This operation attempts to retain precision, but for sufficiently large values of this acceleration or the * specified [duration] some precision may be lost. * * @throws IllegalArgumentException if this acceleration is infinite and [duration] is zero, or if this acceleration * is zero and [duration] is infinite. */ public operator fun times(duration: Duration): Velocity = duration.toComponents { seconds, nanoseconds -> val secondComponent = (rawNanometersPerSecondSquared * seconds).nmPerSecond val preciseNanosecondComponent = ((rawNanometersPerSecondSquared * nanoseconds) / 1_000_000_000).nmPerSecond if (preciseNanosecondComponent.isFinite()) { secondComponent + preciseNanosecondComponent } else { val coarseNanosecondComponent = ((rawNanometersPerSecondSquared / 1_000_000_000) * nanoseconds).nmPerSecond secondComponent + coarseNanosecondComponent } } /** * Returns the resulting [Force] after multiplying this acceleration by the specified [mass]. * * This operation attempts to retain precision, but for sufficiently large values of this acceleration or the * specified [mass] some precision may be lost. * * @throws IllegalArgumentException if this acceleration is infinite and [mass] is zero, or if this acceleration * is zero and [mass] is infinite. */ public operator fun times(mass: Mass): Force { return mass.toInternationalComponents { tera, giga, mega, kilo, grams, milli, micro -> // Try to find the right level which we can perform this operation at without losing precision. // -------------------------------------------------------------------------------------------- // 1 nm/s² * 1 microgram is 1 attonewton. // 1 nm/s² * 1 milligram is 1 femtonewton. // 1 nm/s² * 1 gram is 1 piconewton. // 1 nm/s² * 1 kilogram is 1 nanonewton. // 1 nm/s² * 1 megagram is 1 micronewton. // 1 nm/s² * 1 gigagram is 1 millinewton. // 1 nm/s² * 1 teragram is 1 newton. // -------------------------------------------------------------------------------------------- val newtons = rawNanometersPerSecondSquared * tera val millinewtons = rawNanometersPerSecondSquared * giga val micronewtons = rawNanometersPerSecondSquared * mega val nanonewtons = rawNanometersPerSecondSquared * kilo val piconewtons = rawNanometersPerSecondSquared * grams val femtonewtons = rawNanometersPerSecondSquared * milli val attonewtons = rawNanometersPerSecondSquared * micro // ----------- Try attonewton precision. ------------------------------------------------------ val attoN = attonewtons + (femtonewtons * 1_000) + (piconewtons * 1_000_000) + (nanonewtons * 1_000_000_000) + (micronewtons * 1_000_000_000_000) + (millinewtons * 1_000_000_000_000_000) + (newtons * 1_000_000_000_000_000_000) if (attoN.isFinite()) return@toInternationalComponents Force(attoN / 1_000_000_000) // ----------- Try femtonewton precision. ------------------------------------------------------ val femtoN = (attonewtons / 1_000) + femtonewtons + (piconewtons * 1_000) + (nanonewtons * 1_000_000) + (micronewtons * 1_000_000_000) + (millinewtons * 1_000_000_000_000) + (newtons * 1_000_000_000_000_000) if (femtoN.isFinite()) return@toInternationalComponents Force(femtoN / 1_000_000) // ----------- Try piconewton precision. ------------------------------------------------------ val picoN = (attonewtons / 1_000_000) + (femtonewtons / 1_000) + piconewtons + (nanonewtons * 1_000) + (micronewtons * 1_000_000) + (millinewtons * 1_000_000_000) + (newtons * 1_000_000_000_000) if (picoN.isFinite()) return@toInternationalComponents Force(picoN / 1_000) // ----------- Default nanonewton precision. -------------------------------------------------- val nanoN = (attonewtons / 1_000_000_000) + (femtonewtons / 1_000_000) + (piconewtons / 1_000) + nanonewtons + (micronewtons * 1_000) + (millinewtons * 1_000_000) + (newtons * 1_000_000_000) Force(nanoN) } } // endregion // region Scalar Arithmetic /** * Returns the number that is the ratio of this and the [other] acceleration value. * * @throws IllegalArgumentException when both this and the [other] acceleration are [infinite][isInfinite]. */ public operator fun div(other: Acceleration): Double { return rawNanometersPerSecondSquared.toDouble() / other.rawNanometersPerSecondSquared.toDouble() } /** * Returns an acceleration whose value is this acceleration value divided by the specified [scale]. */ public operator fun div(scale: Int): Acceleration = div(scale.toLong()) /** * Returns an acceleration whose value is this acceleration value divided by the specified [scale]. * * @throws IllegalArgumentException when [scale] is equal to [Long.MAX_VALUE] or [Long.MIN_VALUE] and this * acceleration is [infinite][isInfinite]. */ public operator fun div(scale: Long): Acceleration = Acceleration(rawNanometersPerSecondSquared / scale) /** * Returns the negative of this acceleration value. */ public operator fun unaryMinus(): Acceleration = Acceleration(-rawNanometersPerSecondSquared) /** * Returns an acceleration whose value is the difference between this and the [other] acceleration value. * * @throws IllegalArgumentException if this acceleration and the [other] acceleration are both * [infinite][isInfinite] but have equivalent signs. */ public operator fun minus(other: Acceleration): Acceleration { return Acceleration(rawNanometersPerSecondSquared - other.rawNanometersPerSecondSquared) } /** * Returns an acceleration whose value is the sum between this and the [other] acceleration value. * * @throws IllegalArgumentException if this acceleration and the [other] acceleration are both * [infinite][isInfinite] but have differing signs. */ public operator fun plus(other: Acceleration): Acceleration { return Acceleration(rawNanometersPerSecondSquared + other.rawNanometersPerSecondSquared) } /** * Returns an acceleration whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this acceleration is [infinite][isInfinite] and [scale] is 0. */ public operator fun times(scale: Int): Acceleration = times(scale.toLong()) /** * Returns an acceleration whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this acceleration is [infinite][isInfinite] and [scale] is 0, or when this * acceleration is 0 and scale is [Long.MAX_VALUE] or [Long.MIN_VALUE]. */ public operator fun times(scale: Long): Acceleration = Acceleration(rawNanometersPerSecondSquared * scale) /** * Returns an acceleration whose value is multiplied by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this acceleration is [infinite][isInfinite] and [scale] is 0.0 or when this acceleration is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun times(scale: Double): Acceleration = Acceleration(rawNanometersPerSecondSquared * scale) /** * Returns an acceleration whose value is divided by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this acceleration is [infinite][isInfinite] and [scale] is 0.0 or when this acceleration is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun div(scale: Double): Acceleration = Acceleration(rawNanometersPerSecondSquared / scale) // endregion // region Acceleration to Scalar Conversions /** * Returns the value of this acceleration expressed as a [Long] number of the specified [lengthUnit] per * [durationUnit]². Infinite values are converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its * sign. */ public fun toLong(lengthUnit: LengthUnit, durationUnit: DurationUnit): Long { return toDouble(lengthUnit, durationUnit).roundToLong() } /** * Returns the value of this acceleration expressed as a [Double] number of the specified [lengthUnit] per * [durationUnit]². Infinite values are converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] * depending on its sign. */ public fun toDouble(lengthUnit: LengthUnit, durationUnit: DurationUnit): Double { val lengthPerSecond2 = rawNanometersPerSecondSquared.toDouble() / lengthUnit.nanometerScale.toDouble() return lengthPerSecond2 * durationUnit.secondScale * durationUnit.secondScale } /** * Returns a fractional string representation of this acceleration expressed in the specified [lengthUnit] per * [durationUnit]² and is rounded to the specified [decimals]. */ public fun toString(lengthUnit: LengthUnit, durationUnit: DurationUnit, decimals: Int = 0): String { return when (isInfinite()) { true -> rawNanometersPerSecondSquared.toString() false -> buildString { append(toDouble(lengthUnit, durationUnit).toDecimalString(decimals)) append(nbsp) append(lengthUnit.symbol) append("/") append(durationUnit.shortNameSquared) } } } /** * Returns a fractional string representation of this acceleration expressed in the largest * [LengthUnit.International] per second² quantity which is greater than or equal to 1. */ override fun toString(): String { val lengthUnit = LengthUnit.International.entries.asReversed().firstOrNull { unit -> rawNanometersPerSecondSquared.absoluteValue / unit.nanometerScale > 0 } return toString(lengthUnit ?: LengthUnit.International.Nanometer, DurationUnit.SECONDS, decimals = 2) } // endregion // region Comparisons /** * Returns true if this acceleration value is finite. */ public fun isFinite(): Boolean = rawNanometersPerSecondSquared.isFinite() /** * Returns true if this acceleration value is infinite. */ public fun isInfinite(): Boolean = rawNanometersPerSecondSquared.isInfinite() /** * Compares this acceleration with the [other] acceleration. Returns zero if this acceleration is equal * to the specified [other] acceleration, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */ override fun compareTo(other: Acceleration): Int { return rawNanometersPerSecondSquared.compareTo(other.rawNanometersPerSecondSquared) } // endregion } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/type/Area.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.internal.toDecimalString import io.github.kevincianfarini.alchemist.scalar.mm2 import io.github.kevincianfarini.alchemist.unit.AreaUnit import io.github.kevincianfarini.alchemist.unit.LengthUnit import kotlin.jvm.JvmInline import kotlin.math.pow /** * Represents a measure of area and is capable of storing ±9.22 million kilometers² at millimeter² precision. */ @JvmInline public value class Area internal constructor(internal val rawMillimetersSquared: SaturatingLong) : Comparable { // region SI Arithmetic /** * Returns the resulting length after dividing this area by the specified [length]. * * This operation attempts to retain precision, but for sufficiently large values of this area some precision * may be lost. * * @throws IllegalArgumentException if both this area and [length] are infinite. */ public operator fun div(length: Length): Length { // Try to find the right level which we can perform this operation at without losing precision. // -------------------------------------------------------------------------------------------- // 1 nanometer² / 1 nanometer is 1 nanometer // 1 micrometer² / 1 nanometer is 1,000,000 nanometers. // 1 millimeter² / 1 nanometer is 1,000,000,000,000 nanometers. // -------------------------------------------------------------------------------------------- val nano2 = rawMillimetersSquared * 1_000_000_000_000 if (nano2.isFinite()) return Length(nano2 / length.rawNanometers) val micro2 = rawMillimetersSquared * 1_000_000 if (micro2.isFinite()) return Length((micro2 / length.rawNanometers) * 1_000_000) return Length((rawMillimetersSquared / length.rawNanometers) * 1_000_000_000_000) } /** * Returns the resulting [Volume] after applying this area over the specified [length]. * * This operation attempts to retain precision, but for sufficiently large values of this area or the * specified [length] some precision may be lost. * * @throws IllegalArgumentException if this area is [infinite][isInfinite] and [length] is zero, or if this area * is zero and [length] is infinite. */ public operator fun times(length: Length): Volume { return length.toInternationalComponents { giga, mega, kilo, meters, centi, milli, micro, nano -> // Try to find the right level which we can perform this operation at without losing precision. // -------------------------------------------------------------------------------------------- // 1 millimeter² * 1 nm is 1 picoliter. // 1 millimeter² * 1 μm is 1 nanoliter. // 1 millimeter² * 1 mm is 1 microliter. // 1 millimeter² * 1 cm is 10 microliters. // 1 millimeter² * 1 m is 1 milliliter. // 1 millimeter² * 1 km is 1 liter. // 1 millimeter² * 1 Mm is 1 kiloliter. // 1 millimeter² * 1 Gm is 1 megaliter. // -------------------------------------------------------------------------------------------- val megaliters = rawMillimetersSquared * giga val kiloliters = rawMillimetersSquared * mega val liters = rawMillimetersSquared * kilo val milliliters = rawMillimetersSquared * meters val microliters = (rawMillimetersSquared * centi * 10) + (rawMillimetersSquared * milli) val nanoliters = rawMillimetersSquared * micro val picoliters = rawMillimetersSquared * nano // ----------- Try picoliter precision. ------------------------------------------------------ val picoL = picoliters + (nanoliters * 1_000) + (microliters * 1_000_000) + (milliliters * 1_000_000_000) + (liters * 1_000_000_000_000) + (kiloliters * 1_000_000_000_000_000) + (megaliters * 1_000_000_000_000_000_000) if (picoL.isFinite()) return@toInternationalComponents Volume(picoL / 1_000_000_000) // ----------- Try nanoliter precision. ------------------------------------------------------ val nanoL = (picoliters / 1_000) + nanoliters + (microliters * 1_000) + (milliliters * 1_000_000) + (liters * 1_000_000_000) + (kiloliters * 1_000_000_000_000) + (megaliters * 1_000_000_000_000_000) if (nanoL.isFinite()) return@toInternationalComponents Volume(nanoL / 1_000_000) // ----------- Try microliter precision. ----------------------------------------------------- val microL = (picoliters / 1_000_000) + (nanoliters / 1_000) + microliters + (milliliters * 1_000) + (liters * 1_000_000) + (kiloliters * 1_000_000_000) + (megaliters * 1_000_000_000_000) if (microL.isFinite()) return@toInternationalComponents Volume(microL / 1_000) // ----------- Default milliliter precision. ------------------------------------------------- val milliL = (picoliters / 1_000_000_000) + (nanoliters / 1_000_000) + (microliters / 1_000) + milliliters + (liters * 1_000) + (kiloliters * 1_000_000) + (megaliters * 1_000_000_000) Volume(milliL) } } // endregion // region Scalar Arithmetic /** * Returns an area whose value is this area value divided by the specified [scale]. */ public operator fun div(scale: Int): Area = div(scale.toLong()) /** * Returns an area whose value is this area value divided by the specified [scale]. * * @throws IllegalArgumentException when [scale] is equal to [Long.MAX_VALUE] or [Long.MIN_VALUE] and this * area is [infinite][isInfinite]. */ public operator fun div(scale: Long): Area = Area(rawMillimetersSquared / scale) /** * Returns the number that is the ratio of this and the [other] area value. * * @throws IllegalArgumentException when both this and the [other] area are [infinite][isInfinite]. */ public operator fun div(other: Area): Double { return rawMillimetersSquared.toDouble() / other.rawMillimetersSquared.toDouble() } /** * Returns the negative of this area value. */ public operator fun unaryMinus(): Area = Area(-rawMillimetersSquared) /** * Returns an area whose value is the difference between this and the [other] area value. * * @throws IllegalArgumentException if this area and the [other] area are both * [infinite][isInfinite] but have equivalent signs. */ public operator fun minus(other: Area): Area = Area(rawMillimetersSquared - other.rawMillimetersSquared) /** * Returns an area whose value is the sum between this and the [other] area value. * * @throws IllegalArgumentException if this area and the [other] area are both * [infinite][isInfinite] but have differing signs. */ public operator fun plus(other: Area): Area = Area(rawMillimetersSquared + other.rawMillimetersSquared) /** * Returns an area whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this area is [infinite][isInfinite] and [scale] is 0. */ public operator fun times(scale: Int): Area = times(scale.toLong()) /** * Returns an area whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this area is [infinite][isInfinite] and [scale] is 0, or when this * area is 0 and scale is [Long.MAX_VALUE] or [Long.MIN_VALUE]. */ public operator fun times(scale: Long): Area = Area(rawMillimetersSquared * scale) /** * Returns an area whose value is multiplied by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this area is [infinite][isInfinite] and [scale] is 0.0 or when this area is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun times(scale: Double): Area = Area(rawMillimetersSquared * scale) /** * Returns an area whose value is divided by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this area is [infinite][isInfinite] and [scale] is 0.0 or when this area is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun div(scale: Double): Area = Area(rawMillimetersSquared / scale) // endregion // region Area to Scalar Conversions /** * Splits this area into megameters², kilometers², meters², centimeters², and millimeters² and executes the [action] * with those components. The result of [action] is returned as the result of this function. * * Infinite areas invoke [action] with [Long.MAX_VALUE] or [Long.MIN_VALUE] for every component, depending on the * infinite value's sign. */ public fun toInternationalComponents( action: ( megametersSquared: Long, kilometersSquared: Long, metersSquared: Long, centimetersSquared: Long, millimetersSquared: Long, ) -> T, ): T { val mega = rawMillimetersSquared / 1_000_000_000_000_000_000 val megaRemainder = rawMillimetersSquared % 1_000_000_000_000_000_000 val kilo = megaRemainder / 1_000_000_000_000 val kiloRemainder = megaRemainder % 1_000_000_000_000 val meters = kiloRemainder / 1_000_000 val metersRemainder = kiloRemainder % 1_000_000 val centi = metersRemainder / 100 val milli = metersRemainder % 100 return action(mega.rawValue, kilo.rawValue, meters.rawValue, centi.rawValue, milli.rawValue) } /** * Returns the value of this area expressed as a [Long] number of the specified [unit]. Infinite values are * converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its sign. */ public fun toLong(unit: AreaUnit): Long { return (rawMillimetersSquared / unit.millimetersSquaredScale).rawValue } /** * Returns the value of this area expressed as a [Long] number of the specified [squareUnit]². Infinite * values are converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its sign. */ public fun toLong(squareUnit: LengthUnit): Long { val nm2 = rawMillimetersSquared * 1_000_000_000_000 val nm2Scale = squareUnit.nanometerScale.saturated * squareUnit.nanometerScale return if (nm2.isFinite() && nm2Scale.isFinite()) { nm2 / nm2Scale } else { val um2 = rawMillimetersSquared * 1_000_000 val um2Unit = squareUnit.nanometerScale.saturated / 1_000 val um2Scale = um2Unit * um2Unit if (um2.isFinite() && um2Scale.isFinite()) { um2 / um2Scale } else { val mm2Unit = squareUnit.nanometerScale.saturated / 1_000_000 val mm2Scale = mm2Unit * mm2Unit if (rawMillimetersSquared.isFinite() && mm2Scale.isFinite()) { rawMillimetersSquared / mm2Scale } else { 0L.saturated } } }.rawValue } /** * Returns the value of this area expressed as a [Double] number of the specified [unit]. Infinite values are * converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] depending on its sign. */ public fun toDouble(unit: AreaUnit): Double { return this / unit.millimetersSquaredScale.mm2 } /** * Returns the value of this area expressed as a [Double] number of the specific [LengthUnit]². Infinite values are * converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] depending on its sign. */ public fun toDouble(squareUnit: LengthUnit): Double { val nm2 = rawMillimetersSquared.toDouble() * 1_000_000_000_000 return nm2 / squareUnit.nanometerScale.toDouble().pow(2) } /** * Returns a fractional string representation of this area expressed in the specified [AreaUnit] and is rounded * to the specified [decimals]. */ public fun toString(unit: AreaUnit, decimals: Int = 0): String = when (isInfinite()) { true -> rawMillimetersSquared.toString() false -> buildString { append(toDouble(unit).toDecimalString(decimals)) append(unit.symbol) } } /** * Returns a fractional string representation of this area expressed in the specified [LengthUnit]² and is rounded * to the specified [decimals]. */ public fun toString(squareUnit: LengthUnit, decimals: Int = 0): String = when (isInfinite()) { true -> rawMillimetersSquared.toString() false -> buildString { append(toDouble(squareUnit).toDecimalString(decimals)) append(squareUnit.symbol) append("²") } } /** * Returns a fractional string representation of this area expressed in the largest [LengthUnit]² quantity which is * greater than or equal to 1. */ public override fun toString(): String { val largestUnit = LengthUnit.International.entries.asReversed().firstOrNull { squareUnit -> toDouble(squareUnit) >= 1.0 } return toString(largestUnit ?: LengthUnit.International.Millimeter, decimals = 2) } // endregion // region Comparisons /** * Returns true if this area value is infinite. */ public fun isInfinite(): Boolean = rawMillimetersSquared.isInfinite() /** * Returns true if this area value is finite. */ public fun isFinite(): Boolean = rawMillimetersSquared.isFinite() /** * Compares this area with the [other] area. Returns zero if this area is equal * to the specified [other] area, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */ public override fun compareTo(other: Area): Int { return rawMillimetersSquared.compareTo(other.rawMillimetersSquared) } // endregion } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/type/Energy.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.isPreciseToNanosecond import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.internal.throwIllegalArgumentException import io.github.kevincianfarini.alchemist.internal.toDecimalString import io.github.kevincianfarini.alchemist.scalar.kilonewtons import io.github.kevincianfarini.alchemist.scalar.kilowatts import io.github.kevincianfarini.alchemist.scalar.megawatts import io.github.kevincianfarini.alchemist.scalar.microwatts import io.github.kevincianfarini.alchemist.scalar.millijoules import io.github.kevincianfarini.alchemist.scalar.milliwatts import io.github.kevincianfarini.alchemist.scalar.watts import io.github.kevincianfarini.alchemist.unit.EnergyUnit import kotlin.jvm.JvmInline import kotlin.time.Duration /** * Represents an amount of energy and is capable of storing ±9.2 petajoules or ±2.56 terawatt-hours at millijoule * precision. */ @JvmInline public value class Energy internal constructor(private val rawMillijoules: SaturatingLong) : Comparable { // region SI Arithmetic /** * Returns the constant [Force] applied over the specified [length] required to expend this amount of energy. * * This operation attempts to retain precision, but for sufficiently large values of either this energy or the * other [length], some precision may be lost. * * @throws IllegalArgumentException if both this energy and [length] are infinite. */ public operator fun div(length: Length): Force { // TODO This is simplistic and we should attempt to retain precision in the future. return ((rawMillijoules / length.rawNanometers) * 1_000).kilonewtons } /** * Returns the constant [Power] applied over the specified [duration] to generate this amount of energy. * * This operation attempts to retain precision, but for sufficiently large values of either this energy or the * other [duration], some precision may be lost. * * @throws IllegalArgumentException if both this energy and [duration] are infinite. */ public operator fun div(duration: Duration): Power = when { rawMillijoules.isInfinite() && duration.isInfinite() -> { throwIllegalArgumentException("Dividing two infinite values yields an undefined result.") } rawMillijoules.isInfinite() -> Power(rawMillijoules) duration.isInfinite() -> Power(0L.saturated) else -> calculatePower(duration) } private fun calculatePower(duration: Duration): Power { // Try to find the right level which we can perform this operation at without losing precision. if (duration.isPreciseToNanosecond()) { val power = millijoulesPerNs(rawMillijoules, duration.inWholeNanoseconds) if (power.isFinite()) return power } val ms = duration.inWholeMilliseconds val power = millijoulesPerMs(rawMillijoules, ms) if (power.isFinite()) return power return (rawMillijoules / ms).watts } private fun millijoulesPerMs(millijoules: SaturatingLong, ms: Long): Power { // 1 millijoule per 1 millisecond is 1 watt. val watts = (millijoules / ms).watts return watts + microjoulesPerMs((millijoules % ms) * 1_000, ms) } private fun microjoulesPerMs(microjoules: SaturatingLong, ms: Long): Power { // 1 microjoule per 1 millisecond is 1 milliwatt. val milliwatts = (microjoules / ms).milliwatts return milliwatts + nanojoulesPerMs((microjoules % ms) * 1_000, ms) } private fun nanojoulesPerMs(nanojoules: SaturatingLong, ms: Long): Power { // 1 nanojoule per 1 millisecond is 1 microwatt. return (nanojoules / ms).microwatts } private fun millijoulesPerNs(millijoules: SaturatingLong, ns: Long): Power { // 1 millijoule per 1 nanosecond is 1 megawatt. val megawatts = (millijoules / ns).megawatts return megawatts + microjoulesPerNs((millijoules % ns) * 1_000, ns) } private fun microjoulesPerNs(microjoules: SaturatingLong, ns: Long): Power { // 1 microjoule per 1 nanosecond is 1 kilowatt. val kilowatts = (microjoules / ns).kilowatts return kilowatts + nanojoulesPerNs((microjoules % ns) * 1_000, ns) } private fun nanojoulesPerNs(nanojoules: SaturatingLong, ns: Long): Power { // 1 nanojoule per 1 nanosecond is 1 watt. val watts = (nanojoules / ns).watts return watts + picojoulesPerNs((nanojoules % ns) * 1_000, ns) } private fun picojoulesPerNs(picojoules: SaturatingLong, ns: Long): Power { // 1 picojoule per 1 nanosecond is 1 milliwatt. val milliwatts = (picojoules / ns).milliwatts return milliwatts + femtojoulesPerNs((picojoules % ns) * 1_000, ns) } private fun femtojoulesPerNs(femtojoules: SaturatingLong, ns: Long): Power { // 1 femtojoule per 1 nanosecond is 1 microwatt. return (femtojoules / ns).microwatts } // endregion // region Scalar Arithmetic /** * Returns the number that is the ratio of this and the [other] energy value. * * @throws IllegalArgumentException when both this and the [other] energy are [infinite][isInfinite]. */ public operator fun div(other: Energy): Double { return rawMillijoules.toDouble() / other.rawMillijoules.toDouble() } /** * Returns an energy whose value is this energy value divided by the specified [scale]. */ public operator fun div(scale: Int): Energy { return div(scale.toLong()) } /** * Returns an energy whose value is this energy value divided by the specified [scale]. * * @throws IllegalArgumentException when [scale] is equal to [Long.MAX_VALUE] or [Long.MIN_VALUE] and this * energy is [infinite][isInfinite]. */ public operator fun div(scale: Long): Energy { return Energy(rawMillijoules / scale) } /** * Returns the negative of this energy value. */ public operator fun unaryMinus(): Energy = Energy(-rawMillijoules) /** * Returns an energy whose value is the difference between this and the [other] energy value. * * @throws IllegalArgumentException if this energy and the [other] energy are both * [infinite][isInfinite] but have equivalent signs. */ public operator fun minus(other: Energy): Energy { return Energy(rawMillijoules - other.rawMillijoules) } /** * Returns an energy whose value is the sum between this and the [other] energy value. * * @throws IllegalArgumentException if this energy and the [other] energy are both * [infinite][isInfinite] but have differing signs. */ public operator fun plus(other: Energy): Energy { return Energy(rawMillijoules + other.rawMillijoules) } /** * Returns an energy whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this energy is [infinite][isInfinite] and [scale] is 0. */ public operator fun times(scale: Int): Energy { return div(scale.toLong()) } /** * Returns an energy whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this energy is [infinite][isInfinite] and [scale] is 0, or when this * energy is 0 and scale is [Long.MAX_VALUE] or [Long.MIN_VALUE]. */ public operator fun times(scale: Long): Energy { return Energy(rawMillijoules * scale) } /** * Returns an energy whose value is multiplied by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this energy is [infinite][isInfinite] and [scale] is 0.0 or when this energy is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun times(scale: Double): Energy = Energy(rawMillijoules * scale) /** * Returns an energy whose value is divided by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this energy is [infinite][isInfinite] and [scale] is 0.0 or when this energy is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun div(scale: Double): Energy = Energy(rawMillijoules / scale) // endregion // region Energy to Scalar Conversions /** * Splits this energy into petajoules, tetrajoules, gigajoules, megajoules, kilojoules, joules, and millijoules * and executes the [action] with those components. The result of [action] is returned as the result of this * function. * * Infinite energy values invoke [action] with [Long.MAX_VALUE] or [Long.MIN_VALUE] for every component, depending * on the infinite value's sign. */ public fun toInternationalComponents( action: ( petajoules: Long, tetrajoules: Long, gigajoules: Long, megajoules: Long, kilojoules: Long, joules: Long, millijoules: Long, ) -> T ): T { val peta = rawMillijoules / EnergyUnit.International.Petajoule.millijouleScale val petaRemainder = rawMillijoules % EnergyUnit.International.Petajoule.millijouleScale val tetra = petaRemainder / EnergyUnit.International.Tetrajoule.millijouleScale val tetraRemainder = petaRemainder % EnergyUnit.International.Tetrajoule.millijouleScale val giga = tetraRemainder / EnergyUnit.International.Gigajoule.millijouleScale val gigaRemainder = tetraRemainder % EnergyUnit.International.Gigajoule.millijouleScale val mega = gigaRemainder / EnergyUnit.International.Megajoule.millijouleScale val megaRemainder = gigaRemainder % EnergyUnit.International.Megajoule.millijouleScale val kilo = megaRemainder / EnergyUnit.International.Kilojoule.millijouleScale val kiloRemainder = megaRemainder % EnergyUnit.International.Kilojoule.millijouleScale val joule = kiloRemainder / EnergyUnit.International.Joule.millijouleScale val milliJoule = kiloRemainder % EnergyUnit.International.Joule.millijouleScale return action( peta.rawValue, tetra.rawValue, giga.rawValue, mega.rawValue, kilo.rawValue, joule.rawValue, milliJoule.rawValue, ) } /** * Splits this energy into terawatt-hours, gigawatt-hours, megawatt-hours, kilowatt-hours, watt-hours, * milliwatt-hours, and microwatt-hours and executes the [action] with those components. The result of [action] is * returned as the result of this function. * * Infinite energy values invoke [action] with [Long.MAX_VALUE], [Long.MIN_VALUE], [Double.POSITIVE_INFINITY], or * [Double.NEGATIVE_INFINITY] for every component, depending on the infinite value's sign and the component's type. */ public fun toElectricityComponents( action: ( terawattHours: Long, gigawattHours: Long, megawattHours: Long, kilowattHours: Long, wattHours: Long, milliwattHours: Long, microwattHours: Double, ) -> T ): T { val tera = rawMillijoules / EnergyUnit.Electricity.TerawattHour.millijouleScale val teraRemainder = rawMillijoules % EnergyUnit.Electricity.TerawattHour.millijouleScale val giga = teraRemainder / EnergyUnit.Electricity.GigawattHour.millijouleScale val gigaRemainder = teraRemainder % EnergyUnit.Electricity.GigawattHour.millijouleScale val mega = gigaRemainder / EnergyUnit.Electricity.MegawattHour.millijouleScale val megaRemainder = gigaRemainder % EnergyUnit.Electricity.MegawattHour.millijouleScale val kilo = megaRemainder / EnergyUnit.Electricity.KilowattHour.millijouleScale val kiloRemainder = megaRemainder % EnergyUnit.Electricity.KilowattHour.millijouleScale val wattHour = kiloRemainder / EnergyUnit.Electricity.WattHour.millijouleScale val wattRemainder = kiloRemainder % EnergyUnit.Electricity.WattHour.millijouleScale val milliwattHour = wattRemainder / EnergyUnit.Electricity.MilliwattHour.millijouleScale val milliwattRemainder = wattRemainder % EnergyUnit.Electricity.MilliwattHour.millijouleScale val microwattHours = milliwattRemainder.toDouble() / 3.6 // 3.6 millijoules per microwatt-hour. return action( tera.rawValue, giga.rawValue, mega.rawValue, kilo.rawValue, wattHour.rawValue, milliwattHour.rawValue, microwattHours, ) } /** * Returns the value of this energy expressed as a [Long] number of the specified [unit]. Infinite values are * converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its sign. */ public fun toLong(unit: EnergyUnit): Long { return (rawMillijoules / unit.millijouleScale).rawValue } /** * Returns the value of this energy expressed as a [Double] number of the specified [unit]. Infinite values are * converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] depending on its sign. */ public fun toDouble(unit: EnergyUnit): Double { return this / unit.millijouleScale.millijoules } /** * Returns a fractional string representation of this energy expressed in the specified [EnergyUnit] and is rounded * to the specified [decimals]. */ public fun toString(unit: EnergyUnit, decimals: Int = 0): String = when (rawMillijoules.isInfinite()) { true -> rawMillijoules.toString() false -> buildString { append(toDouble(unit).toDecimalString(decimals)) append(unit.symbol) } } /** * Returns a fractional string representation of this energy expressed in the largest [EnergyUnit.International] * quantity which is greater than or equal to 1. */ public override fun toString(): String { val largestUnit = EnergyUnit.International.entries.asReversed().firstOrNull { unit -> rawMillijoules.absoluteValue / unit.millijouleScale > 0 } return toString(largestUnit ?: EnergyUnit.International.Millijoule, decimals = 2) } // endregion // region Comparisons /** * Returns true if this area value is infinite. */ public fun isInfinite(): Boolean = rawMillijoules.isInfinite() /** * Returns true if this area value is finite. */ public fun isFinite(): Boolean = rawMillijoules.isFinite() /** * Compares this energy with the [other] energy. Returns zero if this energy is equal * to the specified [other] energy, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */ public override fun compareTo(other: Energy): Int { return rawMillijoules.compareTo(other.rawMillijoules) } // endregion } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/type/Force.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.toDecimalString import io.github.kevincianfarini.alchemist.scalar.nmPerSecond2 import io.github.kevincianfarini.alchemist.unit.ForceUnit import kotlin.jvm.JvmInline /** * Represents a measure of force and is capable of storing ±9.2 billion newtons at nanonewton precision. */ @JvmInline public value class Force internal constructor(private val rawNanonewtons: SaturatingLong) : Comparable { // region SI Arithmetic /** * Returns the resulting [Acceleration] from applying this force to the specified [mass]. * * This operation attempts to retain precision, but for sufficiently large values of either this force or the * other [mass], some precision may be lost. * * @throws IllegalArgumentException if both this force and [mass] are infinite. */ public operator fun div(mass: Mass): Acceleration { // 1 nanonewton / 1 microgram is 100 centimeters/second². // TODO This is simplistic and we should attempt to retain precision in the future. return ((rawNanonewtons / mass.rawMicrograms) * 1_000_000_000).nmPerSecond2 } /** * Returns the amount of [Energy] required to apply this force over the specified [length]. * * This operation attempts to retain precision, but for sufficiently large values of either this force or the * specified [length], some precision may be lost. * * @throws IllegalArgumentException when [length] is [infinite][isInfinite] and this force is 0 or vice versa. */ public operator fun times(length: Length): Energy { return length.toInternationalComponents { giga, mega, kilo, meters, centi, milli, micro, nano -> // Try to find the right level which we can perform this operation at without losing precision. // -------------------------------------------------------------------------------------------- // 1 nN * 1 nm is 1 attojoule. // 1 nN * 1 μm is 1 femtojoule. // 1 nN * 1 mm is 1 picojoule. // 1 nN * 1 cm is 10 picojoules. // 1 nN * 1 m is 1 nanojoule. // 1 nN * 1 km is 1 microjoule. // 1 nN * 1 Mm is 1 millijoule. // 1 nN * 1 Gm is 1 joule. // -------------------------------------------------------------------------------------------- val joules = rawNanonewtons * giga val millijoules = rawNanonewtons * mega val microjoules = rawNanonewtons * kilo val nanojoules = rawNanonewtons * meters val picojoules = (rawNanonewtons * centi * 10) + (rawNanonewtons * milli) val femtojoules = rawNanonewtons * micro val attojoules = rawNanonewtons * nano // ----------- Try attojoule precision. ------------------------------------------------------ val attoJ = attojoules + (femtojoules * 1_000) + (picojoules * 1_000_000) + (nanojoules * 1_000_000_000) + (microjoules * 1_000_000_000_000) + (millijoules * 1_000_000_000_000_000) + (joules * 1_000_000_000_000_000_000) if (attoJ.isFinite()) return@toInternationalComponents Energy(attoJ / 1_000_000_000_000_000) // ----------- Try femtojoule precision. ------------------------------------------------------ val femtoJ = (attojoules / 1_000) + femtojoules + (picojoules * 1_000) + (nanojoules * 1_000_000) + (microjoules * 1_000_000_000) + (millijoules * 1_000_000_000_000) + (joules * 1_000_000_000_000_000) if (femtoJ.isFinite()) return@toInternationalComponents Energy(femtoJ / 1_000_000_000_000) // ----------- Try picojoule precision. ------------------------------------------------------ val picoJ = (attojoules / 1_000_000) + (femtojoules / 1_000) + picojoules + (nanojoules * 1_000) + (microjoules * 1_000_000) + (millijoules * 1_000_000_000) + (joules * 1_000_000_000_000) if (picoJ.isFinite()) return@toInternationalComponents Energy(picoJ / 1_000_000_000) // ----------- Try nanojoule precision. ------------------------------------------------------ val nanoJ = (attojoules / 1_000_000_000) + (femtojoules / 1_000_000) + (picojoules / 1_000) + nanojoules + (microjoules * 1_000) + (millijoules * 1_000_000) + (joules * 1_000_000_000) if (nanoJ.isFinite()) return@toInternationalComponents Energy(nanoJ / 1_000_000) // ----------- Try microjoule precision. ------------------------------------------------------ val microJ = (attojoules / 1_000_000_000_000) + (femtojoules / 1_000_000_000) + (picojoules / 1_000_000) + (nanojoules / 1_000) + microjoules + (millijoules * 1_000) + (joules * 1_000_000) if (microJ.isFinite()) return@toInternationalComponents Energy(microJ / 1_000) // ----------- Default millijoule precision. ------------------------------------------------------ val milliJ = (attojoules / 1_000_000_000_000_000) + (femtojoules / 1_000_000_000_000) + (picojoules / 1_000_000_000) + (nanojoules / 1_000_000) + (microjoules / 1_000) + millijoules + (joules * 1_000) Energy(milliJ) } } // endregion // region Scalar Arithmetic /** * Returns the number that is the ratio of this and the [other] force value. * * @throws IllegalArgumentException when both this and the [other] force are [infinite][isInfinite]. */ public operator fun div(other: Force): Double = rawNanonewtons.toDouble() / other.rawNanonewtons.toDouble() /** * Returns a force whose value is this force value divided by the specified [scale]. */ public operator fun div(scale: Int): Force = div(scale.toLong()) /** * Returns a force whose value is this force value divided by the specified [scale]. * * @throws IllegalArgumentException when [scale] is equal to [Long.MAX_VALUE] or [Long.MIN_VALUE] and this * force is [infinite][isInfinite]. */ public operator fun div(scale: Long): Force = Force(rawNanonewtons / scale) /** * Returns the negative of this force value. */ public operator fun unaryMinus(): Force = Force(-rawNanonewtons) /** * Returns a force whose value is the difference between this and the [other] force value. * * @throws IllegalArgumentException if this force and the [other] force are both * [infinite][isInfinite] but have equivalent signs. */ public operator fun minus(other: Force): Force = Force(rawNanonewtons - other.rawNanonewtons) /** * Returns a force whose value is the sum between this and the [other] force value. * * @throws IllegalArgumentException if this force and the [other] force are both * [infinite][isInfinite] but have differing signs. */ public operator fun plus(other: Force): Force = Force(rawNanonewtons + other.rawNanonewtons) /** * Returns a force whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this force is [infinite][isInfinite] and [scale] is 0. */ public operator fun times(scale: Int): Force = times(scale.toLong()) /** * Returns a force whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this force is [infinite][isInfinite] and [scale] is 0, or when this * force is 0 and scale is [Long.MAX_VALUE] or [Long.MIN_VALUE]. */ public operator fun times(scale: Long): Force = Force(rawNanonewtons * scale) /** * Returns a force whose value is multiplied by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this force is [infinite][isInfinite] and [scale] is 0.0 or when this force is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun times(scale: Double): Force = Force(rawNanonewtons * scale) /** * Returns a force whose value is divided by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this force is [infinite][isInfinite] and [scale] is 0.0 or when this force is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun div(scale: Double): Force = Force(rawNanonewtons / scale) // endregion // region Force to Scalar Conversions /** * Returns the value of this force expressed as a [Long] number of the specified [unit]. Infinite values are * converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its sign. */ public fun toLong(unit: ForceUnit): Long { return (rawNanonewtons / unit.nanonewtonScale).rawValue } /** * Returns the value of this force expressed as a [Double] number of the specified [unit]. Infinite values are * converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] depending on its sign. */ public fun toDouble(unit: ForceUnit): Double { return rawNanonewtons.toDouble() / unit.nanonewtonScale } /** * Returns a fractional string representation of this force expressed in the specified [ForceUnit] and is rounded * to the specified [decimals]. */ public fun toString(unit: ForceUnit, decimals: Int = 0): String = when { isInfinite() -> rawNanonewtons.toString() else -> buildString { append(toDouble(unit).toDecimalString(decimals)) append(unit.symbol) } } /** * Returns a fractional string representation of this force expressed in the largest [ForceUnit.International] * quantity which is greater than or equal to 1. */ public override fun toString(): String { val largestUnit = ForceUnit.International.entries.asReversed().firstOrNull { unit -> rawNanonewtons / unit.nanonewtonScale > 0 } return toString(largestUnit ?: ForceUnit.International.Nanonewton, decimals = 2) } // endregion // region Comparisons /** * Returns true if this area value is infinite. */ public fun isInfinite(): Boolean = rawNanonewtons.isInfinite() /** * Returns true if this area value is finite. */ public fun isFinite(): Boolean = rawNanonewtons.isFinite() /** * Compares this force with the [other] force. Returns zero if this force is equal * to the specified [other] force, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */ override fun compareTo(other: Force): Int = rawNanonewtons.compareTo(other.rawNanonewtons) // endregion } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/type/Length.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.POSITIVE_INFINITY import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.isPreciseToNanosecond import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.internal.sign import io.github.kevincianfarini.alchemist.internal.throwIllegalArgumentException import io.github.kevincianfarini.alchemist.internal.toDecimalString import io.github.kevincianfarini.alchemist.scalar.nanometers import io.github.kevincianfarini.alchemist.scalar.nmPerSecond import io.github.kevincianfarini.alchemist.unit.LengthUnit import io.github.kevincianfarini.alchemist.unit.LengthUnit.International.Nanometer import kotlin.jvm.JvmInline import kotlin.time.Duration import kotlin.time.Duration.Companion.microseconds import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds /** * Represents a measure of length and is capable of storing ±9.2 million kilometers at nanometer precision. */ @JvmInline public value class Length internal constructor(internal val rawNanometers: SaturatingLong) : Comparable { // region SI Arithmetic /** * Returns the constant [Velocity] required to travel this length in the specified [duration]. * * This operation attempts to retain precision, but for sufficiently large values of either this length or the * other [duration], some precision may be lost. * * @throws IllegalArgumentException if both this length and [duration] are infinite. */ public operator fun div(duration: Duration): Velocity = when { rawNanometers.isInfinite() && duration.isInfinite() -> { throwIllegalArgumentException("Dividing two infinite values yields an undefined result.") } rawNanometers.isInfinite() -> Velocity(rawNanometers * duration.sign) duration.isInfinite() -> Velocity(0L.saturated) else -> calculateVelocity(duration) } private fun calculateVelocity(duration: Duration): Velocity { // Try to find the right level which we can perform this operation at without losing precision. if (duration.isPreciseToNanosecond()) { val velocity = nanosPerNs(rawNanometers, duration.inWholeNanoseconds) if (velocity.isFinite()) return velocity } val ms = duration.inWholeMilliseconds val velcity = nanosPerMs(rawNanometers, ms) if (velcity.isFinite()) return velcity return Velocity((rawNanometers / ms) * 1_000) } private fun nanosPerMs(nanos: SaturatingLong, ms: Long): Velocity { // 1 nanometer per 1 millisecond is 1,000 nanometers / second. val nanosPerMs = nanos / ms val picoRemainder = (nanos % ms) * 1_000 return (nanosPerMs * 1_000).nmPerSecond + picosPerMs(picoRemainder, ms) } private fun picosPerMs(picos: SaturatingLong, ms: Long): Velocity { // 1 picometer per 1 millisecond is 1 nanometer / second. return Velocity(picos / ms) } private fun nanosPerNs(nanos: SaturatingLong, ns: Long): Velocity { // 1 nanometer per 1 nanosecond is 1,000,000,000 nanometers / second. val nanosPerNs = nanos / ns val picoRemainder = (nanos % ns) * 1_000 return (nanosPerNs * 1_000_000_000).nmPerSecond + picosPerNs(picoRemainder, ns) } private fun picosPerNs(picos: SaturatingLong, ns: Long): Velocity { // 1 picometer per 1 nanosecond is 1,000,000 nanometers / second. val picosPerNs = picos / ns val femtoRemainder = (picos % ns) * 1_000 return (picosPerNs * 1_000_000).nmPerSecond + femtosPerNs(femtoRemainder, ns) } private fun femtosPerNs(femtos: SaturatingLong, ns: Long): Velocity { // 1 femtometer per 1 nanosecond is 1,000 nanometers / second. val femtosPerNs = femtos / ns val attoRemainder = (femtos % ns) * 1_000 return (femtosPerNs * 1_000).nmPerSecond + attometersPerNs(attoRemainder, ns) } private fun attometersPerNs(attos: SaturatingLong, ns: Long): Velocity { // 1 attometer per 1 nanosecond is 1 nanometer / second. return Velocity(attos / ns) } /** * Returns the [Duration] required to travel this length at the specified constant [velocity]. * * This operation attempts to retain precision, but for sufficiently large values of either this length or the * other [velocity], some precision may be lost. * * @throws IllegalArgumentException if both this length and [velocity] are infinite. */ public operator fun div(velocity: Velocity): Duration = when { isInfinite() && velocity.isInfinite() -> { throwIllegalArgumentException("Dividing two infinite values yields an undefined result.") } isInfinite() -> Duration.INFINITE / velocity.rawNanometersPerSecond.sign / rawNanometers.sign velocity.isInfinite() -> Duration.ZERO else -> calculateDuration(velocity) } private fun calculateDuration(velocity: Velocity): Duration { val duration = seconds(rawNanometers, velocity.rawNanometersPerSecond) return if (duration.isFinite()) { duration } else { // Do coarse operation to avoid returning infinity. (rawNanometers / velocity.rawNanometersPerSecond).rawValue.seconds } } private fun seconds(nanometers: SaturatingLong, nanometersPerSecond: SaturatingLong): Duration { // 1 nanometer divided by 1 nm/s is 1 second. val seconds = (nanometers / nanometersPerSecond).rawValue.seconds val picometers = (nanometers % nanometersPerSecond) * 1_000 return seconds + milliseconds(picometers, nanometersPerSecond) } private fun milliseconds(picometers: SaturatingLong, nanometersPerSecond: SaturatingLong): Duration { // 1 picometer divided by 1 nm/s is 1 millisecond. val milliseconds = (picometers / nanometersPerSecond).rawValue.milliseconds val femtometers = (picometers % nanometersPerSecond) * 1_000 return milliseconds + microseconds(femtometers, nanometersPerSecond) } private fun microseconds(femtometers: SaturatingLong, nanometersPerSecond: SaturatingLong): Duration { // 1 femtometer divided by 1 nm/s is 1 microsecond. val microseconds = (femtometers / nanometersPerSecond).rawValue.microseconds val attometers = (femtometers % nanometersPerSecond) * 1_000 return microseconds + nanoseconds(attometers, nanometersPerSecond) } private fun nanoseconds(attometers: SaturatingLong, nanometersPerSecond: SaturatingLong): Duration { // 1 attometer divided by 1 nm/s is 1 nanosecond. return (attometers / nanometersPerSecond).rawValue.nanoseconds } /** * Returns the resulting [Area] after multiplying this length by the [other] length value. * * This operation attempts to retain precision, but for sufficiently large values of either this length or the * [other] length, some precision may be lost. * * @throws IllegalArgumentException when this length is [infinite][isInfinite] and [other] is 0 or vice versa. */ public operator fun times(other: Length): Area { // Omit micrometer and nanometer components for now. The maximum value these components could ever produce is // 998,001,998,001 nanometers², and therefore micrometers and nanometers are always lost to precision rounding // when converting to millimeters². In the future we may choose more precise measures of Area and this might // be revisited. return toSaturatedInternationalComponents { giga, mega, kilo, meters, centi, milli, _, _ -> other.toSaturatedInternationalComponents { otherGiga, otherMega, otherKilo, otherMeters, otherCenti, otherMilli, _, _ -> val gigaSquared = giga * otherGiga val megaSquared = mega * otherMega val kiloSquared = kilo * otherKilo val metersSquared = meters * otherMeters val centiSquared = centi * otherCenti val millisSquared = milli * otherMilli if (gigaSquared != 0L.saturated) { // We can't represent gigameter² at millimeter² precision. Area(POSITIVE_INFINITY * gigaSquared.sign) } else { val megaMillis = megaSquared * 1_000_000_000_000_000_000 val kiloMillis = kiloSquared * 1_000_000_000_000 val meterMillis = metersSquared * 1_000_000 val centiMillis = centiSquared * 100 Area(megaMillis + kiloMillis + meterMillis + centiMillis + millisSquared) } } } } /** * Returns the resulting [Volume] after applying this length over the specified [area]. * * This operation attempts to retain precision, but for sufficiently large values of this length or the * specified [area] some precision may be lost. * * @throws IllegalArgumentException if this length is [infinite][isInfinite] and [area] is zero, or if this length * is zero and [area] is infinite. */ public operator fun times(area: Area): Volume = area * this /** * Returns the amount of [Energy] required to apply the specified [force] over this length. * * This operation attempts to retain precision, but for sufficiently large values of either this length or the * specified [force], some precision may be lost. * * @throws IllegalArgumentException when this length is [infinite][isInfinite] and [force] is 0 or vice versa. */ public operator fun times(force: Force): Energy = force * this /** * Returns an [Area] representing a square with two dimensions of this length. * * This operation attempts to retain precision, but for sufficiently large values of either this length some * precision may be lost. */ public fun squared(): Area = this * this /** * Returns a [Volume] representing a cube with three dimensions of this length. * * This operation attempts to retain precision, but for sufficiently large values of either this length some * precision may be lost. */ public fun cubed(): Volume = this * this * this // endregion // region Scalar Arithmetic /** * Returns the number that is the ratio of this and the [other] length value. * * @throws IllegalArgumentException when both this and the [other] length are [infinite][isInfinite]. */ public operator fun div(other: Length): Double { return rawNanometers.toDouble() / other.rawNanometers.toDouble() } /** * Returns a length whose value is this length value divided by the specified [scale]. */ public operator fun div(scale: Int): Length { return div(scale.toLong()) } /** * Returns a length whose value is this length value divided by the specified [scale]. * * @throws IllegalArgumentException when [scale] is equal to [Long.MAX_VALUE] or [Long.MIN_VALUE] and this * length is [infinite][isInfinite]. */ public operator fun div(scale: Long): Length { return Length(rawNanometers / scale) } /** * Returns the negative of this length value. */ public operator fun unaryMinus(): Length = Length(-rawNanometers) /** * Returns a length whose value is the difference between this and the [other] length value. * * @throws IllegalArgumentException if this length and the [other] length are both * [infinite][isInfinite] but have equivalent signs. */ public operator fun minus(other: Length): Length { return Length(rawNanometers - other.rawNanometers) } /** * Returns a length whose value is the sum between this and the [other] length value. * * @throws IllegalArgumentException if this length and the [other] length are both * [infinite][isInfinite] but have differing signs. */ public operator fun plus(other: Length): Length { return Length(rawNanometers + other.rawNanometers) } /** * Returns a length whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this length is [infinite][isInfinite] and [scale] is 0. */ public operator fun times(scale: Int): Length { return times(scale.toLong()) } /** * Returns a length whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this length is [infinite][isInfinite] and [scale] is 0, or when this * length is 0 and scale is [Long.MAX_VALUE] or [Long.MIN_VALUE]. */ public operator fun times(scale: Long): Length { return Length(rawNanometers * scale) } /** * Returns a length whose value is multiplied by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this length is [infinite][isInfinite] and [scale] is 0.0 or when this length is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun times(scale: Double): Length = Length(rawNanometers * scale) /** * Returns a length whose value is divided by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this length is [infinite][isInfinite] and [scale] is 0.0 or when this length is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun div(scale: Double): Length = Length(rawNanometers / scale) // endregion // region Length to Scalar Conversions private fun toSaturatedInternationalComponents( action: ( gigameters: SaturatingLong, megameters: SaturatingLong, kilometers: SaturatingLong, meters: SaturatingLong, centimeters: SaturatingLong, millimeters: SaturatingLong, micrometers: SaturatingLong, nanometers: SaturatingLong, ) -> T, ): T { val giga = rawNanometers / LengthUnit.International.Gigameter.nanometerScale val gigaRemainder = rawNanometers % LengthUnit.International.Gigameter.nanometerScale val mega = gigaRemainder / LengthUnit.International.Megameter.nanometerScale val megaRemainder = gigaRemainder % LengthUnit.International.Megameter.nanometerScale val kilo = megaRemainder / LengthUnit.International.Kilometer.nanometerScale val kiloRemainder = megaRemainder % LengthUnit.International.Kilometer.nanometerScale val meters = kiloRemainder / LengthUnit.International.Meter.nanometerScale val metersRemainder = kiloRemainder % LengthUnit.International.Meter.nanometerScale val centi = metersRemainder / LengthUnit.International.Centimeter.nanometerScale val centiRemainder = metersRemainder % LengthUnit.International.Centimeter.nanometerScale val milli = centiRemainder / LengthUnit.International.Millimeter.nanometerScale val milliRemainder = centiRemainder % LengthUnit.International.Millimeter.nanometerScale val micro = milliRemainder / LengthUnit.International.Micrometer.nanometerScale val nano = milliRemainder % LengthUnit.International.Micrometer.nanometerScale return action(giga, mega, kilo, meters, centi, milli, micro, nano) } /** * Splits this length into gigameters, megameters, kilometers, meters, centimeters, millimeters, micrometers, and * nanometers and executes the [action] with those components. The result of [action] is returned as the result of * this function. * * Infinite length values invoke [action] with [Long.MAX_VALUE] or [Long.MIN_VALUE] for every component, depending * on the infinite value's sign. */ public fun toInternationalComponents( action: ( gigameters: Long, megameters: Long, kilometers: Long, meters: Long, centimeters: Long, millimeters: Long, micrometers: Long, nanometers: Long, ) -> T, ): T = toSaturatedInternationalComponents { giga, mega, kilo, meters, centi, milli, micro, nano -> action( giga.rawValue, mega.rawValue, kilo.rawValue, meters.rawValue, centi.rawValue, milli.rawValue, micro.rawValue, nano.rawValue, ) } /** * Splits this length into miles, yards, feet, and inches and executes the [action] with those components. The * result of [action] is returned as the result of this function. * * Infinite length values invoke [action] with [Long.MAX_VALUE], [Long.MIN_VALUE], [Double.POSITIVE_INFINITY], or * [Double.NEGATIVE_INFINITY] for every component, depending on the infinite value's sign and the component's type. */ public fun toUnitedStatesCustomaryComponents( action: (miles: Long, yards: Long, feet: Long, inches: Double) -> T, ): T { val miles = rawNanometers / LengthUnit.UnitedStatesCustomary.Mile.nanometerScale val milesRemainder = rawNanometers % LengthUnit.UnitedStatesCustomary.Mile.nanometerScale val yards = milesRemainder / LengthUnit.UnitedStatesCustomary.Yard.nanometerScale val yardRemainder = milesRemainder % LengthUnit.UnitedStatesCustomary.Yard.nanometerScale val feet = yardRemainder / LengthUnit.UnitedStatesCustomary.Foot.nanometerScale val feetRemainder = yardRemainder % LengthUnit.UnitedStatesCustomary.Foot.nanometerScale val inches = feetRemainder.rawValue.nanometers.toDouble(LengthUnit.UnitedStatesCustomary.Inch) return action(miles.rawValue, yards.rawValue, feet.rawValue, inches) } /** * Returns the value of this length expressed as a [Long] number of the specified [unit]. Infinite values are * converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its sign. */ public fun toLong(unit: LengthUnit): Long { return (rawNanometers / unit.nanometerScale).rawValue } /** * Returns the value of this length expressed as a [Double] number of the specified [unit]. Infinite values are * converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] depending on its sign. */ public fun toDouble(unit: LengthUnit): Double { return this / unit.nanometerScale.nanometers } /** * Returns a fractional string representation of this length expressed in the specified [unit] and is rounded * to the specified [decimals]. */ public fun toString(unit: LengthUnit, decimals: Int = 0): String = when (rawNanometers.isInfinite()) { true -> rawNanometers.toString() false -> buildString { append(toDouble(unit).toDecimalString(decimals)) append(unit.symbol) } } /** * Returns a fractional string representation of this length expressed in the largest [LengthUnit.International] * quantity which is greater than or equal to 1. */ public override fun toString(): String { val largestUnit = LengthUnit.International.entries.asReversed().firstOrNull { unit -> rawNanometers.absoluteValue / unit.nanometerScale > 0 } return toString(largestUnit ?: Nanometer, decimals = 2) } // endregion // region Comparisons /** * Returns true if this length value is infinite. */ public fun isInfinite(): Boolean = rawNanometers.isInfinite() /** * Returns true if this length value is finite. */ public fun isFinite(): Boolean = rawNanometers.isFinite() /** * Compares this length with the [other] length. Returns zero if this length is equal * to the specified [other] length, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */ public override fun compareTo(other: Length): Int { return rawNanometers.compareTo(other.rawNanometers) } // endregion } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/type/Mass.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.toDecimalString import io.github.kevincianfarini.alchemist.unit.MassUnit import kotlin.jvm.JvmInline import kotlin.text.Typography.nbsp /** * Represents a measure of mass and is capable of storing ±9.2 billion kilograms at microgram precision. */ @JvmInline public value class Mass internal constructor(internal val rawMicrograms: SaturatingLong) : Comparable { // region SI Arithmetic /** * Returns the [Force] required to apply to this mass to achieve the specified [acceleration]. * * This operation attempts to retain precision, but for sufficiently large values of either this mass or the * specified [acceleration], some precision may be lost. * * @throws IllegalArgumentException when [acceleration] is [infinite][isInfinite] and this mass is 0 or vice versa. */ public operator fun times(acceleration: Acceleration): Force = acceleration * this // endregion // region Scalar Arithmetic /** * Returns a mass whose value is this mass value divided by the specified [scale]. */ public operator fun div(scale: Int): Mass = div(scale.toLong()) /** * Returns a mass whose value is this mass value divided by the specified [scale]. * * @throws IllegalArgumentException when [scale] is equal to [Long.MAX_VALUE] or [Long.MIN_VALUE] and this * mass is [infinite][isInfinite]. */ public operator fun div(scale: Long): Mass = Mass(rawMicrograms / scale) /** * Returns the number that is the ratio of this and the [other] mass value. * * @throws IllegalArgumentException when both this and the [other] mass are [infinite][isInfinite]. */ public operator fun div(other: Mass): Double = rawMicrograms.toDouble() / other.rawMicrograms.toDouble() /** * Returns the negative of this mass value. */ public operator fun unaryMinus(): Mass = Mass(-rawMicrograms) /** * Returns a mass whose value is the difference between this and the [other] mass value. * * @throws IllegalArgumentException if this mass and the [other] mass are both * [infinite][isInfinite] but have equivalent signs. */ public operator fun minus(other: Mass): Mass = Mass(rawMicrograms - other.rawMicrograms) /** * Returns a mass whose value is the sum between this and the [other] mass value. * * @throws IllegalArgumentException if this mass and the [other] mass are both * [infinite][isInfinite] but have differing signs. */ public operator fun plus(other: Mass): Mass = Mass(rawMicrograms + other.rawMicrograms) /** * Returns a mass whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this mass is [infinite][isInfinite] and [scale] is 0. */ public operator fun times(scale: Int): Mass = times(scale.toLong()) /** * Returns a mass whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this mass is [infinite][isInfinite] and [scale] is 0, or when this * mass is 0 and scale is [Long.MAX_VALUE] or [Long.MIN_VALUE]. */ public operator fun times(scale: Long): Mass = Mass(rawMicrograms * scale) /** * Returns a mass whose value is multiplied by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this mass is [infinite][isInfinite] and [scale] is 0.0 or when this mass is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun times(scale: Double): Mass = Mass(rawMicrograms * scale) /** * Returns a mass whose value is divided by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this mass is [infinite][isInfinite] and [scale] is 0.0 or when this mass is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun div(scale: Double): Mass = Mass(rawMicrograms / scale) // endregion // region Mass to Scalar Conversions /** * Splits this mass into teragrams, gigagrams, megagrams, kilograms, grams, milligrams, and micrograms and * executes the [action] with those components. The result of [action] is returned as the result of this function. * * Infinite mass values invoke [action] with [Long.MAX_VALUE] or [Long.MIN_VALUE] for every component, depending * on the infinite value's sign. */ public fun toInternationalComponents( action: ( teragrams: Long, gigagrams: Long, megagrams: Long, kilograms: Long, grams: Long, milligrams: Long, micrograms: Long, ) -> T ): T { val tera = rawMicrograms / MassUnit.International.Teragram.microgramScale val teraRemainder = rawMicrograms % MassUnit.International.Teragram.microgramScale val giga = teraRemainder / MassUnit.International.Gigagram.microgramScale val gigaRemainder = teraRemainder % MassUnit.International.Gigagram.microgramScale val mega = gigaRemainder / MassUnit.International.Megagram.microgramScale val megaRemainder = gigaRemainder % MassUnit.International.Megagram.microgramScale val kilo = megaRemainder / MassUnit.International.Kilogram.microgramScale val kiloRemainder = megaRemainder % MassUnit.International.Kilogram.microgramScale val grams = kiloRemainder / MassUnit.International.Gram.microgramScale val gramRemainder = kiloRemainder % MassUnit.International.Gram.microgramScale val milli = gramRemainder / MassUnit.International.Milligram.microgramScale val micro = gramRemainder % MassUnit.International.Megagram.microgramScale return action( tera.rawValue, giga.rawValue, mega.rawValue, kilo.rawValue, grams.rawValue, milli.rawValue, micro.rawValue ) } /** * Returns the value of this mass expressed as a [Long] number of the specified [unit]. Infinite values are * converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its sign. */ public fun toLong(unit: MassUnit): Long { return (rawMicrograms / unit.microgramScale).rawValue } /** * Returns the value of this mass expressed as a [Double] number of the specified [unit]. Infinite values are * converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] depending on its sign. */ public fun toDouble(unit: MassUnit): Double { return rawMicrograms.toDouble() / unit.microgramScale.toDouble() } /** * Returns a fractional string representation of this mass expressed in the specified [unit] and is rounded * to the specified [decimals]. */ public fun toString(unit: MassUnit, decimals: Int = 0): String = when (isInfinite()) { true -> rawMicrograms.toString() false -> buildString { append(toDouble(unit).toDecimalString(decimals)) append(nbsp) append(unit.symbol) } } /** * Returns a fractional string representation of this mass expressed in the largest [MassUnit.International] * quantity which is greater than or equal to 1. */ override fun toString(): String { val largestUnit = MassUnit.International.entries.asReversed().firstOrNull { unit -> rawMicrograms.absoluteValue / unit.microgramScale > 0 } return toString(largestUnit ?: MassUnit.International.Microgram, decimals = 2) } // endregion // region Comparisons /** * Returns true if this area value is infinite. */ public fun isInfinite(): Boolean = rawMicrograms.isInfinite() /** * Returns true if this area value is finite. */ public fun isFinite(): Boolean = rawMicrograms.isFinite() /** * Compares this mass with the [other] mass. Returns zero if this mass is equal * to the specified [other] mass, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */ override fun compareTo(other: Mass): Int = rawMicrograms.compareTo(other.rawMicrograms) // endregion } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/type/Power.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.POSITIVE_INFINITY import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.sign import io.github.kevincianfarini.alchemist.internal.toDecimalComponents import io.github.kevincianfarini.alchemist.internal.toDecimalString import io.github.kevincianfarini.alchemist.scalar.microwatts import io.github.kevincianfarini.alchemist.unit.PowerUnit import kotlin.jvm.JvmInline import kotlin.time.Duration /** * Represents an amount of power and is capable of storing ±9.22 terawatts at microwatt precision. */ @JvmInline public value class Power internal constructor(private val rawMicrowatts: SaturatingLong) : Comparable { // region SI Arithmetic /** * Returns the resulting [Energy] from applying this power over the specified [duration]. * * This operation attempts to retain precision, but for sufficiently large values of either this power or [duration], * some precision may be lost. * * @throws IllegalArgumentException if this power is infinite and duration is zero, or if this power is zero and * duration is infinite. */ public operator fun times(duration: Duration): Energy { return when { duration.isInfinite() || rawMicrowatts.isInfinite() -> { Energy(POSITIVE_INFINITY * duration.sign * rawMicrowatts) } else -> duration.toDecimalComponents { kiloseconds, seconds, millis, micros, nanos -> // Try to find the right level which we can perform this operation at without losing precision. // -------------------------------------------------------------------------------------------- // 1 microwatt * 1 nanosecond is 1 femtojoule. // 1 microwatt * 1 microsecond is 1 picojoule. // 1 microwatt * 1 millisecond is 1 nanojoule. // 1 microwatt * 1 second is 1 microjoule. // 1 microwatt * 1,000 seconds is 1 millijoule. // -------------------------------------------------------------------------------------------- val millijoules = rawMicrowatts * kiloseconds val microjoules = rawMicrowatts * seconds val nanojoules = rawMicrowatts * millis val picojoules = rawMicrowatts * micros val femtojoules = rawMicrowatts * nanos // ----------- Try femtojoule precision. ------------------------------------------------------ val femtoJ = femtojoules + (picojoules * 1_000) + (nanojoules * 1_000_000) + (microjoules * 1_000_000_000) + (millijoules * 1_000_000_000_000) if (femtoJ.isFinite()) return@toDecimalComponents Energy(femtoJ / 1_000_000_000_000) // ----------- Try picojoule precision. ------------------------------------------------------- val picoJ = (femtojoules / 1_000) + picojoules + (nanojoules * 1_000) + (microjoules * 1_000_000) + (millijoules * 1_000_000_000) if (picoJ.isFinite()) return@toDecimalComponents Energy(picoJ / 1_000_000_000) // ----------- Try nanojoule precision. ------------------------------------------------------- val nanoJ = (femtojoules / 1_000_000) + (picojoules / 1_000) + nanojoules + (microjoules * 1_000) + (millijoules * 1_000_000) if (nanoJ.isFinite()) return@toDecimalComponents Energy(nanoJ / 1_000_000) // ----------- Try microjoule precision. ------------------------------------------------------- val microJ = (femtojoules / 1_000_000_000) + (picojoules / 1_000_000) + (nanojoules / 1_000) + microjoules + (millijoules * 1_000) if (microJ.isFinite()) return@toDecimalComponents Energy(microJ / 1_000) // ----------- Default microjoule precision. --------------------------------------------------- val milliJ = (femtojoules / 1_000_000_000_000) + (picojoules / 1_000_000_000) + (nanojoules / 1_000_000) + (microjoules / 1_000) + millijoules Energy(milliJ) } } } // endregion // region Scalar Arithmetic /** * Returns the number that is the ratio of this and the [other] power value. * * @throws IllegalArgumentException when both this and the [other] power are [infinite][isInfinite]. */ public operator fun div(other: Power): Double { return rawMicrowatts.toDouble() / other.rawMicrowatts.toDouble() } /** * Returns a power whose value is this power value divided by the specified [scale]. */ public operator fun div(scale: Int): Power = div(scale.toLong()) /** * Returns a power whose value is this power value divided by the specified [scale]. * * @throws IllegalArgumentException when [scale] is equal to [Long.MAX_VALUE] or [Long.MIN_VALUE] and this * power is [infinite][isInfinite]. */ public operator fun div(scale: Long): Power = Power(rawMicrowatts / scale) /** * Returns the negative of this power value. */ public operator fun unaryMinus(): Power = Power(-rawMicrowatts) /** * Returns a power whose value is the difference between this and the [other] power value. * * @throws IllegalArgumentException if this power and the [other] power are both * [infinite][isInfinite] but have equivalent signs. */ public operator fun minus(other: Power): Power = Power(rawMicrowatts - other.rawMicrowatts) /** * Returns a power whose value is the sum between this and the [other] power value. * * @throws IllegalArgumentException if this power and the [other] power are both * [infinite][isInfinite] but have differing signs. */ public operator fun plus(other: Power): Power = Power(rawMicrowatts + other.rawMicrowatts) /** * Returns a power whose value is this power multiplied by the specified [scale]. * * @throws IllegalArgumentException when this power is [infinite][isInfinite] and [scale] is 0. */ public operator fun times(scale: Int): Power = times(scale.toLong()) /** * Returns a power whose value is this power multiplied by the specified [scale]. * * @throws IllegalArgumentException when this power is [infinite][isInfinite] and [scale] is 0, or when this * power is 0 and scale is [Long.MAX_VALUE] or [Long.MIN_VALUE]. */ public operator fun times(scale: Long): Power = Power(rawMicrowatts * scale) /** * Returns a power whose value is multiplied by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this power is [infinite][isInfinite] and [scale] is 0.0 or when this power is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun times(scale: Double): Power = Power(rawMicrowatts * scale) /** * Returns a power whose value is divided by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this power is [infinite][isInfinite] and [scale] is 0.0 or when this power is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun div(scale: Double): Power = Power(rawMicrowatts / scale) // endregion // region Power to Scalar Conversions /** * Returns the value of this power expressed as a [Long] number of the specified [unit]. Infinite values are * converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its sign. */ public fun toLong(unit: PowerUnit): Long { return (rawMicrowatts / unit.microwattScale).rawValue } /** * Returns the value of this power expressed as a [Double] number of the specified [unit]. Infinite values are * converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] depending on its sign. */ public fun toDouble(unit: PowerUnit): Double { return this / unit.microwattScale.microwatts } /** * Returns a fractional string representation of this power expressed in the specified [unit] and is rounded * to the specified [decimals]. */ public fun toString(unit: PowerUnit, decimals: Int = 0): String = when (rawMicrowatts.isInfinite()) { true -> rawMicrowatts.toString() false -> buildString { append(toDouble(unit).toDecimalString(decimals)) append(unit.symbol) } } /** * Returns a fractional string representation of this power expressed in the largest [PowerUnit.International] * quantity which is greater than or equal to 1. */ override fun toString(): String { val largestUnit = PowerUnit.International.entries.asReversed().firstOrNull { unit -> rawMicrowatts.absoluteValue / unit.microwattScale > 0 } return toString(largestUnit ?: PowerUnit.International.Microwatt, decimals = 2) } // endregion // region Comparisons /** * Returns true if this area value is infinite. */ public fun isInfinite(): Boolean = rawMicrowatts.isInfinite() /** * Returns true if this area value is finite. */ public fun isFinite(): Boolean = rawMicrowatts.isFinite() /** * Compares this power with the [other] power. Returns zero if this power is equal * to the specified [other] power, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */ public override fun compareTo(other: Power): Int { return rawMicrowatts.compareTo(other.rawMicrowatts) } // endregion } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/type/Temperature.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.toDecimalString import io.github.kevincianfarini.alchemist.unit.TemperatureUnit import io.github.kevincianfarini.alchemist.unit.convertNanokelvinsToThis import kotlin.jvm.JvmInline import kotlin.math.roundToLong /** * Represents a temperature and is capable of storing ±9.2 billion Kelvin (±9.2 billion °C, ±16.6 billion °F) at * nanokelvin precision. */ @JvmInline public value class Temperature internal constructor(private val rawNanokelvin: SaturatingLong) : Comparable { // region Scalar Arithmetic /** * Returns the number that is the ratio of this and the [other] temperature value. * * @throws IllegalArgumentException when both this and the [other] temperature are [infinite][isInfinite]. */ public operator fun div(other: Temperature): Double { return rawNanokelvin.toDouble() / other.rawNanokelvin.toDouble() } /** * Returns a temperature whose value is this temperature value divided by the specified [scale]. */ public operator fun div(scale: Int): Temperature = div(scale.toLong()) /** * Returns a temperature whose value is this temperature value divided by the specified [scale]. * * @throws IllegalArgumentException when [scale] is equal to [Long.MAX_VALUE] or [Long.MIN_VALUE] and this * temperature is [infinite][isInfinite]. */ public operator fun div(scale: Long): Temperature = Temperature(rawNanokelvin / scale) /** * Returns the negative of this temperature value. */ public operator fun unaryMinus(): Temperature = Temperature(-rawNanokelvin) /** * Returns a temperature whose value is the difference between this and the [other] temperature value. * * @throws IllegalArgumentException if this temperature and the [other] temperature are both * [infinite][isInfinite] but have equivalent signs. */ public operator fun minus(other: Temperature): Temperature = Temperature(rawNanokelvin - other.rawNanokelvin) /** * Returns a temperature whose value is the sum between this and the [other] temperature value. * * @throws IllegalArgumentException if this temperature and the [other] temperature are both * [infinite][isInfinite] but have differing signs. */ public operator fun plus(other: Temperature): Temperature = Temperature(rawNanokelvin + other.rawNanokelvin) /** * Returns a temperature whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this temperature is [infinite][isInfinite] and [scale] is 0. */ public operator fun times(scale: Int): Temperature = times(scale.toLong()) /** * Returns a temperature whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this temperature is [infinite][isInfinite] and [scale] is 0, or when this * temperature is 0 and scale is [Long.MAX_VALUE] or [Long.MIN_VALUE]. */ public operator fun times(scale: Long): Temperature = Temperature(rawNanokelvin * scale) // endregion // region Temperature to Scalar Conversions /** * Returns the value of this temperature expressed as a [Long] number of the specified [unit]. Infinite values are * converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its sign. */ public fun toLong(unit: TemperatureUnit): Long = toDouble(unit).roundToLong() /** * Returns the value of this temperature expressed as a [Double] number of the specified [unit]. Infinite values are * converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] depending on its sign. */ public fun toDouble(unit: TemperatureUnit): Double = unit.convertNanokelvinsToThis(rawNanokelvin) /** * Returns a fractional string representation of this temperature expressed in the specified [unit] and is rounded * to the specified [decimals]. */ public fun toString(unit: TemperatureUnit, decimals: Int = 0): String { return buildString { append(unit.convertNanokelvinsToThis(rawNanokelvin).toDecimalString(decimals)) append(unit.symbol) } } /** * Returns a fractional string representation of this temperature expressed in the largest * [TemperatureUnit.International] quantity which is greater than or equal to 1. */ public override fun toString(): String { return toString(toStringUnit(), decimals = 2) } private fun toStringUnit(): TemperatureUnit = when { TemperatureUnit.International.Gigakelvin.convertNanokelvinsToThis(rawNanokelvin) >= 1.0 -> { TemperatureUnit.International.Gigakelvin } TemperatureUnit.International.Megakelvin.convertNanokelvinsToThis(rawNanokelvin) >= 1.0 -> { TemperatureUnit.International.Megakelvin } TemperatureUnit.International.Kilokelvin.convertNanokelvinsToThis(rawNanokelvin) >= 1.0 -> { TemperatureUnit.International.Kilokelvin } TemperatureUnit.International.Kelvin.convertNanokelvinsToThis(rawNanokelvin) >= 1.0 -> { TemperatureUnit.International.Kelvin } TemperatureUnit.International.Millikelvin.convertNanokelvinsToThis(rawNanokelvin) >= 1.0 -> { TemperatureUnit.International.Millikelvin } TemperatureUnit.International.Microkelvin.convertNanokelvinsToThis(rawNanokelvin) >= 1.0 -> { TemperatureUnit.International.Microkelvin } else -> TemperatureUnit.International.Nanokelvin } // endregion // region Comparisons /** * Returns true if this area value is infinite. */ public fun isInfinite(): Boolean = rawNanokelvin.isInfinite() /** * Returns true if this area value is finite. */ public fun isFinite(): Boolean = rawNanokelvin.isFinite() /** * Compares this temperature with the [other] temperature. Returns zero if this temperature is equal * to the specified [other] temperature, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */ public override fun compareTo(other: Temperature): Int { return rawNanokelvin.compareTo(other.rawNanokelvin) } // endregion } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/type/Velocity.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.isPreciseToNanosecond import io.github.kevincianfarini.alchemist.internal.saturated import io.github.kevincianfarini.alchemist.internal.secondScale import io.github.kevincianfarini.alchemist.internal.shortName import io.github.kevincianfarini.alchemist.internal.sign import io.github.kevincianfarini.alchemist.internal.throwIllegalArgumentException import io.github.kevincianfarini.alchemist.internal.toDecimalString import io.github.kevincianfarini.alchemist.scalar.nanometers import io.github.kevincianfarini.alchemist.scalar.nmPerSecond2 import io.github.kevincianfarini.alchemist.unit.LengthUnit import kotlin.jvm.JvmInline import kotlin.math.roundToLong import kotlin.text.Typography.nbsp import kotlin.time.Duration import kotlin.time.DurationUnit /** * Represents a measure of velocity and is capable of storing ±9.2 million km/s at nm/s precision. */ @JvmInline public value class Velocity internal constructor( internal val rawNanometersPerSecond: SaturatingLong ) : Comparable { // region SI Arithmetic /** * Returns the constant [Acceleration] required achieve this velocity in the specified [duration]. * * This operation attempts to retain precision, but for sufficiently large values of either this velocity or the * other [duration], some precision may be lost. * * @throws IllegalArgumentException if both this velocity and [duration] are infinite. */ public operator fun div(duration: Duration): Acceleration = when { isInfinite() && duration.isInfinite() -> { throwIllegalArgumentException("Dividing two infinite values yields an undefined result.") } isInfinite() -> Acceleration(rawNanometersPerSecond * duration.sign) duration.isInfinite() -> Acceleration(0L.saturated) else -> calculateAcceleration(duration) } private fun calculateAcceleration(duration: Duration): Acceleration { // Try to find the right level which we can perform this operation at without losing precision. if (duration.isPreciseToNanosecond()) { val acceleration = nanosPerNs2(rawNanometersPerSecond, duration.inWholeNanoseconds) if (acceleration.isFinite()) return acceleration } val ms = duration.inWholeMilliseconds val acceleration = nanosPerMs2(rawNanometersPerSecond, ms) if (acceleration.isFinite()) return acceleration return Acceleration((rawNanometersPerSecond / ms) * 1_000) } private fun nanosPerNs2(nanosPerSecond: SaturatingLong, ns: Long): Acceleration { val nanos = nanosPerSecond / ns val picoRemainder = (nanosPerSecond % ns) * 1_000 return (nanos * 1_000_000_000).nmPerSecond2 + picosPerNs2(picoRemainder, ns) } private fun picosPerNs2(picosPerSecond: SaturatingLong, ns: Long): Acceleration { val picos = picosPerSecond / ns val femtoRemainder = (picosPerSecond % ns) * 1_000 return (picos * 1_000_000).nmPerSecond2 + femtosPerNs2(femtoRemainder, ns) } private fun femtosPerNs2(femtosPerSecond: SaturatingLong, ns: Long): Acceleration { val femtos = femtosPerSecond / ns val attoRemainder = (femtosPerSecond % ns) * 1_000 return (femtos * 1_000).nmPerSecond2 + attosPerNs2(attoRemainder, ns) } private fun attosPerNs2(attosPerSecond: SaturatingLong, ns: Long): Acceleration { return Acceleration(attosPerSecond / ns) } private fun nanosPerMs2(nanosPerSecond: SaturatingLong, ms: Long): Acceleration { val nanos = nanosPerSecond / ms val picoRemainder = (nanosPerSecond % ms) * 1_000 return (nanos * 1_000).nmPerSecond2 + picosPerMs2(picoRemainder, ms) } private fun picosPerMs2(picosPerSecond: SaturatingLong, ms: Long): Acceleration { return Acceleration(picosPerSecond / ms) } /** * Returns the [Length] traveled at this velocity for the specified [duration]. * * This operation attempts to retain precision, but for sufficiently large values of either this velocity or * [duration], some precision may be lost. * * @throws IllegalArgumentException when this velocity is [infinite][isInfinite] and [duration] is 0 or vice versa. */ public operator fun times(duration: Duration): Length = duration.toComponents { seconds, nanoseconds -> val secondComponent = (rawNanometersPerSecond * seconds).nanometers val preciseNanosecondComponent = ((rawNanometersPerSecond * nanoseconds) / 1_000_000_000).nanometers if (preciseNanosecondComponent.isFinite()) { secondComponent + preciseNanosecondComponent } else { val coarseNanosecondComponent = ((rawNanometersPerSecond / 1_000_000_000) * nanoseconds).nanometers secondComponent + coarseNanosecondComponent } } // endregion // region Scalar Arithmetic /** * Returns the number that is the ratio of this and the [other] velocity value. * * @throws IllegalArgumentException when both this and the [other] velocity are [infinite][isInfinite]. */ public operator fun div(other: Velocity): Double { return rawNanometersPerSecond.toDouble() / other.rawNanometersPerSecond.toDouble() } /** * Returns a velocity whose value is this velocity value divided by the specified [scale]. */ public operator fun div(scale: Int): Velocity = div(scale.toLong()) /** * Returns a velocity whose value is this velocity value divided by the specified [scale]. * * @throws IllegalArgumentException when [scale] is equal to [Long.MAX_VALUE] or [Long.MIN_VALUE] and this * velocity is [infinite][isInfinite]. */ public operator fun div(scale: Long): Velocity = Velocity(rawNanometersPerSecond / scale) /** * Returns the negative of this velocity value. */ public operator fun unaryMinus(): Velocity = Velocity(-rawNanometersPerSecond) /** * Returns a velocity whose value is the difference between this and the [other] velocity value. * * @throws IllegalArgumentException if this velocity and the [other] velocity are both * [infinite][isInfinite] but have equivalent signs. */ public operator fun minus(other: Velocity): Velocity { return Velocity(rawNanometersPerSecond - other.rawNanometersPerSecond) } /** * Returns a velocity whose value is the sum between this and the [other] velocity value. * * @throws IllegalArgumentException if this velocity and the [other] velocity are both * [infinite][isInfinite] but have differing signs. */ public operator fun plus(other: Velocity): Velocity { return Velocity(rawNanometersPerSecond + other.rawNanometersPerSecond) } /** * Returns a velocity whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this velocity is [infinite][isInfinite] and [scale] is 0. */ public operator fun times(scale: Int): Velocity = times(scale.toLong()) /** * Returns a velocity whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this velocity is [infinite][isInfinite] and [scale] is 0, or when this * velocity is 0 and scale is [Long.MAX_VALUE] or [Long.MIN_VALUE]. */ public operator fun times(scale: Long): Velocity = Velocity(rawNanometersPerSecond * scale) /** * Returns a velocity whose value is multiplied by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this velocity is [infinite][isInfinite] and [scale] is 0.0 or when this velocity is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun times(scale: Double): Velocity = Velocity(rawNanometersPerSecond * scale) /** * Returns a velocity whose value is divided by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this velocity is [infinite][isInfinite] and [scale] is 0.0 or when this velocity is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun div(scale: Double): Velocity = Velocity(rawNanometersPerSecond / scale) // endregion // region Velocity to Scalar Conversions /** * Returns the value of this velocity expressed as a [Long] number of the specified [lengthUnit] per [durationUnit]. * Infinite values are converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its sign. */ public fun toLong(lengthUnit: LengthUnit, durationUnit: DurationUnit): Long { return toDouble(lengthUnit, durationUnit).roundToLong() } /** * Returns the value of this velocity expressed as a [Double] number of the specific [lengthUnit] per * [durationUnit]. Infinite values are converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] * depending on its sign. */ public fun toDouble(lengthUnit: LengthUnit, durationUnit: DurationUnit = DurationUnit.SECONDS): Double { return (rawNanometersPerSecond.toDouble() / lengthUnit.nanometerScale.toDouble()) * durationUnit.secondScale } /** * Returns a fractional string representation of this velocity expressed in the specified [lengthUnit] per * [durationUnit]. */ public fun toString( lengthUnit: LengthUnit, durationUnit: DurationUnit = DurationUnit.SECONDS, decimals: Int = 0, ): String { return when (isInfinite()) { true -> rawNanometersPerSecond.toString() false -> buildString { append(toDouble(lengthUnit, durationUnit).toDecimalString(decimals)) append(nbsp) append(lengthUnit.symbol) append("/") append(durationUnit.shortName) } } } /** * Returns a fractional string representation of this velocity expressed in the largest [LengthUnit.International] * per [second][DurationUnit.SECONDS] which is greater than or equal to 1. */ override fun toString(): String { val lengthUnit = LengthUnit.International.entries.asReversed().firstOrNull { unit -> rawNanometersPerSecond.absoluteValue / unit.nanometerScale > 0 } return toString(lengthUnit ?: LengthUnit.International.Nanometer, DurationUnit.SECONDS, decimals = 2) } // endregion // region Comparisons /** * Returns true if this velocity value is infinite. */ public fun isFinite(): Boolean = rawNanometersPerSecond.isFinite() /** * Returns true if this velocity value is finite. */ public fun isInfinite(): Boolean = rawNanometersPerSecond.isInfinite() /** * Compares this velocity with the [other] velocity. Returns zero if this velocity is equal * to the specified [other] velocity, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */ override fun compareTo(other: Velocity): Int = rawNanometersPerSecond.compareTo(other.rawNanometersPerSecond) // endregion } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/type/Volume.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.toDecimalString import io.github.kevincianfarini.alchemist.scalar.centimeters import io.github.kevincianfarini.alchemist.scalar.mm2 import io.github.kevincianfarini.alchemist.unit.LengthUnit import io.github.kevincianfarini.alchemist.unit.VolumeUnit import kotlin.jvm.JvmInline import kotlin.math.pow import kotlin.math.roundToLong /** * Represents a measure of volume and is capable of storing ±9.2 trillion meters³ (±9.2 quadrillion liters) at * centimeter³ (milliliter) precision. */ @JvmInline public value class Volume internal constructor(private val rawCubicCentimeters: SaturatingLong) : Comparable { // region SI Arithmetic /** * Returns the resulting [Length] after dividing this volume over the specified [area]. * * This operation attempts to retain precision, but for sufficiently large values of either this volume or * [area], some precision may be lost. * * @throws IllegalArgumentException if both this volume and [area] are infinite. */ public operator fun div(area: Area): Length { // TODO This is simplistic and we should attempt to retain precision in the future. return ((rawCubicCentimeters / area.rawMillimetersSquared) * 100).centimeters } /** * Returns the resulting [Area] after dividing this volume over the specified [length]. * * This operation attempts to retain precision, but for sufficiently large values of either this volume or * [length], some precision may be lost. * * @throws IllegalArgumentException if both this volume and [length] are infinite. */ public operator fun div(length: Length): Area { // TODO This is simplistic and we should attempt to retain precision in the future. return ((rawCubicCentimeters / length.rawNanometers) * 1_000_000_000).mm2 } // endregion // region Scalar Arithmetic /** * Returns the number that is the ratio of this and the [other] volume value. * * @throws IllegalArgumentException when both this and the [other] volume are [infinite][isInfinite]. */ public operator fun div(other: Volume): Double { return rawCubicCentimeters.toDouble() / other.rawCubicCentimeters.toDouble() } /** * Returns a volume whose value is this volume value divided by the specified [scale]. */ public operator fun div(scale: Int): Volume = div(scale.toLong()) /** * Returns a volume whose value is this volume value divided by the specified [scale]. * * @throws IllegalArgumentException when [scale] is equal to [Long.MAX_VALUE] or [Long.MIN_VALUE] and this * volume is [infinite][isInfinite]. */ public operator fun div(scale: Long): Volume = Volume(rawCubicCentimeters / scale) /** * Returns the negative of this volume value. */ public operator fun unaryMinus(): Volume = Volume(-rawCubicCentimeters) /** * Returns a volume whose value is the difference between this and the [other] volume value. * * @throws IllegalArgumentException if this volume and the [other] volume are both * [infinite][isInfinite] but have equivalent signs. */ public operator fun minus(other: Volume): Volume = Volume(rawCubicCentimeters - other.rawCubicCentimeters) /** * Returns a volume whose value is the sum between this and the [other] volume value. * * @throws IllegalArgumentException if this volume and the [other] volume are both * [infinite][isInfinite] but have differing signs. */ public operator fun plus(other: Volume): Volume = Volume(rawCubicCentimeters + other.rawCubicCentimeters) /** * Returns a volume whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this volume is [infinite][isInfinite] and [scale] is 0. */ public operator fun times(scale: Int): Volume = times(scale.toLong()) /** * Returns a volume whose value is multiplied by the specified [scale]. * * @throws IllegalArgumentException when this volume is [infinite][isInfinite] and [scale] is 0, or when this * volume is 0 and scale is [Long.MAX_VALUE] or [Long.MIN_VALUE]. */ public operator fun times(scale: Long): Volume = Volume(rawCubicCentimeters * scale) /** * Returns a volume whose value is multiplied by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this volume is [infinite][isInfinite] and [scale] is 0.0 or when this volume is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun times(scale: Double): Volume = Volume(rawCubicCentimeters * scale) /** * Returns a volume whose value is divided by the specified [scale]. This operation may be rounded when the result * cannot be precisely represented with a [Double] number. * * @throws IllegalArgumentException when this volume is [infinite][isInfinite] and [scale] is 0.0 or when this volume is 0 * and scale is [infinite][Double.isInfinite]. */ public operator fun div(scale: Double): Volume = Volume(rawCubicCentimeters / scale) // endregion // region Volume to Scalar Conversions /** * Returns the value of this velocity expressed as a [Long] number of the specified [unit]. Infinite values * are converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its sign. */ public fun toLong(unit: VolumeUnit): Long { return (rawCubicCentimeters / unit.cubicCentimetersScale).rawValue } /** * Returns the value of this velocity expressed as a [Long] number of the specified [LengthUnit]³. Infinite values * are converted to either [Long.MAX_VALUE] or [Long.MIN_VALUE] depending on its sign. */ public fun toLong(cubicUnit: LengthUnit): Long { return toDouble(cubicUnit).roundToLong() } /** * Returns the value of this volume expressed as a [Double] number of the specified [unit]. Infinite values * are converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] dependeing on its sign. */ public fun toDouble(unit: VolumeUnit): Double { return rawCubicCentimeters.toDouble() / unit.cubicCentimetersScale.toDouble() } /** * Returns the value of this volume expressed as a [Double] number of the specified [LengthUnit]³. Infinite values * are converted to either [Double.POSITIVE_INFINITY] or [Double.NEGATIVE_INFINITY] dependeing on its sign. */ public fun toDouble(cubicUnit: LengthUnit): Double { val cubicNanos = rawCubicCentimeters.toDouble() * 1e21 return cubicNanos / cubicUnit.nanometerScale.toDouble().pow(3) } /** * Returns a fractional string representation of this volume expressed in the specified [unit] and is rounded * to the specified [decimals]. */ public fun toString(unit: VolumeUnit, decimals: Int = 0): String = when { isInfinite() -> rawCubicCentimeters.toString() else -> buildString { append(toDouble(unit).toDecimalString(decimals)) append(unit.symbol) } } /** * Returns a fractional string representation of this volume expressed in the specified [LengthUnit]³ * and is rounded to the specified [decimals]. */ public fun toString(cubicUnit: LengthUnit, decimals: Int = 0): String = when { isInfinite() -> rawCubicCentimeters.toString() else -> buildString { append(toDouble(cubicUnit).toDecimalString(decimals)) append(cubicUnit.symbol) append("³") } } /** * Returns a fractional string representation of this volume expressed in the largest [LengthUnit]³ quantity * which is greater than or equal to 1. */ public override fun toString(): String { val lengthUnit = LengthUnit.International.entries.asReversed().firstOrNull { cubicUnit -> toDouble(cubicUnit) >= 1.0 } return toString(lengthUnit ?: LengthUnit.International.Centimeter, decimals = 2) } // endregion // region Comparisons /** * Returns true if this volume value is infinite. */ public fun isInfinite(): Boolean = rawCubicCentimeters.isInfinite() /** * Returns true if this volume value is finite. */ public fun isFinite(): Boolean = rawCubicCentimeters.isFinite() /** * Compares this volume with the [other] volume. Returns zero if this volume is equal * to the specified [other] volume, a negative number if it's less than [other], or a positive number * if it's greater than [other]. */ override fun compareTo(other: Volume): Int = rawCubicCentimeters.compareTo(other.rawCubicCentimeters) // endregion } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/type/duration.kt ================================================ package io.github.kevincianfarini.alchemist.type import kotlin.time.Duration /** * Returns the resulting [Velocity] after multiplying this duration by the specified [acceleration]. * * This operation attempts to retain precision, but for sufficiently large values of this duration or the * specified [acceleration] some precision may be lost. * * @throws IllegalArgumentException if this duration is infinite and [acceleration] is zero, or if this duration * is zero and [acceleration] is infinite. */ public operator fun Duration.times(acceleration: Acceleration): Velocity = acceleration * this /** * Returns the resulting [Energy] from applying the specified [power] over this duration. * * This operation attempts to retain precision, but for sufficiently large values of either this duration or [power], * some precision may be lost. * * @throws IllegalArgumentException if this duration is infinite and [power] is zero, or if this duration is zero and * [power] is infinite. */ public operator fun Duration.times(power: Power): Energy = power * this /** * Returns the resulting [Length] from applying the specified [velocity] over this duration. * * This operation attempts to retain precision, but for sufficiently large values of either this duration or [velocity], * some precision may be lost. * * @throws IllegalArgumentException if this duration is infinite and [velocity] is zero, or if this duration is zero and * [velocity] is infinite. */ public operator fun Duration.times(velocity: Velocity): Length = velocity * this ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/unit/AreaUnit.kt ================================================ package io.github.kevincianfarini.alchemist.unit /** * A unit of area precise to the millimeter². */ public interface AreaUnit { /** * The amount of millimeters² in this unit. Implementations of [AreaUnit] should be perfectly divisible by a * quantity of millimeters². */ public val millimetersSquaredScale: Long /** * The symbol of this unit. */ public val symbol: String /** * A non-standard representation of area commonly used as part of the metric system. */ public enum class Metric(override val symbol: String, override val millimetersSquaredScale: Long) : AreaUnit { Decimilliare("dma", 100), Centiare("ca", 1_000_000), Deciare("da", 10_000_000), Are("a", 100_000_000), Decare("daa", 1_000_000_000), Hectare("ha", 10_000_000_000), } } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/unit/EnergyUnit.kt ================================================ package io.github.kevincianfarini.alchemist.unit /** * A unit of energy precise to the millijoule. */ public interface EnergyUnit { /** * The amount of millijoules in this unit. Implementations of [EnergyUnit] should be perfectly divisible by a * quantity of millijoules. */ public val millijouleScale: Long /** * The symbol of this unit. */ public val symbol: String /** * An International System of Units standard representation of energy. */ public enum class International( override val millijouleScale: Long, override val symbol: String, ) : EnergyUnit { Millijoule(1, "mJ"), Joule(1_000, "J"), Kilojoule(1_000_000, "kJ"), Megajoule(1_000_000_000, "MJ"), Gigajoule(1_000_000_000_000, "GJ"), Tetrajoule(1_000_000_000_000_000, "TJ"), Petajoule(1_000_000_000_000_000_000, "PJ"), } /** * A non-standard representation of energy commonly used to measure electrical energy. */ public enum class Electricity( override val millijouleScale: Long, override val symbol: String, ) : EnergyUnit { MilliwattHour(3_600, "mWh"), WattHour(3_600_000, "Wh"), KilowattHour(3_600_000_000, "kWh"), MegawattHour(3_600_000_000_000, "MWh"), GigawattHour(3_600_000_000_000_000, "GWh"), TerawattHour(3_600_000_000_000_000_000, "TWh"), } } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/unit/ForceUnit.kt ================================================ package io.github.kevincianfarini.alchemist.unit /** * A unit of force precise to the nanonewton. */ public interface ForceUnit { /** * The symbol of this unit. */ public val symbol: String /** * The amount of nanonewtons in this unit. Implementations of [ForceUnit] should be perfectly divisible by a * quantity of nanonewtons. */ public val nanonewtonScale: Long /** * An International System of Units standard representation of force. */ public enum class International(override val symbol: String, override val nanonewtonScale: Long) : ForceUnit { Nanonewton("nN", 1), Micronewton("μN", 1_000), Millinewton("mN", 1_000_000), Newton("N", 1_000_000_000), Kilonewton("kN", 1_000_000_000_000), Meganewton("MN", 1_000_000_000_000_000), Giganewton("GN", 1_000_000_000_000_000_000) } } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/unit/LengthUnit.kt ================================================ package io.github.kevincianfarini.alchemist.unit /** * A unit of length precise to the nanometer. */ public interface LengthUnit { /** * The amount of nanometers in this unit. Implementations of [LengthUnit] should be perfectly divisible by a * quantity of nanometers. */ public val nanometerScale: Long /** * The symbol of this unit. */ public val symbol: String /** * An International System of Units standard representation of length. */ public enum class International( override val nanometerScale: Long, override val symbol: String, ) : LengthUnit { Nanometer(1, "nm"), Micrometer(1_000, "μm"), Millimeter(1_000_000, "mm"), Centimeter(10_000_000, "cm"), Meter(1_000_000_000, "m"), Kilometer(1_000_000_000_000, "km"), Megameter(1_000_000_000_000_000, "Mm"), Gigameter(1_000_000_000_000_000_000, "Gm"), } /** * A non-standard representation of length commonly used in the United States. */ public enum class UnitedStatesCustomary( override val nanometerScale: Long, override val symbol: String, ) : LengthUnit { Inch(25_400_000, "in"), Foot(304_800_000, "ft"), Yard(914_400_000, "yd"), Mile(1_609_344_000_000, "mi"), } } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/unit/MassUnit.kt ================================================ package io.github.kevincianfarini.alchemist.unit /** * A unit of mass precise to the microgram. */ public interface MassUnit { /** * The amount of micrograms in this unit. Implementations of [MassUnit] should be perfectly divisible by a * quantity of micrograms. */ public val microgramScale: Long /** * The symbol of this unit. */ public val symbol: String /** * An International System of Units standard representation of mass. */ public enum class International(override val microgramScale: Long, override val symbol: String): MassUnit { Microgram(1, "μg"), Milligram(1_000, "mg"), Gram(1_000_000, "g"), Kilogram(1_000_000_000, "kg"), Megagram(1_000_000_000_000, "Mg"), Gigagram(1_000_000_000_000_000, "Gg"), Teragram(1_000_000_000_000_000_000, "Tg"), } } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/unit/PowerUnit.kt ================================================ package io.github.kevincianfarini.alchemist.unit /** * A unit of power precise to the microwatt. */ public interface PowerUnit { /** * The amount of microwatts in this unit. Implementations of [PowerUnit] should be perfectly divisible by a * quantity of microwatts. */ public val microwattScale: Long /** * The symbol of this unit. */ public val symbol: String /** * An International System of Units standard representation of power. */ public enum class International( override val microwattScale: Long, override val symbol: String, ) : PowerUnit { Microwatt(1, "μW"), Milliwatt(1_000, "mW"), Watt(1_000_000, "W"), Kilowatt(1_000_000_000, "kW"), Megawatt(1_000_000_000_000, "MW"), Gigawatt(1_000_000_000_000_000, "GW"), Terawatt(1_000_000_000_000_000_000, "TW"), } } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/unit/TemperatureUnit.kt ================================================ package io.github.kevincianfarini.alchemist.unit import io.github.kevincianfarini.alchemist.internal.SaturatingLong import io.github.kevincianfarini.alchemist.internal.saturated import kotlin.math.roundToLong import kotlin.text.Typography.nbsp /** * Marks [TemperatureUnit] as delicate for implementation. */ @RequiresOptIn( message = """ Implementing TemperatureUnit requires detecting integer overflow detection, which normal Long values don't expose. Implementors should exercise caution when converting between their custom temperature units and nanokelvins. """, level = RequiresOptIn.Level.ERROR, ) public annotation class DelicateTemperatureUnit /** * A unit of temperature precise to the nanokelvin. */ @SubclassOptInRequired(DelicateTemperatureUnit::class) public interface TemperatureUnit { /** * The symbol of this unit. */ public val symbol: String /** * Convert the degrees of this [TemperatureUnit] to nanokelvins. */ public fun convertToNanokelvin(degrees: Long): Long /** * Convert the degrees of this [TemperatureUnit] to nanokelvins. Depending on its magnitude, some precision may be * lost. */ public fun convertToNanokelvin(degrees: Double): Long /** * Convert the amount of [nanokelvins] of this temperature in this [TemperatureUnit]. */ public fun convertNanokelvinsToThis(nanokelvins: Long): Double /** * An International System of Units standard representation of temperature. */ public enum class International(override val symbol: String) : TemperatureUnit { Nanokelvin("${nbsp}nK") { override fun convertToNanokelvin(degrees: Long): Long = degrees override fun convertToNanokelvin(degrees: Double): Long { require(!degrees.isNaN()) { "Temperature value cannot be NaN." } return degrees.roundToLong() } override fun convertNanokelvinsToThis(nanokelvins: Long): Double = nanokelvins.toDouble() }, Microkelvin("${nbsp}μK") { override fun convertToNanokelvin(degrees: Long): Long = (degrees.saturated * 1_000).rawValue override fun convertToNanokelvin(degrees: Double): Long { val ret = degrees * 1_000 require(!ret.isNaN()) { "Temperature value cannot be NaN." } return ret.roundToLong() } override fun convertNanokelvinsToThis(nanokelvins: Long): Double = nanokelvins.saturated.toDouble() / 1_000 }, Millikelvin("${nbsp}mK") { override fun convertToNanokelvin(degrees: Long): Long = (degrees.saturated * 1_000_000).rawValue override fun convertToNanokelvin(degrees: Double): Long { val ret = degrees * 1_000_000 require(!ret.isNaN()) { "Temperature value cannot be NaN." } return ret.roundToLong() } override fun convertNanokelvinsToThis(nanokelvins: Long): Double = nanokelvins.saturated.toDouble() / 1_000_000 }, Kelvin("${nbsp}K") { override fun convertToNanokelvin(degrees: Long): Long = (degrees.saturated * 1_000_000_000).rawValue override fun convertToNanokelvin(degrees: Double): Long { val ret = degrees * 1_000_000_000 require(!ret.isNaN()) { "Temperature value cannot be NaN." } return ret.roundToLong() } override fun convertNanokelvinsToThis(nanokelvins: Long): Double = nanokelvins.saturated.toDouble() / 1_000_000_000 }, Kilokelvin("${nbsp}kK") { override fun convertToNanokelvin(degrees: Long): Long = (degrees.saturated * 1_000_000_000_000).rawValue override fun convertToNanokelvin(degrees: Double): Long { val ret = degrees * 1_000_000_000_000 require(!ret.isNaN()) { "Temperature value cannot be NaN." } return ret.roundToLong() } override fun convertNanokelvinsToThis(nanokelvins: Long): Double = nanokelvins.saturated.toDouble() / 1_000_000_000_000 }, Megakelvin("${nbsp}MK") { override fun convertToNanokelvin(degrees: Long): Long = (degrees.saturated * 1_000_000_000_000_000).rawValue override fun convertToNanokelvin(degrees: Double): Long { val ret = degrees * 1_000_000_000_000_000 require(!ret.isNaN()) { "Temperature value cannot be NaN." } return ret.roundToLong() } override fun convertNanokelvinsToThis(nanokelvins: Long): Double = nanokelvins.saturated.toDouble() / 1_000_000_000_000_000 }, Gigakelvin("${nbsp}GK") { override fun convertToNanokelvin(degrees: Long): Long = (degrees.saturated * 1_000_000_000_000_000_000).rawValue override fun convertToNanokelvin(degrees: Double): Long { val ret = degrees * 1_000_000_000_000_000_000 require(!ret.isNaN()) { "Temperature value cannot be NaN." } return ret.roundToLong() } override fun convertNanokelvinsToThis(nanokelvins: Long): Double = nanokelvins.saturated.toDouble() / 1_000_000_000_000_000_000 }, Celsius("°C") { override fun convertToNanokelvin(degrees: Long): Long { return ((degrees.saturated * 1_000_000_000) + 273_150_000_000).rawValue } override fun convertToNanokelvin(degrees: Double): Long { val ret = (degrees * 1_000_000_000) + 273_150_000_000 require(!ret.isNaN()) { "Temperature value cannot be NaN." } return ret.roundToLong() } override fun convertNanokelvinsToThis(nanokelvins: Long): Double { return (nanokelvins.saturated.toDouble() - 273_150_000_000) / 1_000_000_000 } }, } /** * A non-standard unit of temperature used in the United States. */ public object Fahrenheit : TemperatureUnit { override val symbol: String get() = "°F" override fun convertToNanokelvin(degrees: Long): Long { val accurate = (((degrees.saturated * 1_000_000_000) + 459_670_000_000) * 5) / 9 return if (accurate.isFinite()) { accurate.rawValue } else { ((degrees.saturated * 555_555_556) + 255_372_222_222).rawValue } } override fun convertToNanokelvin(degrees: Double): Long { val ret = (((degrees * 1_000_000_000) + 459_670_000_000) * 5) / 9 require(!ret.isNaN()) { "Temperature value cannot be NaN." } return ret.roundToLong() } override fun convertNanokelvinsToThis(nanokelvins: Long): Double { return (nanokelvins.saturated - 255_372_222_222).toDouble() / 555_555_556.toDouble() } } } internal fun TemperatureUnit.convertToNanokelvin(value: SaturatingLong): SaturatingLong { return convertToNanokelvin(value.rawValue).saturated } internal fun TemperatureUnit.convertNanokelvinsToThis(nanokelvins: SaturatingLong): Double { return convertNanokelvinsToThis(nanokelvins.rawValue) } ================================================ FILE: src/commonMain/kotlin/io/github/kevincianfarini/alchemist/unit/VolumeUnit.kt ================================================ package io.github.kevincianfarini.alchemist.unit import kotlin.text.Typography.nbsp /** * A unit of volume precise to the centimeter³. */ public interface VolumeUnit { /** * The symbol of this unit. */ public val symbol: String /** * The amount of centimeter³ in this unit. Implementations of [TemperatureUnit] should be perfectly divisible by a * quantity of centimeter³. */ public val cubicCentimetersScale: Long /** * A non-standard representation of volume commonly used as part of the metric system. */ public enum class Metric(override val symbol: String, override val cubicCentimetersScale: Long) : VolumeUnit { Milliliter("${nbsp}mL", 1), Liter("${nbsp}L", 1_000), Kiloliter("${nbsp}kL", 1_000_000), Megaliter("${nbsp}ML", 1_000_000_000), Gigaliter("${nbsp}GL", 1_000_000_000_000), Teraliter("${nbsp}TL", 1_000_000_000_000_000), Petaliter("${nbsp}PL", 1_000_000_000_000_000_000), } } ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/Long.kt ================================================ package io.github.kevincianfarini.alchemist /** * Convert this [Long] to its binary string representation. The result is chunked into 8 bytes. This function is marked * as unused, but it's useful for debugging. * * The following example: * * ```kt * println(Long.MAX_VALUE.toBinaryString()) * println(Long.MIN_VALUE.toBinaryString()) * ``` * * will print the following results: * * ``` * 01111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 * 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 * ``` */ @Suppress("unused") fun Long.toBinaryString(): String = toULong() .toString(radix = 2) .padStart(64, '0') .chunked(8) .joinToString(" ") ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/ignore.kt ================================================ @file:OptIn(ExperimentalMultiplatform::class) package io.github.kevincianfarini.alchemist /** * Ignores a test for the Js target. */ @OptionalExpectation expect annotation class JsIgnore() /** * Ignores a test for the WasmJs target. */ @OptionalExpectation expect annotation class WasmJsIgnore() /** * Ignores a test for the WasmWasi target. */ @OptionalExpectation expect annotation class WasmWasiIgnore() ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/internal/DoubleTest.kt ================================================ package io.github.kevincianfarini.alchemist.internal import io.github.kevincianfarini.alchemist.WasmWasiIgnore import kotlin.test.assertEquals import kotlin.test.Test class DoubleTest { @Test @WasmWasiIgnore // See: https://youtrack.jetbrains.com/issue/KT-60964. fun exact_omits_decimal_point() { assertEquals( expected = "0", actual = 0.0.toDecimalString(0), ) } @Test fun decimals_rounded_up() { assertEquals( expected = "123.457", actual = 123.4566.toDecimalString(3), ) } @Test @WasmWasiIgnore // See: https://youtrack.jetbrains.com/issue/KT-60964. fun pads_with_zeros() { assertEquals( expected = "123.4565000000", actual = 123.4565.toDecimalString(10), ) } @Test fun negative_sign() { assertEquals( expected = "-123.457", actual = (-123.4566).toDecimalString(3) ) } } ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/internal/SaturatingLongTest.kt ================================================ package io.github.kevincianfarini.alchemist.internal import io.github.kevincianfarini.alchemist.WasmJsIgnore import io.github.kevincianfarini.alchemist.WasmWasiIgnore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertFailsWith import kotlin.test.assertTrue class SaturatingLongTest { @Test fun long_max_and_min_value_is_infinite() { assertTrue(Long.MAX_VALUE.saturated.isInfinite()) assertTrue(Long.MIN_VALUE.saturated.isInfinite()) } @Test fun unary_minus_works() { assertEquals(1L.saturated, -((-1L).saturated)) } @Test fun unary_minus_infinite_values() { assertEquals(POSITIVE_INFINITY, -NEGATIVE_INFINITY) assertEquals(NEGATIVE_INFINITY, -POSITIVE_INFINITY) } @Test fun infinite_plus_infinite_equals_infinite() { assertEquals( expected = POSITIVE_INFINITY, actual = POSITIVE_INFINITY + POSITIVE_INFINITY, ) assertEquals( expected = NEGATIVE_INFINITY, actual = NEGATIVE_INFINITY+ NEGATIVE_INFINITY, ) } @Test fun mixed_signed_infinite_addition_error() { val e = assertFailsWith { POSITIVE_INFINITY + NEGATIVE_INFINITY } assertEquals( actual = e.message, expected = "Summing infinite values of different signs yields an undefined result.", ) } @Test fun addition_positive_overflow_returns_positive_infinite() { val a = (Long.MAX_VALUE - 1L).saturated val b = 2L.saturated assertEquals(POSITIVE_INFINITY, a + b) } @Test fun addition_negative_overflow_returns_negative_infinite() { val a = (Long.MIN_VALUE + 1L).saturated val b = (-2L).saturated assertEquals(NEGATIVE_INFINITY, a + b) } @Test fun subtraction_negative_overflow_returns_negative_infinite() { val a = (Long.MIN_VALUE + 1L).saturated val b = 2L.saturated assertEquals(NEGATIVE_INFINITY, a - b) } @Test fun subtraction_positive_overflow_returns_positive_infinite() { val a = -((Long.MAX_VALUE - 1L).saturated) val b = 2L.saturated assertEquals(POSITIVE_INFINITY, b - a) } @Test fun adding_to_infinity_produces_infinity() { assertEquals(POSITIVE_INFINITY, POSITIVE_INFINITY + 50_000L.saturated) } @Test fun adding_to_negative_infinity_produces_negative_infinity() { assertEquals(NEGATIVE_INFINITY, NEGATIVE_INFINITY + 50_000L.saturated) } @Test fun subtracting_from_positive_infinity_produces_positive_infinity() { assertEquals(POSITIVE_INFINITY, POSITIVE_INFINITY - 50_000L.saturated) } @Test fun subtracting_from_negative_infinity_produces_negative_infinity() { assertEquals(NEGATIVE_INFINITY, NEGATIVE_INFINITY - 50_000L.saturated) } @Test fun dividing_works() { assertEquals(2L.saturated, 10L.saturated / 5L.saturated) } @Test fun dividing_two_infinite_values_errors() { assertFailsWith { POSITIVE_INFINITY / POSITIVE_INFINITY } assertFailsWith { POSITIVE_INFINITY / NEGATIVE_INFINITY } assertFailsWith { NEGATIVE_INFINITY / POSITIVE_INFINITY } assertFailsWith { NEGATIVE_INFINITY / NEGATIVE_INFINITY } } @Test fun dividing_by_positive_infinity_produces_zero() { assertEquals(0L.saturated, (Long.MAX_VALUE - 1L).saturated / POSITIVE_INFINITY) } @Test fun dividing_by_negative_infinity_produces_zero() { assertEquals(0L.saturated, (Long.MAX_VALUE - 1L).saturated / NEGATIVE_INFINITY) } @Test fun dividing_positive_infinity_by_a_positive_value_produces_positive_infinity() { assertEquals(POSITIVE_INFINITY, POSITIVE_INFINITY / 2L) } @Test fun dividing_positive_infinity_by_negative_value_produces_negative_infinity() { assertEquals(NEGATIVE_INFINITY, POSITIVE_INFINITY / -1L) } @Test fun dividing_negative_infinity_by_a_positive_value_produces_negative_infinity() { assertEquals(NEGATIVE_INFINITY, NEGATIVE_INFINITY / 2L) } @Test fun dividing_negative_infinity_by_negative_value_produces_positive_infinity() { assertEquals(POSITIVE_INFINITY, NEGATIVE_INFINITY / -1L) } @Test fun dividing_finite_value_by_infinite_value_produces_zero() { assertEquals(0L.saturated, (Long.MAX_VALUE - 1).saturated / POSITIVE_INFINITY) assertEquals(0L.saturated, (Long.MAX_VALUE - 1).saturated / NEGATIVE_INFINITY) assertEquals(0L.saturated, (Long.MIN_VALUE + 1).saturated / POSITIVE_INFINITY) assertEquals(0L.saturated, (Long.MIN_VALUE + 1).saturated / NEGATIVE_INFINITY) } @Test fun multiplication_works() { assertEquals(10L.saturated, 5L.saturated * 2L.saturated) } @Test fun multiplfying_two_positive_infinite_values_produces_positive_infinity() { assertEquals(POSITIVE_INFINITY, POSITIVE_INFINITY * POSITIVE_INFINITY) } @Test fun multiplying_two_negative_infinite_values_produces_positive_infinity() { assertEquals(POSITIVE_INFINITY, NEGATIVE_INFINITY * NEGATIVE_INFINITY) } @Test fun multiplying_mixed_sign_infinite_values_produces_negative_infinity() { assertEquals(NEGATIVE_INFINITY, POSITIVE_INFINITY * NEGATIVE_INFINITY) assertEquals(NEGATIVE_INFINITY, NEGATIVE_INFINITY * POSITIVE_INFINITY) } @Test fun two_large_overflowing_positive_numbers_produces_positive_infinity() { val a = (Long.MAX_VALUE / 3).saturated val b = (Long.MAX_VALUE / 4).saturated assertEquals(POSITIVE_INFINITY, a * b) } @Test fun two_large_overflowing_negative_numbers_produces_positive_infinity() { val a = (Long.MIN_VALUE / 3).saturated val b = (Long.MIN_VALUE / 4).saturated assertEquals(POSITIVE_INFINITY, a * b) } @Test fun two_large_overflowing_mixed_sign_numbers_produces_negative_infinity() { val a = (Long.MAX_VALUE / 3).saturated val b = (Long.MIN_VALUE / 4).saturated assertEquals(NEGATIVE_INFINITY, a * b) } @Test fun aboslute_value_infinity_produces_infinity() { assertEquals(POSITIVE_INFINITY, POSITIVE_INFINITY.absoluteValue) assertEquals(POSITIVE_INFINITY, NEGATIVE_INFINITY.absoluteValue) } @Test fun absolute_value_works() { assertEquals(10L.saturated, (-10L).saturated.absoluteValue) assertEquals(10L.saturated, 10L.saturated.absoluteValue) } @Test fun infinity_times_infinity_preserves_sign() { assertEquals(POSITIVE_INFINITY, POSITIVE_INFINITY * POSITIVE_INFINITY) assertEquals(NEGATIVE_INFINITY, NEGATIVE_INFINITY * POSITIVE_INFINITY) assertEquals(NEGATIVE_INFINITY, POSITIVE_INFINITY * NEGATIVE_INFINITY) assertEquals(POSITIVE_INFINITY, NEGATIVE_INFINITY * NEGATIVE_INFINITY) } @Test fun infinity_times_zero_throws() { assertFailsWith { POSITIVE_INFINITY * 0 } assertFailsWith { NEGATIVE_INFINITY * 0 } } @Test fun does_not_overflow_mul_negative_one() { assertEquals( expected = (-1_000_000_000_000_000_000).saturated, actual = (-1L).saturated * 1_000_000_000_000_000_000, ) } @Test fun multiplication_overflow_does_not_divide_by_zero() { assertEquals(0L.saturated, 0L.saturated * (Long.MAX_VALUE - 1)) } @Test fun multiplying_by_nan_throws() { assertFailsWith { 10L.saturated * Double.NaN } } @Test fun multiplying_by_double_infinity_preserves_sign() { assertEquals(POSITIVE_INFINITY, 10L.saturated * Double.POSITIVE_INFINITY) assertEquals(NEGATIVE_INFINITY, 10L.saturated * Double.NEGATIVE_INFINITY) assertEquals(NEGATIVE_INFINITY, (-10L).saturated * Double.POSITIVE_INFINITY) assertEquals(POSITIVE_INFINITY, (-10L).saturated * Double.NEGATIVE_INFINITY) } @Test fun multiplying_infinity_by_double_preserves_sign() { assertEquals(POSITIVE_INFINITY, POSITIVE_INFINITY * 2.0) assertEquals(NEGATIVE_INFINITY, NEGATIVE_INFINITY * 2.0) assertEquals(NEGATIVE_INFINITY, POSITIVE_INFINITY * -2.0) assertEquals(POSITIVE_INFINITY, NEGATIVE_INFINITY * -2.0) } @Test fun multiplying_by_double_zero_throws_for_infinity() { assertFailsWith { POSITIVE_INFINITY * 0.0 } assertFailsWith { NEGATIVE_INFINITY * 0.0 } } @Test fun multiplying_by_double_overflows_correctly() { assertEquals(POSITIVE_INFINITY, (Long.MAX_VALUE / 2).saturated * 2.1) assertEquals(NEGATIVE_INFINITY, (Long.MAX_VALUE / 2).saturated * -2.1) assertEquals(NEGATIVE_INFINITY, (Long.MIN_VALUE / 2).saturated * 2.1) assertEquals(POSITIVE_INFINITY, (Long.MIN_VALUE / 2).saturated * -2.1) } @Test fun multiplying_by_double_uses_integer_path_when_possible() { val bigLong = 92233720368547758L assertEquals((bigLong * 2L).saturated, bigLong.saturated * 2.0) } @Test fun multiplying_by_double_uses_float_path_when_necessary() { val bigLong = 92233720368547758L assertEquals((bigLong.toDouble() * 0.3).toLong().saturated, bigLong.saturated * 0.3) } @Test fun dividing_by_double_nan_throws() { assertFailsWith { 10L.saturated / Double.NaN } } @Test fun dividing_by_double_infinity_preserves_sign() { assertEquals(0L.saturated, 10L.saturated / Double.POSITIVE_INFINITY) assertEquals(0L.saturated, 10L.saturated / Double.NEGATIVE_INFINITY) assertEquals(0L.saturated, (-10L).saturated / Double.POSITIVE_INFINITY) assertEquals(0L.saturated, (-10L).saturated / Double.NEGATIVE_INFINITY) } @Test fun dividing_infinity_by_double_preserves_sign() { assertEquals(POSITIVE_INFINITY, POSITIVE_INFINITY / 2.0) assertEquals(NEGATIVE_INFINITY, NEGATIVE_INFINITY / 2.0) assertEquals(NEGATIVE_INFINITY, POSITIVE_INFINITY / -2.0) assertEquals(POSITIVE_INFINITY, NEGATIVE_INFINITY / -2.0) } @Test @WasmWasiIgnore @WasmJsIgnore // See: https://youtrack.jetbrains.com/issue/KT-66081. fun dividing_by_double_zero_throws() { assertFails { // Kotlin/JS doesn't throw ArithmeticException for Long.div(0). Don't try to assert against a specific // exception type here. // // See: https://github.com/JetBrains/kotlin/blob/62cfeef19de48b908e8abbd835422cdb1192576c/libraries/stdlib/js/runtime/longJs.kt#L217. 10L.saturated / 0.0 } assertFailsWith { POSITIVE_INFINITY / 0.0 } assertFailsWith { NEGATIVE_INFINITY / 0.0 } } @Test fun dividing_by_double_uses_integer_path_when_possible() { val bigLong = 92233720368547758L assertEquals((bigLong / 2L).saturated, bigLong.saturated / 2.0) } @Test fun dividing_by_double_uses_float_path_when_necessary() { val bigLong = 92233720368547758L assertEquals((bigLong.toDouble() / 0.3).toLong().saturated, bigLong.saturated / 0.3) } } ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/type/AccelerationTest.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.WasmWasiIgnore import io.github.kevincianfarini.alchemist.scalar.grams import io.github.kevincianfarini.alchemist.scalar.nanonewtons import io.github.kevincianfarini.alchemist.scalar.nmPerSecond import io.github.kevincianfarini.alchemist.scalar.nmPerSecond2 import io.github.kevincianfarini.alchemist.unit.LengthUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.text.Typography.nbsp import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit class AccelerationTest { @Test @WasmWasiIgnore // See: https://youtrack.jetbrains.com/issue/KT-60964. fun simple() { assertEquals( expected = "1.00${nbsp}m/s²", actual = 1_000_000_000.nmPerSecond2.toString() ) } @Test fun less_simple() { assertEquals( expected = "8052.97${nbsp}mi/h²", actual = 1_000_000_000.nmPerSecond2.toString( lengthUnit = LengthUnit.UnitedStatesCustomary.Mile, durationUnit = DurationUnit.HOURS, decimals = 2, ) ) } @Test fun acceleration_mul_time_simple() { assertEquals( expected = 12_340_000_000.nmPerSecond, actual = 10_000_000_000.nmPerSecond2 * 1_234.milliseconds, ) } @Test fun acceleration_mul_mass_simple() { // 152345677.626 nanonewtons, but we lose precision. assertEquals( expected = 152_345_677.nanonewtons, actual = 123_456_789.nmPerSecond2 * 1_234.grams, ) } } ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/type/AreaTest.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.WasmWasiIgnore import io.github.kevincianfarini.alchemist.internal.NEGATIVE_INFINITY import io.github.kevincianfarini.alchemist.internal.POSITIVE_INFINITY import io.github.kevincianfarini.alchemist.scalar.kilometers import io.github.kevincianfarini.alchemist.scalar.milliliters import io.github.kevincianfarini.alchemist.scalar.millimeters import io.github.kevincianfarini.alchemist.scalar.mm2 import io.github.kevincianfarini.alchemist.scalar.nanometers import io.github.kevincianfarini.alchemist.unit.LengthUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue class AreaTest { @Test fun infinite_area_div_finite_length_is_infinite() { assertEquals(POSITIVE_INFINITY.nanometers, POSITIVE_INFINITY.mm2 / 1.nanometers) assertEquals(NEGATIVE_INFINITY.nanometers, POSITIVE_INFINITY.mm2 / (-1).nanometers) assertEquals(NEGATIVE_INFINITY.nanometers, NEGATIVE_INFINITY.mm2 / 1.nanometers) assertEquals(POSITIVE_INFINITY.nanometers, NEGATIVE_INFINITY.mm2 / (-1).nanometers) } @Test fun finite_area_div_infinite_length_is_zero() { assertEquals(0.nanometers, (Long.MAX_VALUE - 1).mm2 / POSITIVE_INFINITY.nanometers) assertEquals(0.nanometers, (Long.MAX_VALUE - 1).mm2 / NEGATIVE_INFINITY.nanometers) assertEquals(0.nanometers, (Long.MIN_VALUE + 1).mm2 / POSITIVE_INFINITY.nanometers) assertEquals(0.nanometers, (Long.MIN_VALUE + 1).mm2 / NEGATIVE_INFINITY.nanometers) } @Test fun infinite_area_div_infinite_length_throws() { assertFailsWith { POSITIVE_INFINITY.mm2 / POSITIVE_INFINITY.nanometers } assertFailsWith { NEGATIVE_INFINITY.mm2 / POSITIVE_INFINITY.nanometers } assertFailsWith { NEGATIVE_INFINITY.mm2 / NEGATIVE_INFINITY.nanometers } assertFailsWith { POSITIVE_INFINITY.mm2 / NEGATIVE_INFINITY.nanometers } } @Test fun nano2_precision_div_length_is_accurate() { assertEquals(123_000_000_000_000.nanometers, 123.mm2 / 1.nanometers) } @Test fun micro2_precision_div_length_is_accurate() { assertEquals(9_223_373.millimeters, 9_223_373.mm2 / 1.millimeters) } @Test fun milli2_precision_div_length_is_accurate() { assertEquals(1.kilometers, 9_223_373_000_000.mm2 / 9_223_373_000_000.nanometers) } @Test fun area_div_scale_is_correct() { assertEquals(1.mm2, 2.mm2 / 2) assertEquals(0.mm2, 1.mm2 / 2) assertEquals(POSITIVE_INFINITY.mm2, POSITIVE_INFINITY.mm2 / 2) assertEquals(NEGATIVE_INFINITY.mm2, POSITIVE_INFINITY.mm2 / -2) assertEquals(NEGATIVE_INFINITY.mm2, NEGATIVE_INFINITY.mm2 / 2) assertEquals(POSITIVE_INFINITY.mm2, NEGATIVE_INFINITY.mm2 / -2) } @Test fun area_div_area_is_correct() { assertEquals(1.0, 1.mm2 / 1.mm2) assertEquals(2.5, 5.mm2 / 2.mm2) assertEquals(Double.POSITIVE_INFINITY, POSITIVE_INFINITY.mm2 / 2.mm2) assertEquals(Double.NEGATIVE_INFINITY, NEGATIVE_INFINITY.mm2 / 2.mm2) assertEquals(Double.NEGATIVE_INFINITY, POSITIVE_INFINITY.mm2 / (-2).mm2) assertEquals(Double.POSITIVE_INFINITY, NEGATIVE_INFINITY.mm2 / (-2).mm2) } @Test fun area_minus_area_is_correct() { assertEquals((-1).mm2, 0.mm2 - 1.mm2) assertEquals(5.mm2, 10.mm2 - 5.mm2) assertEquals(12.mm2, 10.mm2 - (-2).mm2) assertEquals(NEGATIVE_INFINITY.mm2, (Long.MIN_VALUE + 1).mm2 - 1.mm2) assertEquals(POSITIVE_INFINITY.mm2, (Long.MAX_VALUE - 1).mm2 - (-1).mm2) assertEquals(POSITIVE_INFINITY.mm2, POSITIVE_INFINITY.mm2 - 100_000_000.mm2) assertEquals(NEGATIVE_INFINITY.mm2, NEGATIVE_INFINITY.mm2 - 100_000_000.mm2) } @Test fun area_plus_area_is_correct() { assertEquals(1.mm2, 0.mm2 + 1.mm2) assertEquals(15.mm2, 10.mm2 + 5.mm2) assertEquals(8.mm2, 10.mm2 + (-2).mm2) assertEquals(NEGATIVE_INFINITY.mm2, (Long.MIN_VALUE + 1).mm2 + (-1).mm2) assertEquals(POSITIVE_INFINITY.mm2, (Long.MAX_VALUE - 1).mm2 + 1.mm2) assertEquals(POSITIVE_INFINITY.mm2, POSITIVE_INFINITY.mm2 + 100_000_000.mm2) assertEquals(NEGATIVE_INFINITY.mm2, NEGATIVE_INFINITY.mm2 + 100_000_000.mm2) } @Test fun area_mul_scale_is_correct() { assertEquals(4.mm2, 2.mm2 * 2) assertEquals(0.mm2, 0.mm2 * 2) assertEquals(POSITIVE_INFINITY.mm2, POSITIVE_INFINITY.mm2 * 2) assertEquals(NEGATIVE_INFINITY.mm2, POSITIVE_INFINITY.mm2 * -2) assertEquals(NEGATIVE_INFINITY.mm2, NEGATIVE_INFINITY.mm2 * 2) assertEquals(POSITIVE_INFINITY.mm2, NEGATIVE_INFINITY.mm2 * -2) } @Test fun area_to_double_is_correct() { val scale = 123_456_789_987_654_321 val area = scale.mm2 assertEquals(scale.toDouble(), area.toDouble(LengthUnit.International.Millimeter)) assertEquals(scale.toDouble() / 1_000_000, area.toDouble(LengthUnit.International.Meter)) assertEquals(scale.toDouble() / 1_000_000_000_000, area.toDouble(LengthUnit.International.Kilometer)) assertEquals(scale.toDouble() / 1_000_000_000_000_000_000, area.toDouble(LengthUnit.International.Megameter)) } @Test fun to_international_components_works() { val mega = 1_000_000_000_000_000_000.mm2 val kilo = 1_000_000_000_000.mm2 val meters = 1_000_000.mm2 val centi = 100.mm2 val milli = 1.mm2 val area = mega + kilo + meters + centi + milli area.toInternationalComponents { megametersSquared, kilometersSquared, metersSquared, centimetersSquared, millimetersSquared -> assertEquals(1, megametersSquared) assertEquals(1, kilometersSquared) assertEquals(1, metersSquared) assertEquals(1, centimetersSquared) assertEquals(1, millimetersSquared) } } @Test fun infinity_to_international_components_works() { POSITIVE_INFINITY.mm2.toInternationalComponents { megametersSquared, kilometersSquared, metersSquared, centimetersSquared, millimetersSquared -> assertEquals(megametersSquared, Long.MAX_VALUE) assertEquals(kilometersSquared, Long.MAX_VALUE) assertEquals(metersSquared, Long.MAX_VALUE) assertEquals(centimetersSquared, Long.MAX_VALUE) assertEquals(millimetersSquared, Long.MAX_VALUE) } NEGATIVE_INFINITY.mm2.toInternationalComponents { megametersSquared, kilometersSquared, metersSquared, centimetersSquared, millimetersSquared -> assertEquals(megametersSquared, Long.MIN_VALUE) assertEquals(kilometersSquared, Long.MIN_VALUE) assertEquals(metersSquared, Long.MIN_VALUE) assertEquals(centimetersSquared, Long.MIN_VALUE) assertEquals(millimetersSquared, Long.MIN_VALUE) } } @Test @WasmWasiIgnore // See: https://youtrack.jetbrains.com/issue/KT-60964. fun to_string_override_works() { assertEquals("0.00mm²", 0.mm2.toString()) assertEquals("1.00mm²", 1.mm2.toString()) assertEquals("1.00cm²", 100.mm2.toString()) assertEquals("1.01cm²", 101.mm2.toString()) assertEquals("1.00m²", 1_000_000.mm2.toString()) assertEquals("1.01m²", 1_010_000.mm2.toString()) assertEquals("1.00km²", 1_000_000_000_000.mm2.toString()) assertEquals("1.01km²", 1_010_000_000_000.mm2.toString()) assertEquals("1.00Mm²", 1_000_000_000_000_000_000.mm2.toString()) assertEquals("1.01Mm²", 1_010_000_000_000_000_000.mm2.toString()) assertEquals("Infinity", POSITIVE_INFINITY.mm2.toString()) assertEquals("-Infinity", NEGATIVE_INFINITY.mm2.toString()) } @Test fun compare_to_works() { assertTrue(1.mm2 < 2.mm2) assertTrue(2.mm2 <= 2.mm2) assertFalse(1.mm2 > 2.mm2) assertFalse(1.mm2 >= 2.mm2) assertFalse(POSITIVE_INFINITY.mm2 > POSITIVE_INFINITY.mm2) assertFalse(POSITIVE_INFINITY.mm2 < POSITIVE_INFINITY.mm2) assertTrue(POSITIVE_INFINITY.mm2 >= POSITIVE_INFINITY.mm2) assertTrue(POSITIVE_INFINITY.mm2 <= POSITIVE_INFINITY.mm2) assertTrue(POSITIVE_INFINITY.mm2 > NEGATIVE_INFINITY.mm2) assertTrue(POSITIVE_INFINITY.mm2 >= NEGATIVE_INFINITY.mm2) assertFalse(NEGATIVE_INFINITY.mm2 > NEGATIVE_INFINITY.mm2) assertFalse(NEGATIVE_INFINITY.mm2 < NEGATIVE_INFINITY.mm2) assertTrue(NEGATIVE_INFINITY.mm2 >= NEGATIVE_INFINITY.mm2) assertTrue(NEGATIVE_INFINITY.mm2 <= NEGATIVE_INFINITY.mm2) assertTrue(NEGATIVE_INFINITY.mm2 < POSITIVE_INFINITY.mm2) assertTrue(NEGATIVE_INFINITY.mm2 <= POSITIVE_INFINITY.mm2) } @Test fun is_finite_works() { assertTrue(1.mm2.isFinite()) assertTrue((Long.MAX_VALUE - 1).mm2.isFinite()) assertTrue((Long.MIN_VALUE + 1).mm2.isFinite()) assertFalse(Long.MAX_VALUE.mm2.isFinite()) assertFalse(Long.MIN_VALUE.mm2.isFinite()) assertFalse(POSITIVE_INFINITY.mm2.isFinite()) assertFalse(NEGATIVE_INFINITY.mm2.isFinite()) } @Test fun area_mul_length_simple() { assertEquals( expected = 1_219_251.milliliters, actual = 123_456_000_000.mm2 * 9_876.nanometers, ) } } ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/type/EnergyTest.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.NEGATIVE_INFINITY import io.github.kevincianfarini.alchemist.internal.POSITIVE_INFINITY import io.github.kevincianfarini.alchemist.scalar.gigajoules import io.github.kevincianfarini.alchemist.scalar.gigawattHours import io.github.kevincianfarini.alchemist.scalar.joules import io.github.kevincianfarini.alchemist.scalar.kilojoules import io.github.kevincianfarini.alchemist.scalar.kilowattHours import io.github.kevincianfarini.alchemist.scalar.megajoules import io.github.kevincianfarini.alchemist.scalar.megawattHours import io.github.kevincianfarini.alchemist.scalar.microwatts import io.github.kevincianfarini.alchemist.scalar.millijoules import io.github.kevincianfarini.alchemist.scalar.milliwattHours import io.github.kevincianfarini.alchemist.scalar.petajoules import io.github.kevincianfarini.alchemist.scalar.terawattHours import io.github.kevincianfarini.alchemist.scalar.tetrajoules import io.github.kevincianfarini.alchemist.scalar.wattHours import io.github.kevincianfarini.alchemist.scalar.watts import io.github.kevincianfarini.alchemist.unit.EnergyUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.time.Duration import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.microseconds import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds class EnergyTest { @Test fun to_international_components_works() { val energy = 1.petajoules + 1.tetrajoules + 1.gigajoules + 1.megajoules + 1.kilojoules + 1.joules + 1.millijoules energy.toInternationalComponents { petajoules, tetrajoules, gigajoules, megajoules, kilojoules, joules, millijoules -> assertEquals(1L, petajoules) assertEquals(1L, tetrajoules) assertEquals(1L, gigajoules) assertEquals(1L, megajoules) assertEquals(1L, kilojoules) assertEquals(1L, joules) assertEquals(1L, millijoules) } } @Test fun infinite_to_international_components() { POSITIVE_INFINITY.millijoules.toInternationalComponents { petajoules, tetrajoules, gigajoules, megajoules, kilojoules, joules, millijoules -> assertEquals(Long.MAX_VALUE, petajoules) assertEquals(Long.MAX_VALUE, tetrajoules) assertEquals(Long.MAX_VALUE, gigajoules) assertEquals(Long.MAX_VALUE, megajoules) assertEquals(Long.MAX_VALUE, kilojoules) assertEquals(Long.MAX_VALUE, joules) assertEquals(Long.MAX_VALUE, millijoules) } } @Test fun to_electricity_components_works() { val energy = 1.terawattHours + 1.gigawattHours + 1.megawattHours + 1.kilowattHours + 1.wattHours + 1.milliwattHours + 3.millijoules energy.toElectricityComponents { terawattHours, gigawattHours, megawattHours, kilowattHours, wattHours, milliwattHours, microwattHours -> assertEquals(1L, terawattHours) assertEquals(1L, gigawattHours) assertEquals(1L, megawattHours) assertEquals(1L, kilowattHours) assertEquals(1L, wattHours) assertEquals(1L, milliwattHours) assertEquals(0.8333333333, microwattHours, absoluteTolerance = 0.000001) } } @Test fun infinite_to_electricity_components() { POSITIVE_INFINITY.millijoules.toElectricityComponents { terawattHours, gigawattHours, megawattHours, kilowattHours, wattHours, milliwattHours, microwattHours -> assertEquals(Long.MAX_VALUE, terawattHours) assertEquals(Long.MAX_VALUE, gigawattHours) assertEquals(Long.MAX_VALUE, megawattHours) assertEquals(Long.MAX_VALUE, kilowattHours) assertEquals(Long.MAX_VALUE, wattHours) assertEquals(Long.MAX_VALUE, milliwattHours) assertEquals(Double.POSITIVE_INFINITY, microwattHours) } } @Test fun to_double_works() { val energy = 12_345.wattHours assertEquals(12.345, energy.toDouble(EnergyUnit.Electricity.KilowattHour)) } @Test fun to_decial_string_works() { val energy = 12_345.wattHours assertEquals("12.345kWh", energy.toString(EnergyUnit.Electricity.KilowattHour, decimals = 3)) } @Test fun to_string_picks_correct_energy_display_unit() { val energy = 12_346.joules assertEquals("12.35kJ", energy.toString()) } @Test fun infinite_energy_div_infinite_duration_throws() { assertFailsWith { POSITIVE_INFINITY.millijoules / Duration.INFINITE } assertFailsWith { NEGATIVE_INFINITY.millijoules / Duration.INFINITE } } @Test fun infinite_energy_div_finite_duration_produces_infinite_power() { assertEquals(POSITIVE_INFINITY.microwatts, POSITIVE_INFINITY.millijoules / 10.hours) assertEquals(NEGATIVE_INFINITY.microwatts, NEGATIVE_INFINITY.millijoules / 10.hours) } @Test fun finite_energy_div_infinite_duration_produces_zero_power() { assertEquals(0.watts, 1.petajoules / Duration.INFINITE) } @Test fun pico_granular_energy_div_micro_granular_time_power_works() { assertEquals(1.watts, 1.joules / 1.seconds) } @Test fun nano_granular_energy_div_micro_granular_time_power_works() { assertEquals(1_000_000_000.watts, 1.gigajoules / 1.seconds) } @Test fun micro_granular_energy_div_micro_granular_time_power_works() { assertEquals(1_000_000_000_000.watts, 1.tetrajoules / 1.seconds) } @Test fun one_microwatt_with_micro_granular_time() { assertEquals(1.microwatts, 1.millijoules / 1_000.seconds) } @Test fun one_microwatt_with_milli_granular_time() { assertEquals(1.microwatts, 1_000_000_000_000.millijoules / 1_000_000_000_000_000.seconds) } @Test fun energy_div_time_precision_femtojoule_nanosecond() { assertEquals( expected = 1_234.microwatts, actual = 1_234.millijoules / 1_000.seconds, ) } @Test fun energy_div_time_precision_picojoule_nanosecond() { // 124,999,998,860.937500014 microwatts, but we lose precision. assertEquals( expected = 124_999_998_860.microwatts, actual = 123_456_789.millijoules / 987_654_321.nanoseconds, ) } @Test fun energy_div_time_precision_nanojoule_nanosecond() { // 124,999,998,860.937500014 microwatts, but we lose precision. assertEquals( expected = 124_999_998_860.microwatts, actual = 123_456_789.joules / 987_654_321.microseconds, ) } @Test fun energy_div_time_precision_microjoule_nanosecond() { // 124,999,998,860.937500014 microwatts, but we lose precision. assertEquals( expected = 124_999_998_860.microwatts, actual = 123_456_789.kilojoules / 987_654_321.milliseconds, ) } @Test fun energy_div_time_precision_millijoule_nanosecond() { // 124,999,998,860.937500014 microwatts, but we lose precision. assertEquals( expected = 124_999_998_860.microwatts, actual = 123_456_789.megajoules / 987_654_321.seconds, ) } @Test fun energy_div_time_precision_nanojoule_millisecond() { // 124.999998861 microwatts, but we lose precision. assertEquals( expected = 124.microwatts, actual = 123_456_789.joules / 987_654_321_000.seconds, ) } @Test fun energy_div_time_precision_microjoule_millisecond() { // 124,999.998860938 microwatts, but we lose precision. assertEquals( expected = 124_999.microwatts, actual = 123_456_789.kilojoules / 987_654_321_000.seconds, ) } @Test fun energy_div_time_precision_millijoule_millisecond() { // 124,999,998.860938 microwatts, but we lose precision. assertEquals( expected = 124_999_998.microwatts, actual = 123_456_789.megajoules / 987_654_321_000.seconds, ) } } ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/type/ForceTest.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.scalar.millijoules import io.github.kevincianfarini.alchemist.scalar.nanometers import io.github.kevincianfarini.alchemist.scalar.nanonewtons import kotlin.test.Test import kotlin.test.assertEquals class ForceTest { @Test fun force_mul_length_energy_simple() { assertEquals( expected = 121.millijoules, actual = 123_456_789.nanonewtons * 987_654_321.nanometers, ) } } ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/type/LengthTest.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.WasmWasiIgnore import io.github.kevincianfarini.alchemist.internal.NEGATIVE_INFINITY import io.github.kevincianfarini.alchemist.internal.POSITIVE_INFINITY import io.github.kevincianfarini.alchemist.scalar.centimeters import io.github.kevincianfarini.alchemist.scalar.feet import io.github.kevincianfarini.alchemist.scalar.gigameters import io.github.kevincianfarini.alchemist.scalar.inches import io.github.kevincianfarini.alchemist.scalar.kilometers import io.github.kevincianfarini.alchemist.scalar.megameters import io.github.kevincianfarini.alchemist.scalar.meters import io.github.kevincianfarini.alchemist.scalar.micrometers import io.github.kevincianfarini.alchemist.scalar.miles import io.github.kevincianfarini.alchemist.scalar.millimeters import io.github.kevincianfarini.alchemist.scalar.mm2 import io.github.kevincianfarini.alchemist.scalar.nanometers import io.github.kevincianfarini.alchemist.scalar.nmPerSecond import io.github.kevincianfarini.alchemist.scalar.yards import io.github.kevincianfarini.alchemist.unit.LengthUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds class LengthTest { @Test fun to_metric_components_works() { val length = 1.gigameters + 1.megameters + 1.kilometers + 1.meters + 1.centimeters + 1.millimeters + 1.micrometers + 1.nanometers length.toInternationalComponents { gm, mega, km, m, cm, milli, um, nm -> assertEquals(1L, gm) assertEquals(1L, mega) assertEquals(1L, km) assertEquals(1L, m) assertEquals(1L, cm) assertEquals(1L, milli) assertEquals(1L, um) assertEquals(1L, nm) } } @Test fun to_metric_components_positive_infinity_max_value_every_component() { POSITIVE_INFINITY.nanometers.toInternationalComponents { gm, mega, km, m, cm, milli, um, nm -> assertEquals(Long.MAX_VALUE, gm) assertEquals(Long.MAX_VALUE, mega) assertEquals(Long.MAX_VALUE, km) assertEquals(Long.MAX_VALUE, m) assertEquals(Long.MAX_VALUE, cm) assertEquals(Long.MAX_VALUE, milli) assertEquals(Long.MAX_VALUE, um) assertEquals(Long.MAX_VALUE, nm) } } @Test fun to_metric_components_negative_infinity_min_value_every_component() { NEGATIVE_INFINITY.nanometers.toInternationalComponents { gm, mega, km, m, cm, milli, um, nm -> assertEquals(Long.MIN_VALUE, gm) assertEquals(Long.MIN_VALUE, mega) assertEquals(Long.MIN_VALUE, km) assertEquals(Long.MIN_VALUE, m) assertEquals(Long.MIN_VALUE, cm) assertEquals(Long.MIN_VALUE, milli) assertEquals(Long.MIN_VALUE, um) assertEquals(Long.MIN_VALUE, nm) } } @Test fun to_imperial_components_works() { val length = 1.miles + 1.yards + 1.feet + 1.inches length.toUnitedStatesCustomaryComponents { miles, yards, feet, inches -> assertEquals(1L, miles) assertEquals(1L, yards) assertEquals(1L, feet) assertEquals(1.0, inches) } } @Test fun to_imperial_components_positive_infinity_max_value_every_component() { POSITIVE_INFINITY.nanometers.toUnitedStatesCustomaryComponents { miles, yards, feet, inches -> assertEquals(Long.MAX_VALUE, miles) assertEquals(Long.MAX_VALUE, yards) assertEquals(Long.MAX_VALUE, feet) assertEquals(Double.POSITIVE_INFINITY, inches) } } @Test fun to_imperial_components_negative_infinity_min_value_every_component() { NEGATIVE_INFINITY.nanometers.toUnitedStatesCustomaryComponents { miles, yards, feet, inches -> assertEquals(Long.MIN_VALUE, miles) assertEquals(Long.MIN_VALUE, yards) assertEquals(Long.MIN_VALUE, feet) assertEquals(Double.NEGATIVE_INFINITY, inches) } } @Test @WasmWasiIgnore // See: https://youtrack.jetbrains.com/issue/KT-60964. fun default_to_string_renders_into_metric_components() { val length = 10_000.meters assertEquals( expected = "10.00km", actual = length.toString(), ) } @Test fun to_string_infinity() { assertEquals("Infinity", POSITIVE_INFINITY.nanometers.toString()) assertEquals("-Infinity", NEGATIVE_INFINITY.nanometers.toString()) } @Test fun to_double_converts_miles_to_km_correctly() { assertEquals(1.609344, 1.miles.toDouble(LengthUnit.International.Kilometer)) } @Test fun length_mul_length_maximum_nanometers_component_is_lost_to_precision() { // 0.998001998 millimeters², but we lose precision. val nanos = 999_999.nanometers assertEquals(0.mm2, nanos * nanos) } @Test fun length_mul_length_gigameters_is_infinite() { assertEquals(POSITIVE_INFINITY.mm2, 1.gigameters * 1.gigameters) assertEquals(NEGATIVE_INFINITY.mm2, 1.gigameters * (-1).gigameters) assertEquals(POSITIVE_INFINITY.mm2, (-1).gigameters * (-1).gigameters) assertEquals(NEGATIVE_INFINITY.mm2, (-1).gigameters * 1.gigameters) } @Test fun length_mul_length_megameter_overflow() { assertEquals(POSITIVE_INFINITY.mm2, 100.megameters * 10.megameters) } @Test fun length_mul_length_megameter_no_overflow() { assertEquals( expected = 8_000_000_000_000_000_000L.mm2, actual = 4.megameters * 2.megameters ) } @Test fun length_squared_is_correct() { assertEquals( expected = 1_000_000.mm2, actual = 1.meters.squared(), ) } @Test fun infinite_length_div_infinite_time_throws() { assertFailsWith { POSITIVE_INFINITY.nanometers / Duration.INFINITE } assertFailsWith { NEGATIVE_INFINITY.nanometers / Duration.INFINITE } assertFailsWith { POSITIVE_INFINITY.nanometers / -Duration.INFINITE } assertFailsWith { NEGATIVE_INFINITY.nanometers / -Duration.INFINITE } } @Test fun infinite_length_div_finite_time() { assertEquals(POSITIVE_INFINITY.nmPerSecond, POSITIVE_INFINITY.nanometers / 1.seconds) assertEquals(NEGATIVE_INFINITY.nmPerSecond, NEGATIVE_INFINITY.nanometers / 1.seconds) assertEquals(NEGATIVE_INFINITY.nmPerSecond, POSITIVE_INFINITY.nanometers / (-1).seconds) assertEquals(POSITIVE_INFINITY.nmPerSecond, NEGATIVE_INFINITY.nanometers / (-1).seconds) } @Test fun finite_length_div_infinite_time() { assertEquals(0.nmPerSecond, (Long.MAX_VALUE - 1).nanometers / Duration.INFINITE) assertEquals(0.nmPerSecond, (Long.MAX_VALUE - 1).nanometers / -Duration.INFINITE) } @Test fun length_div_time_attometer_nanosecond_precision() { // 124,999,998.8609375 nm/s, but we lose precision. assertEquals( expected = 124_999_998.nmPerSecond, actual = 123_456_789.nanometers / 987_654_321.nanoseconds, ) } @Test fun length_div_time_femtometer_nanosecond_precision() { // 124,999,998,860.9375 nm/s, but we lose precision. assertEquals( expected = 124_999_998_860.nmPerSecond, actual = 123_456_789.micrometers / 987_654_321.nanoseconds, ) } @Test fun length_div_time_picometer_nanosecond_precision() { // 124,999,998,860,937.500014 nm/s, but we lose precision. assertEquals( expected = 124_999_998_860_937.nmPerSecond, actual = 123_456_789.millimeters / 987_654_321.nanoseconds, ) } @Test fun length_div_time_nanometer_nanosecond_precision() { // 124,999,998,860,937,500.014238 nm/s, but we lose precision. assertEquals( expected = 124_999_998_860_937_500.nmPerSecond, actual = 123_456_789.meters / 987_654_321.nanoseconds, ) } @Test fun length_div_time_picometer_millisecond_precision() { // 124.999998860937500014 nm/s, but we lose precision. assertEquals( expected = 124.nmPerSecond, actual = 123_456_789.millimeters / 987_654_321_000.seconds, ) } @Test fun length_div_time_nanometer_millisecond_precision() { // 124,999.9988609375 nm/s, but we lose precision. assertEquals( expected = 124_999.nmPerSecond, actual = 123_456_789.meters / 987_654_321_000.seconds, ) } @Test fun length_div_time_defaults_to_millisecond_precision_if_all_nanosecond_are_infinite() { // 124,999.998860937500014 nm/s, but we lose precision. assertEquals( expected = 124_999.nmPerSecond, actual = 123_456_789.millimeters / 987_654_321.seconds, ) } @Test fun length_div_time_defaults_to_coarse_nanometers_per_second_when_picometer_remainder_is_infinite() { // 1,000 nm/s, but we lose precision. assertEquals( expected = 0.nmPerSecond, actual = ((Long.MAX_VALUE / 2) - 2).nanometers / ((Long.MAX_VALUE / 2) - 1).milliseconds ) } @Test fun length_div_velocity_simple() { val length = 10.meters val velocity = 3.meters / 2.seconds assertEquals(6_666_666_666.nanoseconds, length / velocity) } } ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/type/PowerTest.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.internal.NEGATIVE_INFINITY import io.github.kevincianfarini.alchemist.internal.POSITIVE_INFINITY import io.github.kevincianfarini.alchemist.scalar.microwatts import io.github.kevincianfarini.alchemist.scalar.millijoules import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.Duration import kotlin.time.Duration.Companion.microseconds import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds class PowerTest { @Test fun power_multiply_infinite_time() { assertEquals( expected = POSITIVE_INFINITY.millijoules, actual = 1.microwatts * Duration.INFINITE, ) assertEquals( expected = NEGATIVE_INFINITY.millijoules, actual = (-1).microwatts * Duration.INFINITE, ) assertEquals( expected = POSITIVE_INFINITY.millijoules, actual = (-1).microwatts * -Duration.INFINITE, ) assertEquals( expected = NEGATIVE_INFINITY.millijoules, actual = 1.microwatts * -Duration.INFINITE, ) } @Test fun infinite_power_multiply_time() { assertEquals( expected = POSITIVE_INFINITY.millijoules, actual = POSITIVE_INFINITY.microwatts * 1.nanoseconds, ) assertEquals( expected = POSITIVE_INFINITY.millijoules, actual = NEGATIVE_INFINITY.microwatts * (-1).nanoseconds, ) assertEquals( expected = NEGATIVE_INFINITY.millijoules, actual = NEGATIVE_INFINITY.microwatts * 1.nanoseconds, ) assertEquals( expected = NEGATIVE_INFINITY.millijoules, actual = POSITIVE_INFINITY.microwatts * (-1).nanoseconds, ) } @Test fun infinite_power_multiply_infinite_time() { assertEquals( expected = POSITIVE_INFINITY.millijoules, actual = POSITIVE_INFINITY.microwatts * Duration.INFINITE, ) assertEquals( expected = POSITIVE_INFINITY.millijoules, actual = NEGATIVE_INFINITY.microwatts * -Duration.INFINITE, ) assertEquals( expected = NEGATIVE_INFINITY.millijoules, actual = NEGATIVE_INFINITY.microwatts * Duration.INFINITE, ) assertEquals( expected = NEGATIVE_INFINITY.millijoules, actual = POSITIVE_INFINITY.microwatts * -Duration.INFINITE, ) } @Test fun power_multiply_time_precision_femtojoule_precision() { assertEquals( expected = 1_234.millijoules, actual = 1_234.microwatts * 1_000.seconds ) } @Test fun power_multiply_time_precision_picojoule_precision() { // 123,456,788.999074074 millijoules, but we lose precision. assertEquals( expected = 123_456_788.millijoules, actual = 124_999_998_860.microwatts * 987_654_321.nanoseconds, ) } @Test fun power_multiply_time_precision_nanojoule_precision() { // 123,456,788,999.074074 millijoules, but we lose precision. assertEquals( expected = 123_456_788_999.millijoules, actual = 124_999_998_860.microwatts * 987_654_321.microseconds, ) } @Test fun power_multiply_time_precision_microjoule_precision() { // 123,456,788,999,074.07406 millijoules, but we lose precision. assertEquals( expected = 123_456_788_999_074.millijoules, actual = 124_999_998_860.microwatts * 987_654_321.milliseconds, ) } @Test fun power_multiply_time_precision_millijoule_precision() { // 123,456,788,999,074,074.06 millijoules, but we lose precision. assertEquals( expected = 123_456_788_999_074_074.millijoules, actual = 124_999_998_860.microwatts * 987_654_321.seconds, ) } } ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/type/TemperatureTest.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.WasmWasiIgnore import io.github.kevincianfarini.alchemist.scalar.celsius import io.github.kevincianfarini.alchemist.scalar.fahrenheit import io.github.kevincianfarini.alchemist.scalar.kelvins import io.github.kevincianfarini.alchemist.scalar.nanokelvins import io.github.kevincianfarini.alchemist.unit.TemperatureUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.text.Typography.nbsp class TemperatureTest { @Test fun well_known_temperatures_are_precise() { assertEquals(0.celsius, 32.fahrenheit) assertEquals(100.celsius, 212.fahrenheit) assertEquals((-40).celsius, (-40).fahrenheit) assertEquals(255_372_222_222.nanokelvins, 0.fahrenheit) assertEquals(273_150_000_000.nanokelvins, 0.celsius) } @Test @WasmWasiIgnore // See: https://youtrack.jetbrains.com/issue/KT-60964. fun to_string_with_unit_works_properly() { assertEquals("0.00°F", 0.fahrenheit.toString(TemperatureUnit.Fahrenheit, decimals = 2)) assertEquals("0.00°C", 0.celsius.toString(TemperatureUnit.International.Celsius, decimals = 2)) assertEquals("273.85°C", 547.kelvins.toString(TemperatureUnit.International.Celsius, decimals = 2)) assertEquals("0.001${nbsp}μK", 1.nanokelvins.toString(TemperatureUnit.International.Microkelvin, decimals = 3)) } @Test @WasmWasiIgnore // See: https://youtrack.jetbrains.com/issue/KT-60964. fun to_string_without_unit_picks_proper_unit() { assertEquals("0.00${nbsp}nK", 0.nanokelvins.toString()) assertEquals("1.00${nbsp}nK", 1.nanokelvins.toString()) assertEquals("1.00${nbsp}μK", 1_000.nanokelvins.toString()) assertEquals("1.00${nbsp}mK", 1_000_000.nanokelvins.toString()) assertEquals("1.00${nbsp}K", 1_000_000_000.nanokelvins.toString()) assertEquals("1.00${nbsp}kK", 1_000_000_000_000.nanokelvins.toString()) assertEquals("1.00${nbsp}MK", 1_000_000_000_000_000.nanokelvins.toString()) assertEquals("1.00${nbsp}GK", 1_000_000_000_000_000_000.nanokelvins.toString()) assertEquals("255.37${nbsp}K", 0.fahrenheit.toString()) assertEquals("273.15${nbsp}K", 0.celsius.toString()) } } ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/type/VelocityTest.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.WasmWasiIgnore import io.github.kevincianfarini.alchemist.scalar.meters import io.github.kevincianfarini.alchemist.scalar.nanometers import io.github.kevincianfarini.alchemist.scalar.nmPerSecond2 import io.github.kevincianfarini.alchemist.unit.LengthUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.text.Typography.nbsp import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit class VelocityTest { @Test fun to_double_simple_works() { assertEquals( expected = 444.4444404, actual = (123_456_789.nanometers / 1.seconds).toDouble( lengthUnit = LengthUnit.International.Meter, durationUnit = DurationUnit.HOURS, ), absoluteTolerance = 0.000001, ) } @Test fun to_string_with_units_simple_works() { assertEquals( expected = "3.6${nbsp}km/h", actual = (1.meters / 1.seconds).toString( lengthUnit = LengthUnit.International.Kilometer, durationUnit = DurationUnit.HOURS, decimals = 1, ) ) } @Test @WasmWasiIgnore // See: https://youtrack.jetbrains.com/issue/KT-60964. fun default_to_string_simple_works() { assertEquals( expected = "1.00${nbsp}m/s", actual = (1.meters / 1.seconds).toString() ) } @Test fun velocity_mul_time_simple() { assertEquals( expected = 1.meters, actual = (1.meters / 1.seconds) * 1.seconds ) assertEquals( expected = 444_441_600.meters, actual = (123_456.meters / 1.seconds) * 1.hours, ) } @Test fun velocity_div_time_simple() { assertEquals( expected = 10_032_520_325.nmPerSecond2, actual = 1_234.nanometers / 1.seconds / 123.nanoseconds, ) } } ================================================ FILE: src/commonTest/kotlin/io/github/kevincianfarini/alchemist/type/VolumeTest.kt ================================================ package io.github.kevincianfarini.alchemist.type import io.github.kevincianfarini.alchemist.WasmWasiIgnore import io.github.kevincianfarini.alchemist.scalar.kiloliters import io.github.kevincianfarini.alchemist.scalar.milliliters import io.github.kevincianfarini.alchemist.unit.LengthUnit import io.github.kevincianfarini.alchemist.unit.VolumeUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.text.Typography.nbsp class VolumeTest { @Test @WasmWasiIgnore // See: https://youtrack.jetbrains.com/issue/KT-60964. fun to_string_simple() { assertEquals( expected = "10.00m³", actual = 10.kiloliters.toString(), ) } @Test fun to_string_volume_simple() { assertEquals( expected = "1.234${nbsp}L", actual = 1_234.milliliters.toString(VolumeUnit.Metric.Liter, decimals = 3), ) } @Test fun to_double_nanometer_works_as_expected() { assertEquals( expected = 1.234e24, actual = 1_234.milliliters.toDouble(LengthUnit.International.Nanometer), ) } } ================================================ FILE: src/jsMain/kotlin/io/github/kevincianfarini/alchemist/internal/double.js.kt ================================================ package io.github.kevincianfarini.alchemist.internal import kotlin.math.abs import kotlin.math.pow import kotlin.math.round import kotlin.math.sign import kotlin.math.ceil import kotlin.math.log10 internal actual fun Double.toDecimalString(decimals: Int): String { val rounded = if (decimals == 0) { this } else { val pow = (10.0).pow(decimals) round(abs(this) * pow) / pow * sign(this) } return if (abs(rounded) < 1e21) { // toFixed switches to scientific format after 1e21 toFixed(rounded, decimals) } else { // toPrecision outputs the specified number of digits, but only for positive numbers val positive = abs(rounded) val positiveString = toPrecision(positive, ceil(log10(positive)).toInt() + decimals) if (rounded < 0) "-$positiveString" else positiveString } } @Suppress("UNUSED_PARAMETER") private fun toFixed(value: Double, decimals: Int): String = js("value.toFixed(decimals)") @Suppress("UNUSED_PARAMETER") private fun toPrecision(value: Double, decimals: Int): String = js("value.toPrecision(decimals)") ================================================ FILE: src/jsTest/kotlin/io/github/kevincianfarini/alchemist/ignore.js.kt ================================================ package io.github.kevincianfarini.alchemist actual typealias JsIgnore = kotlin.test.Ignore ================================================ FILE: src/jvmMain/kotlin/io/github/kevincianfarini/alchemist/internal/double.jvm.kt ================================================ package io.github.kevincianfarini.alchemist.internal import java.math.RoundingMode import java.text.DecimalFormat internal actual fun Double.toDecimalString(decimals: Int): String { val format = DecimalFormat("0").apply { if (decimals > 0) { minimumFractionDigits = decimals maximumFractionDigits = decimals } roundingMode = RoundingMode.HALF_UP } return format.format(this) } ================================================ FILE: src/nativeMain/kotlin/io/github/kevincianfarini/alchemist/internal/double.native.kt ================================================ package io.github.kevincianfarini.alchemist.internal import kotlinx.cinterop.ByteVar import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.allocArray import kotlinx.cinterop.memScoped import kotlinx.cinterop.toKString import platform.posix.sprintf @OptIn(ExperimentalForeignApi::class) internal actual fun Double.toDecimalString(decimals: Int): String = memScoped { val buff = allocArray(Double.SIZE_BYTES * 2) sprintf(buff, "%.${decimals}f", this@toDecimalString) buff.toKString() } ================================================ FILE: src/wasmJsMain/kotlin/io/github/kevincianfarini/alchemist/internal/double.wasmjs.kt ================================================ package io.github.kevincianfarini.alchemist.internal import kotlin.math.abs import kotlin.math.pow import kotlin.math.round import kotlin.math.sign import kotlin.math.ceil import kotlin.math.log10 internal actual fun Double.toDecimalString(decimals: Int): String { val rounded = if (decimals == 0) { this } else { val pow = (10.0).pow(decimals) round(abs(this) * pow) / pow * sign(this) } return if (abs(rounded) < 1e21) { // toFixed switches to scientific format after 1e21 toFixed(rounded, decimals) } else { // toPrecision outputs the specified number of digits, but only for positive numbers val positive = abs(rounded) val positiveString = toPrecision(positive, ceil(log10(positive)).toInt() + decimals) if (rounded < 0) "-$positiveString" else positiveString } } @Suppress("UNUSED_PARAMETER") private fun toFixed(value: Double, decimals: Int): String = js("value.toFixed(decimals)") @Suppress("UNUSED_PARAMETER") private fun toPrecision(value: Double, decimals: Int): String = js("value.toPrecision(decimals)") ================================================ FILE: src/wasmJsTest/kotlin/io/github/kevincianfarini/alchemist/ignore.wasmjs.kt ================================================ package io.github.kevincianfarini.alchemist actual typealias WasmJsIgnore = kotlin.test.Ignore ================================================ FILE: src/wasmWasiMain/kotlin/io/github/kevincianfarini/alchemist/internal/double.wasmwasi.kt ================================================ @file:Suppress( "LocalVariableName", "JoinDeclarationAndAssignment", "CanBeVal", "FunctionName", "LiftReturnOrAssignment" ) package io.github.kevincianfarini.alchemist.internal import kotlin.math.pow internal actual fun Double.toDecimalString(decimals: Int): String { // Copy-pasted impl from the stdlib, which has an open issue: (KT-60964). val pow = 10.0.pow(decimals) val round = rint(this * pow) return (round / pow).toString() } private val TWO52 = doubleArrayOf( 4.50359962737049600000e+15, /* 0x43300000, 0x00000000 */ -4.50359962737049600000e+15, /* 0xC3300000, 0x00000000 */ ) private fun rint(_x: Double): Double { var x: Double = _x var i0: Int var j0: Int var sx: Int var i: UInt var i1: UInt var w: Double var t: Double i0 = __HI(x) sx = (i0 shr 31) and 1 i1 = __LOu(x) j0 = ((i0 shr 20) and 0x7ff) - 0x3ff if (j0 < 20) { if (j0 < 0) { if (((i0 and 0x7fffffff) or i1.toInt()) == 0) return x i1 = i1 or (i0 and 0x0fffff).toUInt() i0 = i0 and 0xfffe0000.toInt() i0 = i0 or (((i1 or i1.negate()) shr 12) and 0x80000.toUInt()).toInt() x = doubleSetWord(d = x, hi = i0) w = TWO52[sx] + x t = w - TWO52[sx] i0 = __HI(t) t = doubleSetWord(d = t, hi = (i0 and 0x7fffffff) or (sx shl 31)) return t } else { i = ((0x000fffff) shr j0).toUInt() if (((i0 and i.toInt()) or i1.toInt()) == 0) return x /* x is integral */ i = i shr 1 if (((i0 and i.toInt()) or i1.toInt()) != 0) { if (j0 == 19) i1 = 0x40000000.toUInt(); else i0 = (i0 and i.inv().toInt()) or ((0x20000) shr j0) } } } else if (j0 > 51) { if (j0 == 0x400) return x + x /* inf or NaN */ else return x /* x is integral */ } else { i = ((0xffffffff.toUInt())) shr (j0 - 20) if ((i1 and i) == 0U) return x /* x is integral */ i = i shr 1 if ((i1 and i) != 0U) i1 = (i1 and (i.inv())) or ((0x40000000) shr (j0 - 20)).toUInt() } x = doubleSetWord(x, hi = i0, lo = i1.toInt()) w = TWO52[sx] + x return w - TWO52[sx] } private fun __HI(x: Double): Int = (x.toRawBits() ushr 32).toInt() //#define __LO(x) *(int*)&x private fun __LO(x: Double): Int = (x.toRawBits() and 0xFFFFFFFF).toInt() private fun __LOu(x: Double): UInt = (x.toRawBits() and 0xFFFFFFFF).toUInt() private fun doubleSetWord(d: Double = 0.0, hi: Int = __HI(d), lo: Int = __LO(d)): Double = Double.fromBits((hi.toLong() shl 32) or (lo.toLong() and 0xFFFFFFFF)) private fun UInt.negate(): UInt = inv() + 1U ================================================ FILE: src/wasmWasiTest/kotlin/io/github/kevincianfarini/alchemist/ignore.wasmwasi.kt ================================================ package io.github.kevincianfarini.alchemist actual typealias WasmWasiIgnore = kotlin.test.Ignore