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

org.neo4j.values.storable.DateTimeValue 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.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.time.temporal.IsoFields;
import java.time.temporal.TemporalQueries;
import java.time.temporal.TemporalUnit;
import java.time.zone.ZoneRulesException;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.neo4j.exceptions.InvalidArgumentException;
import org.neo4j.exceptions.TemporalParseException;
import org.neo4j.exceptions.UnsupportedTemporalUnitException;
import org.neo4j.internal.helpers.collection.Pair;
import org.neo4j.values.AnyValue;
import org.neo4j.values.StructureBuilder;
import org.neo4j.values.ValueMapper;
import org.neo4j.values.virtual.MapValue;

import static java.time.Instant.ofEpochMilli;
import static java.time.Instant.ofEpochSecond;
import static java.time.ZoneOffset.UTC;
import static java.time.ZonedDateTime.ofInstant;
import static java.util.Objects.requireNonNull;
import static org.neo4j.memory.HeapEstimator.ZONED_DATE_TIME_SIZE;
import static org.neo4j.memory.HeapEstimator.shallowSizeOfInstance;
import static org.neo4j.values.storable.DateValue.DATE_PATTERN;
import static org.neo4j.values.storable.DateValue.parseDate;
import static org.neo4j.values.storable.IntegralValue.safeCastIntegral;
import static org.neo4j.values.storable.LocalDateTimeValue.optTime;
import static org.neo4j.values.storable.TimeValue.OFFSET;
import static org.neo4j.values.storable.TimeValue.TIME_PATTERN;
import static org.neo4j.values.storable.TimeValue.parseOffset;
import static org.neo4j.values.storable.Values.NO_VALUE;

public final class DateTimeValue extends TemporalValue
{
    private static final long INSTANCE_SIZE = shallowSizeOfInstance( DateTimeValue.class ) + ZONED_DATE_TIME_SIZE;

    public static final DateTimeValue MIN_VALUE = new DateTimeValue( ZonedDateTime.of( LocalDateTime.MIN, ZoneOffset.MIN ) );
    public static final DateTimeValue MAX_VALUE = new DateTimeValue( ZonedDateTime.of( LocalDateTime.MAX, ZoneOffset.MAX ) );

    private final ZonedDateTime value;
    private final long epochSeconds;

    private DateTimeValue( ZonedDateTime value )
    {
        ZoneId zone = value.getZone().normalized();
        if ( zone instanceof ZoneOffset )
        {
            this.value = value;
        }
        else
        {
            // Do a 2-way lookup of the zone to make sure we only use the new name of renamed zones
            ZoneId mappedZone = ZoneId.of( TimeZones.map( TimeZones.map( zone.getId() ) ) );
            this.value = value.withZoneSameInstant( mappedZone );
        }
        this.epochSeconds = this.value.toEpochSecond();
    }

    public static DateTimeValue datetime( DateValue date, LocalTimeValue time, ZoneId zone )
    {
        return new DateTimeValue( ZonedDateTime.of( date.temporal(), time.temporal(), zone ) );
    }

    public static DateTimeValue datetime( DateValue date, TimeValue time )
    {
        OffsetTime t = time.temporal();
        return new DateTimeValue( ZonedDateTime.of( date.temporal(), t.toLocalTime(), t.getOffset() ) );
    }

    public static DateTimeValue datetime(
            int year, int month, int day, int hour, int minute, int second, int nanoOfSecond, String zone )
    {
        return datetime( year, month, day, hour, minute, second, nanoOfSecond, parseZoneName( zone ) );
    }

    public static DateTimeValue datetime(
            int year, int month, int day, int hour, int minute, int second, int nanoOfSecond, ZoneId zone )
    {
        return new DateTimeValue( assertValidArgument(
                () -> ZonedDateTime.of( year, month, day, hour, minute, second, nanoOfSecond, zone ) ) );
    }

    public static DateTimeValue datetime( long epochSecond, long nano, ZoneOffset zoneOffset )
    {
        return new DateTimeValue( datetimeRaw( epochSecond, nano, zoneOffset ) );
    }

    public static ZonedDateTime datetimeRaw( long epochSecond, long nano, ZoneOffset zoneOffset )
    {
        return datetimeRaw( epochSecond, nano, (ZoneId) zoneOffset );
    }

    public static DateTimeValue datetime( ZonedDateTime datetime )
    {
        return new DateTimeValue( requireNonNull( datetime, "ZonedDateTime" ) );
    }

    public static DateTimeValue datetime( OffsetDateTime datetime )
    {
        return new DateTimeValue( requireNonNull( datetime, "OffsetDateTime" ).toZonedDateTime() );
    }

    public static DateTimeValue datetime( long epochSecondUTC, long nano, ZoneId zone )
    {
        return new DateTimeValue( datetimeRaw( epochSecondUTC, nano, zone ) );
    }

    public static ZonedDateTime datetimeRaw( long epochSecondUTC, long nano, ZoneId zone )
    {
        return assertValidArgument( () -> ofInstant( ofEpochSecond( epochSecondUTC, nano ), zone ) );
    }

    public static DateTimeValue ofEpoch( IntegralValue epochSecondUTC, IntegralValue nano )
    {
        long ns = safeCastIntegral( "nanosecond", nano, 0 );
        if ( ns < 0 || ns >= 1000_000_000 )
        {
            throw new InvalidArgumentException( "Invalid nanosecond: " + ns );
        }
        return new DateTimeValue( datetimeRaw( epochSecondUTC.longValue(), ns, UTC ) );
    }

    public static DateTimeValue ofEpochMillis( IntegralValue millisUTC )
    {
        return new DateTimeValue(
                assertValidArgument( () -> ofInstant( ofEpochMilli( millisUTC.longValue() ), UTC ) ) );
    }

    public static DateTimeValue parse( CharSequence text, Supplier defaultZone,
            CSVHeaderInformation fieldsFromHeader )
    {
        if ( fieldsFromHeader != null )
        {
            if ( !(fieldsFromHeader instanceof TimeCSVHeaderInformation) )
            {
                throw new IllegalStateException( "Wrong header information type: " + fieldsFromHeader );
            }
            // Override defaultZone
            defaultZone = ((TimeCSVHeaderInformation) fieldsFromHeader).zoneSupplier( defaultZone );
        }
        return parse( DateTimeValue.class, PATTERN, DateTimeValue::parse, text, defaultZone );
    }

    public static DateTimeValue parse( CharSequence text, Supplier defaultZone )
    {
        return parse( DateTimeValue.class, PATTERN, DateTimeValue::parse, text, defaultZone );
    }

    public static DateTimeValue parse( TextValue text, Supplier defaultZone )
    {
        return parse( DateTimeValue.class, PATTERN, DateTimeValue::parse, text, defaultZone );
    }

    public static DateTimeValue now( Clock clock )
    {
        return new DateTimeValue( ZonedDateTime.now( clock ) );
    }

    public static DateTimeValue now( Clock clock, String timezone )
    {
        return now( clock.withZone( parseZoneName( timezone ) ) );
    }

    public static DateTimeValue now( Clock clock, Supplier defaultZone )
    {
        return now( clock.withZone( defaultZone.get() ) );
    }

    public static DateTimeValue build( MapValue map, Supplier defaultZone )
    {
        return StructureBuilder.build( builder( defaultZone ), map );
    }

    public static DateTimeValue select( AnyValue from, Supplier defaultZone )
    {
        return builder( defaultZone ).selectDateTime( from );
    }

    public static DateTimeValue truncate(
            TemporalUnit unit,
            TemporalValue input,
            MapValue fields,
            Supplier defaultZone )
    {
        Pair pair = getTruncatedDateAndTime( unit, input, "date time" );

        LocalDate truncatedDate = pair.first();
        LocalTime truncatedTime = pair.other();

        ZoneId zoneId = input.supportsTimeZone() ? input.getZoneId( defaultZone ) : defaultZone.get();
        ZonedDateTime truncatedZDT = ZonedDateTime.of( truncatedDate, truncatedTime, zoneId );

        if ( fields.size() == 0 )
        {
            return datetime( truncatedZDT );
        }
        else
        {
            // Timezone needs some special handling, since the builder will shift keeping the instant instead of the local time
            AnyValue timezone = fields.get( "timezone" );
            if ( timezone != NO_VALUE )
            {
                truncatedZDT = truncatedZDT.withZoneSameLocal( timezoneOf( timezone ) );
            }

            return updateFieldMapWithConflictingSubseconds( fields, unit, truncatedZDT,
                    ( mapValue, zonedDateTime ) -> {

                        if ( mapValue.size() == 0 )
                        {
                            return datetime( zonedDateTime );
                        }
                        else
                        {
                            return build( mapValue.updatedWith( "datetime", datetime( zonedDateTime ) ),
                                    defaultZone );
                        }

                    } );
        }
    }

    static DateTimeBuilder builder( Supplier defaultZone )
    {
        return new DateTimeBuilder<>( defaultZone )
        {
            @Override
            protected boolean supportsTimeZone()
            {
                return true;
            }

            @Override
            protected boolean supportsEpoch()
            {
                return true;
            }

            private final ZonedDateTime defaultZonedDateTime =
                    ZonedDateTime.of( TemporalFields.year.defaultValue, TemporalFields.month.defaultValue,
                                      TemporalFields.day.defaultValue, TemporalFields.hour.defaultValue,
                                      TemporalFields.minute.defaultValue, TemporalFields.second.defaultValue,
                                      TemporalFields.nanosecond.defaultValue, timezone() );

            @Override
            public DateTimeValue buildInternal()
            {
                boolean selectingDate = fields.containsKey( TemporalFields.date );
                boolean selectingTime = fields.containsKey( TemporalFields.time );
                boolean selectingDateTime = fields.containsKey( TemporalFields.datetime );
                boolean selectingEpoch = fields.containsKey( TemporalFields.epochSeconds ) ||
                                         fields.containsKey( TemporalFields.epochMillis );
                boolean selectingTimeZone;
                ZonedDateTime result;
                if ( selectingDateTime )
                {
                    AnyValue dtField = fields.get( TemporalFields.datetime );
                    if ( !(dtField instanceof TemporalValue) )
                    {
                        throw new InvalidArgumentException(
                                String.format( "Cannot construct date time from: %s", dtField ) );
                    }
                    TemporalValue dt = (TemporalValue) dtField;
                    LocalTime timePart = dt.getTimePart( defaultZone ).toLocalTime();
                    ZoneId zoneId = dt.getZoneId( defaultZone );
                    result = ZonedDateTime.of( dt.getDatePart(), timePart, zoneId );
                    selectingTimeZone = dt.supportsTimeZone();
                }
                else if ( selectingEpoch )
                {
                    if ( fields.containsKey( TemporalFields.epochSeconds ) )
                    {
                        AnyValue epochField = fields.get( TemporalFields.epochSeconds );
                        if ( !(epochField instanceof IntegralValue) )
                        {
                            throw new InvalidArgumentException(
                                    String.format( "Cannot construct date time from: %s", epochField ) );
                        }
                        IntegralValue epochSeconds = (IntegralValue) epochField;
                        result = assertValidArgument( () -> ZonedDateTime
                                .ofInstant( Instant.ofEpochMilli( epochSeconds.longValue() * 1000 ), timezone() ) );
                    }
                    else
                    {
                        AnyValue epochField = fields.get( TemporalFields.epochMillis );
                        if ( !(epochField instanceof IntegralValue) )
                        {
                            throw new InvalidArgumentException(
                                    String.format( "Cannot construct date time from: %s", epochField ) );
                        }
                        IntegralValue epochMillis = (IntegralValue) epochField;
                        result = assertValidArgument( () -> ZonedDateTime
                                .ofInstant( Instant.ofEpochMilli( epochMillis.longValue() ), timezone() ) );
                    }
                    selectingTimeZone = false;
                }
                else if ( selectingTime || selectingDate )
                {

                    LocalTime time;
                    ZoneId zoneId;
                    if ( selectingTime )
                    {
                        AnyValue timeField = fields.get( TemporalFields.time );
                        if ( !(timeField instanceof TemporalValue) )
                        {
                            throw new InvalidArgumentException(
                                    String.format( "Cannot construct time from: %s", timeField ) );
                        }
                        TemporalValue t = (TemporalValue) timeField;
                        time = t.getTimePart( defaultZone ).toLocalTime();
                        zoneId = t.getZoneId( defaultZone );
                        selectingTimeZone = t.supportsTimeZone();
                    }
                    else
                    {
                        time = LocalTimeValue.DEFAULT_LOCAL_TIME;
                        zoneId = timezone();
                        selectingTimeZone = false;
                    }
                    LocalDate date;
                    if ( selectingDate )
                    {
                        AnyValue dateField = fields.get( TemporalFields.date );
                        if ( !(dateField instanceof TemporalValue) )
                        {
                            throw new InvalidArgumentException(
                                    String.format( "Cannot construct date from: %s", dateField ) );
                        }
                        TemporalValue t = (TemporalValue) dateField;
                        date = t.getDatePart();
                    }
                    else
                    {
                        date = DateValue.DEFAULT_CALENDER_DATE;
                    }
                    result = ZonedDateTime.of( date, time, zoneId );
                }
                else
                {
                    result = defaultZonedDateTime;
                    selectingTimeZone = false;
                }

                if ( fields.containsKey( TemporalFields.week ) && !selectingDate && !selectingDateTime &&
                     !selectingEpoch )
                {
                    // Be sure to be in the start of the week based year (which can be later than 1st Jan)
                    result = result
                            .with( IsoFields.WEEK_BASED_YEAR,
                                   safeCastIntegral( TemporalFields.year.name(), fields.get( TemporalFields.year ),
                                                     TemporalFields.year.defaultValue ) )
                            .with( IsoFields.WEEK_OF_WEEK_BASED_YEAR, 1 )
                            .with( ChronoField.DAY_OF_WEEK, 1 );
                }

                result = assignAllFields( result );
                if ( timezone != null )
                {
                    if ( ((selectingTime || selectingDateTime) && selectingTimeZone) || selectingEpoch )
                    {
                        try
                        {
                            result = result.withZoneSameInstant( timezone() );
                        }
                        catch ( DateTimeParseException e )
                        {
                            throw new TemporalParseException( e.getMessage(), e.getParsedString(), e.getErrorIndex(), e );
                        }
                    }
                    else
                    {
                        result = result.withZoneSameLocal( timezone() );
                    }
                }
                return datetime( result );
            }

            @Override
            protected DateTimeValue selectDateTime( AnyValue datetime )
            {
                if ( datetime instanceof DateTimeValue )
                {
                    DateTimeValue value = (DateTimeValue) datetime;
                    ZoneId zone = optionalTimezone();
                    return zone == null ? value : new DateTimeValue(
                            ZonedDateTime.of( value.temporal().toLocalDateTime(), zone ) );
                }
                if ( datetime instanceof LocalDateTimeValue )
                {
                    return new DateTimeValue( ZonedDateTime.of(
                            ((LocalDateTimeValue) datetime).temporal(), timezone() ) );
                }
                throw new UnsupportedTemporalUnitException( "Cannot select datetime from: " + datetime );
            }
        };
    }

    @Override
    ZonedDateTime temporal()
    {
        return value;
    }

    @Override
    LocalDate getDatePart()
    {
        return value.toLocalDate();
    }

    @Override
    LocalTime getLocalTimePart()
    {
        return value.toLocalTime();
    }

    @Override
    OffsetTime getTimePart( Supplier defaultZone )
    {
        ZoneOffset offset = value.getOffset();
        LocalTime localTime = value.toLocalTime();
        return OffsetTime.of( localTime, offset );
    }

    @Override
    ZoneId getZoneId( Supplier defaultZone )
    {
        return value.getZone();
    }

    @Override
    ZoneOffset getZoneOffset()
    {
        return value.getOffset();
    }

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

    @Override
    boolean hasTime()
    {
        return true;
    }

    @Override
    public boolean equals( Value other )
    {
        if ( other instanceof DateTimeValue )
        {
            ZonedDateTime that = ((DateTimeValue) other).value;
            boolean res = value.toLocalDateTime().equals( that.toLocalDateTime() );
            if ( res )
            {
                ZoneId thisZone = value.getZone();
                ZoneId thatZone = that.getZone();
                boolean thisIsOffset = thisZone instanceof ZoneOffset;
                boolean thatIsOffset = thatZone instanceof ZoneOffset;
                if ( thisIsOffset && thatIsOffset )
                {
                    res = thisZone.equals( thatZone );
                }
                else if ( !thisIsOffset && !thatIsOffset )
                {
                    res = TimeZones.map( thisZone.getId() ) == TimeZones.map( thatZone.getId() );
                }
                else
                {
                    res = false;
                }
            }
            return res;

        }
        return false;
    }

    @Override
    public  void writeTo( ValueWriter writer ) throws E
    {
        writer.writeDateTime( value );
    }

    @Override
    protected int unsafeCompareTo( Value other )
    {
        DateTimeValue that = (DateTimeValue) other;
        int cmp = Long.compare( epochSeconds, that.epochSeconds );
        if ( cmp == 0 )
        {
            cmp = value.toLocalTime().getNano() - that.value.toLocalTime().getNano();
            if ( cmp == 0 )
            {
                ZoneOffset thisOffset = value.getOffset();
                ZoneOffset thatOffset = that.value.getOffset();

                cmp = Integer.compare( thisOffset.getTotalSeconds(), thatOffset.getTotalSeconds() );
                if ( cmp == 0 )
                {
                    ZoneId thisZone = value.getZone();
                    ZoneId thatZone = that.value.getZone();
                    boolean thisIsOffset = thisZone instanceof ZoneOffset;
                    boolean thatIsOffset = thatZone instanceof ZoneOffset;
                    // non-named timezone (just offset) before named-time zones, alphabetically
                    cmp = Boolean.compare( thatIsOffset, thisIsOffset );
                    if ( cmp == 0 )
                    {
                        if ( !thisIsOffset ) // => also means !thatIsOffset
                        {
                            cmp = compareNamedZonesWithMapping( thisZone, thatZone );
                        }
                    }
                    if ( cmp == 0 )
                    {
                        cmp = value.getChronology().compareTo( that.value.getChronology() );
                    }
                }
            }
        }
        return cmp;
    }

    private static int compareNamedZonesWithMapping( ZoneId thisZone, ZoneId thatZone )
    {
        String thisZoneNormalized = TimeZones.map( TimeZones.map( thisZone.getId() ) );
        String thatZoneNormalized = TimeZones.map( TimeZones.map( thatZone.getId() ) );
        return thisZoneNormalized.compareTo( thatZoneNormalized );
    }

    @Override
    public String prettyPrint()
    {
        return assertPrintable( () -> value.format( DateTimeFormatter.ISO_DATE_TIME ) );
    }

    @Override
    public ValueRepresentation valueRepresentation()
    {
        return ValueRepresentation.ZONED_DATE_TIME;
    }

    @Override
    protected int computeHashToMemoize()
    {
        return value.toInstant().hashCode();
    }

    @Override
    public String getTypeName()
    {
        return "DateTime";
    }

    @Override
    public  T map( ValueMapper mapper )
    {
        return mapper.mapDateTime( this );
    }

    @Override
    public DateTimeValue add( DurationValue duration )
    {
        return replacement( assertValidArithmetic( () -> value.plus( duration ) ) );
    }

    @Override
    public DateTimeValue sub( DurationValue duration )
    {
        return replacement( assertValidArithmetic( () -> value.minus( duration ) ) );
    }

    @Override
    DateTimeValue replacement( ZonedDateTime datetime )
    {
        return value == datetime ? this : new DateTimeValue( datetime );
    }

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

    private static final Pattern PATTERN = Pattern.compile(
            DATE_PATTERN + "(?




© 2015 - 2024 Weber Informatics LLC | Privacy Policy