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

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

There is a newer version: 5.25.1
Show newest version
/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://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 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.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.neo4j.exceptions.InvalidArgumentException;
import org.neo4j.exceptions.UnsupportedTemporalUnitException;
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;

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.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;

/**
 * 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 );
    }

    static DurationValue build( Map input )
    {
        StructureBuilder builder = builder();
        for ( Map.Entry entry : input.entrySet() )
        {
            builder.add( entry.getKey(), entry.getValue() );
        }
        return builder.build();
    }

    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 )
            {
                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 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 - 2024 Weber Informatics LLC | Privacy Policy