From 53d877c676f0c226222f37138bfc835891d48870 Mon Sep 17 00:00:00 2001 From: demn Date: Sat, 24 Aug 2024 17:23:48 +0500 Subject: [PATCH 1/2] feat(units): add corner case test --- .../calkt/units/parse/CornerCaseTest.kt | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/CornerCaseTest.kt diff --git a/units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/CornerCaseTest.kt b/units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/CornerCaseTest.kt new file mode 100644 index 0000000..8cce9ba --- /dev/null +++ b/units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/CornerCaseTest.kt @@ -0,0 +1,181 @@ +package me.y9san9.calkt.units.parse + +import me.y9san9.calkt.calculate.CalculateContext +import me.y9san9.calkt.calculate.CalculateResult +import me.y9san9.calkt.math.calculate.MathCalculateSuccess +import me.y9san9.calkt.number.PreciseNumber +import me.y9san9.calkt.parse.ParseContext +import me.y9san9.calkt.parse.ParseResult +import me.y9san9.calkt.parse.tryParse +import me.y9san9.calkt.units.UnitKey +import me.y9san9.calkt.units.annotation.UnitKeySubclass +import me.y9san9.calkt.units.calculate.UnitsCalculateFailure +import me.y9san9.calkt.units.calculate.UnitsConvertFunction +import kotlin.test.Test +import kotlin.test.assertTrue + +object TimeConvert { + operator fun invoke( + context: CalculateContext, + value: PreciseNumber, + from: TimeUnitKey, + to: TimeUnitKey + ): CalculateResult { + val result = value.times(multiplier(from)).divide(multiplier(to), context.precision) + return MathCalculateSuccess(result) + } + + private fun multiplier(key: TimeUnitKey): PreciseNumber { + return when (key) { + TimeUnitKey.Hours -> PreciseNumber.of(3_600_000) + TimeUnitKey.Minutes -> PreciseNumber.of(60_000) + TimeUnitKey.Seconds -> PreciseNumber.of(1_000) + TimeUnitKey.Millis -> PreciseNumber.of(1) + } + } +} + +object DistanceConvert { + operator fun invoke( + context: CalculateContext, + value: PreciseNumber, + from: DistanceUnitKey, + to: DistanceUnitKey + ): CalculateResult { + val result = value.times(multiplier(from)).divide(multiplier(to), context.precision) + return MathCalculateSuccess(result) + } + + private fun multiplier( + key: DistanceUnitKey + ): PreciseNumber { + return when (key) { + // Imperial + DistanceUnitKey.Kilometers -> PreciseNumber.of(1_000_000) + DistanceUnitKey.Meters -> PreciseNumber.of(1_000) + DistanceUnitKey.Centimeters -> PreciseNumber.of(10) + DistanceUnitKey.Millimeters -> PreciseNumber.of(1) + // Metric + DistanceUnitKey.Inches -> PreciseNumber.of(25.4) + DistanceUnitKey.Feet -> PreciseNumber.of(304.8) + DistanceUnitKey.Yards -> PreciseNumber.of(914.4) + DistanceUnitKey.Miles -> PreciseNumber.of(1_609_344) + } + } +} + +object DefaultUnitsConvert : UnitsConvertFunction { + override fun invoke(context: CalculateContext, value: PreciseNumber, from: UnitKey, to: UnitKey): CalculateResult { + return when { + from is DistanceUnitKey && to is DistanceUnitKey -> DistanceConvert(context, value, from, to) + from is TimeUnitKey && to is TimeUnitKey -> TimeConvert(context, value, from, to) + else -> UnitsCalculateFailure.UnitsCantBeConverted + } + } +} + +@OptIn(UnitKeySubclass::class) +sealed interface TimeUnitKey : UnitKey { + data object Hours : TimeUnitKey + data object Minutes : TimeUnitKey + data object Seconds : TimeUnitKey + data object Millis : TimeUnitKey +} + +@OptIn(UnitKeySubclass::class) +sealed interface DistanceUnitKey : UnitKey { + // Metric + data object Kilometers : DistanceUnitKey + data object Meters : DistanceUnitKey + data object Centimeters : DistanceUnitKey + data object Millimeters : DistanceUnitKey + + // Imperial + data object Inches : DistanceUnitKey + data object Feet : DistanceUnitKey + data object Yards : DistanceUnitKey + data object Miles : DistanceUnitKey +} + +object ParseDistance { + // Metric + val kilometers = UnitsParseUnitKeyFunction.ofStrings( + DistanceUnitKey.Kilometers, + "km", "kilometer", "kilometers" + ) + val meters = UnitsParseUnitKeyFunction.ofStrings( + DistanceUnitKey.Meters, + "m", "meter", "meters" + ) + val centimeters = UnitsParseUnitKeyFunction.ofStrings( + DistanceUnitKey.Centimeters, + "cm", "centimeter", "centimeters" + ) + val millimeters = UnitsParseUnitKeyFunction.ofStrings( + DistanceUnitKey.Millimeters, + "mm", "millimeter", "millimeters" + ) + + // Imperial + val mile = UnitsParseUnitKeyFunction.ofStrings( + DistanceUnitKey.Miles, + "mi", "mile" + ) + val yard = UnitsParseUnitKeyFunction.ofStrings( + DistanceUnitKey.Yards, + "yd", "yard" + ) + val foot = UnitsParseUnitKeyFunction.ofStrings( + DistanceUnitKey.Feet, + "ft", "foot", "feet" + ) + val inches = UnitsParseUnitKeyFunction.ofStrings( + DistanceUnitKey.Inches, + "in", "inch", "inches" + ) + + val function = kilometers + meters + centimeters + millimeters + + mile + yard + foot + inches +} + +object ParseTime { + val hours = UnitsParseUnitKeyFunction.ofStrings( + TimeUnitKey.Hours, + "h", "hr", "hrs", "hour", "hours" + ) + val minutes = UnitsParseUnitKeyFunction.ofStrings( + TimeUnitKey.Minutes, + "min", "mins", "minute", "minutes" + ) + val seconds = UnitsParseUnitKeyFunction.ofStrings( + TimeUnitKey.Seconds, + "sec", "second", "seconds" + ) + val milliseconds = UnitsParseUnitKeyFunction.ofStrings( + TimeUnitKey.Millis, + "millis", "millisecond", "milliseconds" + ) + + val function = hours + minutes + seconds + minutes + milliseconds +} + +object DefaultParseUnitKey : UnitsParseUnitKeyFunction { + val function = ParseDistance.function + ParseTime.function + + override fun invoke(context: ParseContext): UnitKey { + return function(context) + } +} + +class CornerCaseTest { + @Test + fun testMillimetersToMeters() { + val unitsExpression = "mm to m" + + val parseResult = tryParse(unitsExpression) { context -> + context.parseUnitsExpression(DefaultParseUnitKey) + } + + assertTrue { parseResult is ParseResult.Success } + } +} From 44fa724728e6bb0de5027e2a3554331df5caf7d9 Mon Sep 17 00:00:00 2001 From: y9san9 Date: Sat, 24 Aug 2024 18:42:42 +0300 Subject: [PATCH 2/2] fix: operators now check that the next symbol is other than letter --- gradle/libs.versions.toml | 2 +- .../units/parse/UnitsMathParseOperand.kt | 2 +- .../units/parse/UnitsParseUnitKeyFunction.kt | 2 + .../calkt/units/parse/CornerCaseTest.kt | 181 ------------------ .../units/parse/UnitsParseUnitKeyFunction.kt | 33 ++++ 5 files changed, 37 insertions(+), 183 deletions(-) delete mode 100644 units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/CornerCaseTest.kt create mode 100644 units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/UnitsParseUnitKeyFunction.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 33c4b1d..f090cec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ kotlin = "2.0.0" bignum = "0.3.10" maven-publish = "0.29.0" -calkt = "0.0.4" +calkt = "0.0.5" [libraries] diff --git a/units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsMathParseOperand.kt b/units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsMathParseOperand.kt index 47d50af..5a6dab1 100644 --- a/units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsMathParseOperand.kt +++ b/units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsMathParseOperand.kt @@ -49,7 +49,7 @@ public class UnitsMathParseOperand( val unitKey = parseUnitKey(context) return UnitsExpression.Conversion( - value = MathExpression.Number(PreciseNumber.Companion.of(1)), + value = MathExpression.Number(PreciseNumber.of(1)), key = unitKey ) } diff --git a/units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsParseUnitKeyFunction.kt b/units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsParseUnitKeyFunction.kt index 69d4be2..8f725c1 100644 --- a/units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsParseUnitKeyFunction.kt +++ b/units/src/commonMain/kotlin/me/y9san9/calkt/units/parse/UnitsParseUnitKeyFunction.kt @@ -16,6 +16,8 @@ public fun interface UnitsParseUnitKeyFunction { context.token { for (string in strings) { if (context.startsWith(string)) { + val nextChar = context.string.getOrNull(index = context.position + string.length) + if (nextChar?.isLetter() == true) continue context.drop(string.length) return@UnitsParseUnitKeyFunction key } diff --git a/units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/CornerCaseTest.kt b/units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/CornerCaseTest.kt deleted file mode 100644 index 8cce9ba..0000000 --- a/units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/CornerCaseTest.kt +++ /dev/null @@ -1,181 +0,0 @@ -package me.y9san9.calkt.units.parse - -import me.y9san9.calkt.calculate.CalculateContext -import me.y9san9.calkt.calculate.CalculateResult -import me.y9san9.calkt.math.calculate.MathCalculateSuccess -import me.y9san9.calkt.number.PreciseNumber -import me.y9san9.calkt.parse.ParseContext -import me.y9san9.calkt.parse.ParseResult -import me.y9san9.calkt.parse.tryParse -import me.y9san9.calkt.units.UnitKey -import me.y9san9.calkt.units.annotation.UnitKeySubclass -import me.y9san9.calkt.units.calculate.UnitsCalculateFailure -import me.y9san9.calkt.units.calculate.UnitsConvertFunction -import kotlin.test.Test -import kotlin.test.assertTrue - -object TimeConvert { - operator fun invoke( - context: CalculateContext, - value: PreciseNumber, - from: TimeUnitKey, - to: TimeUnitKey - ): CalculateResult { - val result = value.times(multiplier(from)).divide(multiplier(to), context.precision) - return MathCalculateSuccess(result) - } - - private fun multiplier(key: TimeUnitKey): PreciseNumber { - return when (key) { - TimeUnitKey.Hours -> PreciseNumber.of(3_600_000) - TimeUnitKey.Minutes -> PreciseNumber.of(60_000) - TimeUnitKey.Seconds -> PreciseNumber.of(1_000) - TimeUnitKey.Millis -> PreciseNumber.of(1) - } - } -} - -object DistanceConvert { - operator fun invoke( - context: CalculateContext, - value: PreciseNumber, - from: DistanceUnitKey, - to: DistanceUnitKey - ): CalculateResult { - val result = value.times(multiplier(from)).divide(multiplier(to), context.precision) - return MathCalculateSuccess(result) - } - - private fun multiplier( - key: DistanceUnitKey - ): PreciseNumber { - return when (key) { - // Imperial - DistanceUnitKey.Kilometers -> PreciseNumber.of(1_000_000) - DistanceUnitKey.Meters -> PreciseNumber.of(1_000) - DistanceUnitKey.Centimeters -> PreciseNumber.of(10) - DistanceUnitKey.Millimeters -> PreciseNumber.of(1) - // Metric - DistanceUnitKey.Inches -> PreciseNumber.of(25.4) - DistanceUnitKey.Feet -> PreciseNumber.of(304.8) - DistanceUnitKey.Yards -> PreciseNumber.of(914.4) - DistanceUnitKey.Miles -> PreciseNumber.of(1_609_344) - } - } -} - -object DefaultUnitsConvert : UnitsConvertFunction { - override fun invoke(context: CalculateContext, value: PreciseNumber, from: UnitKey, to: UnitKey): CalculateResult { - return when { - from is DistanceUnitKey && to is DistanceUnitKey -> DistanceConvert(context, value, from, to) - from is TimeUnitKey && to is TimeUnitKey -> TimeConvert(context, value, from, to) - else -> UnitsCalculateFailure.UnitsCantBeConverted - } - } -} - -@OptIn(UnitKeySubclass::class) -sealed interface TimeUnitKey : UnitKey { - data object Hours : TimeUnitKey - data object Minutes : TimeUnitKey - data object Seconds : TimeUnitKey - data object Millis : TimeUnitKey -} - -@OptIn(UnitKeySubclass::class) -sealed interface DistanceUnitKey : UnitKey { - // Metric - data object Kilometers : DistanceUnitKey - data object Meters : DistanceUnitKey - data object Centimeters : DistanceUnitKey - data object Millimeters : DistanceUnitKey - - // Imperial - data object Inches : DistanceUnitKey - data object Feet : DistanceUnitKey - data object Yards : DistanceUnitKey - data object Miles : DistanceUnitKey -} - -object ParseDistance { - // Metric - val kilometers = UnitsParseUnitKeyFunction.ofStrings( - DistanceUnitKey.Kilometers, - "km", "kilometer", "kilometers" - ) - val meters = UnitsParseUnitKeyFunction.ofStrings( - DistanceUnitKey.Meters, - "m", "meter", "meters" - ) - val centimeters = UnitsParseUnitKeyFunction.ofStrings( - DistanceUnitKey.Centimeters, - "cm", "centimeter", "centimeters" - ) - val millimeters = UnitsParseUnitKeyFunction.ofStrings( - DistanceUnitKey.Millimeters, - "mm", "millimeter", "millimeters" - ) - - // Imperial - val mile = UnitsParseUnitKeyFunction.ofStrings( - DistanceUnitKey.Miles, - "mi", "mile" - ) - val yard = UnitsParseUnitKeyFunction.ofStrings( - DistanceUnitKey.Yards, - "yd", "yard" - ) - val foot = UnitsParseUnitKeyFunction.ofStrings( - DistanceUnitKey.Feet, - "ft", "foot", "feet" - ) - val inches = UnitsParseUnitKeyFunction.ofStrings( - DistanceUnitKey.Inches, - "in", "inch", "inches" - ) - - val function = kilometers + meters + centimeters + millimeters + - mile + yard + foot + inches -} - -object ParseTime { - val hours = UnitsParseUnitKeyFunction.ofStrings( - TimeUnitKey.Hours, - "h", "hr", "hrs", "hour", "hours" - ) - val minutes = UnitsParseUnitKeyFunction.ofStrings( - TimeUnitKey.Minutes, - "min", "mins", "minute", "minutes" - ) - val seconds = UnitsParseUnitKeyFunction.ofStrings( - TimeUnitKey.Seconds, - "sec", "second", "seconds" - ) - val milliseconds = UnitsParseUnitKeyFunction.ofStrings( - TimeUnitKey.Millis, - "millis", "millisecond", "milliseconds" - ) - - val function = hours + minutes + seconds + minutes + milliseconds -} - -object DefaultParseUnitKey : UnitsParseUnitKeyFunction { - val function = ParseDistance.function + ParseTime.function - - override fun invoke(context: ParseContext): UnitKey { - return function(context) - } -} - -class CornerCaseTest { - @Test - fun testMillimetersToMeters() { - val unitsExpression = "mm to m" - - val parseResult = tryParse(unitsExpression) { context -> - context.parseUnitsExpression(DefaultParseUnitKey) - } - - assertTrue { parseResult is ParseResult.Success } - } -} diff --git a/units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/UnitsParseUnitKeyFunction.kt b/units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/UnitsParseUnitKeyFunction.kt new file mode 100644 index 0000000..b67c8b8 --- /dev/null +++ b/units/src/commonTest/kotlin/me/y9san9/calkt/units/parse/UnitsParseUnitKeyFunction.kt @@ -0,0 +1,33 @@ +package me.y9san9.calkt.units.parse + +import me.y9san9.calkt.Expression +import me.y9san9.calkt.annotation.ExpressionSubclass +import me.y9san9.calkt.calculate.CalculateResult +import me.y9san9.calkt.parse.ParseContext +import me.y9san9.calkt.parse.ParseResult +import me.y9san9.calkt.parse.tryParse +import me.y9san9.calkt.units.UnitKey +import me.y9san9.calkt.units.annotation.UnitKeySubclass +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class UnitsParseUnitKeyFunctionTest { + @Test + fun testDefaultImplementationChecksForNextChar() { + val function = create() + + val result = tryParse("prefixtest") { context -> + function(context) as Expression + } + assertIs>(result) + assertEquals(TestKey, result.value) + } + + private fun create(): UnitsParseUnitKeyFunction { + return UnitsParseUnitKeyFunction.ofStrings(TestKey, "prefix", "prefixtest") + } + + @OptIn(UnitKeySubclass::class, ExpressionSubclass::class) + private data object TestKey : UnitKey, Expression +}