All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.partiql.lang.eval.ExprValueExtensions.kt Maven / Gradle / Ivy

There is a newer version: 1.0.0-perf.1
Show newest version
/*
 * Copyright 2019 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://aws.amazon.com/apache2.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 org.partiql.lang.eval

import com.amazon.ion.IntegerSize
import com.amazon.ion.IonInt
import com.amazon.ion.IonStruct
import com.amazon.ion.IonSystem
import com.amazon.ion.IonType
import com.amazon.ion.IonValue
import com.amazon.ion.Timestamp
import org.partiql.lang.ast.SourceLocationMeta
import org.partiql.lang.errors.ErrorCode
import org.partiql.lang.errors.Property
import org.partiql.lang.errors.PropertyValueMap
import org.partiql.lang.eval.time.NANOS_PER_SECOND
import org.partiql.lang.eval.time.Time
import org.partiql.lang.syntax.DATE_TIME_PART_KEYWORDS
import org.partiql.lang.syntax.DateTimePart
import org.partiql.lang.types.BagType
import org.partiql.lang.types.BlobType
import org.partiql.lang.types.BoolType
import org.partiql.lang.types.ClobType
import org.partiql.lang.types.DateType
import org.partiql.lang.types.DecimalType
import org.partiql.lang.types.FloatType
import org.partiql.lang.types.IntType
import org.partiql.lang.types.ListType
import org.partiql.lang.types.MissingType
import org.partiql.lang.types.NullType
import org.partiql.lang.types.NumberConstraint
import org.partiql.lang.types.SexpType
import org.partiql.lang.types.SingleType
import org.partiql.lang.types.StringType
import org.partiql.lang.types.SymbolType
import org.partiql.lang.types.TimeType
import org.partiql.lang.types.TimestampType
import org.partiql.lang.util.ConfigurableExprValueFormatter
import org.partiql.lang.util.bigDecimalOf
import org.partiql.lang.util.coerce
import org.partiql.lang.util.compareTo
import org.partiql.lang.util.downcast
import org.partiql.lang.util.getPrecisionFromTimeString
import org.partiql.lang.util.ionValue
import org.partiql.lang.util.isNaN
import org.partiql.lang.util.isNegInf
import org.partiql.lang.util.isPosInf
import java.math.BigDecimal
import java.math.MathContext
import java.math.RoundingMode
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.util.TreeMap
import java.util.TreeSet
import kotlin.math.round

/**
 * Wraps the given [ExprValue] with a delegate that provides the [OrderedBindNames] facet.
 */
fun ExprValue.orderedNamesValue(names: List): ExprValue =
    object : ExprValue by this, OrderedBindNames {
        override val orderedNames = names
        override fun  asFacet(type: Class?): T? =
            downcast(type) ?: [email protected](type)
        override fun toString(): String = stringify()
    }

val ExprValue.orderedNames: List?
    get() = asFacet(OrderedBindNames::class.java)?.orderedNames

/** Wraps this [ExprValue] as a [Named] instance. */
fun ExprValue.asNamed(): Named = object : Named {
    override val name: ExprValue
        get() = this@asNamed
}

/** Binds the given name value as a [Named] facet delegate over this [ExprValue]. */
fun ExprValue.namedValue(nameValue: ExprValue): ExprValue = object : ExprValue by this, Named {
    override val name = nameValue
    override fun  asFacet(type: Class?): T? =
        downcast(type) ?: [email protected](type)
    override fun toString(): String = stringify()
}

/** Wraps this [ExprValue] in a delegate that always masks the [Named] facet. */
fun ExprValue.unnamedValue(): ExprValue = when (asFacet(Named::class.java)) {
    null -> this
    else -> object : ExprValue by this {
        override fun  asFacet(type: Class?): T? =
            when (type) {
                // always mask the name facet
                Named::class.java -> null
                else -> [email protected](type)
            }
        override fun toString(): String = stringify()
    }
}

val ExprValue.name: ExprValue?
    get() = asFacet(Named::class.java)?.name

val ExprValue.address: ExprValue?
    get() = asFacet(Addressed::class.java)?.address

fun ExprValue.booleanValue(): Boolean =
    scalar.booleanValue() ?: errNoContext("Expected boolean: $this", errorCode = ErrorCode.EVALUATOR_UNEXPECTED_VALUE_TYPE, internal = false)

fun ExprValue.numberValue(): Number =
    scalar.numberValue() ?: errNoContext("Expected number: $this", errorCode = ErrorCode.EVALUATOR_UNEXPECTED_VALUE_TYPE, internal = false)

fun ExprValue.dateValue(): LocalDate =
    scalar.dateValue() ?: errNoContext("Expected date: $this", errorCode = ErrorCode.EVALUATOR_UNEXPECTED_VALUE_TYPE, internal = false)

fun ExprValue.timeValue(): Time =
    scalar.timeValue() ?: errNoContext("Expected time: $this", errorCode = ErrorCode.EVALUATOR_UNEXPECTED_VALUE_TYPE, internal = false)

fun ExprValue.timestampValue(): Timestamp =
    scalar.timestampValue() ?: errNoContext("Expected timestamp: $this", errorCode = ErrorCode.EVALUATOR_UNEXPECTED_VALUE_TYPE, internal = false)

fun ExprValue.stringValue(): String =
    scalar.stringValue() ?: errNoContext("Expected string: $this", errorCode = ErrorCode.EVALUATOR_UNEXPECTED_VALUE_TYPE, internal = false)

fun ExprValue.bytesValue(): ByteArray =
    scalar.bytesValue() ?: errNoContext("Expected boolean: $this", errorCode = ErrorCode.EVALUATOR_UNEXPECTED_VALUE_TYPE, internal = false)

internal fun ExprValue.dateTimePartValue(): DateTimePart =
    try {
        DateTimePart.valueOf(this.stringValue().toUpperCase())
    } catch (e: IllegalArgumentException) {
        throw EvaluationException(
            cause = e,
            message = "invalid datetime part, valid values: [${DATE_TIME_PART_KEYWORDS.joinToString()}]",
            errorCode = ErrorCode.EVALUATOR_INVALID_ARGUMENTS_FOR_DATE_PART,
            internal = false
        )
    }

internal fun ExprValue.intValue(): Int = this.numberValue().toInt()

internal fun ExprValue.longValue(): Long = this.numberValue().toLong()

internal fun ExprValue.bigDecimalValue(): BigDecimal = this.numberValue().toString().toBigDecimal()

/**
 * Implements the `FROM` range operation.
 * Specifically, this is distinct from the normal [ExprValue.iterator] in that
 * types that are **not** [ExprValueType.isRangeFrom] get treated as a singleton
 * as per PartiQL specification.
 */
fun ExprValue.rangeOver(): Iterable = when {
    type.isRangedFrom -> this
    // everything else ranges as a singleton unnamed value
    else -> listOf(this.unnamedValue())
}

/** A very simple string representation--to be used for diagnostic purposes only. */
fun ExprValue.stringify(): String =
    ConfigurableExprValueFormatter.standard.format(this)

val DEFAULT_COMPARATOR = NaturalExprValueComparators.NULLS_FIRST_ASC

/** Provides the default equality function. */
fun ExprValue.exprEquals(other: ExprValue): Boolean = DEFAULT_COMPARATOR.compare(this, other) == 0

/**
 * Provides the comparison predicate--which is not a total ordering.
 *
 * In particular, this operation will fail for non-comparable types.
 * For a total ordering over the PartiQL type space, see [NaturalExprValueComparators]
 */
operator fun ExprValue.compareTo(other: ExprValue): Int {
    return when {
        type.isUnknown || other.type.isUnknown ->
            throw EvaluationException("Null value cannot be compared: $this, $other", errorCode = ErrorCode.EVALUATOR_INVALID_COMPARISION, internal = false)
        isDirectlyComparableTo(other) -> DEFAULT_COMPARATOR.compare(this, other)
        else -> errNoContext("Cannot compare values: $this, $other", errorCode = ErrorCode.EVALUATOR_INVALID_COMPARISION, internal = false)
    }
}

/**
 * Checks if the two ExprValues are directly comparable.
 * Directly comparable is used in the context of the `<`/`<=`/`>`/`>=` operators.
 */
internal fun ExprValue.isDirectlyComparableTo(other: ExprValue): Boolean =
    when {
        // The ExprValue type for TIME and TIME WITH TIME ZONE is same
        // and thus needs to be checked explicitly for the timezone values.
        type == ExprValueType.TIME && other.type == ExprValueType.TIME ->
            timeValue().isDirectlyComparableTo(other.timeValue())
        else -> type.isDirectlyComparableTo(other.type)
    }

/** Types that are cast to the [ExprValueType.isText] types by calling `IonValue.toString()`. */
private val ION_TEXT_STRING_CAST_TYPES = setOf(ExprValueType.BOOL, ExprValueType.TIMESTAMP)

/** Regex to match DATE strings of the format yyyy-MM-dd */
private val datePatternRegex = Regex("\\d\\d\\d\\d-\\d\\d-\\d\\d")

private val genericTimeRegex = Regex("\\d\\d:\\d\\d:\\d\\d(\\.\\d*)?([+|-]\\d\\d:\\d\\d)?")

/**
 * Casts this [ExprValue] to the target type.
 *
 * `MISSING` and `NULL` always convert to themselves no matter the target type.  When the
 * source type and target type are the same, this operation is a no-op.
 *
 * The conversion *to* a particular type is as follows, any conversion not specified raises
 * an [EvaluationException]:
 *
 *  * `BOOL`
 *      * Number types will convert to `false` if numerically equal to zero, `true` otherwise.
 *      * Text types will convert to `true` if case-insensitive text is `"true"`,
 *      convert to `false` if case-insensitive text is `"true"` and throw an error otherwise.
 *  * `INT`, `FLOAT`, and `DECIMAL`
 *      * `BOOL` converts as `1` for `true` and `0` for `false`
 *      * Number types will narrow or widen from the source type.  Narrowing is a truncation
 *      * Text types will convert using base-10 integral notation
 *          * For `FLOAT` and `DECIMAL` targets, decimal and e-notation is also supported.
 *  * `TIMESTAMP`
 *      * Text types will convert using the Ion text notation for timestamp (W3C/ISO-8601).
 *  * `DATE`
 *      * `TIMESTAMP` converts as `DATE` throwing away the additional information such as time.
 *      * Text types converts as `DATE` if the case-insensitive text is a valid ISO 8601 format date string.
 *  * `TIME`
 *      * `TIMESTAMP` converts as `TIME` throwing away the additional information such as date and time zone.
 *      * Text types converts as `TIME` if the case-insensitive text is a valid ISO 8601 format time string.
 *      * `TIME` and `TIME WITH TIME ZONE` converts as `TIME` throwing away the time zone information.
 *  * `TIME WITH TIME ZONE`
 *      * `TIMESTAMP` converts as `TIME WITH TIME ZONE` only if the timezone is defined in the TIMESTAMP value.
 *      The conversion throws away the additional information such as date.
 *      * Text types converts as `TIME WITH TIME ZONE` if the case-insensitive text is a valid ISO 8601 format time string.
 *      If the time zone is not specified, then the default time zone is used.
 *      * `TIME` and `TIME WITH TIME ZONE` converts as `TIME WITH TIME ZONE`.
 *      If the time zone is not specified, then the default time zone is used.
 *  * `STRING` and `SYMBOL`
 *      * `BOOL` converts to `STRING` as `"true"` and `"false"`;
 *        converts to `SYMBOL` as `'true'` and `'false'`.
 *      * Number types convert to decimal form with optional e-notation.
 *      * `TIMESTAMP` converts to the ISO-8601 format.
 *  * `BLOB` and `CLOB` can only convert between each other directly.
 *  * `LIST` and `SEXP`
 *      * Convert directly between each other.
 *      * `BAG` converts with an *arbitrary* order.
 *  * `STRUCT` only supports casting from itself.
 *  * `BAG` converts from `LIST` and `SEXP` by drops order guarantees.
 *
 * Note that *text types* is defined by [ExprValueType.isText], *number types* is defined by
 * [ExprValueType.isNumber], and *LOB types* is defined by [ExprValueType.isLob]
 *
 * @param targetType The target type to cast this value to.
 * @param valueFactory The ExprValueFactory used to create ExprValues.
 * @param typedOpBehavior TypedOpBehavior indicating how CAST should behave.
 * @param locationMeta The source location for the CAST. Used for error reporting.
 * @param defaultTimezoneOffset Default timezone offset to be used when TIME WITH TIME ZONE does not explicitly
 * specify the time zone.
 */
fun ExprValue.cast(
    targetType: SingleType,
    valueFactory: ExprValueFactory,
    typedOpBehavior: TypedOpBehavior,
    locationMeta: SourceLocationMeta?,
    defaultTimezoneOffset: ZoneOffset
): ExprValue {
    fun castExceptionContext(): PropertyValueMap {
        val errorContext = PropertyValueMap().also {
            it[Property.CAST_FROM] = this.type.toString()
            it[Property.CAST_TO] = targetType.runtimeType.toString()
        }

        locationMeta?.let { fillErrorContext(errorContext, it) }

        return errorContext
    }

    fun castFailedErr(message: String, internal: Boolean, cause: Throwable? = null): Nothing {
        val errorContext = castExceptionContext()

        val errorCode = if (locationMeta == null) {
            ErrorCode.EVALUATOR_CAST_FAILED_NO_LOCATION
        } else {
            ErrorCode.EVALUATOR_CAST_FAILED
        }

        throw EvaluationException(
            message = message,
            errorCode = errorCode,
            errorContext = errorContext,
            internal = internal,
            cause = cause
        )
    }

    val longMaxDecimal = bigDecimalOf(Long.MAX_VALUE)
    val longMinDecimal = bigDecimalOf(Long.MIN_VALUE)

    fun Number.exprValue(type: SingleType) = when (type) {
        is IntType -> {
            // If the source is Positive/Negative Infinity or Nan, We throw an error
            if (this.isNaN || this.isNegInf || this.isPosInf) {
                castFailedErr("Can't convert Infinity or NaN to INT.", internal = false)
            }

            val rangeForType = when (typedOpBehavior) {
                // Legacy behavior doesn't honor SMALLINT, INT4 constraints
                TypedOpBehavior.LEGACY -> LongRange(Long.MIN_VALUE, Long.MAX_VALUE)
                TypedOpBehavior.HONOR_PARAMETERS ->
                    when (type.rangeConstraint) {
                        // There is not CAST syntax to that can execute this branch today.
                        IntType.IntRangeConstraint.SHORT -> LongRange(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong())
                        IntType.IntRangeConstraint.INT4 -> LongRange(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong())
                        IntType.IntRangeConstraint.LONG, IntType.IntRangeConstraint.UNCONSTRAINED ->
                            LongRange(Long.MIN_VALUE, Long.MAX_VALUE)
                    }
            }

            // Here, we check if there is a possibility of being able to fit this number into
            // any of the integer types. We allow the buffer of 1 because we allow rounding into min/max values.
            if (this <= (longMinDecimal - BigDecimal.ONE) || this >= (longMaxDecimal + BigDecimal.ONE)) {
                errIntOverflow(8)
            }

            // We round the value to the nearest integral value
            // In legacy behavior, this always picks the floor integer value
            // Else, rounding is done through https://en.wikipedia.org/wiki/Rounding#Round_half_to_even
            // We don't convert the result to Long within the when block here
            //  because the rounded values can still be out of range for Kotlin's Long.
            val result = when (typedOpBehavior) {
                TypedOpBehavior.LEGACY -> when (this) {
                    // BigDecimal.toLong inflates the internal BigInteger to the scale before converting it to a long.
                    // For example to convert 1e-6000 it needs to create a BigInteger with value equal to
                    // `unscaledNumber^(10^abs(scale))` to them drop it and return 0L. The BigInteger creation is very
                    // expensive and completely wasted. The division to integral skips all that.
                    is BigDecimal -> this.divideToIntegralValue(BigDecimal.ONE)
                    else -> this
                }
                TypedOpBehavior.HONOR_PARAMETERS -> when (this) {
                    is BigDecimal -> this.setScale(0, RoundingMode.HALF_EVEN)
                    // [kotlin.math.round] rounds towards the closes even number on tie
                    //   https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.math/round.html
                    is Float -> round(this)
                    is Double -> round(this)
                    else -> this
                }
            }.let {
                // after rounding, check that the value can fit into range of the type being casted into
                if (it < rangeForType.first || it > rangeForType.last) {
                    errIntOverflow(8)
                }
                it.toLong()
            }
            valueFactory.newInt(result)
        }
        is FloatType -> valueFactory.newFloat(this.toDouble())
        is DecimalType -> {
            if (this.isNaN || this.isNegInf || this.isPosInf) {
                castFailedErr("Can't convert Infinity or NaN to DECIMAL.", internal = false)
            }

            when (typedOpBehavior) {
                TypedOpBehavior.LEGACY -> valueFactory.newFromIonValue(
                    this.coerce(BigDecimal::class.java).ionValue(valueFactory.ion)
                )
                TypedOpBehavior.HONOR_PARAMETERS ->
                    when (type.precisionScaleConstraint) {
                        DecimalType.PrecisionScaleConstraint.Unconstrained -> valueFactory.newFromIonValue(
                            this.coerce(BigDecimal::class.java).ionValue(valueFactory.ion)
                        )
                        is DecimalType.PrecisionScaleConstraint.Constrained -> {
                            val constraint = type.precisionScaleConstraint
                            val decimal = this.coerce(BigDecimal::class.java) as BigDecimal
                            val result = decimal.round(MathContext(constraint.precision))
                                .setScale(constraint.scale, RoundingMode.HALF_UP)
                            if (result.precision() > constraint.precision) {
                                // Following PostgresSQL behavior here. Java will increase precision if needed.
                                castFailedErr("target type DECIMAL(${constraint.precision}, ${constraint.scale}) too small for value $decimal.", internal = false)
                            } else {
                                valueFactory.newFromIonValue(result.ionValue(valueFactory.ion))
                            }
                        }
                    }
            }
        }
        else -> castFailedErr("Invalid type for numeric conversion: $type (this code should be unreachable)", internal = true)
    }

    fun String.exprValue(type: SingleType) = when (type) {
        is StringType -> when (typedOpBehavior) {
            TypedOpBehavior.LEGACY -> valueFactory.newString(this)
            TypedOpBehavior.HONOR_PARAMETERS -> when (type.lengthConstraint) {
                StringType.StringLengthConstraint.Unconstrained -> valueFactory.newString(this)
                is StringType.StringLengthConstraint.Constrained -> {
                    val actualCodepointCount = this.codePointCount(0, this.length)
                    val lengthConstraint = type.lengthConstraint.length.value
                    val truncatedString = if (actualCodepointCount <= lengthConstraint) {
                        this // no truncation needed
                    } else {
                        this.substring(0, this.offsetByCodePoints(0, lengthConstraint))
                    }

                    valueFactory.newString(
                        when (type.lengthConstraint.length) {
                            is NumberConstraint.Equals -> truncatedString.trimEnd { c -> c == '\u0020' }
                            is NumberConstraint.UpTo -> truncatedString
                        }
                    )
                }
            }
        }
        is SymbolType -> valueFactory.newSymbol(this)

        else -> castFailedErr("Invalid type for textual conversion: $type (this code should be unreachable)", internal = true)
    }

    when {
        type.isUnknown && targetType is MissingType -> return valueFactory.missingValue
        type.isUnknown && targetType is NullType -> return valueFactory.nullValue
        type.isUnknown -> return this
        // Note that the ExprValueType for TIME and TIME WITH TIME ZONE is the same i.e. ExprValueType.TIME.
        // We further need to check for the time zone and hence we do not short circuit here when the type is TIME.
        type == targetType.runtimeType && type != ExprValueType.TIME -> {
            return when (targetType) {
                is IntType, is FloatType, is DecimalType -> numberValue().exprValue(targetType)
                is StringType -> stringValue().exprValue(targetType)
                else -> this
            }
        }
        else -> {
            when (targetType) {
                is BoolType -> when {
                    type.isNumber -> return when {
                        numberValue().compareTo(0L) == 0 -> valueFactory.newBoolean(false)
                        else -> valueFactory.newBoolean(true)
                    }
                    type.isText -> return when (stringValue().toLowerCase()) {
                        "true" -> valueFactory.newBoolean(true)
                        "false" -> valueFactory.newBoolean(false)
                        else -> castFailedErr("can't convert string value to BOOL", internal = false)
                    }
                }
                is IntType -> when {
                    type == ExprValueType.BOOL -> return if (booleanValue()) 1L.exprValue(targetType) else 0L.exprValue(targetType)
                    type.isNumber -> return numberValue().exprValue(targetType)
                    type.isText -> {
                        val value = try {
                            val normalized = stringValue().normalizeForCastToInt()
                            valueFactory.ion.singleValue(normalized) as IonInt
                        } catch (e: Exception) {
                            castFailedErr("can't convert string value to INT", internal = false, cause = e)
                        }

                        return when (value.integerSize) {
                            // Our numbers comparison machinery does not handle big integers yet, fail fast
                            IntegerSize.BIG_INTEGER -> errIntOverflow(8, errorContextFrom(locationMeta))
                            else -> value.longValue().exprValue(targetType)
                        }
                    }
                }
                is FloatType -> when {
                    type == ExprValueType.BOOL -> return if (booleanValue()) 1.0.exprValue(targetType) else 0.0.exprValue(targetType)
                    type.isNumber -> return numberValue().toDouble().exprValue(targetType)
                    type.isText ->
                        try {
                            return stringValue().toDouble().exprValue(targetType)
                        } catch (e: NumberFormatException) {
                            castFailedErr("can't convert string value to FLOAT", internal = false, cause = e)
                        }
                }
                is DecimalType -> when {
                    type == ExprValueType.BOOL -> return if (booleanValue()) {
                        BigDecimal.ONE.exprValue(targetType)
                    } else {
                        BigDecimal.ZERO.exprValue(targetType)
                    }
                    type.isNumber -> return numberValue().exprValue(targetType)
                    type.isText -> try {
                        return bigDecimalOf(stringValue()).exprValue(targetType)
                    } catch (e: NumberFormatException) {
                        castFailedErr("can't convert string value to DECIMAL", internal = false, cause = e)
                    }
                }
                is TimestampType -> when {
                    type.isText -> try {
                        return valueFactory.newTimestamp(Timestamp.valueOf(stringValue()))
                    } catch (e: IllegalArgumentException) {
                        castFailedErr("can't convert string value to TIMESTAMP", internal = false, cause = e)
                    }
                }
                is DateType -> when {
                    type == ExprValueType.TIMESTAMP -> {
                        val ts = timestampValue()
                        return valueFactory.newDate(LocalDate.of(ts.year, ts.month, ts.day))
                    }
                    type.isText -> try {
                        // validate that the date string follows the format YYYY-MM-DD
                        if (!datePatternRegex.matches(stringValue())) {
                            castFailedErr(
                                "Can't convert string value to DATE. Expected valid date string " +
                                    "and the date format to be YYYY-MM-DD",
                                internal = false
                            )
                        }
                        val date = LocalDate.parse(stringValue())
                        return valueFactory.newDate(date)
                    } catch (e: DateTimeParseException) {
                        castFailedErr(
                            "Can't convert string value to DATE. Expected valid date string " +
                                "and the date format to be YYYY-MM-DD",
                            internal = false, cause = e
                        )
                    }
                }
                is TimeType -> {
                    val precision = targetType.precision
                    when {
                        type == ExprValueType.TIME -> {
                            val time = timeValue()
                            val timeZoneOffset = when (targetType.withTimeZone) {
                                true -> time.zoneOffset ?: defaultTimezoneOffset
                                else -> null
                            }
                            return valueFactory.newTime(
                                Time.of(
                                    time.localTime,
                                    precision ?: time.precision,
                                    timeZoneOffset
                                )
                            )
                        }
                        type == ExprValueType.TIMESTAMP -> {
                            val ts = timestampValue()
                            val timeZoneOffset = when (targetType.withTimeZone) {
                                true -> ts.localOffset ?: castFailedErr(
                                    "Can't convert timestamp value with unknown local offset (i.e. -00:00) to TIME WITH TIME ZONE.",
                                    internal = false
                                )
                                else -> null
                            }
                            return valueFactory.newTime(
                                Time.of(
                                    ts.hour,
                                    ts.minute,
                                    ts.second,
                                    (ts.decimalSecond.remainder(BigDecimal.ONE).multiply(NANOS_PER_SECOND.toBigDecimal())).toInt(),
                                    precision ?: ts.decimalSecond.scale(),
                                    timeZoneOffset
                                )
                            )
                        }
                        type.isText -> try {
                            // validate that the time string follows the format HH:MM:SS[.ddddd...][+|-HH:MM]
                            val matcher = genericTimeRegex.toPattern().matcher(stringValue())
                            if (!matcher.find()) {
                                castFailedErr(
                                    "Can't convert string value to TIME. Expected valid time string " +
                                        "and the time to be of the format HH:MM:SS[.ddddd...][+|-HH:MM]",
                                    internal = false
                                )
                            }

                            val localTime = LocalTime.parse(stringValue(), DateTimeFormatter.ISO_TIME)

                            // Note that the [genericTimeRegex] has a group to extract the zone offset.
                            val zoneOffsetString = matcher.group(2)
                            val zoneOffset = zoneOffsetString?.let { ZoneOffset.of(it) } ?: defaultTimezoneOffset

                            return valueFactory.newTime(
                                Time.of(
                                    localTime,
                                    precision ?: getPrecisionFromTimeString(stringValue()),
                                    when (targetType.withTimeZone) {
                                        true -> zoneOffset
                                        else -> null
                                    }
                                )
                            )
                        } catch (e: DateTimeParseException) {
                            castFailedErr(
                                "Can't convert string value to TIME. Expected valid time string " +
                                    "and the time format to be HH:MM:SS[.ddddd...][+|-HH:MM]",
                                internal = false, cause = e
                            )
                        }
                    }
                }
                is StringType, is SymbolType -> when {
                    type.isNumber -> return numberValue().toString().exprValue(targetType)
                    type.isText -> return stringValue().exprValue(targetType)
                    type == ExprValueType.DATE -> return dateValue().toString().exprValue(targetType)
                    type == ExprValueType.TIME -> return timeValue().toString().exprValue(targetType)
                    type == ExprValueType.BOOL -> return booleanValue().toString().exprValue(targetType)
                    type == ExprValueType.TIMESTAMP -> return timestampValue().toString().exprValue(targetType)
                }
                is ClobType -> when {
                    type.isLob -> return valueFactory.newClob(bytesValue())
                }
                is BlobType -> when {
                    type.isLob -> return valueFactory.newBlob(bytesValue())
                }
                is ListType -> if (type.isSequence) return valueFactory.newList(asSequence())
                is SexpType -> if (type.isSequence) return valueFactory.newSexp(asSequence())
                is BagType -> if (type.isSequence) return valueFactory.newBag(asSequence())
                // no support for anything else
                else -> {}
            }
        }
    }

    val errorCode = if (locationMeta == null) {
        ErrorCode.EVALUATOR_INVALID_CAST_NO_LOCATION
    } else {
        ErrorCode.EVALUATOR_INVALID_CAST
    }

    // incompatible types
    err("Cannot convert $type to $targetType", errorCode, castExceptionContext(), internal = false)
}
/**
 * Remove leading spaces in decimal notation and the plus sign
 *
 * Examples:
 * - `"00001".normalizeForIntCast() == "1"`
 * - `"-00001".normalizeForIntCast() == "-1"`
 * - `"0x00001".normalizeForIntCast() == "0x00001"`
 * - `"+0x00001".normalizeForIntCast() == "0x00001"`
 * - `"000a".normalizeForIntCast() == "a"`
 */
private fun String.normalizeForCastToInt(): String {
    fun Char.isSign() = this == '-' || this == '+'
    fun Char.isHexOrBase2Marker(): Boolean {
        val c = this.toLowerCase()

        return c == 'x' || c == 'b'
    }

    fun String.possiblyHexOrBase2() = (length >= 2 && this[1].isHexOrBase2Marker()) ||
        (length >= 3 && this[0].isSign() && this[2].isHexOrBase2Marker())

    return when {
        length == 0 -> this
        possiblyHexOrBase2() -> {
            if (this[0] == '+') {
                this.drop(1)
            } else {
                this
            }
        }
        else -> {
            val (isNegative, startIndex) = when (this[0]) {
                '-' -> Pair(true, 1)
                '+' -> Pair(false, 1)
                else -> Pair(false, 0)
            }

            var toDrop = startIndex
            while (toDrop < length && this[toDrop] == '0') {
                toDrop += 1
            }

            when {
                toDrop == length -> "0" // string is all zeros
                toDrop == 0 -> this
                toDrop == 1 && isNegative -> this
                toDrop > 1 && isNegative -> '-' + this.drop(toDrop)
                else -> this.drop(toDrop)
            }
        }
    }
}

/**
 * An Unknown value is one of `MISSING` or `NULL`
 */
internal fun ExprValue.isUnknown(): Boolean = this.type.isUnknown
/**
 * The opposite of [isUnknown].
 */
internal fun ExprValue.isNotUnknown(): Boolean = !this.type.isUnknown

/**
 * Creates a filter for unique ExprValues consistent with exprEquals. This filter is stateful keeping track of
 * seen [ExprValue]s.
 *
 * This filter is **stateful**!
 *
 * @return false if the value was seen before
 */
internal fun createUniqueExprValueFilter(): (ExprValue) -> Boolean {
    val seen = TreeSet(DEFAULT_COMPARATOR)

    return { exprValue -> seen.add(exprValue) }
}

fun Sequence.distinct(): Sequence {
    return sequence {
        val seen = TreeSet(DEFAULT_COMPARATOR)
        [email protected] {
            if (!seen.contains(it)) {
                seen.add(it.unnamedValue())
                yield(it)
            }
        }
    }
}

fun Sequence.multiplicities(): TreeMap {
    val multiplicities: TreeMap = TreeMap(DEFAULT_COMPARATOR)
    this.forEach {
        multiplicities.compute(it) { _, v -> (v ?: 0) + 1 }
    }
    return multiplicities
}

/**
 * This method should only be used in case we want to get result from querying an Ion file or an [IonValue]
 */
fun ExprValue.toIonValue(ion: IonSystem): IonValue =
    when (type) {
        ExprValueType.NULL -> ion.newNull(asFacet(IonType::class.java))
        ExprValueType.MISSING -> ion.newNull().apply { addTypeAnnotation(MISSING_ANNOTATION) }
        ExprValueType.BOOL -> ion.newBool(booleanValue())
        ExprValueType.INT -> ion.newInt(longValue())
        ExprValueType.FLOAT -> ion.newFloat(numberValue().toDouble())
        ExprValueType.DECIMAL -> ion.newDecimal(bigDecimalValue())
        ExprValueType.DATE -> {
            val value = dateValue()
            ion.newTimestamp(Timestamp.forDay(value.year, value.monthValue, value.dayOfMonth)).apply {
                addTypeAnnotation(DATE_ANNOTATION)
            }
        }
        ExprValueType.TIMESTAMP -> ion.newTimestamp(timestampValue())
        ExprValueType.TIME -> timeValue().toIonValue(ion)
        ExprValueType.SYMBOL -> ion.newSymbol(stringValue())
        ExprValueType.STRING -> ion.newString(stringValue())
        ExprValueType.CLOB -> ion.newClob(bytesValue())
        ExprValueType.BLOB -> ion.newBlob(bytesValue())
        ExprValueType.LIST -> mapTo(ion.newEmptyList()) {
            if (it is StructExprValue)
                it.toIonStruct(ion)
            else
                it.toIonValue(ion).clone()
        }
        ExprValueType.SEXP -> mapTo(ion.newEmptySexp()) {
            if (it is StructExprValue)
                it.toIonStruct(ion)
            else
                it.toIonValue(ion).clone()
        }
        ExprValueType.BAG -> mapTo(
            ion.newEmptyList().apply { addTypeAnnotation(BAG_ANNOTATION) }
        ) {
            if (it is StructExprValue)
                it.toIonStruct(ion)
            else
                it.toIonValue(ion).clone()
        }
        ExprValueType.STRUCT -> toIonStruct(ion)
    }

private fun ExprValue.toIonStruct(ion: IonSystem): IonStruct {
    return ion.newEmptyStruct().apply {
        [email protected] {
            val nameVal = it.name
            if (nameVal != null && nameVal.type.isText && it.type != ExprValueType.MISSING) {
                val name = nameVal.stringValue()
                add(name, it.toIonValue(ion).clone())
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy