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