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

net.sf.saxon.value.DateValue Maven / Gradle / Ivy

There is a newer version: 12.5
Show newest version
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2018-2023 Saxonica Limited
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

package net.sf.saxon.value;

import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.expr.sort.XPathComparable;
import net.sf.saxon.lib.ConversionRules;
import net.sf.saxon.lib.StringCollator;
import net.sf.saxon.str.UnicodeBuilder;
import net.sf.saxon.str.UnicodeString;
import net.sf.saxon.trans.NoDynamicContextException;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.transpile.CSharpReplaceBody;
import net.sf.saxon.type.*;

import java.time.LocalDate;
import java.time.temporal.WeekFields;
import java.util.Calendar;
import java.util.GregorianCalendar;

/**
 * A value of type Date. Note that a Date may include a TimeZone.
 */

public class DateValue extends GDateValue implements XPathComparable {

    /**
     * Private constructor of a skeletal DateValue
     */

    private DateValue(MutableGDateValue m) {
        super(m);
    }

    /**
     * Constructor given a year, month, and day. Performs no validation.
     *
     * @param year  The year as held internally (note that the year before 1AD is supplied as 0,
     *              but will be displayed on output as -0001)
     * @param month The month, 1-12
     * @param day   The day, 1-31
     */

    public DateValue(int year, byte month, byte day) {
        this(new MutableGDateValue(year, month, day, true,NO_TIMEZONE, BuiltInAtomicType.DATE));
    }

    /**
     * Constructor given a year, month, day and xsd10. Performs no validation.
     *
     * @param year  The year as held internally (note that the year before 1AD is supplied as 0)
     * @param month The month, 1-12
     * @param day   The day, 1-31
     * @param xsd10 Schema version flag. If set to true, the year before 1AD is displayed as -0001;
     *              if false, it is displayed as 0000; but in both cases, it is held internally as zero. The flag
     *              makes no difference to dates later than 1AD.
     */

    public DateValue(int year, byte month, byte day, boolean xsd10) {
        this(new MutableGDateValue(year, month, day, xsd10, NO_TIMEZONE, BuiltInAtomicType.DATE));
    }

    /**
     * Constructor given a year, month, and day, and timezone. Performs no validation.
     *
     * @param year  The year as held internally (note that the year before 1AD is 0)
     * @param month The month, 1-12
     * @param day   The day, 1-31
     * @param tz    the timezone displacement in minutes from UTC. Supply the value
     *              {@link CalendarValue#NO_TIMEZONE} if there is no timezone component.
     * @param xsd10 Schema version flag. If set to true, the year before 1AD is displayed as -0001;
     *              if false, it is displayed as 0000; but in both cases, it is held internally as zero. The flag
     *              makes no difference to dates later than 1AD.
     */

    public DateValue(int year, byte month, byte day, int tz, boolean xsd10) {
        // Method is called by generated Java code.
        this(new MutableGDateValue(year, month, day, xsd10, tz, BuiltInAtomicType.DATE));
    }

    /**
     * Constructor given a year, month, and day, and timezone, and an AtomicType representing
     * a subtype of xs:date. Performs no validation.
     *
     * @param year  The year as held internally (note that the year before 1AD is 0)
     * @param month The month, 1-12
     * @param day   The day 1-31
     * @param tz    the timezone displacement in minutes from UTC. Supply the value
     *              {@link CalendarValue#NO_TIMEZONE} if there is no timezone component.
     * @param type  the type. This must be a type derived from xs:date, and the value
     *              must conform to this type. The method does not check these conditions.
     */

    public DateValue(int year, byte month, byte day, int tz, AtomicType type) {
        this(new MutableGDateValue(year, month, day, false, tz, type));
    }

    /**
     * Constructor: create a date value from a supplied string, in
     * ISO 8601 format
     *
     * @param s the lexical form of the date value. Currently this assumes XSD 1.0
     *          conventions for BC years (that is, no year zero), but this may change at some time.
     * @throws ValidationException if the supplied string is not a valid date
     */
    public DateValue(UnicodeString s) throws ValidationException {
        this(s, ConversionRules.DEFAULT);
    }

    /**
     * Constructor: create a date value from a supplied string, in
     * ISO 8601 format
     *
     * @param s     the lexical form of the date value
     * @param rules the conversion rules (determining whether year zero is allowed)
     * @throws ValidationException if the supplied string is not a valid date
     */
    public DateValue(UnicodeString s, ConversionRules rules) throws ValidationException {
        this(fromUnicodeString(s, rules));
    }

    private static MutableGDateValue fromUnicodeString(UnicodeString s, ConversionRules rules) throws ValidationException {
        MutableGDateValue m = new MutableGDateValue();
        setLexicalValue(m, s, rules.isAllowYearZero());
        if (m.error == null) {
            return m;
        } else {
            throw m.error.makeException();
        }
    }

    /**
     * Create a DateValue (with no timezone) from a Java LocalDate object
     * @param localDate the supplied local date
     * @since 10.0
     */

    public DateValue(LocalDate localDate) {
        this(localDate.getYear(), (byte)localDate.getMonthValue(), (byte)localDate.getDayOfMonth());
    }

    /**
     * Create a DateValue from a Java GregorianCalendar object
     *
     * @param calendar the absolute date/time value
     * @param tz       The timezone offset from GMT in minutes, positive or negative; or the special
     *                 value NO_TIMEZONE indicating that the value is not in a timezone
     */
    public DateValue(GregorianCalendar calendar, int tz) {
        this(fromGregorianCalendar(calendar, tz));
    }

    private static MutableGDateValue fromGregorianCalendar(GregorianCalendar calendar, int tz) {
        // Note: this constructor is not used by Saxon itself, but might be used by applications
        MutableGDateValue g = new MutableGDateValue();
        int era = calendar.get(GregorianCalendar.ERA);
        g.year = calendar.get(Calendar.YEAR);
        if (era == GregorianCalendar.BC) {
            g.year = 1 - g.year;
        }
        g.month = (byte) (calendar.get(Calendar.MONTH) + 1);
        g.day = (byte) calendar.get(Calendar.DATE);
        g.tzMinutes = tz;
        g.typeLabel = BuiltInAtomicType.DATE;
        return g;
    }


    /**
     * Static factory method: construct a DateValue from a string in the lexical form
     * of a date, returning a ValidationFailure if the supplied string is invalid
     *
     * @param in    the lexical form of the date
     * @param rules  conversion rules to apply (affects handling of year 0)
     * @return either a DateValue or a ValidationFailure
     */

    public static ConversionResult makeDateValue(UnicodeString in, ConversionRules rules) {
        MutableGDateValue g = new MutableGDateValue();
        g.typeLabel = BuiltInAtomicType.DATE;
        setLexicalValue(g, in, rules.isAllowYearZero());
        return g.error == null ? new DateValue(g) : g.error;
    }

    /**
     * Determine the primitive type of the value. This delivers the same answer as
     * getItemType().getPrimitiveItemType(). The primitive types are
     * the 19 primitive types of XML Schema, plus xs:integer, xs:dayTimeDuration and xs:yearMonthDuration,
     * and xs:untypedAtomic. For external objects, the result is AnyAtomicType.
     */

    @Override
    public BuiltInAtomicType getPrimitiveType() {
        return BuiltInAtomicType.DATE;
    }

    /**
     * Get the date that immediately follows a given date
     *
     * @param year  the year
     * @param month the month (1-12)
     * @param day   the day (1-31)
     * @return a new DateValue with no timezone information
     */

    public static DateValue tomorrow(int year, byte month, byte day) {
        if (DateValue.isValidDate(year, month, day + 1)) {
            return new DateValue(year, month, (byte) (day + 1), true);
        } else if (month < 12) {
            return new DateValue(year, (byte) (month + 1), (byte) 1, true);
        } else {
            return new DateValue(year + 1, (byte) 1, (byte) 1, true);
        }
    }

    /**
     * Get the date that immediately precedes a given date
     *
     * @param year  the year
     * @param month the month (1-12)
     * @param day   the day (1-31)
     * @return a new DateValue with no timezone information
     */

    public static DateValue yesterday(int year, byte month, byte day) {
        if (day > 1) {
            return new DateValue(year, month, (byte) (day - 1), true);
        } else if (month > 1) {
            if (month == 3 && isLeapYear(year)) {
                return new DateValue(year, (byte) 2, (byte) 29, true);
            } else {
                return new DateValue(year, (byte) (month - 1), daysPerMonth[month - 2], true);
            }
        } else {
            return new DateValue(year - 1, (byte) 12, (byte) 31, true);
        }
    }

    /**
     * Convert to string
     *
     * @return ISO 8601 representation.
     */

    @Override
    public UnicodeString getPrimitiveStringValue() {

        UnicodeBuilder sb = new UnicodeBuilder(16);
        int yr = year;
        if (year <= 0) {
            yr = -yr + (hasNoYearZero ? 1 : 0);           // no year zero in lexical space for XSD 1.0
            if (yr != 0) {
                sb.append('-');
            }
        }
        appendString(sb, yr, yr > 9999 ? (yr + "").length() : 4);
        sb.append('-');
        appendTwoDigits(sb, month);
        sb.append('-');
        appendTwoDigits(sb, day);

        if (hasTimezone()) {
            appendTimezone(sb);
        }

        return sb.toUnicodeString();

    }

    /**
     * Get the canonical lexical representation as defined in XML Schema. This is not always the same
     * as the result of casting to a string according to the XPath rules. For xs:date, the timezone is
     * adjusted to be in the range +12:00 to -11:59
     *
     * @return the canonical lexical representation if defined in XML Schema; otherwise, the result
     *         of casting to string according to the XPath 2.0 rules
     */

    @Override
    public UnicodeString getCanonicalLexicalRepresentation() {
        DateValue target = this;
        if (hasTimezone()) {
            if (getTimezoneInMinutes() > 12 * 60) {
                target = adjustTimezone(getTimezoneInMinutes() - 24 * 60);
            } else if (getTimezoneInMinutes() <= -12 * 60) {
                target = adjustTimezone(getTimezoneInMinutes() + 24 * 60);
            }
        }
        return target.getUnicodeStringValue();
    }

    /**
     * Make a copy of this date value, but with a new type label
     *
     * @param typeLabel the new type label: must be a subtype of xs:date
     * @return the new xs:date value
     */

    /*@NotNull*/
    @Override
    public AtomicValue copyAsSubType(AtomicType typeLabel) {
        MutableGDateValue m = makeMutableCopy();
        m.typeLabel = typeLabel;
        return new DateValue(m);
    }

    /**
     * Return a new date with the same normalized value, but
     * in a different timezone. This is called only for a DateValue that has an explicit timezone
     *
     * @param timezone the new timezone offset, in minutes
     * @return the time in the new timezone. This will be a new TimeValue unless no change
     *         was required to the original value
     */

    @Override
    public DateValue adjustTimezone(int timezone) {
        DateTimeValue dt = toDateTime().adjustTimezone(timezone);
        return new DateValue(dt.getYear(), dt.getMonth(), dt.getDay(), dt.getTimezoneInMinutes(), hasNoYearZero);
    }

    /**
     * Add a duration to a date
     *
     * @param duration the duration to be added (may be negative)
     * @return the new date
     * @throws net.sf.saxon.trans.XPathException
     *          if the duration is an xs:duration, as distinct from
     *          a subclass thereof
     */

    @Override
    public DateValue add(DurationValue duration) throws XPathException {
        if (duration instanceof DayTimeDurationValue) {
            long microseconds = ((DayTimeDurationValue) duration).getLengthInMicroseconds();
            boolean negative = microseconds < 0;
            microseconds = Math.abs(microseconds);
            int days = (int) Math.floor((double) microseconds / (1000000L * 60L * 60L * 24L));
            boolean partDay = (microseconds % (1000000L * 60L * 60L * 24L)) > 0;
            int julian = getJulianDayNumber();
            MutableGDateValue d = mutableDateFromJulianDayNumber(julian + (negative ? -days : days));
            if (partDay) {
                if (negative) {
                    d = yesterday(d.year, d.month, d.day).makeMutableCopy();
                }
            }
            d.tzMinutes = getTimezoneInMinutes();
            d.hasNoYearZero = this.hasNoYearZero;
            return new DateValue(d);
        } else if (duration instanceof YearMonthDurationValue) {
            int months = ((YearMonthDurationValue) duration).getLengthInMonths();
            int m = (month - 1) + months;
            int y = year + m / 12;
            m = m % 12;
            if (m < 0) {
                m += 12;
                y -= 1;
            }
            m++;
            int d = day;
            while (!isValidDate(y, m, d)) {
                d -= 1;
            }
            return new DateValue(y, (byte) m, (byte) d, getTimezoneInMinutes(), hasNoYearZero);
        } else {
            XPathException err = new XPathException("Date arithmetic is not available for xs:duration, only for its subtypes");
            err.setIsTypeError(true);
            err.setErrorCode("XPTY0004");
            throw err;
        }
    }

    /**
     * Determine the difference between two points in time, as a duration
     *
     * @param other   the other point in time
     * @param context the XPath dynamic context. May be set to null
     *                only if both values contain an explicit timezone, or if neither does so.
     * @return the duration as an xs:dayTimeDuration
     * @throws XPathException for example if one value is a date and the other is a time
     */

    @Override
    public DayTimeDurationValue subtract(/*@NotNull*/ CalendarValue other, /*@Nullable*/ XPathContext context) throws XPathException {
        if (!(other instanceof DateValue)) {
            XPathException err = new XPathException("First operand of '-' is a date, but the second is not");
            err.setIsTypeError(true);
            err.setErrorCode("XPTY0004");
            throw err;
        }
        return super.subtract(other, context);
    }

    @Override
    public XPathComparable getXPathComparable(StringCollator collator, int implicitTimezone) throws NoDynamicContextException {
        if (hasTimezone()) {
            return this;
        } else if (implicitTimezone == MISSING_TIMEZONE) {
            throw new NoDynamicContextException("Unknown implicit timezone");
        } else {
            return adjustTimezone(implicitTimezone);
        }
    }

    /**
     * Context-free comparison of two DateValue values. For this to work,
     * the two values must either both have a timezone or both have none.
     *
     * @param v2 the other value
     * @return the result of the comparison: -1 if the first is earlier, 0 if they
     *         are equal, +1 if the first is later
     * @throws ClassCastException if the values are not comparable (which might be because
     *                            no timezone is available)
     */

    @Override
    public int compareTo(XPathComparable v2) {
        if (v2 instanceof DateValue) {
            try {
                return compareTo((DateValue)v2, MISSING_TIMEZONE);
            } catch (Exception err) {
                throw new ClassCastException("Date comparison requires access to implicit timezone");
            }
        } else {
            throw new ClassCastException("Cannot compare xs:date to " + v2.toString());
        }
    }

    /**
     * Calculate the Julian day number at 00:00 on a given date. This algorithm is taken from
     * http://vsg.cape.com/~pbaum/date/jdalg.htm and
     * http://vsg.cape.com/~pbaum/date/jdalg2.htm
     * (adjusted to handle BC dates correctly)
     * 

Note that this assumes dates in the proleptic Gregorian calendar

* * @param year the year * @param month the month (1-12) * @param day the day (1-31) * @return the Julian day number */ public static int getJulianDayNumber(int year, int month, int day) { int z = year - (month < 3 ? 1 : 0); short f = monthData[month - 1]; if (z >= 0) { return day + f + 365 * z + z / 4 - z / 100 + z / 400 + 1721118; } else { // for negative years, add 12000 years and then subtract the days! z += 12000; int j = day + f + 365 * z + z / 4 - z / 100 + z / 400 + 1721118; return j - (365 * 12000 + 12000 / 4 - 12000 / 100 + 12000 / 400); // number of leap years in 12000 years } } /** * Calculate the Julian day number at 00:00 on this date. *

Note that this assumes dates in the proleptic Gregorian calendar

* * @return the Julian day number */ public int getJulianDayNumber() { return getJulianDayNumber(year, month, day); } /** * Get the Gregorian date corresponding to a particular Julian day number. The algorithm * is taken from http://www.hermetic.ch/cal_stud/jdn.htm#comp * * @param julianDayNumber the Julian day number * @return a DateValue with no timezone information set */ public static DateValue dateFromJulianDayNumber(int julianDayNumber) { return new DateValue(mutableDateFromJulianDayNumber(julianDayNumber)); } private static MutableGDateValue mutableDateFromJulianDayNumber(int julianDayNumber) { if (julianDayNumber >= 0) { int L = julianDayNumber + 68569 + 1; // +1 adjustment for days starting at noon int n = (4 * L) / 146097; L = L - (146097 * n + 3) / 4; int i = (4000 * (L + 1)) / 1461001; L = L - (1461 * i) / 4 + 31; int j = (80 * L) / 2447; int d = L - (2447 * j) / 80; L = j / 11; int m = j + 2 - (12 * L); int y = 100 * (n - 49) + i + L; return new MutableGDateValue(y, m, d, true, NO_TIMEZONE, BuiltInAtomicType.DATE); } else { // add 12000 years and subtract them again... MutableGDateValue dt = mutableDateFromJulianDayNumber(julianDayNumber + 365 * 12000 + 12000 / 4 - 12000 / 100 + 12000 / 400); dt.year -= 12000; return dt; } } /** * Get the ordinal day number within the year (1 Jan = 1, 1 Feb = 32, etc) * * @param year the year * @param month the month (1-12) * @param day the day (1-31) * @return the ordinal day number within the year */ public static int getDayWithinYear(int year, int month, int day) { int j = getJulianDayNumber(year, month, day); int k = getJulianDayNumber(year, 1, 1); return j - k + 1; } /** * Get the day of the week. The days of the week are numbered from * 1 (Monday) to 7 (Sunday) * * @param year the year * @param month the month (1-12) * @param day the day (1-31) * @return the day of the week, 1=Monday .... 7=Sunday */ public static int getDayOfWeek(int year, int month, int day) { int d = getJulianDayNumber(year, month, day); d -= 2378500; // 1800-01-05 - any Monday would do while (d <= 0) { d += 70000000; // any sufficiently high multiple of 7 would do } return (d - 1) % 7 + 1; } /** * Get the ISO week number for a given date. The days of the week are numbered from * 1 (Monday) to 7 (Sunday), and week 1 in any calendar year is the week (from Monday to Sunday) * that includes the first Thursday of that year * * @param year the year * @param month the month (1-12) * @param day the day (1-31) * @return the ISO week number */ @CSharpReplaceBody(code="return System.Globalization.ISOWeek.GetWeekOfYear(new System.DateTime(year, month, day));") public static int getWeekNumber(int year, int month, int day) { LocalDate date = LocalDate.of(year, month, day); return date.get(WeekFields.ISO.weekOfWeekBasedYear()); } /** * Get the week number within a month. This is required for the XSLT format-date() function. * The days of the week are numbered from 1 (Monday) to 7 (Sunday), and week 1 * in any calendar month is the week (from Monday to Sunday) that includes the first Thursday * of that month. *

See bug 21370 which clarified the specification. This caused a change to the Saxon * implementation such that the days before the start of week 1 go in the last week of the previous * month, not week zero.

* * @param year the year * @param month the month (1-12) * @param day the day (1-31) * @return the week number within a month */ public static int getWeekNumberWithinMonth(int year, int month, int day) { int firstDay = getDayOfWeek(year, month, 1); if (firstDay > 4 && (firstDay + day) <= 8) { // days before week one are part of the last week of the previous month (4 or 5) DateValue lastDayPrevMonth = yesterday(year, (byte) month, (byte) 1); return getWeekNumberWithinMonth(lastDayPrevMonth.year, lastDayPrevMonth.month, lastDayPrevMonth.day); } int inc = firstDay < 5 ? 1 : 0; // implements the First Thursday rule return ((day + firstDay - 2) / 7) + inc; } /** * Convert the value to a Java {@link LocalDate} value, dropping any timezone information * @return the corresponding Java {@link LocalDate}; any timezone information is simply discarded */ public LocalDate toLocalDate() { return LocalDate.of(getYear(), getMonth(), getDay()); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy