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

org.neo4j.values.storable.DateValue 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.Integer.parseInt;
import static java.util.Objects.requireNonNull;
import static org.neo4j.memory.HeapEstimator.LOCAL_DATE_SIZE;
import static org.neo4j.memory.HeapEstimator.shallowSizeOfInstance;
import static org.neo4j.util.FeatureToggles.flag;
import static org.neo4j.values.storable.DateTimeValue.parseZoneName;
import static org.neo4j.values.storable.IntegralValue.safeCastIntegral;

import java.time.Clock;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.IsoFields;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.TemporalUnit;
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.values.StructureBuilder;
import org.neo4j.values.ValueMapper;
import org.neo4j.values.virtual.MapValue;

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

    public static final DateValue MIN_VALUE = new DateValue(LocalDate.MIN);
    public static final DateValue MAX_VALUE = new DateValue(LocalDate.MAX);

    private final LocalDate value;

    private DateValue(LocalDate value) {
        this.value = value;
    }

    public static DateValue date(LocalDate value) {
        return new DateValue(requireNonNull(value, "LocalDate"));
    }

    public static DateValue date(int year, int month, int day) {
        return new DateValue(assertValidArgument(() -> LocalDate.of(year, month, day)));
    }

    public static DateValue weekDate(int year, int week, int dayOfWeek) {
        return new DateValue(assertValidArgument(() -> localWeekDate(year, week, dayOfWeek)));
    }

    public static DateValue quarterDate(int year, int quarter, int dayOfQuarter) {
        return new DateValue(assertValidArgument(() -> localQuarterDate(year, quarter, dayOfQuarter)));
    }

    public static DateValue ordinalDate(int year, int dayOfYear) {
        return new DateValue(assertValidArgument(() -> LocalDate.ofYearDay(year, dayOfYear)));
    }

    public static DateValue epochDate(long epochDay) {
        return new DateValue(epochDateRaw(epochDay));
    }

    public static LocalDate epochDateRaw(long epochDay) {
        return assertValidArgument(() -> LocalDate.ofEpochDay(epochDay));
    }

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

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

    public static DateValue now(Clock clock) {
        return new DateValue(LocalDate.now(clock));
    }

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

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

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

    public static DateValue select(org.neo4j.values.AnyValue from, Supplier defaultZone) {
        return builder(defaultZone).selectDate(from);
    }

    public static DateValue truncate(
            TemporalUnit unit, TemporalValue input, MapValue fields, Supplier defaultZone) {
        LocalDate localDate = input.getDatePart();
        DateValue truncated = date(truncateTo(localDate, unit));
        if (fields.size() == 0) {
            return truncated;
        } else {
            MapValue updatedFields = fields.updatedWith("date", truncated);
            return build(updatedFields, defaultZone);
        }
    }

    static LocalDate truncateTo(LocalDate value, TemporalUnit unit) {
        if (unit == ChronoUnit.MILLENNIA) {
            return value.with(Neo4JTemporalField.YEAR_OF_MILLENNIUM, 0);
        } else if (unit == ChronoUnit.CENTURIES) {
            return value.with(Neo4JTemporalField.YEAR_OF_CENTURY, 0);
        } else if (unit == ChronoUnit.DECADES) {
            return value.with(Neo4JTemporalField.YEAR_OF_DECADE, 0);
        } else if (unit == ChronoUnit.YEARS) {
            return value.with(TemporalAdjusters.firstDayOfYear());
        } else if (unit == IsoFields.WEEK_BASED_YEARS) {
            return value.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 1).with(ChronoField.DAY_OF_WEEK, 1);
        } else if (unit == IsoFields.QUARTER_YEARS) {
            return value.with(IsoFields.DAY_OF_QUARTER, 1);
        } else if (unit == ChronoUnit.MONTHS) {
            return value.with(TemporalAdjusters.firstDayOfMonth());
        } else if (unit == ChronoUnit.WEEKS) {
            return value.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
        } else if (unit == ChronoUnit.DAYS) {
            return value;
        } else {
            throw new UnsupportedTemporalUnitException("Unit too small for truncation: " + unit);
        }
    }

    private static DateBuilder builder(Supplier defaultZone) {
        return new DateBuilder(defaultZone);
    }

    @Override
    protected int unsafeCompareTo(Value otherValue) {
        DateValue other = (DateValue) otherValue;
        return value.compareTo(other.value);
    }

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

    @Override
    LocalDate temporal() {
        return value;
    }

    @Override
    LocalDate getDatePart() {
        return value;
    }

    @Override
    LocalTime getLocalTimePart() {
        throw new UnsupportedTemporalUnitException(String.format("Cannot get the time of: %s", this));
    }

    @Override
    OffsetTime getTimePart(Supplier defaultZone) {
        throw new UnsupportedTemporalUnitException(String.format("Cannot get the time of: %s", this));
    }

    @Override
    ZoneId getZoneId(Supplier defaultZone) {
        throw new UnsupportedTemporalUnitException(String.format("Cannot get the time zone of: %s", this));
    }

    @Override
    ZoneOffset getZoneOffset() {
        throw new UnsupportedTemporalUnitException(String.format("Cannot get the offset of: %s", this));
    }

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

    @Override
    boolean hasTime() {
        return false;
    }

    @Override
    public boolean equals(Value other) {
        return other instanceof DateValue && value.equals(((DateValue) other).value);
    }

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

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

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

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

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

    @Override
    public DateValue add(DurationValue duration) {
        return replacement(assertValidArithmetic(
                () -> value.plusMonths(duration.totalMonths()).plusDays(duration.totalDays())));
    }

    @Override
    public DateValue sub(DurationValue duration) {
        return replacement(assertValidArithmetic(
                () -> value.minusMonths(duration.totalMonths()).minusDays(duration.totalDays())));
    }

    @Override
    DateValue replacement(LocalDate date) {
        return date == value ? this : new DateValue(date);
    }

    static final boolean QUARTER_DATES = flag(DateValue.class, "QUARTER_DATES", true);
    /**
     * The regular expression pattern for parsing dates. All fields come in two versions - long and short form, the
     * long form is for formats containing dashes, the short form is for formats without dashes. The long format is
     * the only one that handles signed years, since that is how the only case that supports years with other than 4
     * numbers, and not having dashes then would make the format ambiguous. In order to not have two cases that can
     * parse only the year, we let the long case handle that.
     * 

* Valid formats: *

    *
  • Year:
      *
    • {@code [0-9]{4}} - unique without dashes, since it is the only one 4 numbers long
      * Parsing: {@code longYear}
    • *
    • {@code [+-] [0-9]{1,9}}
      * Parsing: {@code longYear}
    • *
  • *
  • Year & Month:
      *
    • {@code [0-9]{4} [0-9]{2}} - unique without dashes, since it is the only one 6 numbers long
      * Parsing: {@code shortYear, shortMonth}
    • *
    • {@code [0-9]{4} - [0-9]{1,2}}
      * Parsing: {@code longYear, longMonth}
    • *
    • {@code [+-] [0-9]{1,9} - [0-9]{1,2}}
      * Parsing: {@code longYear, longMonth}
    • *
  • *
  • Calendar date (Year & Month & Day):
      *
    • {@code [0-9]{4} [0-9]{2} [0-9]{2}} - unique without dashes, since it is the only one 8 numbers long
      * Parsing: {@code shortYear, shortMonth, shortDay}
    • *
    • {@code [0-9]{4} - [0-9]{1,2} - [0-9]{1,2}}
      * Parsing: {@code longYear, longMonth, longDay}
    • *
    • {@code [+-] [0-9]{1,9} - [0-9]{1,2} - [0-9]{1,2}}
      * Parsing: {@code longYear, longMonth, longDay}
    • *
  • *
  • Year & Week:
      *
    • {@code [0-9]{4} W [0-9]{2}}
      * Parsing: {@code shortYear, shortWeek}
    • *
    • {@code [0-9]{4} -? W [0-9]{1,2}} - dash optional
      * Parsing: {@code longYear, longWeek}
    • *
    • {@code [+-] [0-9]{1,9} -? W [0-9]{2}} - dash optional
      * Parsing: {@code longYear, longWeek}
    • *
  • *
  • Week date (year & week & day of week):
      *
    • {@code [0-9]{4} W [0-9]{2} [0-9]} - unique without dashes, contains W followed by 2 numbers
      * Parsing: {@code shortYear, shortWeek, shortDOW}
    • *
    • {@code [0-9]{4} -? W [0-9]{1,2} - [0-9]} - dash before week optional
      * Parsing: {@code longYear, longWeek, longDOW}
    • *
    • {@code [+-] [0-9]{1,9} -? W [0-9]{2} - [0-9]} - dash before week optional
      * Parsing: {@code longYear, longWeek, longDOW}
    • *
  • *
  • Ordinal date (year & day of year):
      *
    • {@code [0-9]{4} [0-9]{3}} - unique without dashes, since it is the only one 7 number long
      * Parsing: {@code shortYear, shortDOY}
    • *
    • {@code [0-9]{4} - [0-9]{3}} - needs to be exactly 3 numbers long to distinguish from Year & Month
      * Parsing: {@code longYear, longDOY}
    • *
    • {@code [+-] [0-9]{1,9} - [0-9]{3}} - needs to be exactly 3 numbers long to distinguish from Year & Month
      * Parsing: {@code longYear, longDOY}
    • *
  • *
*/ static final String DATE_PATTERN = "(?:" // short formats - without dashes: + "(?[0-9]{4})(?:" + "(?[0-9]{2})(?[0-9]{2})?|" // calendar date + "W(?[0-9]{2})(?[0-9])?|" // week date + (QUARTER_DATES ? "Q(?[0-9])(?[0-9]{2})?|" : "") // quarter date + "(?[0-9]{3}))" + "|" // ordinal date // long formats - includes dashes: + "(?(?:[0-9]{4}|[+-][0-9]{1,9}))(?:" + "-(?[0-9]{1,2})(?:-(?[0-9]{1,2}))?|" // calendar date + "-?W(?[0-9]{1,2})(?:-(?[0-9]))?|" // week date + (QUARTER_DATES ? "-?Q(?[0-9])(?:-(?[0-9]{1,2}))?|" : "") // quarter date + "-(?[0-9]{3}))?" + ")"; // ordinal date private static final Pattern PATTERN = Pattern.compile(DATE_PATTERN); /** * Creates a {@link LocalDate} from a {@link Matcher} that matches the regular expression defined by * {@link #DATE_PATTERN}. The decision tree in the implementation of this method is guided by the parsing notes * for {@link #DATE_PATTERN}. * * @param matcher a {@link Matcher} that matches the regular expression defined in {@link #DATE_PATTERN}. * @return a {@link LocalDate} parsed from the given {@link Matcher}. */ static LocalDate parseDate(Matcher matcher) { String longYear = matcher.group("longYear"); if (longYear != null) { return parse( matcher, parseInt(longYear), "longMonth", "longDay", "longWeek", "longDOW", "longQuarter", "longDOQ", "longDOY"); } else { return parse( matcher, parseInt(matcher.group("shortYear")), "shortMonth", "shortDay", "shortWeek", "shortDOW", "shortQuarter", "shortDOQ", "shortDOY"); } } private static LocalDate parse( Matcher matcher, int year, String MONTH, String DAY, String WEEK, String DOW, String QUARTER, String DOQ, String DOY) { String month = matcher.group(MONTH); if (month != null) { var day = matcher.group(DAY); /* * Validation * * ISO 8601 standard specifies the use of two digits for month. * We sometimes allow a single digit. But for year+month dates * with a single digit for month (e.g. 2015-2), which are ambiguous * to ordinal dates, we fail in this validation. */ if (day == null && month.length() == 1) { throw TemporalParseException.cannotParseToDateHint(matcher.group()); } return assertParsable(matcher.group(), () -> LocalDate.of(year, parseInt(month), optInt(day))); } String week = matcher.group(WEEK); if (week != null) { return assertParsable( matcher.group(), () -> localWeekDate(year, parseInt(week), optInt(matcher.group(DOW)))); } String quarter = matcher.group(QUARTER); if (quarter != null) { return assertParsable( matcher.group(), () -> localQuarterDate(year, parseInt(quarter), optInt(matcher.group(DOQ)))); } String doy = matcher.group(DOY); if (doy != null) { return assertParsable(matcher.group(), () -> LocalDate.ofYearDay(year, parseInt(doy))); } return assertParsable(matcher.group(), () -> LocalDate.of(year, 1, 1)); } private static DateValue parse(Matcher matcher) { return new DateValue(parseDate(matcher)); } private static int optInt(String value) { return value == null ? 1 : parseInt(value); } private static LocalDate localWeekDate(int year, int week, int dayOfWeek) { LocalDate weekOne = LocalDate.of(year, 1, 4); // the fourth is guaranteed to be in week 1 by definition LocalDate withWeek = weekOne.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, week); // the implementation of WEEK_OF_WEEK_BASED_YEAR uses addition to adjust the date, this means that it accepts // week 53 of years that don't have 53 weeks, so we have to guard for this: if (week == 53 && withWeek.get(IsoFields.WEEK_BASED_YEAR) != year) { throw InvalidArgumentException.tooManyWeeksThisYear(week, year); } return withWeek.with(ChronoField.DAY_OF_WEEK, dayOfWeek); } private static LocalDate localQuarterDate(int year, int quarter, int dayOfQuarter) { // special handling for the range of Q1 and Q2, since they are shorter than Q3 and Q4 if (quarter == 2 && dayOfQuarter == 92) { // TODO: Decide if you want > 91 here - might be more correct throw InvalidArgumentException.only91DaysInQuarter2(year, dayOfQuarter); } // instantiate the yearDate now, because we use it to know if it is a leap year LocalDate yearDate = LocalDate.ofYearDay(year, dayOfQuarter); // guess on the day if (quarter == 1 && dayOfQuarter > 90 && (!yearDate.isLeapYear() || dayOfQuarter == 92)) { var dayLimit = yearDate.isLeapYear() ? 91 : 90; throw InvalidArgumentException.only90Or91DaysInQuarter1(year, dayOfQuarter, dayLimit); } return yearDate.with(IsoFields.QUARTER_OF_YEAR, quarter).with(IsoFields.DAY_OF_QUARTER, dayOfQuarter); } static final LocalDate DEFAULT_CALENDER_DATE = LocalDate.of( TemporalFields.year.defaultValue, TemporalFields.month.defaultValue, TemporalFields.day.defaultValue); private static class DateBuilder extends Builder { @Override protected boolean supportsTimeZone() { return false; } @Override protected boolean supportsEpoch() { return false; } DateBuilder(Supplier defaultZone) { super(defaultZone); } @Override protected final boolean supportsDate() { return true; } @Override protected final boolean supportsTime() { return false; } private static LocalDate getDateOf(org.neo4j.values.AnyValue temporal) { if (temporal instanceof TemporalValue v) { return v.getDatePart(); } String prettyVal = temporal instanceof Value v ? v.prettyPrint() : String.valueOf(temporal); throw InvalidArgumentException.cannotConstructTemporal("date", String.valueOf(temporal), prettyVal); } @Override public DateValue buildInternal() { LocalDate result; if (fields.containsKey(TemporalFields.date)) { result = getDateOf(fields.get(TemporalFields.date)); } else if (fields.containsKey(TemporalFields.week)) { // Be sure to be in the start of the week based year (which can be later than 1st Jan) result = DEFAULT_CALENDER_DATE .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); } else { result = DEFAULT_CALENDER_DATE; } result = assignAllFields(result); return date(result); } static DateValue selectDate(org.neo4j.values.AnyValue date) { if (date instanceof DateValue) { return (DateValue) date; } return date(getDateOf(date)); } } @Override public long estimatedHeapUsage() { return INSTANCE_SIZE; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy