packages/core/src/format/DateTimeBuilder.js
/*
* @copyright (c) 2016, Philipp Thürwächter & Pattrick Hüper
* @copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
* @license BSD-3-Clause (see LICENSE in the root directory of this source tree)
*/
import { requireNonNull } from '../assert';
import { DateTimeException } from '../errors';
import { MathUtil } from '../MathUtil';
import { EnumMap } from './EnumMap';
import { ResolverStyle } from './ResolverStyle';
import { IsoChronology } from '../chrono/IsoChronology';
import { ChronoLocalDate } from '../chrono/ChronoLocalDate';
import { ChronoField } from '../temporal/ChronoField';
import { TemporalAccessor } from '../temporal/TemporalAccessor';
import { TemporalQueries } from '../temporal/TemporalQueries';
import { LocalTime } from '../LocalTime';
import { LocalDate } from '../LocalDate';
import { Period } from '../Period';
import { ZoneOffset } from '../ZoneOffset';
/**
* Builder that can holds date and time fields and related date and time objects.
*
* The builder is used to hold onto different elements of date and time.
* It is designed as two separate maps:
*
* * from {@link TemporalField} to `long` value, where the value may be
* outside the valid range for the field
* * from {@link Class} to {@link TemporalAccessor}, holding larger scale objects
* like {@link LocalDateTime}.
*
* @private
*/
export class DateTimeBuilder extends TemporalAccessor {
/**
* Creates a new instance of the builder with a single field-value.
*
* This is equivalent to using {@link addFieldValue} on an empty builder.
*
* @param {TemporalField} field - the field to add, not null
* @param {number} value - the value to add, not null
* @return {DateTimeBuilder}
*/
static create(field, value) {
const dtb = new DateTimeBuilder();
dtb._addFieldValue(field, value);
return dtb;
}
constructor(){
super();
/**
* The map of other fields.
*/
this.fieldValues = new EnumMap();
/**
* The chronology.
*/
this.chrono = null;
/**
* The zone.
*/
this.zone = null;
/**
* The date.
*/
this.date = null;
/**
* The time.
*/
this.time = null;
/**
* The leap second flag.
*/
this.leapSecond = false;
/**
* The excess days.
*/
this.excessDays = null;
}
/**
*
* @param {TemporalField} field
* @return {Number} field value
*/
getFieldValue0(field) {
return this.fieldValues.get(field);
}
/**
* Adds a field-value pair to the builder.
*
* This adds a field to the builder.
* If the field is not already present, then the field-value pair is added to the map.
* If the field is already present and it has the same value as that specified, no action occurs.
* If the field is already present and it has a different value to that specified, then
* an exception is thrown.
*
* @param {TemporalField} field - the field to add, not null
* @param {Number} value - the value to add, not null
* @return {DateTimeBuilder}, this for method chaining
* @throws DateTimeException if the field is already present with a different value
*/
_addFieldValue(field, value) {
requireNonNull(field, 'field');
const old = this.getFieldValue0(field); // check first for better error message
if (old != null && old !== value) {
throw new DateTimeException(`Conflict found: ${field} ${old} differs from ${field} ${value}: ${this}`);
}
return this._putFieldValue0(field, value);
}
/**
* @param {TemporalField} field
* @param {Number} value
* @return {DateTimeBuilder}, this for method chaining
*/
_putFieldValue0(field, value) {
this.fieldValues.put(field, value);
return this;
}
/**
* Resolves the builder, evaluating the date and time.
*
* This examines the contents of the build.er and resolves it to produce the best
* available date and time, throwing an exception if a problem occurs.
* Calling this method changes the state of the builder.
*
* @param {ResolverStyle} resolverStyle - how to resolve
* @param {TemporalField[]} resolverFields
* @return {DateTimeBuilder} this, for method chaining
*/
resolve(resolverStyle, resolverFields) {
if (resolverFields != null) {
this.fieldValues.retainAll(resolverFields);
}
// handle standard fields
// this._mergeInstantFields();
this._mergeDate(resolverStyle);
this._mergeTime(resolverStyle);
//if (resolveFields(resolverStyle)) {
// mergeInstantFields();
// mergeDate(resolverStyle);
// mergeTime(resolverStyle);
//}
this._resolveTimeInferZeroes(resolverStyle);
//this._crossCheck();
if (this.excessDays != null && this.excessDays.isZero() === false && this.date != null && this.time != null) {
this.date = this.date.plus(this.excessDays);
this.excessDays = Period.ZERO;
}
//resolveFractional();
this._resolveInstant();
return this;
}
/**
*
* @param {ResolverStyle} resolverStyle
* @private
*/
_mergeDate(resolverStyle) {
//if (this.chrono instanceof IsoChronology) {
this._checkDate(IsoChronology.INSTANCE.resolveDate(this.fieldValues, resolverStyle));
//} else {
// if (this.fieldValues.containsKey(ChronoField.EPOCH_DAY)) {
// this._checkDate(LocalDate.ofEpochDay(this.fieldValues.remove(ChronoField.EPOCH_DAY)));
// return;
// }
//}
}
/**
*
* @param {LocalDate} date
* @private
*/
_checkDate(date) {
if (date != null) {
this._addObject(date);
for (const fieldName in this.fieldValues.keySet()) {
const field = ChronoField.byName(fieldName);
if (field) {
if (this.fieldValues.get(field) !== undefined) { // undefined if "removed" in EnumMap
if (field.isDateBased()) {
let val1;
try {
val1 = date.getLong(field);
} catch (ex) {
if (ex instanceof DateTimeException) {
continue;
} else {
throw ex;
}
}
const val2 = this.fieldValues.get(field);
if (val1 !== val2) {
throw new DateTimeException(`Conflict found: Field ${field} ${val1} differs from ${field} ${val2} derived from ${date}`);
}
}
}
}
}
}
}
/**
*
* @param {ResolverStyle} resolverStyle
* @private
*/
_mergeTime(resolverStyle) {
if (this.fieldValues.containsKey(ChronoField.CLOCK_HOUR_OF_DAY)) {
const ch = this.fieldValues.remove(ChronoField.CLOCK_HOUR_OF_DAY);
if (resolverStyle !== ResolverStyle.LENIENT) {
if (resolverStyle === ResolverStyle.SMART && ch === 0) {
// ok
} else {
ChronoField.CLOCK_HOUR_OF_DAY.checkValidValue(ch);
}
}
this._addFieldValue(ChronoField.HOUR_OF_DAY, ch === 24 ? 0 : ch);
}
if (this.fieldValues.containsKey(ChronoField.CLOCK_HOUR_OF_AMPM)) {
const ch = this.fieldValues.remove(ChronoField.CLOCK_HOUR_OF_AMPM);
if (resolverStyle !== ResolverStyle.LENIENT) {
if (resolverStyle === ResolverStyle.SMART && ch === 0) {
// ok
} else {
ChronoField.CLOCK_HOUR_OF_AMPM.checkValidValue(ch);
}
}
this._addFieldValue(ChronoField.HOUR_OF_AMPM, ch === 12 ? 0 : ch);
}
if (resolverStyle !== ResolverStyle.LENIENT) {
if (this.fieldValues.containsKey(ChronoField.AMPM_OF_DAY)) {
ChronoField.AMPM_OF_DAY.checkValidValue(this.fieldValues.get(ChronoField.AMPM_OF_DAY));
}
if (this.fieldValues.containsKey(ChronoField.HOUR_OF_AMPM)) {
ChronoField.HOUR_OF_AMPM.checkValidValue(this.fieldValues.get(ChronoField.HOUR_OF_AMPM));
}
}
if (this.fieldValues.containsKey(ChronoField.AMPM_OF_DAY) && this.fieldValues.containsKey(ChronoField.HOUR_OF_AMPM)) {
const ap = this.fieldValues.remove(ChronoField.AMPM_OF_DAY);
const hap = this.fieldValues.remove(ChronoField.HOUR_OF_AMPM);
this._addFieldValue(ChronoField.HOUR_OF_DAY, ap * 12 + hap);
}
// if (timeFields.containsKey(HOUR_OF_DAY) && timeFields.containsKey(MINUTE_OF_HOUR)) {
// const hod = timeFields.remove(HOUR_OF_DAY);
// const moh = timeFields.remove(MINUTE_OF_HOUR);
// this._addFieldValue(MINUTE_OF_DAY, hod * 60 + moh);
// }
// if (timeFields.containsKey(MINUTE_OF_DAY) && timeFields.containsKey(SECOND_OF_MINUTE)) {
// const mod = timeFields.remove(MINUTE_OF_DAY);
// const som = timeFields.remove(SECOND_OF_MINUTE);
// this._addFieldValue(SECOND_OF_DAY, mod * 60 + som);
// }
if (this.fieldValues.containsKey(ChronoField.NANO_OF_DAY)) {
const nod = this.fieldValues.remove(ChronoField.NANO_OF_DAY);
if (resolverStyle !== ResolverStyle.LENIENT) {
ChronoField.NANO_OF_DAY.checkValidValue(nod);
}
this._addFieldValue(ChronoField.SECOND_OF_DAY, MathUtil.intDiv(nod, 1000000000));
this._addFieldValue(ChronoField.NANO_OF_SECOND, MathUtil.intMod(nod, 1000000000));
}
if (this.fieldValues.containsKey(ChronoField.MICRO_OF_DAY)) {
const cod = this.fieldValues.remove(ChronoField.MICRO_OF_DAY);
if (resolverStyle !== ResolverStyle.LENIENT) {
ChronoField.MICRO_OF_DAY.checkValidValue(cod);
}
this._addFieldValue(ChronoField.SECOND_OF_DAY, MathUtil.intDiv(cod, 1000000));
this._addFieldValue(ChronoField.MICRO_OF_SECOND, MathUtil.intMod(cod, 1000000));
}
if (this.fieldValues.containsKey(ChronoField.MILLI_OF_DAY)) {
const lod = this.fieldValues.remove(ChronoField.MILLI_OF_DAY);
if (resolverStyle !== ResolverStyle.LENIENT) {
ChronoField.MILLI_OF_DAY.checkValidValue(lod);
}
this._addFieldValue(ChronoField.SECOND_OF_DAY, MathUtil.intDiv(lod, 1000));
this._addFieldValue(ChronoField.MILLI_OF_SECOND, MathUtil.intMod(lod, 1000));
}
if (this.fieldValues.containsKey(ChronoField.SECOND_OF_DAY)) {
const sod = this.fieldValues.remove(ChronoField.SECOND_OF_DAY);
if (resolverStyle !== ResolverStyle.LENIENT) {
ChronoField.SECOND_OF_DAY.checkValidValue(sod);
}
this._addFieldValue(ChronoField.HOUR_OF_DAY, MathUtil.intDiv(sod, 3600));
this._addFieldValue(ChronoField.MINUTE_OF_HOUR, MathUtil.intMod(MathUtil.intDiv(sod, 60), 60));
this._addFieldValue(ChronoField.SECOND_OF_MINUTE, MathUtil.intMod(sod, 60));
}
if (this.fieldValues.containsKey(ChronoField.MINUTE_OF_DAY)) {
const mod = this.fieldValues.remove(ChronoField.MINUTE_OF_DAY);
if (resolverStyle !== ResolverStyle.LENIENT) {
ChronoField.MINUTE_OF_DAY.checkValidValue(mod);
}
this._addFieldValue(ChronoField.HOUR_OF_DAY, MathUtil.intDiv(mod, 60));
this._addFieldValue(ChronoField.MINUTE_OF_HOUR, MathUtil.intMod(mod, 60));
}
// const sod = MathUtil.intDiv(nod, 1000000000L);
// this._addFieldValue(HOUR_OF_DAY, MathUtil.intDiv(sod, 3600));
// this._addFieldValue(MINUTE_OF_HOUR, MathUtil.intMod(MathUtil.intDiv(sod, 60), 60));
// this._addFieldValue(SECOND_OF_MINUTE, MathUtil.intMod(sod, 60));
// this._addFieldValue(NANO_OF_SECOND, MathUtil.intMod(nod, 1000000000L));
if (resolverStyle !== ResolverStyle.LENIENT) {
if (this.fieldValues.containsKey(ChronoField.MILLI_OF_SECOND)) {
ChronoField.MILLI_OF_SECOND.checkValidValue(this.fieldValues.get(ChronoField.MILLI_OF_SECOND));
}
if (this.fieldValues.containsKey(ChronoField.MICRO_OF_SECOND)) {
ChronoField.MICRO_OF_SECOND.checkValidValue(this.fieldValues.get(ChronoField.MICRO_OF_SECOND));
}
}
if (this.fieldValues.containsKey(ChronoField.MILLI_OF_SECOND) && this.fieldValues.containsKey(ChronoField.MICRO_OF_SECOND)) {
const los = this.fieldValues.remove(ChronoField.MILLI_OF_SECOND);
const cos = this.fieldValues.get(ChronoField.MICRO_OF_SECOND);
this._putFieldValue0(ChronoField.MICRO_OF_SECOND, los * 1000 + (MathUtil.intMod(cos, 1000)));
}
if (this.fieldValues.containsKey(ChronoField.MICRO_OF_SECOND) && this.fieldValues.containsKey(ChronoField.NANO_OF_SECOND)) {
const nos = this.fieldValues.get(ChronoField.NANO_OF_SECOND);
this._putFieldValue0(ChronoField.MICRO_OF_SECOND, MathUtil.intDiv(nos, 1000));
this.fieldValues.remove(ChronoField.MICRO_OF_SECOND);
}
if (this.fieldValues.containsKey(ChronoField.MILLI_OF_SECOND) && this.fieldValues.containsKey(ChronoField.NANO_OF_SECOND)) {
const nos = this.fieldValues.get(ChronoField.NANO_OF_SECOND);
this._putFieldValue0(ChronoField.MILLI_OF_SECOND, MathUtil.intDiv(nos, 1000000));
this.fieldValues.remove(ChronoField.MILLI_OF_SECOND);
}
if (this.fieldValues.containsKey(ChronoField.MICRO_OF_SECOND)) {
const cos = this.fieldValues.remove(ChronoField.MICRO_OF_SECOND);
this._putFieldValue0(ChronoField.NANO_OF_SECOND, cos * 1000);
} else if (this.fieldValues.containsKey(ChronoField.MILLI_OF_SECOND)) {
const los = this.fieldValues.remove(ChronoField.MILLI_OF_SECOND);
this._putFieldValue0(ChronoField.NANO_OF_SECOND, los * 1000000);
}
}
/**
*
* @param {ResolverStyle} resolverStyle
* @private
*/
_resolveTimeInferZeroes(resolverStyle) {
let hod = this.fieldValues.get(ChronoField.HOUR_OF_DAY);
const moh = this.fieldValues.get(ChronoField.MINUTE_OF_HOUR);
const som = this.fieldValues.get(ChronoField.SECOND_OF_MINUTE);
let nos = this.fieldValues.get(ChronoField.NANO_OF_SECOND);
if (hod == null) {
return;
}
if (moh == null && (som != null || nos != null)) {
return;
}
if (moh != null && som == null && nos != null) {
return;
}
if (resolverStyle !== ResolverStyle.LENIENT) {
if (hod != null) {
if (resolverStyle === ResolverStyle.SMART &&
hod === 24 &&
(moh == null || moh === 0) &&
(som == null || som === 0) &&
(nos == null || nos === 0)) {
hod = 0;
this.excessDays = Period.ofDays(1);
}
const hodVal = ChronoField.HOUR_OF_DAY.checkValidIntValue(hod);
if (moh != null) {
const mohVal = ChronoField.MINUTE_OF_HOUR.checkValidIntValue(moh);
if (som != null) {
const somVal = ChronoField.SECOND_OF_MINUTE.checkValidIntValue(som);
if (nos != null) {
const nosVal = ChronoField.NANO_OF_SECOND.checkValidIntValue(nos);
this._addObject(LocalTime.of(hodVal, mohVal, somVal, nosVal));
} else {
this._addObject(LocalTime.of(hodVal, mohVal, somVal));
}
} else {
if (nos == null) {
this._addObject(LocalTime.of(hodVal, mohVal));
}
}
} else {
if (som == null && nos == null) {
this._addObject(LocalTime.of(hodVal, 0));
}
}
}
} else {
if (hod != null) {
let hodVal = hod;
if (moh != null) {
if (som != null) {
if (nos == null) {
nos = 0;
}
let totalNanos = MathUtil.safeMultiply(hodVal, 3600000000000);
totalNanos = MathUtil.safeAdd(totalNanos, MathUtil.safeMultiply(moh, 60000000000));
totalNanos = MathUtil.safeAdd(totalNanos, MathUtil.safeMultiply(som, 1000000000));
totalNanos = MathUtil.safeAdd(totalNanos, nos);
const excessDays = MathUtil.floorDiv(totalNanos, 86400000000000); // safe int cast
const nod = MathUtil.floorMod(totalNanos, 86400000000000);
this._addObject(LocalTime.ofNanoOfDay(nod));
this.excessDays = Period.ofDays(excessDays);
} else {
let totalSecs = MathUtil.safeMultiply(hodVal, 3600);
totalSecs = MathUtil.safeAdd(totalSecs, MathUtil.safeMultiply(moh, 60));
const excessDays = MathUtil.floorDiv(totalSecs, 86400); // safe int cast
const sod = MathUtil.floorMod(totalSecs, 86400);
this._addObject(LocalTime.ofSecondOfDay(sod));
this.excessDays = Period.ofDays(excessDays);
}
} else {
const excessDays = MathUtil.safeToInt(MathUtil.floorDiv(hodVal, 24));
hodVal = MathUtil.floorMod(hodVal, 24);
this._addObject(LocalTime.of(hodVal, 0));
this.excessDays = Period.ofDays(excessDays);
}
}
}
this.fieldValues.remove(ChronoField.HOUR_OF_DAY);
this.fieldValues.remove(ChronoField.MINUTE_OF_HOUR);
this.fieldValues.remove(ChronoField.SECOND_OF_MINUTE);
this.fieldValues.remove(ChronoField.NANO_OF_SECOND);
}
/**
*
* @param {ChronoLocalDate|LocalTime} dateOrTime
* @private
*/
_addObject(dateOrTime) {
if (dateOrTime instanceof ChronoLocalDate){
this.date = dateOrTime;
} else if (dateOrTime instanceof LocalTime){
this.time = dateOrTime;
}
}
_resolveInstant() {
if (this.date != null && this.time != null) {
const offsetSecs = this.fieldValues.get(ChronoField.OFFSET_SECONDS);
if (offsetSecs != null) {
const offset = ZoneOffset.ofTotalSeconds(offsetSecs);
const instant = this.date.atTime(this.time).atZone(offset).getLong(ChronoField.INSTANT_SECONDS);
this.fieldValues.put(ChronoField.INSTANT_SECONDS, instant);
} else if (this.zone != null) {
const instant = this.date.atTime(this.time).atZone(this.zone).getLong(ChronoField.INSTANT_SECONDS);
this.fieldValues.put(ChronoField.INSTANT_SECONDS, instant);
}
}
}
/**
* Builds the specified type from the values in this builder.
*
* This attempts to build the specified type from this builder.
* If the builder cannot return the type, an exception is thrown.
*
* @param {!TemporalQuery} type - the type to invoke `from` on, not null
* @return {*} the extracted value, not null
* @throws DateTimeException if an error occurs
*/
build(type) {
return type.queryFrom(this);
}
/**
*
* @param {TemporalField} field
* @returns {number}
*/
isSupported(field) {
if (field == null) {
return false;
}
return (this.fieldValues.containsKey(field) && this.fieldValues.get(field) !== undefined) ||
(this.date != null && this.date.isSupported(field)) ||
(this.time != null && this.time.isSupported(field));
}
/**
*
* @param {TemporalField} field
* @returns {number}
*/
getLong(field) {
requireNonNull(field, 'field');
const value = this.getFieldValue0(field);
if (value == null) {
if (this.date != null && this.date.isSupported(field)) {
return this.date.getLong(field);
}
if (this.time != null && this.time.isSupported(field)) {
return this.time.getLong(field);
}
throw new DateTimeException(`Field not found: ${field}`);
}
return value;
}
/**
*
* @param {!TemporalQuery} query
* @returns {*}
*/
query(query) {
if (query === TemporalQueries.zoneId()) {
return this.zone;
} else if (query === TemporalQueries.chronology()) {
return this.chrono;
} else if (query === TemporalQueries.localDate()) {
return this.date != null ? LocalDate.from(this.date) : null;
} else if (query === TemporalQueries.localTime()) {
return this.time;
} else if (query === TemporalQueries.zone() || query === TemporalQueries.offset()) {
return query.queryFrom(this);
} else if (query === TemporalQueries.precision()) {
return null; // not a complete date/time
}
// inline TemporalAccessor.super.query(query) as an optimization
// non-JDK classes are not permitted to make this optimization
return query.queryFrom(this);
}
}