/* * ----------------------------------------------------------------------- * Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/> * ----------------------------------------------------------------------- * This file (CalendarQuarter.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.Quarter; import net.time4j.SystemClock; 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.DisplayStyle; import net.time4j.engine.ElementRule; import net.time4j.engine.FormattableElement; import net.time4j.engine.IntElementRule; import net.time4j.engine.ValidationElement; import net.time4j.format.Attributes; import net.time4j.format.CalendarText; import net.time4j.format.CalendarType; import net.time4j.format.Leniency; import net.time4j.format.LocalizedPatternSupport; import net.time4j.tz.Timezone; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.text.DateFormat; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; /** * <p>Represents the quarter of a gregorian calendar year as interval * (like from 1st of January until end of March). </p> * * <p>The elements registered by this class are: </p> * * <ul> * <li>{@link #YEAR}</li> * <li>{@link #QUARTER_OF_YEAR}</li> * </ul> * * <p>Note: The current quarter of calendar year can be determined by an expression like: * {@code CalendarQuarter current = SystemClock.inLocalView().now(CalendarQuarter.chronology())}. </p> * * @author Meno Hochschild * @since 3.21/4.17 * @doctags.concurrency {immutable} */ /*[deutsch] * <p>Repräsentiert das Quartal eines Kalenderjahres als Intervall * (zum Beispiel vom ersten Januar bis Ende März). </p> * * <p>Die von dieser Klasse registrierten Elemente sind: </p> * * <ul> * <li>{@link #YEAR}</li> * <li>{@link #QUARTER_OF_YEAR}</li> * </ul> * * <p>Hinweis: Das aktuelle Quartal kann mit einem Ausdruck wie folgt bestimmt werden: * {@code CalendarQuarter current = SystemClock.inLocalView().now(CalendarQuarter.chronology())}. </p> * * @author Meno Hochschild * @since 3.21/4.17 * @doctags.concurrency {immutable} */ @CalendarType("iso8601") public final class CalendarQuarter extends FixedCalendarInterval<CalendarQuarter> implements LocalizedPatternSupport { //~ Statische Felder/Initialisierungen -------------------------------- /** * <p>Element with the proleptic iso-year without any era reference and * the value range {@code -999999999} until {@code 999999999}. </p> * <p/> * <p>The term "proleptic" means that the rules of the gregorian * calendar and the associated way of year counting is applied backward * even before the introduction of gregorian calendar. The year {@code 0} * is permitted - and negative years, too. For historic year numbers, * this mathematical extrapolation is not recommended and usually * wrong. </p> */ /*[deutsch] * <p>Element mit dem proleptischen ISO-Jahr ohne Ära-Bezug mit dem * Wertebereich {@code -999999999} bis {@code 999999999}. </p> * * <p>Der Begriff "proleptisch" bedeutet, daß die Regeln * des gregorianischen Kalenders und damit verbunden die Jahreszählung * auch rückwirkend vor der Einführung des Kalenders angewandt * werden. Insbesondere ist auch das Jahr {@code 0} zugelassen - nebst * negativen Jahreszahlen. Für historische Jahreszahlen ist diese * mathematische Extrapolation nicht geeignet. </p> */ @FormattableElement(format = "u") public static final ChronoElement<Integer> YEAR = PlainDate.YEAR; /** * <p>Element with the quarter of year in the value range * {@code Q1-Q4}. </p> */ /*[deutsch] * <p>Element mit dem Quartal des Jahres (Wertebereich {@code Q1-Q4}). </p> */ @FormattableElement(format = "Q", standalone="q") public static final ChronoElement<Quarter> QUARTER_OF_YEAR = PlainDate.QUARTER_OF_YEAR; private static final Chronology<CalendarQuarter> ENGINE = Chronology.Builder .setUp(CalendarQuarter.class, new Merger()) .appendElement(YEAR, new YearRule()) .appendElement(QUARTER_OF_YEAR, new QuarterRule()) .build(); private static final long serialVersionUID = -4871348693353897858L; //~ Instanzvariablen -------------------------------------------------- private transient final int year; private transient final Quarter quarter; private transient final Boundary<PlainDate> start; private transient final Boundary<PlainDate> end; //~ Konstruktoren ----------------------------------------------------- private CalendarQuarter( int year, Quarter quarter ) { super(); if ((year < GregorianMath.MIN_YEAR) || (year > GregorianMath.MAX_YEAR)) { throw new IllegalArgumentException("Year out of bounds: " + year); } else if (quarter == null) { throw new NullPointerException("Missing quarter of calendar year."); } this.year = year; this.quarter = quarter; PlainDate date = PlainDate.of(this.year, 1, 1).with(QUARTER_OF_YEAR, quarter); this.start = Boundary.ofClosed(date); this.end = Boundary.ofClosed(date.with(PlainDate.DAY_OF_QUARTER.maximized())); } //~ Methoden ---------------------------------------------------------- /** * <p>Creates a new instance based on given gregorian calendar year and quarter year. </p> * * @param year gregorian year within range {@code -999,999,999 / +999,999,999} * @param quarter quarter year * @return new instance * @throws IllegalArgumentException if given year is out of range */ /*[deutsch] * <p>Erzeugt eine neue Instanz mit dem angegebenen gregorianischen Kalendarjahr und Quartal. </p> * * @param year gregorian year within range {@code -999,999,999 / +999,999,999} * @param quarter quarter year * @return new instance * @throws IllegalArgumentException if given year is out of range */ public static CalendarQuarter of( int year, Quarter quarter ) { return new CalendarQuarter(year, quarter); } /** * <p>Obtains the current calendar quarter year in system time. </p> * * <p>Convenient short-cut for: {@code SystemClock.inLocalView().now(CalendarQuarter.chronology())}. </p> * * @return current calendar quarter year 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 das aktuelle Kalenderquartal in der Systemzeit. </p> * * <p>Bequeme Abkürzung für: {@code SystemClock.inLocalView().now(CalendarQuarter.chronology())}. </p> * * @return current calendar quarter year 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 CalendarQuarter nowInSystemTime() { return SystemClock.inLocalView().now(CalendarQuarter.chronology()); } /** * <p>Combines this calendar quarter with given day of quarter year to a calendar date. </p> * * @param dayOfQuarter day of quarter in range 1-90/91/92 * @return calendar date * @throws IllegalArgumentException if the day-of-quarter is out of range */ /*[deutsch] * <p>Kombiniert dieses Quartal mit dem angegebenen Quartalstag zu einem Kalenderdatum. </p> * * @param dayOfQuarter day of quarter in range 1-90/91/92 * @return calendar date * @throws IllegalArgumentException if the day-of-quarter is out of range */ public PlainDate atDayOfQuarter(int dayOfQuarter) { if (dayOfQuarter == 1) { return this.start.getTemporal(); } return this.start.getTemporal().with(PlainDate.DAY_OF_QUARTER, dayOfQuarter); } /** * <p>Yields the date of the end of this quarter year. </p> * * @return PlainDate */ /*[deutsch] * <p>Liefert das Endedatum dieses Kalenderquartals. </p> * * @return PlainDate */ public PlainDate atEndOfQuarter() { return this.end.getTemporal(); } /** * <p>Yields the year number. </p> * * @return int */ /*[deutsch] * <p>Liefert die Jahreszahl. </p> * * @return int */ public int getYear() { return this.year; } /** * <p>Yields the quarter year. </p> * * @return Quarter */ /*[deutsch] * <p>Liefert das Quartal. </p> * * @return Quarter */ public Quarter getQuarter() { return this.quarter; } @Override public Boundary<PlainDate> getStart() { return this.start; } @Override public Boundary<PlainDate> getEnd() { return this.end; } @Override public boolean contains(PlainDate temporal) { return ((temporal.getYear() == this.year) && (temporal.get(QUARTER_OF_YEAR) == this.quarter)); } @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>Determines the count of days belonging to this quarter year. </p> * * @return int */ /** * <p>Ermittelt die Anzahl der Tage, die zu diesem Kalenderquartal gehören. </p> * * @return int */ public int length() { return this.start.getTemporal().getMaximum(PlainDate.DAY_OF_QUARTER); } /** * <p>Adds given years to this quarter year. </p> * * @param years the count of years to be added * @return result of addition */ /*[deutsch] * <p>Addiert die angegebenen Jahre zu diesem Kalenderquartal. </p> * * @param years the count of years to be added * @return result of addition */ public CalendarQuarter plus(Years<CalendarUnit> years) { if (years.isEmpty()) { return this; } return CalendarQuarter.of(MathUtils.safeAdd(this.year, years.getAmount()), this.quarter); } /** * <p>Adds given quarter years to this quarter year. </p> * * @param quarters the count of quarter years to be added * @return result of addition */ /*[deutsch] * <p>Addiert die angegebenen Quartale zu diesem Kalenderquartal. </p> * * @param quarters the count of quarter years to be added * @return result of addition */ public CalendarQuarter plus(Quarters quarters) { if (quarters.isEmpty()) { return this; } long value = this.year * 4L + this.quarter.getValue() - 1 + quarters.getAmount(); int y = MathUtils.safeCast(MathUtils.floorDivide(value, 4)); Quarter q = Quarter.valueOf(MathUtils.floorModulo(value, 4) + 1); return CalendarQuarter.of(y, q); } /** * <p>Subtracts given years from this quarter year. </p> * * @param years the count of years to be subtracted * @return result of subtraction */ /*[deutsch] * <p>Subtrahiert die angegebenen Jahre von diesem Kalenderquartal. </p> * * @param years the count of years to be subtracted * @return result of subtraction */ public CalendarQuarter minus(Years<CalendarUnit> years) { if (years.isEmpty()) { return this; } return CalendarQuarter.of(MathUtils.safeSubtract(this.year, years.getAmount()), this.quarter); } /** * <p>Subtracts given quarter years from this quarter year. </p> * * @param quarters the count of quarter years to be subtracted * @return result of subtraction */ /*[deutsch] * <p>Subtrahiert die angegebenen Quartale von diesem Kalenderquartal. </p> * * @param quarters the count of quarter years to be subtracted * @return result of subtraction */ public CalendarQuarter minus(Quarters quarters) { if (quarters.isEmpty()) { return this; } long value = this.year * 4L + this.quarter.getValue() - 1 - quarters.getAmount(); int y = MathUtils.safeCast(MathUtils.floorDivide(value, 4)); Quarter q = Quarter.valueOf(MathUtils.floorModulo(value, 4) + 1); return CalendarQuarter.of(y, q); } @Override public int compareTo(CalendarQuarter other) { if (this.year < other.year) { return -1; } else if (this.year > other.year) { return 1; } else { return this.quarter.compareTo(other.quarter); } } /** * <p>Iterates over all days of this calendar quarter year. </p> * * @return Iterator */ /*[deutsch] * <p>Iteratiert über alle Tage dieses Kalenderquartals. </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 CalendarQuarter) { CalendarQuarter that = (CalendarQuarter) obj; return ((this.year == that.year) && (this.quarter == that.quarter)); } else { return false; } } @Override public int hashCode() { return this.year ^ this.quarter.hashCode(); } /** * <p>Outputs this instance as a String in CLDR-format "uuuu-'Q'Q" (like "2016-Q1"). </p> * * @return String */ /*[deutsch] * <p>Gibt diese Instanz als String im CLDR-Format "uuuu-'Q'Q" (wie "2016-Q1") aus. </p> * * @return String */ @Override public String toString() { StringBuilder sb = new StringBuilder(); formatYear(sb, this.year); sb.append("-Q"); sb.append(this.quarter.getValue()); return sb.toString(); } /** * <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<CalendarQuarter> chronology() { return ENGINE; } @Override protected Chronology<CalendarQuarter> getChronology() { return ENGINE; } @Override protected CalendarQuarter getContext() { return this; } /** * @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 37}. Then the year number * and the quarter number are written as int-primitives. * * Schematic algorithm: * * <pre> * int header = 37; * header <<= 2; * out.writeByte(header); * out.writeInt(getYear()); * out.writeInt(getQuarterOfYear().getValue()); * </pre> * * @return replacement object in serialization graph */ private Object writeReplace() { return new SPX(this, SPX.QUARTER_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<CalendarQuarter> { //~ Methoden ------------------------------------------------------ @Override public CalendarQuarter 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 CalendarQuarter.of(date.getYear(), date.get(QUARTER_OF_YEAR)); } @Override public CalendarQuarter 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 CalendarQuarter createFrom( ChronoEntity<?> entity, AttributeQuery attributes, boolean lenient, boolean preparsing ) { int y = entity.getInt(YEAR); if ( (y >= GregorianMath.MIN_YEAR) && (y <= GregorianMath.MAX_YEAR) && entity.contains(QUARTER_OF_YEAR) ) { return CalendarQuarter.of(y, entity.get(QUARTER_OF_YEAR)); } else if (y > Integer.MIN_VALUE) { entity.with(ValidationElement.ERROR_MESSAGE, "Year out of bounds: " + y); } return null; } @Override public String getFormatPattern( DisplayStyle style, Locale locale ) { Map<String, String> map = CalendarText.getIsoInstance(locale).getTextForms(); String key = null; switch (style.getStyleValue()) { case DateFormat.FULL: key = "F_yQQQQ"; break; case DateFormat.LONG: key = "F_yQQQ"; break; case DateFormat.MEDIUM: key = "F_yQQ"; break; case DateFormat.SHORT: key = "F_yQ"; break; } String pattern = getFormatPattern(map, key); return ((pattern == null) ? "uuuu-'Q'Q" : pattern); } private static String getFormatPattern( Map<String, String> map, String key ) { if (map.containsKey(key)) { return map.get(key); } switch (key) { case "F_yQQQQ": return getFormatPattern(map, "F_yQQQ"); case "F_yQQQ": return getFormatPattern(map, "F_yQQ"); case "F_yQQ": return getFormatPattern(map, "F_yQ"); default: return null; } } } private static class YearRule implements IntElementRule<CalendarQuarter> { //~ Methoden ------------------------------------------------------ @Override public Integer getValue(CalendarQuarter context) { return Integer.valueOf(context.year); } @Override public Integer getMinimum(CalendarQuarter context) { return Integer.valueOf(GregorianMath.MIN_YEAR); } @Override public Integer getMaximum(CalendarQuarter context) { return Integer.valueOf(GregorianMath.MAX_YEAR); } @Override public boolean isValid( CalendarQuarter context, Integer value ) { if (value == null) { return false; } int v = value.intValue(); return ((v >= GregorianMath.MIN_YEAR) && (v <= GregorianMath.MAX_YEAR)); } @Override public CalendarQuarter withValue( CalendarQuarter context, Integer value, boolean lenient ) { if (this.isValid(context, value)) { return CalendarQuarter.of(value.intValue(), context.quarter); } else { throw new IllegalArgumentException("Not valid: " + value); } } @Override public ChronoElement<?> getChildAtFloor(CalendarQuarter context) { return QUARTER_OF_YEAR; } @Override public ChronoElement<?> getChildAtCeiling(CalendarQuarter context) { return QUARTER_OF_YEAR; } @Override public int getInt(CalendarQuarter context) { return context.year; } @Override public boolean isValid( CalendarQuarter context, int value ) { return ((value >= GregorianMath.MIN_YEAR) && (value <= GregorianMath.MAX_YEAR)); } @Override public CalendarQuarter withValue( CalendarQuarter context, int value, boolean lenient ) { if (this.isValid(context, value)) { return CalendarQuarter.of(value, context.quarter); } else { throw new IllegalArgumentException("Not valid: " + value); } } } private static class QuarterRule implements ElementRule<CalendarQuarter, Quarter> { //~ Methoden ------------------------------------------------------ @Override public Quarter getValue(CalendarQuarter context) { return context.quarter; } @Override public Quarter getMinimum(CalendarQuarter context) { return Quarter.Q1; } @Override public Quarter getMaximum(CalendarQuarter context) { return Quarter.Q4; } @Override public boolean isValid( CalendarQuarter context, Quarter value ) { return (value != null); } @Override public CalendarQuarter withValue( CalendarQuarter context, Quarter value, boolean lenient ) { if (this.isValid(context, value)) { return CalendarQuarter.of(context.year, value); } else { throw new IllegalArgumentException("Not valid: " + value); } } @Override public ChronoElement<?> getChildAtFloor(CalendarQuarter context) { return null; } @Override public ChronoElement<?> getChildAtCeiling(CalendarQuarter context) { return null; } } private class Iter implements Iterator<PlainDate> { //~ Instanzvariablen ---------------------------------------------- private PlainDate current = CalendarQuarter.this.start.getTemporal(); //~ Methoden ------------------------------------------------------ @Override public boolean hasNext() { return (this.current != null); } @Override public PlainDate next() { if (this.current == null) { throw new NoSuchElementException(); } else { PlainDate result = this.current; PlainDate next = result.plus(1, CalendarUnit.DAYS); this.current = ((next.isAfter(CalendarQuarter.this.end.getTemporal())) ? null : next); return result; } } @Override public void remove() { throw new UnsupportedOperationException(); } } }