diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..78510fc --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,35 @@ +name: Testing on various Java versions on Linux, Windows, and MacOS + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ windows-latest, macos-latest, ubuntu-latest ] + jdk: [ 11, 17, 20 ] + steps: + - uses: actions/checkout@v3 + - name: Set up JDK ${{ matrix.jdk }} + uses: actions/setup-java@v3 + with: + java-version: ${{ matrix.jdk }} + distribution: 'temurin' + - name: Build with Gradle + uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 + with: + arguments: build + - name: Store test results + if: failure() + uses: actions/upload-artifact@v3 + with: + name: test-results + path: build/reports \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efd6738 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea +/.gradle +/build +/out diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6a49c50 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 knokko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..506300f --- /dev/null +++ b/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' version '1.9.0' +} + +apply plugin: 'java' +apply plugin: 'org.jetbrains.kotlin.jvm' + +repositories { + mavenCentral() +} + +compileKotlin { + kotlinOptions.jvmTarget = "11" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "11" +} + +java { + sourceCompatibility = "11" + targetCompatibility = "11" +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +test { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..033e24c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..62f495d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..fcb6fca --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/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. +# + +############################################################################## +# +# 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/subprojects/plugins/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##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || 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=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# 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=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=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 $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@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 + +@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. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +: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 diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..46c8529 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +jdk: + - openjdk11 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9ee668b --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'fixie' diff --git a/src/main/kotlin/fixie/FixNano64.kt b/src/main/kotlin/fixie/FixNano64.kt new file mode 100644 index 0000000..1103bd1 --- /dev/null +++ b/src/main/kotlin/fixie/FixNano64.kt @@ -0,0 +1,223 @@ +package fixie + +import java.math.BigInteger +import kotlin.math.absoluteValue +import kotlin.math.roundToLong + +private const val RAW_ONE = 1024L * 1024L * 1024L + +@JvmInline +value class FixNano64 private constructor(val raw: Long): Comparable { + + @Throws(ArithmeticException::class) + fun toInt() = Math.toIntExact(toLong()) + + fun toLong() = raw / RAW_ONE + + fun toFloat() = toDouble().toFloat() + + fun toDouble() = raw / RAW_ONE.toDouble() + + @Throws(FixedPointException::class) + operator fun unaryMinus() = if (raw != Long.MIN_VALUE) FixNano64(-raw) else throw FixedPointException("negating Long.MIN_VALUE") + + @Throws(FixedPointException::class) + operator fun plus(other: FixNano64): FixNano64 { + try { + return FixNano64(Math.addExact(raw, other.raw)) + } catch (overflow: ArithmeticException) { + throw FixedPointException("Tried to compute $this + $other") + } + } + + @Throws(FixedPointException::class) + operator fun minus(right: FixNano64): FixNano64 { + try { + return FixNano64(Math.subtractExact(raw, right.raw)) + } catch (overflow: ArithmeticException) { + throw FixedPointException("Tried to compute $this - $right") + } + } + + operator fun times(other: Int) = this * other.toLong() + + operator fun times(other: Long): FixNano64 { + try { + return FixNano64(Math.multiplyExact(raw, other)) + } catch (overflow: ArithmeticException) { + throw FixedPointException("Tried to compute $this * $other") + } + } + + operator fun times(other: Float) = this * fromFloat(other) + + operator fun times(other: Double) = this * fromDouble(other) + + operator fun times(other: FixNano64): FixNano64 { + /* + * Theoretically: + * this = this.raw * 2^-30 + * other = other.raw * 2^-30 + * result = this * other = this.raw * other.raw * 2^-60 -> result.raw = this.raw * other.raw * 2^-30 + * + * Practically, computing this.raw * other.raw could overflow, so we need a more complicated approach... + */ + + val highProductBits = Math.multiplyHigh(this.raw, other.raw) + if (highProductBits < -(1 shl 29) || highProductBits >= (1 shl 29)) { + throw FixedPointException("Attempted to compute $this * $other") + } + + val lowProductBits = this.raw * other.raw + + return FixNano64((lowProductBits ushr 30) or (highProductBits shl 34)) + } + + operator fun div(other: Int) = this / other.toLong() + + operator fun div(other: Long): FixNano64 { + if (other == -1L && raw == Long.MIN_VALUE) throw FixedPointException("Attempted to compute $this / -1") + if (other == 0L) throw FixedPointException("Attempted to compute $this / 0") + return FixNano64(raw / other) + } + + operator fun div(other: Float) = this / other.toDouble() + + operator fun div(other: Double): FixNano64 { + val doubleResult = this.raw.toDouble() / other + if (doubleResult >= Long.MIN_VALUE.toDouble() && doubleResult <= Long.MAX_VALUE.toDouble()) { + return FixNano64(doubleResult.roundToLong()) + } else { + throw FixedPointException("Attempted to compute $this / $other") + } + } + + operator fun div(other: FixNano64): FixNano64 { + /* + * Theoretically: + * this = this.raw * 2^-30 + * other = other.raw * 2^-30 + * result = this / other = this.raw / other.raw -> result.raw = 2^30 * this.raw / other.raw + * + * Practically, computing 2^30 * this.raw could overflow, so we need a more complicated approach... + * Unfortunately, I couldn't find a way that avoids BigInteger, so this will be rather slow. + */ + + // TODO Optimize case when this.raw * 2^30 doesn't overflow + val product = BigInteger.valueOf(this.raw) shl 30 + val bigResult = product / BigInteger.valueOf(other.raw) + try { + return FixNano64(bigResult.longValueExact()) + } catch (overflow: ArithmeticException) { + throw FixedPointException("Attempted to compute $this / $other") + } + } + + override fun compareTo(other: FixNano64) = raw.compareTo(other.raw) + + override fun toString(): String { + if (raw == Long.MIN_VALUE) return "MIN" + if (raw == Long.MAX_VALUE) return "MAX" + + var remaining = (raw.absoluteValue.toBigInteger() * (1000L * 1000L * 1000L).toBigInteger() shr 30).toLong() + + val signString = if (raw < 0) "-" else "" + + if (remaining < 1000) return signString + remaining + "n" + + var numShifts = 0 + while (remaining / 1_000_000L != 0L) { + val shouldRoundUp = (remaining % 1000) >= 500 + remaining /= 1000 + if (shouldRoundUp) remaining += 1 + numShifts += 1 + } + + val integerPart = (remaining / 1000) % 1000 + val fractionalPart = remaining % 1000 + + val unitString = when (numShifts) { + 0 -> "u" + 1 -> "m" + 2 -> "" + 3 -> "k" + 4 -> "M" + 5 -> "G" + else -> throw Error("Unexpected value for numShifts: $numShifts") + } + + var fractionalString = if (fractionalPart > 0) { + if (fractionalPart < 10) ".00$fractionalPart" + else if (fractionalPart < 100) ".0$fractionalPart" + else ".$fractionalPart" + } else "" + while (fractionalString.endsWith("0")) fractionalString = fractionalString.substring(0, fractionalString.length - 1) + return signString + integerPart + fractionalString + unitString + } + + companion object { + + val ZERO = fromInt(0) + + fun raw(rawValue: Long) = FixNano64(rawValue) + + fun fromInt(value: Int) = FixNano64(value.toLong() * RAW_ONE) + + fun fromLong(value: Long): FixNano64 { + try { + return FixNano64(Math.multiplyExact(value, RAW_ONE)) + } catch (overflow: ArithmeticException) { + throw FixedPointException("Can't convert $value to FixNano64") + } + } + + fun fromFloat(value: Float) = fromDouble(value.toDouble()) + + fun fromDouble(value: Double): FixNano64 { + val doubleValue = RAW_ONE.toDouble() * value + if (doubleValue > Long.MAX_VALUE.toDouble() || doubleValue < Long.MIN_VALUE.toDouble()) { + throw FixedPointException("Can't represent $value") + } + return FixNano64(doubleValue.roundToLong()) + } + + // Note that overflow is impossible since RAW_ONE == 2^30 and |numerator| < 2^31 + fun fraction(numerator: Int, denominator: Int) = FixNano64(RAW_ONE * numerator / denominator) + + fun fraction(numerator: Long, denominator: Long): FixNano64 { + // TODO Optimize common case when RAW_ONE * numerator doesn't overflow + try { + return FixNano64( + (BigInteger.valueOf(RAW_ONE) * BigInteger.valueOf(numerator) / BigInteger.valueOf( + denominator + )).longValueExact() + ) + } catch (finalOverflow: ArithmeticException) { + throw FixedPointException("Can't represent $numerator / $denominator") + } + } + } +} + +operator fun Int.times(other: FixNano64) = other * this + +operator fun Long.times(other: FixNano64) = other * this + +operator fun Float.times(other: FixNano64) = other * this + +operator fun Double.times(other: FixNano64) = other * this + +operator fun Int.div(other: FixNano64) = FixNano64.fromInt(this) / other + +operator fun Long.div(other: FixNano64) = FixNano64.fromLong(this) / other + +operator fun Float.div(other: FixNano64) = FixNano64.fromFloat(this) / other + +operator fun Double.div(other: FixNano64) = FixNano64.fromDouble(this) / other + +@Throws(AssertionError::class) +fun assertEquals(expected: FixNano64, actual: FixNano64, margin: Double) { + if ((expected.toDouble() - actual.toDouble()).absoluteValue > margin) { + throw AssertionError("Expected $expected, but got $actual (difference is ${expected - actual})") + } +} diff --git a/src/main/kotlin/fixie/FixedPointException.kt b/src/main/kotlin/fixie/FixedPointException.kt new file mode 100644 index 0000000..8b9b9ad --- /dev/null +++ b/src/main/kotlin/fixie/FixedPointException.kt @@ -0,0 +1,3 @@ +package fixie + +class FixedPointException(message: String): RuntimeException(message) diff --git a/src/main/kotlin/fixie/Norm32.kt b/src/main/kotlin/fixie/Norm32.kt new file mode 100644 index 0000000..62b676a --- /dev/null +++ b/src/main/kotlin/fixie/Norm32.kt @@ -0,0 +1,87 @@ +package fixie + +import java.util.* +import kotlin.math.roundToLong + +@JvmInline +value class Norm32 private constructor(val raw: Int) : Comparable { + + init { + if (raw == Int.MIN_VALUE) throw IllegalStateException("raw value must not be Int.MIN_VALUE") + } + + operator fun unaryMinus() = Norm32(-raw) + + @Throws(FixedPointException::class) + operator fun plus(other: Norm32): Norm32 { + val rawResult = this.raw.toLong() + other.raw.toLong() + + if (rawResult > Int.MAX_VALUE) { + + // Prevent overflow exceptions due to small rounding errors + if (rawResult < Int.MAX_VALUE + 1000L) return ONE + throw FixedPointException("$this + $other would overflow") + } + + if (rawResult < Int.MIN_VALUE) { + + // Prevent underflow exceptions due to small rounding errors + if (rawResult > -Int.MAX_VALUE - 1000L) return -ONE + throw FixedPointException("$this + $other would underflow") + } + + return Norm32(rawResult.toInt()) + } + + @Throws(FixedPointException::class) + operator fun minus(right: Norm32) = this + -right + + fun toFloat() = toDouble().toFloat() + + fun toDouble() = raw.toDouble() / Int.MAX_VALUE.toDouble() + + override fun compareTo(other: Norm32) = raw.compareTo(other.raw) + + override fun toString(): String { + val originalString = String.format(Locale.ROOT, "%.4f", toDouble()) + + var prefix: String + var valueString: String + if (originalString.startsWith("-")) { + prefix = "-" + valueString = originalString.substring(1) + } else { + prefix = "" + valueString = originalString + } + + while (valueString.length > 1 && (valueString.endsWith("0") || valueString.endsWith("."))) { + valueString = valueString.substring(0 until valueString.length - 1) + } + + if (valueString == "0") prefix = "" + + return prefix + valueString + } + + companion object { + + val ZERO = Norm32(0) + val ONE = Norm32(Int.MAX_VALUE) + + fun fromFloat(value: Float) = fromDouble(value.toDouble()) + + @Throws(FixedPointException::class) + fun fromDouble(value: Double): Norm32 { + val longValue = (value * Int.MAX_VALUE).roundToLong() + if (longValue < -Int.MAX_VALUE || longValue > Int.MAX_VALUE) throw FixedPointException("Can't normalize $value") + + return Norm32(longValue.toInt()) + } + + fun createRaw(rawValue: Int): Norm32 { + if (rawValue == Int.MIN_VALUE) throw FixedPointException("Using Int.MIN_VALUE is forbidden") + return Norm32(rawValue) + } + } +} diff --git a/src/test/kotlin/Playground.kt b/src/test/kotlin/Playground.kt new file mode 100644 index 0000000..d984d1b --- /dev/null +++ b/src/test/kotlin/Playground.kt @@ -0,0 +1,56 @@ +import fixie.FixNano64 +import java.util.* +import kotlin.math.absoluteValue +import kotlin.system.measureNanoTime + +fun main() { + tryDoublePerformance() +} + +private fun tryFloatPerformance() { + val rng = Random() + val values = FloatArray(100_000_000) { 0.5f + rng.nextFloat() } + + val spentTime = measureNanoTime { + var result = 0f + for (index in 1 until values.size) { + result += values[index - 1] * values[index] + if (result.absoluteValue > 10_000f) result = 0f + } + println("result is $result") + } + + println("Took ${spentTime / 1_000_000.0} ms") +} + +private fun tryDoublePerformance() { + val rng = Random() + val values = DoubleArray(100_000_000) { 0.5 + rng.nextDouble() } + + val spentTime = measureNanoTime { + var result = 0.0 + for (index in 1 until values.size) { + result += values[index - 1] * values[index] + if (result.absoluteValue > 10_000.0) result = 0.0 + } + println("result is $result") + } + + println("Took ${spentTime / 1_000_000.0} ms") +} + +private fun tryFixedPerformance() { + val rng = Random() + val values = LongArray(100_000_000) { FixNano64.fromDouble(0.5 + 50.0 * rng.nextDouble()).raw } + + val spentTime = measureNanoTime { + var result = FixNano64.ZERO + for (index in 1 until values.size) { + result += FixNano64.raw(values[index - 1]) * FixNano64.raw(values[index]) + if (result.toDouble().absoluteValue > 10_000.0) result = FixNano64.ZERO + } + println("result is $result") + } + + println("Took ${spentTime / 1_000_000.0} ms") +} diff --git a/src/test/kotlin/fixie/TestFixNano64.kt b/src/test/kotlin/fixie/TestFixNano64.kt new file mode 100644 index 0000000..2826404 --- /dev/null +++ b/src/test/kotlin/fixie/TestFixNano64.kt @@ -0,0 +1,464 @@ +package fixie + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +private fun fi(value: Int) = FixNano64.fromInt(value) +private fun fl(value: Long) = FixNano64.fromLong(value) +private fun fd(value: Double) = FixNano64.fromDouble(value) + +class TestFixNano64 { + + @Test + fun testToFloat() { + for (value in arrayOf(-8_000_000_000f, -10f, 0.0001f, 1.2f, 43.2f, 8_000_000_000f)) { + assertEquals(value, FixNano64.fromFloat(value).toFloat(), 0.001f) + } + assertEquals(-5f, fi(-5).toFloat(), 0.001f) + } + + @Test + fun testToDouble() { + for (value in arrayOf(-8_500_000_000.0, -10.0, 0.0001, 1.2, 43.2, 8_500_000_000.0)) { + assertEquals(value, fd(value).toDouble(), 0.001) + } + assertEquals(-5.0, fi(-5).toDouble(), 0.001) + } + + @Test + fun testFromFloatOverflow() { + assertThrows { FixNano64.fromFloat(-8_600_000_000f) } + assertThrows { FixNano64.fromFloat(8_600_000_000f) } + } + + @Test + fun testFromDoubleOverflow() { + assertThrows { FixNano64.fromDouble(-8_600_000_000.0) } + assertThrows { FixNano64.fromDouble(8_600_000_000.0) } + } + + @Test + fun testToString() { + assertEquals("MIN", FixNano64.raw(Long.MIN_VALUE).toString()) + assertEquals("MAX", FixNano64.raw(Long.MAX_VALUE).toString()) + assertEquals("8.59G", FixNano64.raw(Long.MAX_VALUE - 1).toString()) + assertEquals("-8.59G", FixNano64.raw(Long.MIN_VALUE + 1).toString()) + assertEquals("12M", fd(12_000_000.0).toString()) + assertEquals("-100.324M", fi(-100_324_000).toString()) + assertEquals("-1.2k", fd(-1200.0).toString()) + assertEquals("15", fi(15).toString()) + assertEquals("-999", fi(-999).toString()) + assertEquals("1.512", FixNano64.fromFloat(1.512f).toString()) + assertEquals("100.53m", FixNano64.fromFloat(0.100_53f).toString()) + assertEquals("13.45u", fd(0.000_013_45).toString()) + assertEquals("-999.987u", fd(-0.000_999_987).toString()) + assertEquals("254n", fd(0.000_000_254).toString()) + assertEquals("100.004k", fl(100_004).toString()) + assertEquals("100k", fd(100_000.4).toString()) + assertEquals("100.001k", fd(100_000.6).toString()) + } + + @Test + fun testAdd() { + val smallMargin = 0.000_000_01 + val largeMargin = 0.000_001 + + assertEquals(fd(1.8), fi(1) + fd(0.8), smallMargin) + assertEquals(FixNano64.fromFloat(-12.45f), fi(-25) + fd(12.55), largeMargin) + assertEquals(fi(50), FixNano64.fromFloat(70f) + fd(-20.0), largeMargin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE - 10) + FixNano64.raw(10), smallMargin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), FixNano64.raw(Long.MIN_VALUE + 50) + FixNano64.raw(-50), smallMargin) + + assertThrows { FixNano64.raw(Long.MIN_VALUE) + FixNano64.raw(-1) } + assertThrows { FixNano64.raw(Long.MAX_VALUE) + FixNano64.raw(1) } + assertThrows { fl(5_000_000_000) + fd(3_600_000_000.0) } + assertThrows { fd(-5_000_000_000.0) + fl(-3_600_000_000) } + + assertEquals(fd(8_550_000_000.0), fi(2_000_000_000) + fl(6_550_000_000), largeMargin) + assertEquals(fd(-8_550_000_000.0), -fi(2_000_000_000) + fl(-6_550_000_000), largeMargin) + } + + @Test + fun testSubtract() { + val smallMargin = 0.000_000_01 + val largeMargin = 0.000_001 + + assertEquals(fd(1.8), fi(1) - fd(-0.8), smallMargin) + assertEquals(FixNano64.fromFloat(-12.45f), fi(-25) - fd(-12.55), largeMargin) + assertEquals(fi(50), FixNano64.fromFloat(70f) - fd(20.0), largeMargin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE - 10) - FixNano64.raw(-10), smallMargin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), FixNano64.raw(Long.MIN_VALUE + 50) - FixNano64.raw(50), smallMargin) + + assertThrows { FixNano64.raw(Long.MIN_VALUE) - FixNano64.raw(1) } + assertThrows { FixNano64.raw(Long.MAX_VALUE) - FixNano64.raw(-1) } + assertThrows { fl(5_000_000_000) - fl(-3_600_000_000) } + assertThrows { fl(-5_000_000_000) - fl(3_600_000_000) } + + assertEquals(fd(8_550_000_000.0), fi(2_000_000_000) - fd(-6_550_000_000.0), largeMargin) + assertEquals(fl(-8_550_000_000), -fi(2_000_000_000) - fl(6_550_000_000), largeMargin) + } + + @Test + fun testMultiplyInt() { + val margin = 0.000_000_01 + + assertEquals(fi(5000), fi(100) * 50, margin) + assertEquals(fi(5000), 50 * fi(100), margin) + + assertEquals(fd(0.1), fd(0.02) * 5, margin) + assertEquals(fd(-0.1), fd(0.02) * -5, margin) + assertEquals(fd(-0.1), fd(-0.02) * 5, margin) + assertEquals(fd(0.1), fd(-0.02) * -5, margin) + + assertEquals(fl(8_400_000_000), fi(2_100_000_000) * 4) + assertEquals(fl(-8_400_000_000), 2_100_000_000 * -fi(4)) + + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) * -1 * -1, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), 1 * FixNano64.raw(Long.MAX_VALUE), margin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), 1 * FixNano64.raw(Long.MIN_VALUE), margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), -1 * FixNano64.raw(Long.MIN_VALUE + 1), margin) + + assertThrows { -1 * FixNano64.raw(Long.MIN_VALUE) } + assertThrows { fi(1_000_000_000) * 8100 } + assertThrows { fi(-1_000_000_000) * 8100 } + assertThrows { fi(1_000_000_000) * -8100 } + assertThrows { fi(-1_000_000_000) * -8100 } + } + + @Test + fun testMultiplyLong() { + val margin = 0.000_000_01 + + assertEquals(fi(5000), fi(100) * 50L, margin) + assertEquals(fi(5000), 50L * fi(100), margin) + + assertEquals(fd(0.1), fd(0.02) * 5L, margin) + assertEquals(fd(-0.1), fd(0.02) * -5L, margin) + assertEquals(fd(-0.1), fd(-0.02) * 5L, margin) + assertEquals(fd(0.1), fd(-0.02) * -5L, margin) + + assertEquals(fl(8_400_000_000), fi(2_100_000_000) * 4L) + assertEquals(fl(-8_400_000_000), 2_100_000_000L * -fi(4)) + + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) * -1L * -1L, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), 1L * FixNano64.raw(Long.MAX_VALUE), margin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), 1L * FixNano64.raw(Long.MIN_VALUE), margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), -1L * FixNano64.raw(Long.MIN_VALUE + 1), margin) + + assertThrows { -1L * FixNano64.raw(Long.MIN_VALUE) } + assertThrows { fi(1_000_000_000) * 8100L } + assertThrows { fi(-1_000_000_000) * 8100L } + assertThrows { fi(1_000_000_000) * -8100L } + assertThrows { fi(-1_000_000_000) * -8100L } + } + + @Test + fun testMultiplyFloat() { + val margin = 0.000_000_01 + + assertEquals(fi(5000), fi(100) * 50f, margin) + assertEquals(fi(5000), 50f * fi(100), margin) + + assertEquals(fd(0.1), fd(0.02) * 5f, margin) + assertEquals(fd(-0.1), fd(0.02) * -5f, margin) + assertEquals(fd(-0.1), fd(-0.02) * 5f, margin) + assertEquals(fd(0.1), fd(-0.02) * -5f, margin) + + assertEquals(fl(8_400_000_000), fi(2_100_000_000) * 4f) + assertEquals(fl(-8_400_000_000), 2_100_000_000f * -fi(4)) + + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) * -1f * -1f, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), 1f * FixNano64.raw(Long.MAX_VALUE), margin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), 1f * FixNano64.raw(Long.MIN_VALUE), margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), -1f * FixNano64.raw(Long.MIN_VALUE + 1), margin) + + assertThrows { -1f * FixNano64.raw(Long.MIN_VALUE) } + assertThrows { fi(1_000_000_000) * 8100f } + assertThrows { fi(-1_000_000_000) * 8100f } + assertThrows { fi(1_000_000_000) * -8100f } + assertThrows { fi(-1_000_000_000) * -8100f } + } + + @Test + fun testMultiplyDouble() { + val margin = 0.000_000_01 + + assertEquals(fi(5000), fi(100) * 50.0, margin) + assertEquals(fi(5000), 50.0 * fi(100), margin) + + assertEquals(fd(0.1), fd(0.02) * 5.0, margin) + assertEquals(fd(-0.1), fd(0.02) * -5.0, margin) + assertEquals(fd(-0.1), fd(-0.02) * 5.0, margin) + assertEquals(fd(0.1), fd(-0.02) * -5.0, margin) + + assertEquals(fl(8_400_000_000), fi(2_100_000_000) * 4.0) + assertEquals(fl(-8_400_000_000), 2_100_000_000.0 * -fi(4)) + + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) * -1.0 * -1.0, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), 1.0 * FixNano64.raw(Long.MAX_VALUE), margin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), 1.0 * FixNano64.raw(Long.MIN_VALUE), margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), -1.0 * FixNano64.raw(Long.MIN_VALUE + 1), margin) + + assertThrows { -1.0 * FixNano64.raw(Long.MIN_VALUE) } + assertThrows { fi(1_000_000_000) * 8100.0 } + assertThrows { fi(-1_000_000_000) * 8100.0 } + assertThrows { fi(1_000_000_000) * -8100.0 } + assertThrows { fi(-1_000_000_000) * -8100.0 } + } + + @Test + fun testMultiplyFixed() { + val margin = 0.000_000_01 + + assertEquals(fi(5000), fi(100) * fi(50), margin) + + assertEquals(fd(0.1), fd(0.02) * fi(5), margin) + assertEquals(fd(-0.1), fd(0.02) * fi(-5), margin) + assertEquals(fd(-0.1), fd(-0.02) * fi(5), margin) + assertEquals(fd(0.1), fd(-0.02) * -fi(5), margin) + + assertEquals(fl(8_400_000_000), fi(2_100_000_000) * fi(4)) + assertEquals(fd(-8_400_000_000.0), fi(2_100_000_000) * -fi(4)) + + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) * -fi(1) * -fi(1), margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), fi(1) * FixNano64.raw(Long.MAX_VALUE), margin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), fi(1) * FixNano64.raw(Long.MIN_VALUE), margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), fi(-1) * FixNano64.raw(Long.MIN_VALUE + 1), margin) + + assertThrows { fi(-1) * FixNano64.raw(Long.MIN_VALUE) } + assertThrows { fi(2) * FixNano64.raw(Long.MIN_VALUE / 2 - 1) } + assertThrows { FixNano64.raw(Long.MAX_VALUE / 2 + 1) * fi(2) } + assertThrows { fi(1_000_000_000) * fi(8100) } + assertThrows { fi(-1_000_000_000) * fi(8100) } + assertThrows { fi(1_000_000_000) * fi(-8100) } + assertThrows { fi(-1_000_000_000) * fi(-8100) } + } + + @Test + fun testDivideInt() { + val margin = 0.000_000_01 + + assertEquals(fi(100), fi(5000) / 50, margin) + assertEquals(fi(50), fi(5000) / 100, margin) + assertEquals(fi(50), 5000 / fi(100)) + + assertEquals(fd(0.02), fd(0.1) / 5, margin) + assertEquals(fd(0.02), fd(-0.1) / -5, margin) + assertEquals(fd(-0.02), fd(-0.1) / 5, margin) + assertEquals(-fd(0.02), fd(0.1) / -5, margin) + + assertEquals(fi(4), fl(8_400_000_000) / 2_100_000_000, margin) + assertEquals(-fi(4), fl(8_400_000_000) / -2_100_000_000, margin) + + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) / -1 / -1, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) / 1, margin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), FixNano64.raw(Long.MIN_VALUE) / 1, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MIN_VALUE + 1) / -1, margin) + + assertThrows { FixNano64.raw(Long.MIN_VALUE) / -1 } + } + + @Test + fun testDivideLong() { + val margin = 0.000_000_01 + + assertEquals(fi(100), fi(5000) / 50L, margin) + assertEquals(fi(50), fi(5000) / 100L, margin) + assertEquals(fi(100), 5000L / fi(50)) + + assertEquals(fd(0.02), fd(0.1) / 5L, margin) + assertEquals(fd(0.02), fd(-0.1) / -5L, margin) + assertEquals(fd(-0.02), fd(-0.1) / 5L, margin) + assertEquals(-fd(0.02), fd(0.1) / -5L, margin) + + assertEquals(fi(4), fl(8_400_000_000) / 2_100_000_000L, margin) + assertEquals(-fi(4), fl(8_400_000_000) / -2_100_000_000L, margin) + + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) / -1L / -1L, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) / 1L, margin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), FixNano64.raw(Long.MIN_VALUE) / 1L, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MIN_VALUE + 1L) / -1L, margin) + + assertThrows { FixNano64.raw(Long.MIN_VALUE) / -1L } + } + + @Test + fun testDivideFloat() { + val margin = 0.000_000_01 + + assertEquals(fi(100), fi(5000) / 50f, margin) + assertEquals(fi(50), fi(5000) / 100f, margin) + assertEquals(fi(100), 5000f / fi(50), margin) + + assertEquals(fd(0.02), fd(0.1) / 5f, margin) + assertEquals(fd(0.02), fd(-0.1) / -5f, margin) + assertEquals(fd(-0.02), fd(-0.1) / 5f, margin) + assertEquals(fd(-0.02), fd(0.1) / -5f, margin) + + assertEquals(fi(5), fd(0.1) / 0.02f, 0.000_001) + assertEquals(fi(5), 0.1f / fd(0.02), 0.000_001) + + assertEquals(fi(2_100_000_000), fl(8_400_000_000) / 4f, margin) + assertEquals(fi(-2_100_000_000), fl(8_400_000_000) / -4f, margin) + + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) / -1f / -1f, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) / 1f, margin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), FixNano64.raw(Long.MIN_VALUE) / 1f, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MIN_VALUE + 1) / -1f, margin) + + assertThrows { fi(1_000_000_000) / (1f / 8100f) } + assertThrows { fi(-1_000_000_000) / (1f / 8100f) } + assertThrows { fi(1_000_000_000) / (1f / -8100f) } + assertThrows { fi(-1_000_000_000) / (1f / -8100f) } + } + + @Test + fun testDivideDouble() { + val margin = 0.000_000_01 + + assertEquals(fi(100), fi(5000) / 50.0, margin) + assertEquals(fi(50), fi(5000) / 100.0, margin) + assertEquals(fi(50), 5000.0 / fi(100), margin) + + assertEquals(fd(0.02), fd(0.1) / 5.0, margin) + assertEquals(fd(0.02), fd(-0.1) / -5.0, margin) + assertEquals(fd(-0.02), fd(-0.1) / 5.0, margin) + assertEquals(fd(-0.02), fd(0.1) / -5.0, margin) + + assertEquals(fi(5), fd(0.1) / 0.02, 0.000_001) + assertEquals(fi(5), 0.1 / fd(0.02), 0.000_001) + + assertEquals(fi(2_100_000_000), fl(8_400_000_000) / 4.0, margin) + assertEquals(fi(-2_100_000_000), fl(8_400_000_000) / -4.0, margin) + + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) / -1.0 / -1.0, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) / 1.0, margin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), FixNano64.raw(Long.MIN_VALUE) / 1.0, margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MIN_VALUE + 1) / -1.0, margin) + + assertThrows { fi(1_000_000_000) / (1.0 / 8100.0) } + assertThrows { fi(-1_000_000_000) / (1.0 / 8100.0) } + assertThrows { fi(1_000_000_000) / (1.0 / -8100.0) } + assertThrows { fi(-1_000_000_000) / (1.0 / -8100.0) } + } + + @Test + fun testDivisionFixed() { + val margin = 0.000_000_01 + + assertEquals(fi(50), fi(5000) / fi(100), margin) + + assertEquals(fd(0.02), fd(0.1) / fi(5), margin) + assertEquals(fd(0.02), fd(-0.1) / fi(-5), margin) + assertEquals(fd(-0.02), fd(0.1) / fi(-5), margin) + assertEquals(fd(-0.02), fd(-0.1) / fi(5), margin) + + assertEquals(fi(2_100_000_000), fl(8_400_000_000) / fi(4)) + assertEquals(fi(-2_100_000_000), fl(8_400_000_000) / fi(-4)) + + assertEquals(fi(100_000), fi(1) / fd(0.00001), 100.0) + assertEquals(fi(1_000_000), fi(1) / fd(0.000001), 1000.0) + assertEquals(fi(0), fi(1) / fl(5_000_000_000L)) + + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) / -fi(1) / -fi(1), margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MAX_VALUE) / fi(1), margin) + assertEquals(FixNano64.raw(Long.MIN_VALUE), FixNano64.raw(Long.MIN_VALUE) / fi(1), margin) + assertEquals(FixNano64.raw(Long.MAX_VALUE), FixNano64.raw(Long.MIN_VALUE + 1) / fi(-1), margin) + + assertThrows { FixNano64.raw(Long.MIN_VALUE) / fi(-1) } + assertThrows { FixNano64.raw(Long.MIN_VALUE / 2 - 1) / fd(0.5) } + assertThrows { FixNano64.raw(Long.MAX_VALUE / 2 + 1) / fd(0.5) } + assertThrows { fi(1_000_000_000) / fd(1.0 / 8100) } + assertThrows { fi(-1_000_000_000) / fd(1.0 / 8100) } + assertThrows { fi(1_000_000_000) / fd(1.0 / -8100) } + assertThrows { fi(-1_000_000_000) / fd(1.0 / -8100) } + assertThrows { fi(1_000_000) / fd(1.0 / 8_600_000.0) } + } + + @Test + fun testFixedMultiplyAndDivisionEdgeCases() { + // Systematically test edge cases + for (bitCount1 in 0 until 64) { + for (bitCount2 in 0 until 64) { + + val power1 = 1L shl bitCount1 + val power2 = 1L shl bitCount2 + + val ones1 = (Long.MAX_VALUE shr bitCount1 shl bitCount1) xor Long.MAX_VALUE + val ones2 = (Long.MAX_VALUE shr bitCount2 shl bitCount2) xor Long.MAX_VALUE + + fun test(value1: Long, value2: Long) { + var shouldOverflow = false + try { + fl(Math.multiplyExact(value1, value2)) + fl(value1) + fl(value2) + } catch (overflow: FixedPointException) { + shouldOverflow = true + } catch (overflow: ArithmeticException) { + shouldOverflow = true + } + + if (shouldOverflow) { + assertThrows { println(fl(value1) * fl(value2)) } + } else { + assertEquals(fl(value1 * value2), fl(value1) * fl(value2), 0.001) + if (value2 != 0L) assertEquals(fl(value1), fl(value1 * value2) / fl(value2), 0.001) + if (value1 != 0L) assertEquals(fl(value2), fl(value1 * value2) / fl(value1), 0.001) + } + } + + test(power1, power2) + test(ones1, ones2) + test(power1, ones2) + } + } + } + + @Test + fun testFractionInt() { + + val margin = 0.000_000_01 + + // Simple cases + assertEquals(fd(0.04), FixNano64.fraction(4, 100), margin) + assertEquals(fd(-43.71), FixNano64.fraction(4371, -100), margin) + assertEquals(fi(82), FixNano64.fraction(-82, -1), margin) + + // Cases with large absolute value + assertEquals(fi(100), FixNano64.fraction(2_000_000_000, 20_000_000), margin) + assertEquals(fi(-100), FixNano64.fraction(2_000_000_000, -20_000_000), margin) + assertEquals(fi(-100), FixNano64.fraction(-2_000_000_000, 20_000_000), margin) + assertEquals(fi(100), FixNano64.fraction(-2_000_000_000, -20_000_000), margin) + assertEquals(fd(1.5), FixNano64.fraction(1_500_000_000, 1_000_000_000)) + + // Cases with the largest possible absolute values + assertEquals(fi(Integer.MAX_VALUE), FixNano64.fraction(Integer.MAX_VALUE, 1), margin) + assertEquals(fi(Integer.MAX_VALUE), FixNano64.fraction(-Integer.MAX_VALUE, -1), margin) + assertEquals(fi(Integer.MIN_VALUE), FixNano64.fraction(Integer.MIN_VALUE, 1), margin) + assertEquals(fi(Integer.MIN_VALUE + 1), FixNano64.fraction(-Integer.MAX_VALUE, 1), margin) + assertEquals(fi(Integer.MIN_VALUE + 1), FixNano64.fraction(Integer.MAX_VALUE, -1), margin) + } + + @Test + fun testFractionLong() { + + val margin = 0.000_000_01 + + // Simple cases + assertEquals(fd(0.04), FixNano64.fraction(4L, 100L), margin) + assertEquals(fd(-43.71), FixNano64.fraction(4371L, -100L), margin) + assertEquals(fi(82), FixNano64.fraction(-82L, -1L), margin) + + // Cases with large absolute value + + val bil = 1_000_000_000L + + assertEquals(fl(8 * bil), FixNano64.fraction(8 * bil * bil, bil), margin) + assertEquals(fl(8 * bil), FixNano64.fraction(-8 * bil * bil, -bil), margin) + assertEquals(-fl(8 * bil), FixNano64.fraction(8 * bil * bil, -bil), margin) + assertEquals(fl(-8 * bil), FixNano64.fraction(-8 * bil * bil, bil), margin) + assertEquals(fd(-1.1234), FixNano64.fraction(-11_234 * bil, 10_000 * bil)) + } +} diff --git a/src/test/kotlin/fixie/TestNorm32.kt b/src/test/kotlin/fixie/TestNorm32.kt new file mode 100644 index 0000000..ff2f636 --- /dev/null +++ b/src/test/kotlin/fixie/TestNorm32.kt @@ -0,0 +1,67 @@ +package fixie + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class TestNorm32 { + + @Test + fun testToString() { + assertEquals("0.1", Norm32.fromFloat(0.1f).toString()) + assertEquals("-0.1234", Norm32.fromDouble(-0.1234).toString()) + assertEquals("0.5294", Norm32.fromDouble(0.5294).toString()) + assertEquals("-0.529", (-Norm32.fromDouble(0.529)).toString()) + assertEquals("0.57", Norm32.fromFloat(0.57f).toString()) + assertEquals("0.0001", Norm32.fromDouble(0.0001).toString()) + assertEquals("-0.0001", Norm32.fromDouble(-0.0001).toString()) + assertEquals("0.9999", Norm32.fromDouble(0.9999).toString()) + assertEquals("-0.9999", (-Norm32.fromDouble(0.9999)).toString()) + assertEquals("0", Norm32.ZERO.toString()) + assertEquals("0", Norm32.createRaw(1).toString()) + assertEquals("0", Norm32.createRaw(-1).toString()) + assertEquals("1", Norm32.ONE.toString()) + assertEquals("1", Norm32.fromDouble(0.9999999).toString()) + assertEquals("-1", (-Norm32.ONE).toString()) + assertEquals("-1", Norm32.fromDouble(-0.9999999).toString()) + } + + @Test + fun testToFloat() { + val margin = 0.0000001f + + for (value in arrayOf(-1f, -0.99f, -0.56f, -0.32f, -0.1f, -0.001f, 0f, 0.001f, 0.1f, 0.32f, 0.56f, 0.99f, 1f)) { + assertEquals(value, Norm32.fromFloat(value).toFloat(), margin) + } + } + + @Test + fun testToDouble() { + val margin = 0.00000001 + + for (value in arrayOf(-1.0, -0.99, -0.56, -0.32, -0.1, -0.001, 0.0, 0.001, 0.1, 0.32, 0.56, 0.99, 1.0)) { + assertEquals(value, Norm32.fromDouble(value).toDouble(), margin) + } + } + + @Test + fun testFromFloatOverflow() { + for (value in arrayOf(1000f, 100f, 10f, 1.1f, 1.001f)) { + assertThrows { Norm32.fromFloat(value) } + assertThrows { Norm32.fromFloat(-value) } + } + } + + @Test + fun testFromDoubleOverflow() { + for (value in arrayOf(1000.0, 100.0, 10.0, 1.1, 1.001)) { + assertThrows { Norm32.fromDouble(value) } + assertThrows { Norm32.fromDouble(-value) } + } + } + + @Test + fun testAdd() { + + } +}