-
-
Notifications
You must be signed in to change notification settings - Fork 81
Expand file tree
/
Copy pathTemporals.java
More file actions
454 lines (426 loc) · 17.5 KB
/
Temporals.java
File metadata and controls
454 lines (426 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
/*
* Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.threeten.extra;
import static java.time.temporal.ChronoField.DAY_OF_WEEK;
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
import static java.time.temporal.ChronoField.NANO_OF_SECOND;
import static java.time.temporal.ChronoUnit.DAYS;
import static java.time.temporal.ChronoUnit.ERAS;
import static java.time.temporal.ChronoUnit.FOREVER;
import static java.time.temporal.ChronoUnit.WEEKS;
import java.text.ParsePosition;
import java.time.Duration;
import java.time.DateTimeException;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.time.temporal.IsoFields;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalQuery;
import java.time.temporal.TemporalUnit;
import java.time.temporal.UnsupportedTemporalTypeException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Additional utilities for working with temporal classes.
* <p>
* This includes:
* <ul>
* <li>adjusters that ignore Saturday/Sunday weekends
* <li>conversion between {@code TimeUnit} and {@code ChronoUnit}
* <li>converting an amount to another unit
* <li>adjusters that round time
* </ul>
*
* <h3>Implementation Requirements:</h3>
* This is a thread-safe utility class.
* All returned classes are immutable and thread-safe.
*/
public final class Temporals {
/**
* Restricted constructor.
*/
private Temporals() {
}
//-------------------------------------------------------------------------
/**
* Returns an adjuster that returns the next working day, ignoring Saturday and Sunday.
* <p>
* Some territories have weekends that do not consist of Saturday and Sunday.
* No implementation is supplied to support this, however an adjuster
* can be easily written to do so.
*
* @return the next working day adjuster, not null
*/
public static TemporalAdjuster nextWorkingDay() {
return Adjuster.NEXT_WORKING;
}
/**
* Returns an adjuster that returns the next working day or same day if already working day, ignoring Saturday and Sunday.
* <p>
* Some territories have weekends that do not consist of Saturday and Sunday.
* No implementation is supplied to support this, however an adjuster
* can be easily written to do so.
*
* @return the next working day or same adjuster, not null
*/
public static TemporalAdjuster nextWorkingDayOrSame() {
return Adjuster.NEXT_WORKING_OR_SAME;
}
/**
* Returns an adjuster that returns the previous working day, ignoring Saturday and Sunday.
* <p>
* Some territories have weekends that do not consist of Saturday and Sunday.
* No implementation is supplied to support this, however an adjuster
* can be easily written to do so.
*
* @return the previous working day adjuster, not null
*/
public static TemporalAdjuster previousWorkingDay() {
return Adjuster.PREVIOUS_WORKING;
}
/**
* Returns an adjuster that returns the previous working day or same day if already working day, ignoring Saturday and Sunday.
* <p>
* Some territories have weekends that do not consist of Saturday and Sunday.
* No implementation is supplied to support this, however an adjuster
* can be easily written to do so.
*
* @return the previous working day or same adjuster, not null
*/
public static TemporalAdjuster previousWorkingDayOrSame() {
return Adjuster.PREVIOUS_WORKING_OR_SAME;
}
/**
* Returns an adjuster that offers time clock rounding.
* <p>
* Time clock rounding divides the minutes of the hour into equal fractions of the same length. Every fraction has
* a start-inclusive and end-exclusive minute of the hour. A rounded result is either at the lower or upper end of
* one of these fractions.
* <p>
* Time clock rounding views time as hour-minute. Therefore it always truncates to minutes.
*
* @param duration the fraction of the hour, must be a divisor of 60
* @param roundingMode the time clock rounding mode
* @return time rounded to a fraction of the hour
*/
public static TemporalAdjuster roundTime(Duration duration, RoundingMode roundingMode) {
Objects.requireNonNull(duration, "duration");
Objects.requireNonNull(roundingMode, "mode");
long minutes = duration.toMinutes();
if (minutes < 1 || 60 % minutes != 0) {
throw new DateTimeException("duration is not a divisor of 60");
}
int divisor = (int) minutes;
return temporal -> {
int minuteOfHour = temporal.get(MINUTE_OF_HOUR);
temporal = temporal.with(SECOND_OF_MINUTE, 0)
.with(NANO_OF_SECOND, 0);
if (minuteOfHour % divisor == 0) {
return temporal;
}
int down = minuteOfHour / divisor * divisor;
int up = down + divisor;
RoundingMode mode = roundingMode;
if (roundingMode == RoundingMode.HALF_UP) {
mode = (minuteOfHour - down) < (up - minuteOfHour) ? RoundingMode.DOWN : RoundingMode.UP;
}
if (mode == RoundingMode.DOWN) {
temporal = temporal.with(MINUTE_OF_HOUR, down);
}
if (mode == RoundingMode.UP) {
if (up == 60) {
if (temporal.isSupported(ChronoUnit.HOURS)) {
temporal = temporal.plus(1, ChronoUnit.HOURS);
}
up = 0;
}
temporal = temporal.with(MINUTE_OF_HOUR, up);
}
return temporal;
};
}
//-----------------------------------------------------------------------
/**
* Enum implementing the adjusters.
*/
private static enum Adjuster implements TemporalAdjuster {
/** Next working day adjuster. */
NEXT_WORKING {
@Override
public Temporal adjustInto(Temporal temporal) {
int dow = temporal.get(DAY_OF_WEEK);
switch (dow) {
case 6: // Saturday
return temporal.plus(2, DAYS);
case 5: // Friday
return temporal.plus(3, DAYS);
default:
return temporal.plus(1, DAYS);
}
}
},
/** Previous working day adjuster. */
PREVIOUS_WORKING {
@Override
public Temporal adjustInto(Temporal temporal) {
int dow = temporal.get(DAY_OF_WEEK);
switch (dow) {
case 1: // Monday
return temporal.minus(3, DAYS);
case 7: // Sunday
return temporal.minus(2, DAYS);
default:
return temporal.minus(1, DAYS);
}
}
},
/** Next working day or same adjuster. */
NEXT_WORKING_OR_SAME {
@Override
public Temporal adjustInto(Temporal temporal) {
int dow = temporal.get(DAY_OF_WEEK);
switch (dow) {
case 6: // Saturday
return temporal.plus(2, DAYS);
case 7: // Sunday
return temporal.plus(1, DAYS);
default:
return temporal;
}
}
},
/** Previous working day or same adjuster. */
PREVIOUS_WORKING_OR_SAME {
@Override
public Temporal adjustInto(Temporal temporal) {
int dow = temporal.get(DAY_OF_WEEK);
switch (dow) {
case 6: //Saturday
return temporal.minus(1, DAYS);
case 7: // Sunday
return temporal.minus(2, DAYS);
default:
return temporal;
}
}
}
}
//-------------------------------------------------------------------------
/**
* Parses the text using one of the formatters.
* <p>
* This will try each formatter in turn, attempting to fully parse the specified text.
* The temporal query is typically a method reference to a {@code from(TemporalAccessor)} method.
* For example:
* <pre>
* LocalDateTime dt = Temporals.parseFirstMatching(str, LocalDateTime::from, fmt1, fm2, fm3);
* </pre>
* If the parse completes without reading the entire length of the text,
* or a problem occurs during parsing or merging, then an exception is thrown.
*
* @param <T> the type of the parsed date-time
* @param text the text to parse, not null
* @param query the query defining the type to parse to, not null
* @param formatters the formatters to try, not null
* @return the parsed date-time, not null
* @throws DateTimeParseException if unable to parse the requested result
*/
public static <T> T parseFirstMatching(CharSequence text, TemporalQuery<T> query, DateTimeFormatter... formatters) {
Objects.requireNonNull(text, "text");
Objects.requireNonNull(query, "query");
Objects.requireNonNull(formatters, "formatters");
if (formatters.length == 0) {
throw new DateTimeParseException("No formatters specified", text, 0);
}
if (formatters.length == 1) {
return formatters[0].parse(text, query);
}
for (DateTimeFormatter formatter : formatters) {
try {
ParsePosition pp = new ParsePosition(0);
formatter.parseUnresolved(text, pp);
int len = text.length();
if (pp.getErrorIndex() == -1 && pp.getIndex() == len) {
return formatter.parse(text, query);
}
} catch (RuntimeException ex) {
// should not happen, but ignore if it does
}
}
throw new DateTimeParseException("Text '" + text + "' could not be parsed", text, 0);
}
//-------------------------------------------------------------------------
/**
* Converts a {@code TimeUnit} to a {@code ChronoUnit}.
* <p>
* This handles the seven units declared in {@code TimeUnit}.
*
* @param unit the unit to convert, not null
* @return the converted unit, not null
*/
public static ChronoUnit chronoUnit(TimeUnit unit) {
Objects.requireNonNull(unit, "unit");
switch (unit) {
case NANOSECONDS:
return ChronoUnit.NANOS;
case MICROSECONDS:
return ChronoUnit.MICROS;
case MILLISECONDS:
return ChronoUnit.MILLIS;
case SECONDS:
return ChronoUnit.SECONDS;
case MINUTES:
return ChronoUnit.MINUTES;
case HOURS:
return ChronoUnit.HOURS;
case DAYS:
return ChronoUnit.DAYS;
default:
throw new IllegalArgumentException("Unknown TimeUnit constant");
}
}
/**
* Converts a {@code ChronoUnit} to a {@code TimeUnit}.
* <p>
* This handles the seven units declared in {@code TimeUnit}.
*
* @param unit the unit to convert, not null
* @return the converted unit, not null
* @throws IllegalArgumentException if the unit cannot be converted
*/
public static TimeUnit timeUnit(ChronoUnit unit) {
Objects.requireNonNull(unit, "unit");
switch (unit) {
case NANOS:
return TimeUnit.NANOSECONDS;
case MICROS:
return TimeUnit.MICROSECONDS;
case MILLIS:
return TimeUnit.MILLISECONDS;
case SECONDS:
return TimeUnit.SECONDS;
case MINUTES:
return TimeUnit.MINUTES;
case HOURS:
return TimeUnit.HOURS;
case DAYS:
return TimeUnit.DAYS;
default:
throw new IllegalArgumentException("ChronoUnit cannot be converted to TimeUnit: " + unit);
}
}
//-------------------------------------------------------------------------
/**
* Converts an amount from one unit to another.
* <p>
* This works on the units in {@code ChronoUnit} and {@code IsoFields}.
* The {@code DAYS} and {@code WEEKS} units are handled as exact multiple of 24 hours.
* The {@code ERAS} and {@code FOREVER} units are not supported.
*
* @param amount the input amount in terms of the {@code fromUnit}
* @param fromUnit the unit to convert from, not null
* @param toUnit the unit to convert to, not null
* @return the conversion array,
* element 0 is the signed whole number,
* element 1 is the signed remainder in terms of the input unit,
* not null
* @throws DateTimeException if the units cannot be converted
* @throws UnsupportedTemporalTypeException if the units are not supported
* @throws ArithmeticException if numeric overflow occurs
*/
public static long[] convertAmount(long amount, TemporalUnit fromUnit, TemporalUnit toUnit) {
Objects.requireNonNull(fromUnit, "fromUnit");
Objects.requireNonNull(toUnit, "toUnit");
validateUnit(fromUnit);
validateUnit(toUnit);
if (fromUnit.equals(toUnit)) {
return new long[] {amount, 0};
}
// precise-based
if (isPrecise(fromUnit) && isPrecise(toUnit)) {
long fromNanos = fromUnit.getDuration().toNanos();
long toNanos = toUnit.getDuration().toNanos();
if (fromNanos > toNanos) {
long multiple = fromNanos / toNanos;
return new long[] {Math.multiplyExact(amount, multiple), 0};
} else {
long multiple = toNanos / fromNanos;
return new long[] {amount / multiple, amount % multiple};
}
}
// month-based
int fromMonthFactor = monthMonthFactor(fromUnit, fromUnit, toUnit);
int toMonthFactor = monthMonthFactor(toUnit, fromUnit, toUnit);
if (fromMonthFactor > toMonthFactor) {
long multiple = fromMonthFactor / toMonthFactor;
return new long[] {Math.multiplyExact(amount, multiple), 0};
} else {
long multiple = toMonthFactor / fromMonthFactor;
return new long[] {amount / multiple, amount % multiple};
}
}
private static void validateUnit(TemporalUnit unit) {
if (unit instanceof ChronoUnit) {
if (unit.equals(ERAS) || unit.equals(FOREVER)) {
throw new UnsupportedTemporalTypeException("Unsupported TemporalUnit: " + unit);
}
} else if (unit.equals(IsoFields.QUARTER_YEARS) == false) {
throw new UnsupportedTemporalTypeException("Unsupported TemporalUnit: " + unit);
}
}
private static boolean isPrecise(TemporalUnit unit) {
return unit instanceof ChronoUnit && ((ChronoUnit) unit).compareTo(WEEKS) <= 0;
}
private static int monthMonthFactor(TemporalUnit unit, TemporalUnit fromUnit, TemporalUnit toUnit) {
if (unit instanceof ChronoUnit) {
switch ((ChronoUnit) unit) {
case MONTHS:
return 1;
case YEARS:
return 12;
case DECADES:
return 120;
case CENTURIES:
return 1200;
case MILLENNIA:
return 12000;
default:
throw new DateTimeException(
String.format("Unable to convert between units: %s to %s", fromUnit, toUnit));
}
}
return 3; // quarters
}
}