-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds support for Ion Schema 2.0 ieee754_float constraint
- Loading branch information
Showing
3 changed files
with
138 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
ion-schema/src/main/kotlin/com/amazon/ionschema/internal/constraint/Ieee754Float.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"). | ||
* You may not use this file except in compliance with the License. | ||
* A copy of the License is located at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* or in the "license" file accompanying this file. This file 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. | ||
*/ | ||
|
||
package com.amazon.ionschema.internal.constraint | ||
|
||
import com.amazon.ion.IonFloat | ||
import com.amazon.ion.IonSymbol | ||
import com.amazon.ion.IonValue | ||
import com.amazon.ionschema.Violation | ||
import com.amazon.ionschema.Violations | ||
import com.amazon.ionschema.internal.constraint.Ieee754Float.Ieee754InterchangeFormat.binary16 | ||
import com.amazon.ionschema.internal.constraint.Ieee754Float.Ieee754InterchangeFormat.binary32 | ||
import com.amazon.ionschema.internal.constraint.Ieee754Float.Ieee754InterchangeFormat.binary64 | ||
import com.amazon.ionschema.internal.util.islRequire | ||
import com.amazon.ionschema.internal.util.islRequireIonTypeNotNull | ||
import com.amazon.ionschema.internal.util.islRequireNotNull | ||
import kotlin.math.absoluteValue | ||
|
||
/** | ||
* Implements the ieee754_float constraint. | ||
* | ||
* @see https://amzn.github.io/ion-schema/docs/isl-2-0/spec#ieee754_float | ||
*/ | ||
internal class Ieee754Float(ion: IonValue) : ConstraintBase(ion) { | ||
|
||
/** | ||
* Represents a range of floating point numbers and the interval between the representable values in that range. | ||
*/ | ||
private data class PrecisionInfo(val range: ClosedRange<Double>, val interval: Double) | ||
|
||
companion object { | ||
/** | ||
* Available precision can be calculated to produce a table, such as the following table for 16-bit floats. | ||
* | ||
* Rather than create a generalized solution, we're going to hardcode some values. This is justifiable because | ||
* 16-bit floats are small enough that we can do it, this is the only floating point format for which it is | ||
* needed, and it avoids the complexity of a generalized solution. | ||
* | ||
* Technically, there is overlap between the ranges, but that's okay because the overlap is only the boundary | ||
* value, which will be valid regardless of which interval we test it against. | ||
*/ | ||
private val BINARY_16_PRECISION_RANGES = listOf( | ||
PrecisionInfo(0.0..1.0 / 16_384, interval = 1.0 / 16_777_216), // Subnormal numbers | ||
PrecisionInfo(1.0 / 16_384..1.0 / 8_192, interval = 1.0 / 16_777_216), | ||
PrecisionInfo(1.0 / 8_192..1.0 / 4_096, interval = 1.0 / 8_388_608), | ||
PrecisionInfo(1.0 / 4_096..1.0 / 2_048, interval = 1.0 / 4_194_304), | ||
PrecisionInfo(1.0 / 2_048..1.0 / 1_024, interval = 1.0 / 2_097_152), | ||
PrecisionInfo(1.0 / 1_024..1.0 / 512, interval = 1.0 / 1_048_576), | ||
PrecisionInfo(1.0 / 512..1.0 / 256, interval = 1.0 / 524_288), | ||
PrecisionInfo(1.0 / 256..1.0 / 128, interval = 1.0 / 262_144), | ||
PrecisionInfo(1.0 / 128..1.0 / 64, interval = 1.0 / 131_072), | ||
PrecisionInfo(1.0 / 64..1.0 / 32, interval = 1.0 / 65_536), | ||
PrecisionInfo(1.0 / 32..1.0 / 16, interval = 1.0 / 32_768), | ||
PrecisionInfo(1.0 / 16..1.0 / 8, interval = 1.0 / 16_384), | ||
PrecisionInfo(1.0 / 8..1.0 / 4, interval = 1.0 / 8_192), | ||
PrecisionInfo(1.0 / 4..1.0 / 2, interval = 1.0 / 4_096), | ||
PrecisionInfo(1.0 / 2..1.0, interval = 1.0 / 2_048), | ||
PrecisionInfo(1.0..2.0, interval = 1.0 / 1_024), | ||
PrecisionInfo(2.0..4.0, interval = 1.0 / 512), | ||
PrecisionInfo(4.0..8.0, interval = 1.0 / 256), | ||
PrecisionInfo(8.0..16.0, interval = 1.0 / 128), | ||
PrecisionInfo(16.0..32.0, interval = 1.0 / 64), | ||
PrecisionInfo(32.0..64.0, interval = 1.0 / 32), | ||
PrecisionInfo(64.0..128.0, interval = 1.0 / 16), | ||
PrecisionInfo(128.0..256.0, interval = 1.0 / 8), | ||
PrecisionInfo(256.0..512.0, interval = 1.0 / 4), | ||
PrecisionInfo(512.0..1_024.0, interval = 1.0 / 2), | ||
PrecisionInfo(1_024.0..2_048.0, interval = 1.0), | ||
PrecisionInfo(2_048.0..4_096.0, interval = 2.0), | ||
PrecisionInfo(4_096.0..8_192.0, interval = 4.0), | ||
PrecisionInfo(8_192.0..16_384.0, interval = 8.0), | ||
PrecisionInfo(16_384.0..32_768.0, interval = 16.0), | ||
PrecisionInfo(32_768.0..65_504.0, interval = 32.0), | ||
) | ||
} | ||
|
||
/** | ||
* The possible values for the `ieee754_float` constraint. | ||
*/ | ||
private enum class Ieee754InterchangeFormat { | ||
binary16, | ||
binary32, | ||
binary64; | ||
companion object { | ||
val names = Ieee754InterchangeFormat.values().map { it.name } | ||
} | ||
} | ||
|
||
private val interchangeFormat = ion.let { | ||
islRequireIonTypeNotNull<IonSymbol>(it) { "ieee754_float must be a non-null Ion symbol" } | ||
islRequire(it.typeAnnotations.isEmpty()) { "ieee754_float must not have annotations" } | ||
islRequire(it.stringValue() in Ieee754InterchangeFormat.names) { "ieee754_float must be one of ${Ieee754InterchangeFormat.names}" } | ||
return@let Ieee754InterchangeFormat.valueOf(it.stringValue()) | ||
} | ||
|
||
override fun validate(value: IonValue, issues: Violations) { | ||
validateAs<IonFloat>(value, issues) { | ||
if (!(interchangeFormat losslesslyEncodes it)) { | ||
issues.add(Violation(ion, "invalid_ieee754_float", "value cannot be losslessly represented by the IEEE-754 $interchangeFormat interchange format.")) | ||
} | ||
} | ||
} | ||
|
||
private infix fun Ieee754InterchangeFormat.losslesslyEncodes(value: IonFloat): Boolean { | ||
// +inf, -inf, and nan are always valid | ||
if (!value.doubleValue().isFinite()) return true | ||
|
||
return when (this) { | ||
binary16 -> value.doubleValue().isExactHalfValue() | ||
binary32 -> value.doubleValue().toFloat().toDouble() == value.doubleValue() | ||
binary64 -> true // All Ion Floats are 64 bits or smaller. | ||
} | ||
} | ||
|
||
private fun Double.isExactHalfValue(): Boolean { | ||
if (this.absoluteValue > 65504) return false | ||
// Find the appropriate interval for the Double value we are testing | ||
val (_, interval) = BINARY_16_PRECISION_RANGES.first { (range, _) -> absoluteValue in range } | ||
// If the value is an exact multiple of the interval between two Half values, then we know it | ||
// is possible to _exactly_ represent the value as a Half precision float. | ||
return this % interval == 0.0 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters