/* * ----------------------------------------------------------------------- * Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/> * ----------------------------------------------------------------------- * This file (CalendarWeek.java) is part of project Time4J. * * Time4J is free software: You can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * Time4J 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Time4J. If not, see <http://www.gnu.org/licenses/>. * ----------------------------------------------------------------------- */ package net.time4j.range; import net.time4j.CalendarUnit; import net.time4j.Moment; import net.time4j.PlainDate; import net.time4j.SystemClock; import net.time4j.Weekcycle; import net.time4j.Weekday; import net.time4j.Weekmodel; import net.time4j.base.GregorianMath; import net.time4j.base.MathUtils; import net.time4j.base.TimeSource; import net.time4j.engine.AttributeQuery; import net.time4j.engine.ChronoElement; import net.time4j.engine.ChronoEntity; import net.time4j.engine.ChronoMerger; import net.time4j.engine.Chronology; import net.time4j.engine.FormattableElement; import net.time4j.engine.IntElementRule; import net.time4j.engine.ValidationElement; import net.time4j.format.Attributes; import net.time4j.format.CalendarType; import net.time4j.format.Leniency; import net.time4j.format.expert.ChronoFormatter; import net.time4j.format.expert.PatternType; import net.time4j.tz.Timezone; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.text.ParseException; import java.util.Iterator; import java.util.Locale; import java.util.NoSuchElementException; /** * <p>Represents the calendar week starting on Monday according to ISO-8601-paper. </p> * * <p>The elements registered by this class are: </p> * * <ul> * <li>{@link #YEAR_OF_WEEKDATE}</li> * <li>{@link #WEEK_OF_YEAR}</li> * </ul> * * <p>This class does not offer localized formatting. However, a combination of numerical formatting and * fixed literals is possible using the root locale. Example for using the German abbreviation KW for * a calendar week: </p> * * <pre> * ChronoFormatter<CalendarWeek> f = * ChronoFormatter.setUp(CalendarWeek.chronology(), Locale.ROOT) * .addPattern("w. 'KW'", PatternType.CLDR).build(); * System.out.println(f.format(CalendarWeek.of(2016, 4)); // 4. KW * </pre> * * <p>Note: The current calendar week can be determined by an expression like: * {@code CalendarWeek current = SystemClock.inLocalView().now(CalendarWeek.chronology())}. </p> * * @author Meno Hochschild * @since 3.21/4.17 * @doctags.concurrency {immutable} */ /*[deutsch] * <p>Repräsentiert die Kalenderwoche entsprechend der ISO-8601-Norm. </p> * * <p>Die von dieser Klasse registrierten Elemente sind: </p> * * <ul> * <li>{@link #YEAR_OF_WEEKDATE}</li> * <li>{@link #WEEK_OF_YEAR}</li> * </ul> * * <p>Diese Klasse bietet keine lokalisierten Formatierungsoptionen an. Allerdings ist eine Kombination * aus numerischer Darstellung und festen Literalen unter Verwendung der ROOT-locale möglich. Beispiel: </p> * * <pre> * ChronoFormatter<CalendarWeek> f = * ChronoFormatter.setUp(CalendarWeek.chronology(), Locale.ROOT) * .addPattern("w. 'KW'", PatternType.CLDR).build(); * System.out.println(f.format(CalendarWeek.of(2016, 4)); // 4. KW * </pre> * * <p>Hinweis: Die aktuelle Kalenderwoche kann mit einem Ausdruck wie folgt bestimmt werden: * {@code CalendarWeek current = SystemClock.inLocalView().now(CalendarWeek.chronology())}. </p> * * @author Meno Hochschild * @since 3.21/4.17 * @doctags.concurrency {immutable} */ @CalendarType("iso8601") public final class CalendarWeek extends FixedCalendarInterval<CalendarWeek> { //~ Statische Felder/Initialisierungen -------------------------------- /** * <p>Defines an element for the week-based year in an * ISO-8601-weekdate. </p> * * <p>The week-based year is usually the same as the calendar year. * However, at the begin or end of a calendar year the situation is * different because the first week of the weekdate can start after * New Year and the last week of the weekdate can end before the last * day of the calendar year. Examples: </p> * * <ul><li>Sunday, [1995-01-01] => [1994-W52-7]</li> * <li>Tuesday, [1996-31-12] => [1997-W01-2]</li></ul> */ /*[deutsch] * <p>Definiert ein Element für das wochenbasierte Jahr in einem * ISO-Wochendatum. </p> * * <p>Das wochenbasierte Jahr stimmt in der Regel mit dem * Kalenderjahr überein. Ausnahmen sind der Beginn und * das Ende des Kalenderjahres, weil die erste Woche des Jahres * erst nach Neujahr anfangen und die letzte Woche des Jahres * bereits vor Sylvester enden kann. Beispiele: </p> * * <ul><li>Sonntag, [1995-01-01] => [1994-W52-7]</li> * <li>Dienstag, [1996-31-12] => [1997-W01-2]</li></ul> */ @FormattableElement(format = "Y") public static final ChronoElement<Integer> YEAR_OF_WEEKDATE = PlainDate.YEAR_OF_WEEKDATE; /** * <p>Element with the week of year in the value range {@code 1-52/53}. </p> * * <p>The calendar week always starts on Monday. The first week which has at least four days within current * calendar year is considered as the first week of year. </p> * * @see Weekmodel#ISO * @see Weekmodel#weekOfYear() */ /*[deutsch] * <p>Element mit der Kalenderwoche des Jahres (Wertebereich {@code 1-52/53}). </p> * * <p>Die Kalenderwoche startet immer am Montag. Als erste Kalenderwoche des Jahres gilt die, die innerhalb * des aktuellen Kalenderjahres wenigstens vier Tage hat. </p> * * @see Weekmodel#ISO * @see Weekmodel#weekOfYear() */ @FormattableElement(format = "w") public static final ChronoElement<Integer> WEEK_OF_YEAR = Weekmodel.ISO.weekOfYear(); private static final Chronology<CalendarWeek> ENGINE = Chronology.Builder .setUp(CalendarWeek.class, new Merger()) .appendElement(YEAR_OF_WEEKDATE, new YearRule()) .appendElement(WEEK_OF_YEAR, new WeekRule()) .build(); private static final ChronoFormatter<CalendarWeek> PARSER = ChronoFormatter.setUp(CalendarWeek.chronology(), Locale.ROOT) .addPattern("YYYY[-]'W'ww", PatternType.CLDR).build(); private static final long serialVersionUID = -3948942660009645060L; //~ Instanzvariablen -------------------------------------------------- private transient final int year; private transient final int week; private transient final Boundary<PlainDate> start; private transient final Boundary<PlainDate> end; private transient final int lastWeek; //~ Konstruktoren ----------------------------------------------------- private CalendarWeek( int year, int week ) { super(); int wmax = maximumOfWeek(year); if ((year < GregorianMath.MIN_YEAR) || (year > GregorianMath.MAX_YEAR)) { throw new IllegalArgumentException("Year out of bounds: " + year); } else if ((week < 1) || (week > wmax)) { throw new NullPointerException("Week-of-year out of bounds: " + week); } this.year = year; this.week = week; this.lastWeek = wmax; PlainDate date = PlainDate.of(this.year, week, Weekday.MONDAY); this.start = Boundary.ofClosed(date); this.end = Boundary.ofClosed(date.plus(6, CalendarUnit.DAYS)); } //~ Methoden ---------------------------------------------------------- /** * <p>Creates a new instance based on given week-based year and week-of-year * according to ISO-8601. </p> * * <p>In order to create the last calendar week of given year, it is recommended to use following * expression: </p> * * <pre> * CalendarWeek first = CalendarWeek.of(2016, 1); * CalendarWeek last = first.withLastWeekOfYear(); * </pre> * * @param yearOfWeekdate week-based year within range {@code -999,999,999 / +999,999,999} * @param weekOfYear week of year based on ISO-8601 in range {@code 1-52/53} * @return new instance * @throws IllegalArgumentException if any argument is out of range * @see #withLastWeekOfYear() */ /*[deutsch] * <p>Erzeugt eine neue Instanz mit dem angegebenen wochenbasierten Jahr und der ISO-Kalenderwoche. </p> * * <p>Um die letzte Kalenderwoche des angegebenen Jahres zu erhalten, wird empfohlen, folgenden * Ausdruck zu verwenden: </p> * * <pre> * CalendarWeek first = CalendarWeek.of(2016, 1); * CalendarWeek last = first.withLastWeekOfYear(); * </pre> * * @param yearOfWeekdate week-based year within range {@code -999,999,999 / +999,999,999} * @param weekOfYear week of year based on ISO-8601 in range {@code 1-52/53} * @return new instance * @throws IllegalArgumentException if any argument is out of range * @see #withLastWeekOfYear() */ public static CalendarWeek of( int yearOfWeekdate, int weekOfYear ) { return new CalendarWeek(yearOfWeekdate, weekOfYear); } /** * <p>Obtains the current calendar week in system time. </p> * * <p>Convenient short-cut for: {@code SystemClock.inLocalView().now(CalendarWeek.chronology())}. </p> * * @return current calendar week in system time zone using the system clock * @see SystemClock#inLocalView() * @see net.time4j.ZonalClock#now(net.time4j.engine.Chronology) * @since 3.24/4.20 */ /*[deutsch] * <p>Ermittelt die aktuelle Kalenderwoche in der Systemzeit. </p> * * <p>Bequeme Abkürzung für: {@code SystemClock.inLocalView().now(CalendarWeek.chronology())}. </p> * * @return current calendar week in system time zone using the system clock * @see SystemClock#inLocalView() * @see net.time4j.ZonalClock#now(net.time4j.engine.Chronology) * @since 3.24/4.20 */ public static CalendarWeek nowInSystemTime() { return SystemClock.inLocalView().now(CalendarWeek.chronology()); } /** * <p>Combines this calendar week with given day of week to a calendar date. </p> * * @param dayOfWeek day of week in range MONDAY - SUNDAY * @return calendar date */ /*[deutsch] * <p>Kombiniert diese Kalenderwoche mit dem angegebenen Wochentag zu einem Kalenderdatum. </p> * * @param dayOfWeek day of week in range MONDAY - SUNDAY * @return calendar date */ public PlainDate at(Weekday dayOfWeek) { if (dayOfWeek == Weekday.MONDAY) { return this.start.getTemporal(); } else if (dayOfWeek == Weekday.SUNDAY) { return this.end.getTemporal(); } return PlainDate.of(this.year, this.week, dayOfWeek); } /** * <p>Yields the year number. </p> * * @return int * @see #YEAR_OF_WEEKDATE */ /*[deutsch] * <p>Liefert die Jahreszahl. </p> * * @return int * @see #YEAR_OF_WEEKDATE */ public int getYear() { return this.year; } /** * <p>Yields the number of calendar week. </p> * * <p>Note: The last week of year can be obtained by the expression {@code getMaximum(WEEK_OF_YEAR)}. </p> * * @return int * @see #WEEK_OF_YEAR * @see ChronoEntity#getMaximum(ChronoElement) * @see #withLastWeekOfYear() */ /*[deutsch] * <p>Liefert die Kalenderwoche. </p> * * <p>Hinweis: Die letzte Kalenderwoche des Jahres kann mit Hilfe von {@code getMaximum(WEEK_OF_YEAR)} * ermittelt werden. </p> * * @return int * @see #WEEK_OF_YEAR * @see ChronoEntity#getMaximum(ChronoElement) * @see #withLastWeekOfYear() */ public int getWeek() { return this.week; } @Override public Boundary<PlainDate> getStart() { return this.start; } @Override public Boundary<PlainDate> getEnd() { return this.end; } @Override public boolean contains(PlainDate temporal) { return (!temporal.isBefore(this.start.getTemporal())) && !temporal.isAfter(this.end.getTemporal()); } @Override public boolean isAfter(PlainDate temporal) { return this.start.getTemporal().isAfter(temporal); } @Override public boolean isBefore(PlainDate temporal) { return this.end.getTemporal().isBefore(temporal); } /** * <p>Calendar week always consist of seven days. </p> * * @return {@code 7} */ /** * <p>Eine Kalenderwoche hat immer 7 Tage. </p> * * @return {@code 7} */ public int length() { return 7; } /** * <p>Yields the last calendar week of the year of this instance. </p> * * @return calendar week with week number {@code 52} or {@code 53} */ /*[deutsch] * <p>Liefert die letzte Kalenderwoche des Jahres dieser Instanz. </p> * * @return calendar week with week number {@code 52} or {@code 53} */ public CalendarWeek withLastWeekOfYear() { if (this.week == this.lastWeek) { return this; } return CalendarWeek.of(this.year, this.lastWeek); } /** * <p>Adds given years to this calendar week. </p> * * @param years the count of week-based years to be added * @return result of addition */ /*[deutsch] * <p>Addiert die angegebenen Jahre zu dieser Kalenderwoche. </p> * * @param years the count of week-based years to be added * @return result of addition */ public CalendarWeek plus(Years<Weekcycle> years) { if (years.isEmpty()) { return this; } int y = MathUtils.safeAdd(this.year, years.getAmount()); int effectiveWeek = this.week; if ((this.week == 53) && (maximumOfWeek(y) < 53)) { effectiveWeek = 52; } return CalendarWeek.of(y, effectiveWeek); } /** * <p>Adds given weeks to this calendar week. </p> * * @param weeks the count of weeks to be added * @return result of addition */ /*[deutsch] * <p>Addiert die angegebenen Wochen zu dieser Kalenderwoche. </p> * * @param weeks the count of weeks to be added * @return result of addition */ public CalendarWeek plus(Weeks weeks) { if (weeks.isEmpty()) { return this; } PlainDate date = this.start.getTemporal().plus(weeks.getAmount(), CalendarUnit.WEEKS); int y = date.getInt(YEAR_OF_WEEKDATE); int w = date.getInt(WEEK_OF_YEAR); return CalendarWeek.of(y, w); } /** * <p>Subtracts given years from this calendar week. </p> * * @param years the count of week-based years to be subtracted * @return result of subtraction */ /*[deutsch] * <p>Subtrahiert die angegebenen Jahre von dieser Kalenderwoche. </p> * * @param years the count of week-based years to be subtracted * @return result of subtraction */ public CalendarWeek minus(Years<Weekcycle> years) { if (years.isEmpty()) { return this; } int y = MathUtils.safeSubtract(this.year, years.getAmount()); int effectiveWeek = this.week; if ((this.week == 53) && (maximumOfWeek(y) < 53)) { effectiveWeek = 52; } return CalendarWeek.of(y, effectiveWeek); } /** * <p>Subtracts given weeks from this calendar week. </p> * * @param weeks the count of weeks to be subtracted * @return result of subtraction */ /*[deutsch] * <p>Subtrahiert die angegebenen Wochen von dieser Kalenderwoche. </p> * * @param weeks the count of weeks to be subtracted * @return result of subtraction */ public CalendarWeek minus(Weeks weeks) { if (weeks.isEmpty()) { return this; } PlainDate date = this.start.getTemporal().minus(weeks.getAmount(), CalendarUnit.WEEKS); int y = date.getInt(YEAR_OF_WEEKDATE); int w = date.getInt(WEEK_OF_YEAR); return CalendarWeek.of(y, w); } @Override public int compareTo(CalendarWeek other) { if (this.year < other.year) { return -1; } else if (this.year > other.year) { return 1; } else { return (this.week - other.week); } } /** * <p>Iterates over all seven days from Monday to Sunday. </p> * * @return Iterator */ /*[deutsch] * <p>Iteratiert über alle sieben Tage von Montag bis Sonntag. </p> * * @return Iterator */ @Override public Iterator<PlainDate> iterator() { return new Iter(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } else if (obj instanceof CalendarWeek) { CalendarWeek that = (CalendarWeek) obj; return ((this.year == that.year) && (this.week == that.week)); } else { return false; } } @Override public int hashCode() { return this.year ^ (this.week >> 16); } /** * <p>Outputs this instance as a String in CLDR-format "YYYY-'W'ww" (like "2016-W01"). </p> * * @return String * @see #parseISO(String) */ /*[deutsch] * <p>Gibt diese Instanz als String im CLDR-Format "YYYY-'W'ww" (wie "2016-W01") aus. </p> * * @return String * @see #parseISO(String) */ @Override public String toString() { StringBuilder sb = new StringBuilder(); formatYear(sb, this.year); sb.append("-W"); if (this.week < 10) { sb.append('0'); } sb.append(this.week); return sb.toString(); } /** * <p>Interpretes given ISO-conforming text as calendar week. </p> * * <p>The underlying parser uses the CLDR-pattern "YYYY[-]'W'ww". </p> * * @param text text to be parsed * @return parsed calendar week * @throws IndexOutOfBoundsException if given text is empty * @throws ParseException if the text is not parseable * @see #toString() */ /*[deutsch] * <p>Interpretiert den angegebenen ISO-kompatiblen Text als Kalenderwoche. </p> * * <p>Der zugrundeliegende Interpretierer verwendet das CLDR-Formatmuster "YYYY[-]'W'ww". </p> * * @param text text to be parsed * @return parsed calendar week * @throws IndexOutOfBoundsException if given text is empty * @throws ParseException if the text is not parseable * @see #toString() */ public static CalendarWeek parseISO(String text) throws ParseException { return PARSER.parse(text); } /** * <p>Yields the associated chronology. </p> * * @return the underlying rule engine */ /*[deutsch] * <p>Liefert die assoziierte Chronologie. </p> * * @return the underlying rule engine */ public static Chronology<CalendarWeek> chronology() { return ENGINE; } @Override protected Chronology<CalendarWeek> getChronology() { return ENGINE; } @Override protected CalendarWeek getContext() { return this; } private static int maximumOfWeek(int yearOfWeekdate) { return PlainDate.of(yearOfWeekdate, 7, 1).getMaximum(WEEK_OF_YEAR).intValue(); } /** * @serialData Uses <a href="../../../serialized-form.html#net.time4j.range.SPX"> * a dedicated serialization form</a> as proxy. The format * is bit-compressed. The first byte contains in the six * most significant bits the type-ID {@code 39}. Then the year number * and the quarter number are written as int-primitives. * * Schematic algorithm: * * <pre> * int header = 39; * header <<= 2; * out.writeByte(header); * out.writeInt(getYear()); * out.writeInt(getWeek()); * </pre> * * @return replacement object in serialization graph */ private Object writeReplace() { return new SPX(this, SPX.WEEK_TYPE); } /** * @serialData Blocks because a serialization proxy is required. * @param in object input stream * @throws InvalidObjectException (always) */ private void readObject(ObjectInputStream in) throws IOException { throw new InvalidObjectException("Serialization proxy required."); } //~ Innere Klassen ---------------------------------------------------- private static class Merger implements ChronoMerger<CalendarWeek> { //~ Methoden ------------------------------------------------------ @Override public CalendarWeek createFrom( TimeSource<?> clock, AttributeQuery attributes ) { Timezone zone; if (attributes.contains(Attributes.TIMEZONE_ID)) { zone = Timezone.of(attributes.get(Attributes.TIMEZONE_ID)); } else if (attributes.get(Attributes.LENIENCY, Leniency.SMART).isLax()) { zone = Timezone.ofSystem(); } else { return null; } PlainDate date = Moment.from(clock.currentTime()).toZonalTimestamp(zone.getID()).toDate(); return CalendarWeek.of(date.getInt(YEAR_OF_WEEKDATE), date.getInt(WEEK_OF_YEAR)); } @Override public CalendarWeek createFrom( ChronoEntity<?> entity, AttributeQuery attributes, boolean preparsing ) { boolean lenient = attributes.get(Attributes.LENIENCY, Leniency.SMART).isLax(); return this.createFrom(entity, attributes, lenient, preparsing); } @Override public CalendarWeek createFrom( ChronoEntity<?> entity, AttributeQuery attributes, boolean lenient, boolean preparsing ) { int y = entity.getInt(YEAR_OF_WEEKDATE); int w = entity.getInt(WEEK_OF_YEAR); if ( (y >= GregorianMath.MIN_YEAR) && (y <= GregorianMath.MAX_YEAR) && (w >= 1) && (w <= maximumOfWeek(y)) ) { return CalendarWeek.of(y, w); } else if (y > Integer.MIN_VALUE) { entity.with(ValidationElement.ERROR_MESSAGE, "Year out of bounds: " + y); } else if (w > Integer.MIN_VALUE) { entity.with(ValidationElement.ERROR_MESSAGE, "Week-of-year out of bounds: " + w); } return null; } } private static class YearRule implements IntElementRule<CalendarWeek> { //~ Methoden ------------------------------------------------------ @Override public Integer getValue(CalendarWeek context) { return Integer.valueOf(context.year); } @Override public Integer getMinimum(CalendarWeek context) { return Integer.valueOf(GregorianMath.MIN_YEAR); } @Override public Integer getMaximum(CalendarWeek context) { return Integer.valueOf(GregorianMath.MAX_YEAR); } @Override public boolean isValid( CalendarWeek context, Integer value ) { if (value == null) { return false; } int v = value.intValue(); return ((v >= GregorianMath.MIN_YEAR) && (v <= GregorianMath.MAX_YEAR)); } @Override public CalendarWeek withValue( CalendarWeek context, Integer value, boolean lenient ) { if (this.isValid(context, value)) { int y = value.intValue(); return CalendarWeek.of(y, Math.min(maximumOfWeek(y), context.week)); } else { throw new IllegalArgumentException("Not valid: " + value); } } @Override public ChronoElement<?> getChildAtFloor(CalendarWeek context) { return WEEK_OF_YEAR; } @Override public ChronoElement<?> getChildAtCeiling(CalendarWeek context) { return WEEK_OF_YEAR; } @Override public int getInt(CalendarWeek context) { return context.year; } @Override public boolean isValid( CalendarWeek context, int value ) { return ((value >= GregorianMath.MIN_YEAR) && (value <= GregorianMath.MAX_YEAR)); } @Override public CalendarWeek withValue( CalendarWeek context, int value, boolean lenient ) { if (this.isValid(context, value)) { int w = context.week; if (w == 53) { w = Math.min(maximumOfWeek(value), context.week); } return CalendarWeek.of(value, w); } else { throw new IllegalArgumentException("Not valid: " + value); } } } private static class WeekRule implements IntElementRule<CalendarWeek> { //~ Methoden ------------------------------------------------------ @Override public Integer getValue(CalendarWeek context) { return Integer.valueOf(context.week); } @Override public Integer getMinimum(CalendarWeek context) { return Integer.valueOf(1); } @Override public Integer getMaximum(CalendarWeek context) { return Integer.valueOf(context.lastWeek); } @Override public boolean isValid( CalendarWeek context, Integer value ) { if (value == null) { return false; } int v = value.intValue(); return ((v >= 1) && ((v < 53) || (v == context.lastWeek))); } @Override public CalendarWeek withValue( CalendarWeek context, Integer value, boolean lenient ) { if (this.isValid(context, value)) { return CalendarWeek.of(context.year, value.intValue()); } else { throw new IllegalArgumentException("Not valid: " + value); } } @Override public ChronoElement<?> getChildAtFloor(CalendarWeek context) { return null; } @Override public ChronoElement<?> getChildAtCeiling(CalendarWeek context) { return null; } @Override public int getInt(CalendarWeek context) { return context.week; } @Override public boolean isValid( CalendarWeek context, int value ) { return ((value >= 1) && ((value < 53) || (value == context.lastWeek))); } @Override public CalendarWeek withValue( CalendarWeek context, int value, boolean lenient ) { if (this.isValid(context, value)) { return CalendarWeek.of(context.year, value); } else { throw new IllegalArgumentException("Not valid: " + value); } } } private class Iter implements Iterator<PlainDate> { //~ Instanzvariablen ---------------------------------------------- private int count = 0; //~ Methoden ------------------------------------------------------ @Override public boolean hasNext() { return (this.count < 7); } @Override public PlainDate next() { if (this.count >= 7) { throw new NoSuchElementException(); } else { PlainDate result = CalendarWeek.this.start.getTemporal().plus(this.count, CalendarUnit.DAYS); this.count++; return result; } } @Override public void remove() { throw new UnsupportedOperationException(); } } }