/*
* Copyright DataStax, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.datastax.oss.driver.api.core.data;
import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting;
import com.datastax.oss.driver.shaded.guava.common.base.Objects;
import com.datastax.oss.driver.shaded.guava.common.base.Preconditions;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.time.Duration;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalUnit;
import java.time.temporal.UnsupportedTemporalTypeException;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.jcip.annotations.Immutable;
A duration, as defined in CQL.
It stores months, days, and seconds separately due to the fact that the number of days in a month varies, and a day can have 23 or 25 hours if a daylight saving is involved. As such, this type differs from Duration
(which only represents an amount between two points in time, regardless of the calendar).
/**
* A duration, as defined in CQL.
*
* <p>It stores months, days, and seconds separately due to the fact that the number of days in a
* month varies, and a day can have 23 or 25 hours if a daylight saving is involved. As such, this
* type differs from {@link java.time.Duration} (which only represents an amount between two points
* in time, regardless of the calendar).
*/
@Immutable
public final class CqlDuration implements TemporalAmount {
@VisibleForTesting static final long NANOS_PER_MICRO = 1000L;
@VisibleForTesting static final long NANOS_PER_MILLI = 1000 * NANOS_PER_MICRO;
@VisibleForTesting static final long NANOS_PER_SECOND = 1000 * NANOS_PER_MILLI;
@VisibleForTesting static final long NANOS_PER_MINUTE = 60 * NANOS_PER_SECOND;
@VisibleForTesting static final long NANOS_PER_HOUR = 60 * NANOS_PER_MINUTE;
@VisibleForTesting static final int DAYS_PER_WEEK = 7;
@VisibleForTesting static final int MONTHS_PER_YEAR = 12;
The Regexp used to parse the duration provided as String. /** The Regexp used to parse the duration provided as String. */
private static final Pattern STANDARD_PATTERN =
Pattern.compile(
"\\G(\\d+)(y|Y|mo|MO|mO|Mo|w|W|d|D|h|H|s|S|ms|MS|mS|Ms|us|US|uS|Us|µs|µS|ns|NS|nS|Ns|m|M)");
The Regexp used to parse the duration when provided in the ISO 8601 format with designators.
/**
* The Regexp used to parse the duration when provided in the ISO 8601 format with designators.
*/
private static final Pattern ISO8601_PATTERN =
Pattern.compile("P((\\d+)Y)?((\\d+)M)?((\\d+)D)?(T((\\d+)H)?((\\d+)M)?((\\d+)S)?)?");
The Regexp used to parse the duration when provided in the ISO 8601 format with designators.
/**
* The Regexp used to parse the duration when provided in the ISO 8601 format with designators.
*/
private static final Pattern ISO8601_WEEK_PATTERN = Pattern.compile("P(\\d+)W");
The Regexp used to parse the duration when provided in the ISO 8601 alternative format. /** The Regexp used to parse the duration when provided in the ISO 8601 alternative format. */
private static final Pattern ISO8601_ALTERNATIVE_PATTERN =
Pattern.compile("P(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})");
private static final ImmutableList<TemporalUnit> TEMPORAL_UNITS =
ImmutableList.of(ChronoUnit.MONTHS, ChronoUnit.DAYS, ChronoUnit.NANOS);
private final int months;
private final int days;
private final long nanoseconds;
private CqlDuration(int months, int days, long nanoseconds) {
// Makes sure that all the values are negative if one of them is
if ((months < 0 || days < 0 || nanoseconds < 0)
&& (months > 0 || days > 0 || nanoseconds > 0)) {
throw new IllegalArgumentException(
String.format(
"All values must be either negative or positive, got %d months, %d days, %d nanoseconds",
months, days, nanoseconds));
}
this.months = months;
this.days = days;
this.nanoseconds = nanoseconds;
}
Creates a duration with the given number of months, days and nanoseconds.
A duration can be negative. In this case, all the non zero values must be negative.
Params: - months – the number of months
- days – the number of days
- nanoseconds – the number of nanoseconds
Throws: - IllegalArgumentException – if the values are not all negative or all positive
/**
* Creates a duration with the given number of months, days and nanoseconds.
*
* <p>A duration can be negative. In this case, all the non zero values must be negative.
*
* @param months the number of months
* @param days the number of days
* @param nanoseconds the number of nanoseconds
* @throws IllegalArgumentException if the values are not all negative or all positive
*/
public static CqlDuration newInstance(int months, int days, long nanoseconds) {
return new CqlDuration(months, days, nanoseconds);
}
Converts a String
into a duration.
The accepted formats are:
- multiple digits followed by a time unit like: 12h30m where the time unit can be:
y
: years mo
: months w
: weeks d
: days h
: hours m
: minutes s
: seconds ms
: milliseconds us
or µs
: microseconds ns
: nanoseconds
- ISO 8601 format: P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W
- ISO 8601 alternative format: P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]
Params: - input – the
String
to convert
/**
* Converts a <code>String</code> into a duration.
*
* <p>The accepted formats are:
*
* <ul>
* <li>multiple digits followed by a time unit like: 12h30m where the time unit can be:
* <ul>
* <li>{@code y}: years
* <li>{@code mo}: months
* <li>{@code w}: weeks
* <li>{@code d}: days
* <li>{@code h}: hours
* <li>{@code m}: minutes
* <li>{@code s}: seconds
* <li>{@code ms}: milliseconds
* <li>{@code us} or {@code µs}: microseconds
* <li>{@code ns}: nanoseconds
* </ul>
* <li>ISO 8601 format: P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W
* <li>ISO 8601 alternative format: P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]
* </ul>
*
* @param input the <code>String</code> to convert
*/
public static CqlDuration from(@NonNull String input) {
boolean isNegative = input.startsWith("-");
String source = isNegative ? input.substring(1) : input;
if (source.startsWith("P")) {
if (source.endsWith("W")) {
return parseIso8601WeekFormat(isNegative, source);
}
if (source.contains("-")) {
return parseIso8601AlternativeFormat(isNegative, source);
}
return parseIso8601Format(isNegative, source);
}
return parseStandardFormat(isNegative, source);
}
private static CqlDuration parseIso8601Format(boolean isNegative, @NonNull String source) {
Matcher matcher = ISO8601_PATTERN.matcher(source);
if (!matcher.matches())
throw new IllegalArgumentException(
String.format("Unable to convert '%s' to a duration", source));
Builder builder = new Builder(isNegative);
if (matcher.group(1) != null) {
builder.addYears(groupAsLong(matcher, 2));
}
if (matcher.group(3) != null) {
builder.addMonths(groupAsLong(matcher, 4));
}
if (matcher.group(5) != null) {
builder.addDays(groupAsLong(matcher, 6));
}
// Checks if the String contains time information
if (matcher.group(7) != null) {
if (matcher.group(8) != null) {
builder.addHours(groupAsLong(matcher, 9));
}
if (matcher.group(10) != null) {
builder.addMinutes(groupAsLong(matcher, 11));
}
if (matcher.group(12) != null) {
builder.addSeconds(groupAsLong(matcher, 13));
}
}
return builder.build();
}
private static CqlDuration parseIso8601AlternativeFormat(
boolean isNegative, @NonNull String source) {
Matcher matcher = ISO8601_ALTERNATIVE_PATTERN.matcher(source);
if (!matcher.matches()) {
throw new IllegalArgumentException(
String.format("Unable to convert '%s' to a duration", source));
}
return new Builder(isNegative)
.addYears(groupAsLong(matcher, 1))
.addMonths(groupAsLong(matcher, 2))
.addDays(groupAsLong(matcher, 3))
.addHours(groupAsLong(matcher, 4))
.addMinutes(groupAsLong(matcher, 5))
.addSeconds(groupAsLong(matcher, 6))
.build();
}
private static CqlDuration parseIso8601WeekFormat(boolean isNegative, @NonNull String source) {
Matcher matcher = ISO8601_WEEK_PATTERN.matcher(source);
if (!matcher.matches()) {
throw new IllegalArgumentException(
String.format("Unable to convert '%s' to a duration", source));
}
return new Builder(isNegative).addWeeks(groupAsLong(matcher, 1)).build();
}
private static CqlDuration parseStandardFormat(boolean isNegative, @NonNull String source) {
Matcher matcher = STANDARD_PATTERN.matcher(source);
if (!matcher.find()) {
throw new IllegalArgumentException(
String.format("Unable to convert '%s' to a duration", source));
}
Builder builder = new Builder(isNegative);
boolean done;
do {
long number = groupAsLong(matcher, 1);
String symbol = matcher.group(2);
add(builder, number, symbol);
done = matcher.end() == source.length();
} while (matcher.find());
if (!done) {
throw new IllegalArgumentException(
String.format("Unable to convert '%s' to a duration", source));
}
return builder.build();
}
private static long groupAsLong(@NonNull Matcher matcher, int group) {
return Long.parseLong(matcher.group(group));
}
private static Builder add(@NonNull Builder builder, long number, @NonNull String symbol) {
String s = symbol.toLowerCase(Locale.ROOT);
if (s.equals("y")) {
return builder.addYears(number);
} else if (s.equals("mo")) {
return builder.addMonths(number);
} else if (s.equals("w")) {
return builder.addWeeks(number);
} else if (s.equals("d")) {
return builder.addDays(number);
} else if (s.equals("h")) {
return builder.addHours(number);
} else if (s.equals("m")) {
return builder.addMinutes(number);
} else if (s.equals("s")) {
return builder.addSeconds(number);
} else if (s.equals("ms")) {
return builder.addMillis(number);
} else if (s.equals("us") || s.equals("µs")) {
return builder.addMicros(number);
} else if (s.equals("ns")) {
return builder.addNanos(number);
}
throw new IllegalArgumentException(String.format("Unknown duration symbol '%s'", symbol));
}
Appends the result of the division to the specified builder if the dividend is not zero.
Params: - builder – the builder to append to
- dividend – the dividend
- divisor – the divisor
- unit – the time unit to append after the result of the division
Returns: the remainder of the division
/**
* Appends the result of the division to the specified builder if the dividend is not zero.
*
* @param builder the builder to append to
* @param dividend the dividend
* @param divisor the divisor
* @param unit the time unit to append after the result of the division
* @return the remainder of the division
*/
private static long append(
@NonNull StringBuilder builder, long dividend, long divisor, @NonNull String unit) {
if (dividend == 0 || dividend < divisor) {
return dividend;
}
builder.append(dividend / divisor).append(unit);
return dividend % divisor;
}
Returns the number of months in this duration.
Returns: the number of months in this duration.
/**
* Returns the number of months in this duration.
*
* @return the number of months in this duration.
*/
public int getMonths() {
return months;
}
Returns the number of days in this duration.
Returns: the number of days in this duration.
/**
* Returns the number of days in this duration.
*
* @return the number of days in this duration.
*/
public int getDays() {
return days;
}
Returns the number of nanoseconds in this duration.
Returns: the number of months in this duration.
/**
* Returns the number of nanoseconds in this duration.
*
* @return the number of months in this duration.
*/
public long getNanoseconds() {
return nanoseconds;
}
{@inheritDoc}
This implementation converts the months and days components to a Period
, and the nanosecond component to a Duration
, and adds those two amounts to the temporal object. Therefore the chronology of the temporal must be either the ISO chronology or null.
See Also:
/**
* {@inheritDoc}
*
* <p>This implementation converts the months and days components to a {@link Period}, and the
* nanosecond component to a {@link Duration}, and adds those two amounts to the temporal object.
* Therefore the chronology of the temporal must be either the ISO chronology or null.
*
* @see Period#addTo(Temporal)
* @see Duration#addTo(Temporal)
*/
@Override
public Temporal addTo(Temporal temporal) {
return temporal.plus(Period.of(0, months, days)).plus(Duration.ofNanos(nanoseconds));
}
{@inheritDoc}
This implementation converts the months and days components to a Period
, and the nanosecond component to a Duration
, and subtracts those two amounts to the temporal object. Therefore the chronology of the temporal must be either the ISO chronology or null.
See Also:
/**
* {@inheritDoc}
*
* <p>This implementation converts the months and days components to a {@link Period}, and the
* nanosecond component to a {@link Duration}, and subtracts those two amounts to the temporal
* object. Therefore the chronology of the temporal must be either the ISO chronology or null.
*
* @see Period#subtractFrom(Temporal)
* @see Duration#subtractFrom(Temporal)
*/
@Override
public Temporal subtractFrom(Temporal temporal) {
return temporal.minus(Period.of(0, months, days)).minus(Duration.ofNanos(nanoseconds));
}
@Override
public long get(TemporalUnit unit) {
if (unit == ChronoUnit.MONTHS) {
return months;
} else if (unit == ChronoUnit.DAYS) {
return days;
} else if (unit == ChronoUnit.NANOS) {
return nanoseconds;
} else {
throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit);
}
}
@Override
public List<TemporalUnit> getUnits() {
return TEMPORAL_UNITS;
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
} else if (other instanceof CqlDuration) {
CqlDuration that = (CqlDuration) other;
return this.days == that.days
&& this.months == that.months
&& this.nanoseconds == that.nanoseconds;
} else {
return false;
}
}
@Override
public int hashCode() {
return Objects.hashCode(days, months, nanoseconds);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
if (months < 0 || days < 0 || nanoseconds < 0) {
builder.append('-');
}
long remainder = append(builder, Math.abs(months), MONTHS_PER_YEAR, "y");
append(builder, remainder, 1, "mo");
append(builder, Math.abs(days), 1, "d");
if (nanoseconds != 0) {
remainder = append(builder, Math.abs(nanoseconds), NANOS_PER_HOUR, "h");
remainder = append(builder, remainder, NANOS_PER_MINUTE, "m");
remainder = append(builder, remainder, NANOS_PER_SECOND, "s");
remainder = append(builder, remainder, NANOS_PER_MILLI, "ms");
remainder = append(builder, remainder, NANOS_PER_MICRO, "us");
append(builder, remainder, 1, "ns");
}
return builder.toString();
}
private static class Builder {
private final boolean isNegative;
private int months;
private int days;
private long nanoseconds;
We need to make sure that the values for each units are provided in order. /** We need to make sure that the values for each units are provided in order. */
private int currentUnitIndex;
public Builder(boolean isNegative) {
this.isNegative = isNegative;
}
Adds the specified amount of years.
Params: - numberOfYears – the number of years to add.
Returns: this Builder
/**
* Adds the specified amount of years.
*
* @param numberOfYears the number of years to add.
* @return this {@code Builder}
*/
@NonNull
public Builder addYears(long numberOfYears) {
validateOrder(1);
validateMonths(numberOfYears, MONTHS_PER_YEAR);
// Cast to avoid http://errorprone.info/bugpattern/NarrowingCompoundAssignment
// We could also change the method to accept an int, but keeping long allows us to keep the
// calling code generic.
months += (int) numberOfYears * MONTHS_PER_YEAR;
return this;
}
Adds the specified amount of months.
Params: - numberOfMonths – the number of months to add.
Returns: this Builder
/**
* Adds the specified amount of months.
*
* @param numberOfMonths the number of months to add.
* @return this {@code Builder}
*/
@NonNull
public Builder addMonths(long numberOfMonths) {
validateOrder(2);
validateMonths(numberOfMonths, 1);
months += (int) numberOfMonths;
return this;
}
Adds the specified amount of weeks.
Params: - numberOfWeeks – the number of weeks to add.
Returns: this Builder
/**
* Adds the specified amount of weeks.
*
* @param numberOfWeeks the number of weeks to add.
* @return this {@code Builder}
*/
@NonNull
public Builder addWeeks(long numberOfWeeks) {
validateOrder(3);
validateDays(numberOfWeeks, DAYS_PER_WEEK);
days += (int) numberOfWeeks * DAYS_PER_WEEK;
return this;
}
Adds the specified amount of days.
Params: - numberOfDays – the number of days to add.
Returns: this Builder
/**
* Adds the specified amount of days.
*
* @param numberOfDays the number of days to add.
* @return this {@code Builder}
*/
@NonNull
public Builder addDays(long numberOfDays) {
validateOrder(4);
validateDays(numberOfDays, 1);
days += (int) numberOfDays;
return this;
}
Adds the specified amount of hours.
Params: - numberOfHours – the number of hours to add.
Returns: this Builder
/**
* Adds the specified amount of hours.
*
* @param numberOfHours the number of hours to add.
* @return this {@code Builder}
*/
@NonNull
public Builder addHours(long numberOfHours) {
validateOrder(5);
validateNanos(numberOfHours, NANOS_PER_HOUR);
nanoseconds += numberOfHours * NANOS_PER_HOUR;
return this;
}
Adds the specified amount of minutes.
Params: - numberOfMinutes – the number of minutes to add.
Returns: this Builder
/**
* Adds the specified amount of minutes.
*
* @param numberOfMinutes the number of minutes to add.
* @return this {@code Builder}
*/
@NonNull
public Builder addMinutes(long numberOfMinutes) {
validateOrder(6);
validateNanos(numberOfMinutes, NANOS_PER_MINUTE);
nanoseconds += numberOfMinutes * NANOS_PER_MINUTE;
return this;
}
Adds the specified amount of seconds.
Params: - numberOfSeconds – the number of seconds to add.
Returns: this Builder
/**
* Adds the specified amount of seconds.
*
* @param numberOfSeconds the number of seconds to add.
* @return this {@code Builder}
*/
@NonNull
public Builder addSeconds(long numberOfSeconds) {
validateOrder(7);
validateNanos(numberOfSeconds, NANOS_PER_SECOND);
nanoseconds += numberOfSeconds * NANOS_PER_SECOND;
return this;
}
Adds the specified amount of milliseconds.
Params: - numberOfMillis – the number of milliseconds to add.
Returns: this Builder
/**
* Adds the specified amount of milliseconds.
*
* @param numberOfMillis the number of milliseconds to add.
* @return this {@code Builder}
*/
@NonNull
public Builder addMillis(long numberOfMillis) {
validateOrder(8);
validateNanos(numberOfMillis, NANOS_PER_MILLI);
nanoseconds += numberOfMillis * NANOS_PER_MILLI;
return this;
}
Adds the specified amount of microseconds.
Params: - numberOfMicros – the number of microseconds to add.
Returns: this Builder
/**
* Adds the specified amount of microseconds.
*
* @param numberOfMicros the number of microseconds to add.
* @return this {@code Builder}
*/
@NonNull
public Builder addMicros(long numberOfMicros) {
validateOrder(9);
validateNanos(numberOfMicros, NANOS_PER_MICRO);
nanoseconds += numberOfMicros * NANOS_PER_MICRO;
return this;
}
Adds the specified amount of nanoseconds.
Params: - numberOfNanos – the number of nanoseconds to add.
Returns: this Builder
/**
* Adds the specified amount of nanoseconds.
*
* @param numberOfNanos the number of nanoseconds to add.
* @return this {@code Builder}
*/
@NonNull
public Builder addNanos(long numberOfNanos) {
validateOrder(10);
validateNanos(numberOfNanos, 1);
nanoseconds += numberOfNanos;
return this;
}
Validates that the total number of months can be stored.
Params: - units – the number of units that need to be added
- monthsPerUnit – the number of days per unit
/**
* Validates that the total number of months can be stored.
*
* @param units the number of units that need to be added
* @param monthsPerUnit the number of days per unit
*/
private void validateMonths(long units, int monthsPerUnit) {
validate(units, (Integer.MAX_VALUE - months) / monthsPerUnit, "months");
}
Validates that the total number of days can be stored.
Params: - units – the number of units that need to be added
- daysPerUnit – the number of days per unit
/**
* Validates that the total number of days can be stored.
*
* @param units the number of units that need to be added
* @param daysPerUnit the number of days per unit
*/
private void validateDays(long units, int daysPerUnit) {
validate(units, (Integer.MAX_VALUE - days) / daysPerUnit, "days");
}
Validates that the total number of nanoseconds can be stored.
Params: - units – the number of units that need to be added
- nanosPerUnit – the number of nanoseconds per unit
/**
* Validates that the total number of nanoseconds can be stored.
*
* @param units the number of units that need to be added
* @param nanosPerUnit the number of nanoseconds per unit
*/
private void validateNanos(long units, long nanosPerUnit) {
validate(units, (Long.MAX_VALUE - nanoseconds) / nanosPerUnit, "nanoseconds");
}
Validates that the specified amount is less than the limit.
Params: - units – the number of units to check
- limit – the limit on the number of units
- unitName – the unit name
/**
* Validates that the specified amount is less than the limit.
*
* @param units the number of units to check
* @param limit the limit on the number of units
* @param unitName the unit name
*/
private void validate(long units, long limit, @NonNull String unitName) {
Preconditions.checkArgument(
units <= limit,
"Invalid duration. The total number of %s must be less or equal to %s",
unitName,
Integer.MAX_VALUE);
}
Validates that the duration values are added in the proper order.
Params: - unitIndex – the unit index (e.g. years=1, months=2, ...)
/**
* Validates that the duration values are added in the proper order.
*
* @param unitIndex the unit index (e.g. years=1, months=2, ...)
*/
private void validateOrder(int unitIndex) {
if (unitIndex == currentUnitIndex) {
throw new IllegalArgumentException(
String.format(
"Invalid duration. The %s are specified multiple times", getUnitName(unitIndex)));
}
if (unitIndex <= currentUnitIndex) {
throw new IllegalArgumentException(
String.format(
"Invalid duration. The %s should be after %s",
getUnitName(currentUnitIndex), getUnitName(unitIndex)));
}
currentUnitIndex = unitIndex;
}
Returns the name of the unit corresponding to the specified index.
Params: - unitIndex – the unit index
Returns: the name of the unit corresponding to the specified index.
/**
* Returns the name of the unit corresponding to the specified index.
*
* @param unitIndex the unit index
* @return the name of the unit corresponding to the specified index.
*/
@NonNull
private String getUnitName(int unitIndex) {
switch (unitIndex) {
case 1:
return "years";
case 2:
return "months";
case 3:
return "weeks";
case 4:
return "days";
case 5:
return "hours";
case 6:
return "minutes";
case 7:
return "seconds";
case 8:
return "milliseconds";
case 9:
return "microseconds";
case 10:
return "nanoseconds";
default:
throw new AssertionError("unknown unit index: " + unitIndex);
}
}
@NonNull
public CqlDuration build() {
return isNegative
? new CqlDuration(-months, -days, -nanoseconds)
: new CqlDuration(months, days, nanoseconds);
}
}
}