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

org.neo4j.values.storable.DurationValue Maven / Gradle / Ivy

/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.neo4j.values.storable;

import static java.lang.Double.parseDouble;
import static java.lang.Long.parseLong;
import static java.time.temporal.ChronoField.EPOCH_DAY;
import static java.time.temporal.ChronoField.NANO_OF_SECOND;
import static java.time.temporal.ChronoField.OFFSET_SECONDS;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
import static java.time.temporal.ChronoUnit.DAYS;
import static java.time.temporal.ChronoUnit.MONTHS;
import static java.time.temporal.ChronoUnit.NANOS;
import static java.time.temporal.ChronoUnit.SECONDS;
import static java.util.Objects.requireNonNull;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static org.neo4j.memory.HeapEstimator.shallowSizeOfInstance;
import static org.neo4j.values.storable.NumberType.NO_NUMBER;
import static org.neo4j.values.storable.NumberValue.castToLong;
import static org.neo4j.values.storable.NumberValue.safeCastFloatingPoint;
import static org.neo4j.values.utils.TemporalUtil.AVG_NANOS_PER_MONTH;
import static org.neo4j.values.utils.TemporalUtil.AVG_SECONDS_PER_MONTH;
import static org.neo4j.values.utils.TemporalUtil.NANOS_PER_SECOND;
import static org.neo4j.values.utils.TemporalUtil.SECONDS_PER_DAY;
import static org.neo4j.values.utils.ValueMath.HASH_CONSTANT;

import java.time.DateTimeException;
import java.time.Duration;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalUnit;
import java.time.temporal.UnsupportedTemporalTypeException;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.neo4j.exceptions.InvalidArgumentException;
import org.neo4j.exceptions.UnsupportedTemporalUnitException;
import org.neo4j.gqlstatus.GqlHelper;
import org.neo4j.hashing.HashFunction;
import org.neo4j.values.AnyValue;
import org.neo4j.values.Comparison;
import org.neo4j.values.Equality;
import org.neo4j.values.StructureBuilder;
import org.neo4j.values.ValueMapper;
import org.neo4j.values.virtual.MapValue;

/**
 * We use our own implementation because neither {@link java.time.Duration} nor {@link java.time.Period} fits our needs.
 * {@link java.time.Duration} only works with seconds, assumes 24H days, and is unable to handle larger units than days.
 * {@link java.time.Period} only works with units from days or larger, and does not deal with time.
 */
public final class DurationValue extends ScalarValue implements TemporalAmount, Comparable {
    static final long SHALLOW_SIZE = shallowSizeOfInstance(DurationValue.class);

    public static final DurationValue MIN_VALUE = duration(0, 0, Long.MIN_VALUE, 0);
    public static final DurationValue MAX_VALUE = duration(0, 0, Long.MAX_VALUE, 999_999_999);

    public static final DurationValue ZERO = new DurationValue(0, 0, 0, 0);
    private static final List UNITS = List.of(MONTHS, DAYS, SECONDS, NANOS);
    // This comparator is safe until 292,271,023,045 years. After that, we have an overflow.
    private static final Comparator COMPARATOR = Comparator.comparingLong(
                    DurationValue::getAverageLengthInSeconds)
            .thenComparingLong(d -> d.nanos) // nanos are guaranteed to be smaller than NANOS_PER_SECOND
            .thenComparingLong(
                    d -> d.months) // At this point, the durations have the same length and we compare by the individual
            // fields.
            .thenComparingLong(d -> d.days)
            .thenComparingLong(d -> d.seconds);
    private final long months;
    private final long days;
    private final long seconds;
    private final int nanos;

    private DurationValue(long months, long days, long seconds, long nanos) {
        assertNoOverflow(months, days, seconds, nanos);
        seconds = secondsWithNanos(seconds, nanos);
        nanos %= NANOS_PER_SECOND;
        // normalize nanos to be between 0 and NANOS_PER_SECOND-1
        if (nanos < 0) {
            seconds -= 1;
            nanos += NANOS_PER_SECOND;
        }
        this.months = months;
        this.days = days;
        this.seconds = seconds;
        this.nanos = (int) nanos;
    }

    private static DurationValue newDuration(long months, long days, long seconds, long nanos) {
        return seconds == 0 && days == 0 && months == 0 && nanos == 0 // ordered by probability of non-zero
                ? ZERO
                : new DurationValue(months, days, seconds, nanos);
    }

    public static DurationValue duration(Duration value) {
        requireNonNull(value, "Duration");
        return newDuration(0, 0, value.getSeconds(), value.getNano());
    }

    public static DurationValue duration(Period value) {
        requireNonNull(value, "Period");
        return newDuration(value.toTotalMonths(), value.getDays(), 0, 0);
    }

    public static DurationValue duration(long months, long days, long seconds, long nanos) {
        return newDuration(months, days, seconds, nanos);
    }

    public static DurationValue parse(CharSequence text) {
        return TemporalValue.parse(DurationValue.class, PATTERN, DurationValue::parse, text);
    }

    public static DurationValue parse(TextValue text) {
        return TemporalValue.parse(DurationValue.class, PATTERN, DurationValue::parse, text);
    }

    public static DurationValue build(MapValue map) {
        return StructureBuilder.build(builder(), map);
    }

    public static DurationValue between(TemporalUnit unit, Temporal from, Temporal to) {
        if (unit == null) {
            return durationBetween(from, to);
        } else if (unit instanceof ChronoUnit) {
            switch ((ChronoUnit) unit) {
                case MONTHS:
                    return newDuration(assertValidUntil(from, to, unit), 0, 0, 0);
                case DAYS:
                    return newDuration(0, assertValidUntil(from, to, unit), 0, 0);
                case SECONDS:
                    return durationInSecondsAndNanos(from, to);
                default:
                    throw new UnsupportedTemporalUnitException("Unsupported unit: " + unit);
            }
        } else {
            throw new UnsupportedTemporalUnitException("Unsupported unit: " + unit);
        }
    }

    private static StructureBuilder builder() {
        return new DurationBuilder<>() {
            @Override
            DurationValue create(
                    AnyValue years,
                    AnyValue months,
                    AnyValue weeks,
                    AnyValue days,
                    AnyValue hours,
                    AnyValue minutes,
                    AnyValue seconds,
                    AnyValue milliseconds,
                    AnyValue microseconds,
                    AnyValue nanoseconds) {
                var allIntegralValues = Stream.of(
                                years,
                                months,
                                weeks,
                                days,
                                hours,
                                minutes,
                                seconds,
                                milliseconds,
                                microseconds,
                                nanoseconds)
                        .allMatch(t -> t == null || t instanceof IntegralValue);

                if (allIntegralValues) {
                    return duration(
                            castToLong("years", years) * 12 + castToLong("months", months),
                            castToLong("weeks", weeks) * 7 + castToLong("days", days),
                            castToLong("hours", hours) * 3600
                                    + castToLong("minutes", minutes) * 60
                                    + castToLong("seconds", seconds),
                            castToLong("milliseconds", milliseconds) * 1_000_000
                                    + castToLong("microseconds", microseconds) * 1_000
                                    + castToLong("nanoseconds", nanoseconds));
                } else {
                    return approximate(
                            safeCastFloatingPoint("years", years, 0) * 12 + safeCastFloatingPoint("months", months, 0),
                            safeCastFloatingPoint("weeks", weeks, 0) * 7 + safeCastFloatingPoint("days", days, 0),
                            safeCastFloatingPoint("hours", hours, 0) * 3600
                                    + safeCastFloatingPoint("minutes", minutes, 0) * 60
                                    + safeCastFloatingPoint("seconds", seconds, 0),
                            safeCastFloatingPoint("milliseconds", milliseconds, 0) * 1_000_000
                                    + safeCastFloatingPoint("microseconds", microseconds, 0) * 1_000
                                    + safeCastFloatingPoint("nanoseconds", nanoseconds, 0));
                }
            }
        };
    }

    @Override
    public int compareTo(DurationValue other) {
        return COMPARATOR.compare(this, other);
    }

    @Override
    protected int unsafeCompareTo(Value otherValue) {
        return compareTo((DurationValue) otherValue);
    }

    @Override
    Comparison unsafeTernaryCompareTo(Value other) {

        if (ternaryEquals(other) == Equality.TRUE) {
            return Comparison.EQUAL;
        } else {
            return Comparison.UNDEFINED;
        }
    }

    @Override
    public boolean isIncomparableType() {
        return true;
    }

    @Override
    public long estimatedHeapUsage() {
        return SHALLOW_SIZE;
    }

    private long getAverageLengthInSeconds() {
        return calcAverageLengthInSeconds(this.months, this.days, this.seconds);
    }

    private static long calcAverageLengthInSeconds(long months, long days, long seconds) {
        long daysInSeconds = Math.multiplyExact(days, SECONDS_PER_DAY);
        long monthsInSeconds = Math.multiplyExact(months, AVG_SECONDS_PER_MONTH);
        return Math.addExact(seconds, Math.addExact(daysInSeconds, monthsInSeconds));
    }

    private static long secondsWithNanos(long seconds, long nanos) {
        return Math.addExact(seconds, nanos / NANOS_PER_SECOND);
    }

    private static void assertNoOverflow(long months, long days, long seconds, long nanos) {
        try {
            // This is a mess really..
            // Since different values can have different signs (as allowed by Cypher & embedded)
            // We need to check all combinations of values for overflow

            // Nanos are normalized to [0, NANOS_PER_SEC-1] so first we check that seconds don't overflow
            long secondsWithNanos = secondsWithNanos(seconds, nanos);
            if (nanos < 0) {
                secondsWithNanos = Math.subtractExact(secondsWithNanos, 1);
            }
            // Then we check that the days+months+seconds dont overflow, with and without the nanos included
            calcAverageLengthInSeconds(months, days, seconds);
            calcAverageLengthInSeconds(months, days, secondsWithNanos);
        } catch (java.lang.ArithmeticException | org.neo4j.exceptions.ArithmeticException e) {
            throw invalidDuration(months, days, seconds, nanos, e);
        }
    }

    long nanosOfDay() {
        return (seconds % SECONDS_PER_DAY) * NANOS_PER_SECOND + nanos;
    }

    long totalMonths() {
        return months;
    }

    /**
     * The number of days of this duration, as computed by the days and the whole days made up of seconds. This
     * excludes the days contributed by the months.
     *
     * @return the total number of days of this duration.
     */
    long totalDays() {
        return days + (seconds / SECONDS_PER_DAY);
    }

    private static final String UNIT_BASED_PATTERN = "(?:(?[-+]?[0-9]+(?:[.,][0-9]+)?)Y)?"
            + "(?:(?[-+]?[0-9]+(?:[.,][0-9]+)?)M)?"
            + "(?:(?[-+]?[0-9]+(?:[.,][0-9]+)?)W)?"
            + "(?:(?[-+]?[0-9]+(?:[.,][0-9]+)?)D)?"
            + "(?T"
            + "(?:(?[-+]?[0-9]+(?:[.,][0-9]+)?)H)?"
            + "(?:(?[-+]?[0-9]+(?:[.,][0-9]+)?)M)?"
            + "(?:(?[-+]?[0-9]+)(?:[.,](?[0-9]{1,9}))?S)?)?";
    private static final String DATE_BASED_PATTERN = "(?:"
            + "(?[0-9]{4})(?:"
            + "-(?[0-9]{2})-(?[0-9]{2})|"
            + "(?[0-9]{2})(?[0-9]{2}))"
            + ")?(?




© 2015 - 2025 Weber Informatics LLC | Privacy Policy