net.time4j.ZonalDateTime Maven / Gradle / Ivy
/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild,
* -----------------------------------------------------------------------
* This file (ZonalDateTime.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 .
* -----------------------------------------------------------------------
*/
package net.time4j;
import net.time4j.engine.ChronoDisplay;
import net.time4j.engine.ChronoElement;
import net.time4j.engine.ThreetenAdapter;
import net.time4j.format.RawValues;
import net.time4j.format.TemporalFormatter;
import net.time4j.scale.TimeScale;
import net.time4j.scale.UniversalTime;
import net.time4j.tz.TZID;
import net.time4j.tz.Timezone;
import net.time4j.tz.ZonalOffset;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.text.ParseException;
import java.text.ParsePosition;
import java.time.ZonedDateTime;
import static net.time4j.PlainTime.SECOND_OF_MINUTE;
import static net.time4j.format.Attributes.TIMEZONE_ID;
/**
* Combination of UTC-moment and timezone.
*
* An instance can be created by {@code Moment.inLocalView()} or
* {@code Moment.inZonalView(...)}. This type mainly serves for various
* type conversions and incorporates a valid local timestamp as well as an
* universal time in UTC. If users wish to apply any kind of data
* manipulation then an object of this type has first to be converted
* to a local timestamp or to a global UTC-moment. Example:
*
*
* Moment moment = ...;
* ZonalDateTime zdt = moment.inLocalView();
*
* // manipulation on local timeline
* PlainTimestamp localTSP = zdt.toTimestamp().plus(30, ClockUnit.SECONDS);
*
* // manipulation on global timeline
* Moment globalTSP = zdt.toMoment().plus(30, SI.SECONDS);
*
*
* This class supports all elements which are supported by {@link Moment}
* and {@link PlainTimestamp}, too.
*
* @author Meno Hochschild
* @since 2.0
* @doctags.concurrency {immutable}
* @see Moment#inLocalView()
* @see Moment#inZonalView(TZID)
* @see Moment#inZonalView(String)
*/
/*[deutsch]
* Kombination aus UTC-Moment und Zeitzone.
*
* Eine Instanz kann mit Hilfe von {@code Moment.inLocalView()} oder
* {@code Moment.inZonalView(...)} erzeugt werden. Dieser Typ dient vorwiegend
* der Typkonversion und verkörpert sowohl einen gültigen lokalen
* Zeitstempel als auch eine Universalzeit in UTC. Wenn Anwender irgendeine
* Art von Datenmanipulation anwenden möchten, dann muß ein
* Objekt dieses Typs zuerst in einen lokalen Zeitstempel oder einen
* globalen UTC-Moment umgewandelt werden. Beispiel:
*
*
* Moment moment = ...;
* ZonalDateTime zdt = moment.inLocalView();
*
* // manipulation on local timeline
* PlainTimestamp localTSP = zdt.toTimestamp().plus(30, ClockUnit.SECONDS);
*
* // manipulation on global timeline
* Moment globalTSP = zdt.toMoment().plus(30, SI.SECONDS);
*
*
* Diese Klasse unterstützt alle Elemente, die auch von {@link Moment}
* und {@link PlainTimestamp} unterstützt werden.
*
* @author Meno Hochschild
* @since 2.0
* @doctags.concurrency {immutable}
* @see Moment#inLocalView()
* @see Moment#inZonalView(TZID)
* @see Moment#inZonalView(String)
*/
public final class ZonalDateTime
implements ChronoDisplay, UniversalTime, ThreetenAdapter {
//~ Instanzvariablen --------------------------------------------------
private final Moment moment;
private final Timezone zone;
private transient final PlainTimestamp timestamp;
//~ Konstruktoren -----------------------------------------------------
private ZonalDateTime(
Moment moment,
Timezone tz
) {
super();
this.zone = tz;
ZonalOffset offset = tz.getOffset(moment);
if (moment.isLeapSecond()) {
if (
(offset.getFractionalAmount() != 0)
|| ((offset.getAbsoluteSeconds() % 60) != 0)
) {
throw new IllegalArgumentException(
"Leap second can only be represented "
+ " with timezone-offset in full minutes: "
+ offset);
}
}
this.moment = moment;
this.timestamp = PlainTimestamp.from(moment, offset);
}
private ZonalDateTime(
PlainTimestamp tsp,
ZonalOffset offset
) {
super();
this.moment = tsp.at(offset);
this.zone = Timezone.of(offset);
this.timestamp = tsp;
}
//~ Methoden ----------------------------------------------------------
/**
* Erzeugt einen zonalen Moment.
*
* @param moment global timestamp
* @param tz timezone
* @return ZonalDateTime
* @throws IllegalArgumentException if leapsecond shall be formatted
* with non-full-minute-timezone-offset
*/
static ZonalDateTime of(
Moment moment,
Timezone tz
) {
return new ZonalDateTime(moment, tz);
}
/**
* Erzeugt einen zonalen Moment.
*
* @param tsp zonal timestamp
* @param offset timezone offset
* @return ZonalDateTime
*/
static ZonalDateTime of(
PlainTimestamp tsp,
ZonalOffset offset
) {
return new ZonalDateTime(tsp, offset);
}
/**
* Compares this instance with another instance on the global timeline (UTC).
*
* If the UTC-times are equal then and only then the local timestamps will be taken into account. Example:
*
*
* List<String> dates =
* Arrays.asList("Tue, 29 Feb 2016 17:45:00 CET", "Tue, 29 Feb 2016 16:00:00 EST");
* TemporalFormatter<Moment> formatter =
* Moment.formatter(
* "EEE, dd MMM yyyy HH:mm:ss z", PatternType.CLDR, Locale.ENGLISH, ZonalOffset.UTC);
* ZonalDateTime maxDate =
* dates.stream()
* .map(s -> ZonalDateTime.parse(s, formatter, new ParsePosition(0)))
* .max(ZonalDateTime::compareByMoment)
* .get();
* System.out.println(maxDate); // 2016-02-29T16UTC-05:00[America/New_York]
*
*
* @param zdt other instance to be compared with
* @return negative, zero or positive integer if this instance is earlier, simultaneous or later than given arg
* @see #compareByLocalTimestamp(ZonalDateTime)
* @since 3.16/4.13
*/
/*[deutsch]
* Vergleicht diese Instanz mit der angegebenen Instanz auf der globalen Zeitachse (UTC).
*
* Die lokalen Zeitstempel werden genau dann in Betracht gezogen, wenn die UTC-Zeitpunkte gleich sind.
* Beispiel:
*
*
* List<String> dates =
* Arrays.asList("Tue, 29 Feb 2016 17:45:00 CET", "Tue, 29 Feb 2016 16:00:00 EST");
* TemporalFormatter<Moment> formatter =
* Moment.formatter(
* "EEE, dd MMM yyyy HH:mm:ss z", PatternType.CLDR, Locale.ENGLISH, ZonalOffset.UTC);
* ZonalDateTime maxDate =
* dates.stream()
* .map(s -> ZonalDateTime.parse(s, formatter, new ParsePosition(0)))
* .max(ZonalDateTime::compareByMoment)
* .get();
* System.out.println(maxDate); // 2016-02-29T16UTC-05:00[America/New_York]
*
*
* @param zdt other instance to be compared with
* @return negative, zero or positive integer if this instance is earlier, simultaneous or later than given arg
* @see #compareByLocalTimestamp(ZonalDateTime)
* @since 3.16/4.13
*/
public int compareByMoment(ZonalDateTime zdt) {
int cmp = this.moment.compareTo(zdt.moment);
if (cmp == 0) {
cmp = this.timestamp.compareTo(zdt.timestamp);
}
return cmp;
}
/**
* Compares this instance with another instance on the local timeline.
*
* If the local timestamps are equal then and only then the UTC-times will be taken into account.
*
* @param zdt other instance to be compared with
* @return negative, zero or positive integer if this instance is earlier, simultaneous or later than given arg
* @see #compareByMoment(ZonalDateTime)
* @since 3.16/4.13
*/
/*[deutsch]
* Vergleicht diese Instanz mit der angegebenen Instanz auf der lokalen Zeitachse.
*
* Die UTC-Zeiten werden genau dann in Betracht gezogen, wenn die lokalen Zeitstempel gleich sind.
*
* @param zdt other instance to be compared with
* @return negative, zero or positive integer if this instance is earlier, simultaneous or later than given arg
* @see #compareByMoment(ZonalDateTime)
* @since 3.16/4.13
*/
public int compareByLocalTimestamp(ZonalDateTime zdt) {
int cmp = this.timestamp.compareTo(zdt.timestamp);
if (cmp == 0) {
cmp = this.moment.compareTo(zdt.moment);
}
return cmp;
}
@Override
public boolean contains(ChronoElement> element) {
return (
this.timestamp.contains(element)
|| this.moment.contains(element)
);
}
@Override
public V get(ChronoElement element) {
if (
this.moment.isLeapSecond()
&& (element == SECOND_OF_MINUTE)
) {
return element.getType().cast(Integer.valueOf(60));
}
if (this.timestamp.contains(element)) {
return this.timestamp.get(element);
} else {
return this.moment.get(element);
}
}
@Override
public int getInt(ChronoElement element) {
if (this.moment.isLeapSecond() && (element == SECOND_OF_MINUTE)) {
return 60;
}
int value = this.timestamp.getInt(element);
if (value == Integer.MIN_VALUE) {
value = this.moment.getInt(element);
}
return value;
}
// benutzt in ChronoFormatter/FractionProcessor
@Override
public V getMinimum(ChronoElement element) {
if (this.timestamp.contains(element)) {
return this.timestamp.getMinimum(element);
} else {
return this.moment.getMinimum(element);
}
}
// benutzt in ChronoFormatter/FractionProcessor
@Override
public V getMaximum(ChronoElement element) {
V max;
if (this.timestamp.contains(element)) {
max = this.timestamp.getMaximum(element);
} else {
max = this.moment.getMaximum(element);
}
if (
(element == SECOND_OF_MINUTE)
&& (this.timestamp.getYear() >= 1972)
) {
PlainTimestamp ts = this.timestamp.with(element, max);
if (!this.zone.isInvalid(ts, ts)) {
Moment transformed = ts.in(this.zone);
Moment test = transformed.plus(1, SI.SECONDS);
if (test.isLeapSecond()) {
return element.getType().cast(Integer.valueOf(60));
}
}
}
return max;
}
/**
* This object always has a timezone.
*
* @return {@code true}
*/
/*[deutsch]
* Dieses Objekt hat immer eine Zeitzone.
*
* @return {@code true}
*/
@Override
public boolean hasTimezone() {
return true;
}
@Override
public TZID getTimezone() {
return this.zone.getID();
}
/**
* Yields the timezone offset.
*
* @return offset relative to UTC+00:00
* @since 2.0
*/
/*[deutsch]
* Liefert den Zeitzonen-Offset.
*
* @return offset relative to UTC+00:00
* @since 2.0
*/
public ZonalOffset getOffset() {
return this.zone.getOffset(this.moment);
}
/**
* Converts this object to a global UTC-moment.
*
* @return Moment
*/
/*[deutsch]
* Konvertiert dieses Objekt zu einem globalen UTC-Moment.
*
* @return Moment
*/
public Moment toMoment() {
return this.moment;
}
/**
* Converts this object to a zonal timestamp.
*
* @return PlainTimestamp
*/
/*[deutsch]
* Konvertiert dieses Objekt zu einem zonalen Zeitstempel.
*
* @return PlainTimestamp
*/
public PlainTimestamp toTimestamp() {
return this.timestamp;
}
/**
* Short cut for {@code TemporalType.ZONED_DATE_TIME.translate(zdt)}.
*
* @param zdt Threeten-equivalent of a zonal date-time
* @return ZonalDateTime
* @since 4.0
* @see TemporalType#ZONED_DATE_TIME
*/
/*[deutsch]
* Abkürzung für {@code TemporalType.ZONED_DATE_TIME.translate(zdt)}.
*
* @param zdt Threeten-equivalent of a zonal date-time
* @return ZonalDateTime
* @since 4.0
* @see TemporalType#ZONED_DATE_TIME
*/
public static ZonalDateTime from(ZonedDateTime zdt) {
return TemporalType.ZONED_DATE_TIME.translate(zdt);
}
@Override
public long getElapsedTime(TimeScale scale) {
return this.moment.getElapsedTime(scale);
}
@Override
public int getNanosecond(TimeScale scale) {
return this.moment.getNanosecond(scale);
}
@Override
public boolean isLeapSecond() {
return this.moment.isLeapSecond();
}
@Override
public long getPosixTime() {
return this.moment.getPosixTime();
}
@Override
public int getNanosecond() {
return this.moment.getNanosecond();
}
/**
* Creates a formatted output of this instance.
*
* @param printer helps to format this instance
* @return formatted string
* @since 3.0
*/
/*[deutsch]
* Erzeugt eine formatierte Ausgabe dieser Instanz.
*
* @param printer helps to format this instance
* @return formatted string
* @since 3.0
*/
public String print(TemporalFormatter printer) {
return printer.withTimezone(this.getTimezone()).format(this.moment);
}
/**
* Parses given text to a {@code ZonalDateTime}.
*
* @param text text to be parsed
* @param parser helps to parse given text
* @return parsed result
* @throws IndexOutOfBoundsException if the text is empty
* @throws ParseException if the text is not parseable
* @since 3.0
*/
/*[deutsch]
* Interpretiert den angegebenen Text als {@code ZonalDateTime}.
*
* @param text text to be parsed
* @param parser helps to parse given text
* @return parsed result
* @throws IndexOutOfBoundsException if the text is empty
* @throws ParseException if the text is not parseable
* @since 3.0
*/
public static ZonalDateTime parse(
String text,
TemporalFormatter parser
) throws ParseException {
ParsePosition pos = new ParsePosition(0);
RawValues rawValues = new RawValues();
Moment moment = parser.parse(text, pos, rawValues);
Timezone tz;
if (moment == null) {
moment = parser.parse(text); // will throw an exception with better error message
}
if (moment == null) {
throw new ParseException("Cannot parse: " + text, pos.getErrorIndex());
} else if (rawValues.get().hasTimezone()) {
tz = toTimezone(rawValues.get().getTimezone(), text);
} else if (parser.getAttributes().contains(TIMEZONE_ID)) {
tz = toTimezone(parser.getAttributes().get(TIMEZONE_ID), text);
} else {
throw new ParseException("Missing timezone: " + text, 0);
}
return ZonalDateTime.of(moment, tz);
}
/**
* Parses given text to a {@code ZonalDateTime}.
*
* Note: This method can be used in lambda expressions because it avoids checked exceptions.
*
* @param text text to be parsed
* @param parser helps to parse given text
* @param position parse position (always as new instance)
* @return parsed result or {@code null} if parsing does not work (for example missing timezone information)
* @throws IndexOutOfBoundsException if the text is empty
* @throws IllegalArgumentException if timezone data cannot be loaded
* @since 3.16/4.13
*/
/*[deutsch]
* Interpretiert den angegebenen Text als {@code ZonalDateTime}.
*
* Hinweis: Diese Methode kann in Lambda-Ausdrücken verwendet werden, weil sie checked exceptions
* vermeidet.
*
* @param text text to be parsed
* @param parser helps to parse given text
* @param position parse position (always as new instance)
* @return parsed result or {@code null} if parsing does not work (for example missing timezone information)
* @throws IndexOutOfBoundsException if the text is empty
* @throws IllegalArgumentException if timezone data cannot be loaded
* @since 3.16/4.13
*/
public static ZonalDateTime parse(
String text,
TemporalFormatter parser,
ParsePosition position
) {
RawValues rawValues = new RawValues();
Moment moment = parser.parse(text, position, rawValues);
Timezone tz;
if (moment == null) {
return null;
} else if (rawValues.get().hasTimezone()) {
tz = Timezone.of(rawValues.get().getTimezone());
} else if (parser.getAttributes().contains(TIMEZONE_ID)) {
tz = Timezone.of(parser.getAttributes().get(TIMEZONE_ID));
} else {
position.setErrorIndex(0);
return null;
}
return ZonalDateTime.of(moment, tz);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof ZonalDateTime) {
ZonalDateTime that = (ZonalDateTime) obj;
return (
this.moment.equals(that.moment)
&& this.zone.equals(that.zone)
);
} else {
return false;
}
}
@Override
public int hashCode() {
return (this.moment.hashCode() ^ this.zone.hashCode());
}
/**
* Yields a canonical representation in ISO-like-style.
*
* @return String
*/
/*[deutsch]
* Liefert eine kanonische Darstellung ähnlich zu ISO-8601.
*
* @return String
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder(40);
sb.append(this.timestamp);
sb.append(this.getOffset().canonical());
TZID tzid = this.getTimezone();
boolean offset = (tzid instanceof ZonalOffset);
if (!offset) {
sb.append('[');
sb.append(tzid.canonical());
sb.append(']');
}
return sb.toString();
}
@Override
public ZonedDateTime toTemporalAccessor() {
return TemporalType.ZONED_DATE_TIME.from(this);
}
/**
* Writes this instance to given output (serialization).
*
* Warning: Serializing this instance is a heavy-weight-operation because the
* whole relevant timezone data will be written to given stream, not only the timezone-id.
*
* @param output object output
* @throws IOException if writing fails
* @since 3.1
*/
/*[deutsch]
* Schreibt diese Instanz in den angegebenen Ausgabestrom (Serialisierung).
*
* Warnung: Die Serialisierung dieser Instanz ist schwergewichtig, weil alle
* relevanten Zeitzonendaten komplett geschrieben werden, nicht nur die Zeitzonen-ID.
*
* @param output object output
* @throws IOException if writing fails
* @since 3.1
*/
public void write(ObjectOutput output) throws IOException {
output.writeObject(this.moment);
output.writeObject(this.zone);
}
/**
* This is the reverse operation of {@link #write(ObjectOutput)}.
*
* @param input object input
* @return reconstructed instance of serialized {@code ZonalDateTime}
* @throws IOException if reading fails
* @throws ClassNotFoundException if class-loading fails
* @throws IllegalArgumentException in case of inconsistent data
* @since 3.1
*/
/*[deutsch]
* Das ist die Umkehroperation zu {@link #write(ObjectOutput)}.
*
* @param input object input
* @return reconstructed instance of serialized {@code ZonalDateTime}
* @throws IOException if reading fails
* @throws ClassNotFoundException if class-loading fails
* @throws IllegalArgumentException in case of inconsistent data
* @since 3.1
*/
public static ZonalDateTime read(ObjectInput input) throws IOException, ClassNotFoundException {
Moment moment = (Moment) input.readObject();
Timezone tz = (Timezone) input.readObject();
return new ZonalDateTime(moment, tz);
}
private static Timezone toTimezone(
TZID tzid,
String text
) throws ParseException {
try {
return Timezone.of(tzid);
} catch (IllegalArgumentException iae) {
ParseException pe =
new ParseException("Timezone error: " + text, 0);
pe.initCause(iae);
throw pe;
}
}
}