com.datastax.dse.driver.internal.core.search.DateRangeUtil Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.dse.driver.internal.core.search;
import com.datastax.dse.driver.api.core.data.time.DateRangePrecision;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.text.ParseException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.Calendar;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
public class DateRangeUtil {
/** Sets all the fields smaller than the given unit to their lowest possible value. */
@NonNull
public static ZonedDateTime roundDown(@NonNull ZonedDateTime date, @NonNull ChronoUnit unit) {
switch (unit) {
case YEARS:
return date.with(TemporalAdjusters.firstDayOfYear()).truncatedTo(ChronoUnit.DAYS);
case MONTHS:
return date.with(TemporalAdjusters.firstDayOfMonth()).truncatedTo(ChronoUnit.DAYS);
case DAYS:
case HOURS:
case MINUTES:
case SECONDS:
case MILLIS:
return date.truncatedTo(unit);
default:
throw new IllegalArgumentException("Unsupported unit for rounding: " + unit);
}
}
/** Sets all the fields smaller than the given unit to their highest possible value. */
@NonNull
public static ZonedDateTime roundUp(@NonNull ZonedDateTime date, @NonNull ChronoUnit unit) {
return roundDown(date, unit)
.plus(1, unit)
// Even though ZDT has nanosecond-precision, DSE only rounds to millisecond precision so be
// consistent with that
.minus(1, ChronoUnit.MILLIS);
}
/**
* Parses the given string as a date in a range bound.
*
* This method deliberately uses legacy time APIs, in order to be as close as possible to the
* server-side parsing logic. We want the client to behave exactly like the server, i.e. parsing a
* date locally and inlining it in a CQL query should always yield the same result as binding the
* date as a value.
*/
public static Calendar parseCalendar(String source) throws ParseException {
// The contents of this method are based on Lucene's DateRangePrefixTree#parseCalendar, released
// under the Apache License, Version 2.0.
// Following is the original notice from that file:
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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.
if (source == null || source.isEmpty()) {
throw new IllegalArgumentException("Can't parse a null or blank string");
}
Calendar calendar = newCalendar();
if (source.equals("*")) {
return calendar;
}
int offset = 0; // a pointer
try {
// year & era:
int lastOffset =
(source.charAt(source.length() - 1) == 'Z') ? source.length() - 1 : source.length();
int hyphenIdx = source.indexOf('-', 1); // look past possible leading hyphen
if (hyphenIdx < 0) {
hyphenIdx = lastOffset;
}
int year = Integer.parseInt(source.substring(offset, hyphenIdx));
calendar.set(Calendar.ERA, year <= 0 ? 0 : 1);
calendar.set(Calendar.YEAR, year <= 0 ? -1 * year + 1 : year);
offset = hyphenIdx + 1;
if (lastOffset < offset) {
return calendar;
}
// NOTE: We aren't validating separator chars, and we unintentionally accept leading +/-.
// The str.substring()'s hopefully get optimized to be stack-allocated.
// month:
calendar.set(
Calendar.MONTH,
Integer.parseInt(source.substring(offset, offset + 2)) - 1); // starts at 0
offset += 3;
if (lastOffset < offset) {
return calendar;
}
// day:
calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(source.substring(offset, offset + 2)));
offset += 3;
if (lastOffset < offset) {
return calendar;
}
// hour:
calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(source.substring(offset, offset + 2)));
offset += 3;
if (lastOffset < offset) {
return calendar;
}
// minute:
calendar.set(Calendar.MINUTE, Integer.parseInt(source.substring(offset, offset + 2)));
offset += 3;
if (lastOffset < offset) {
return calendar;
}
// second:
calendar.set(Calendar.SECOND, Integer.parseInt(source.substring(offset, offset + 2)));
offset += 3;
if (lastOffset < offset) {
return calendar;
}
// ms:
calendar.set(Calendar.MILLISECOND, Integer.parseInt(source.substring(offset, offset + 3)));
offset += 3; // last one, move to next char
if (lastOffset == offset) {
return calendar;
}
} catch (Exception e) {
ParseException pe = new ParseException("Improperly formatted date: " + source, offset);
pe.initCause(e);
throw pe;
}
throw new ParseException("Improperly formatted date: " + source, offset);
}
private static Calendar newCalendar() {
Calendar calendar = Calendar.getInstance(UTC, Locale.ROOT);
calendar.clear();
return calendar;
}
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
/**
* Returns the precision of a calendar obtained through {@link #parseCalendar(String)}, or {@code
* null} if no field was set.
*/
@Nullable
public static DateRangePrecision getPrecision(Calendar calendar) {
DateRangePrecision lastPrecision = null;
for (Map.Entry entry : FIELD_BY_PRECISION.entrySet()) {
DateRangePrecision precision = entry.getKey();
int field = entry.getValue();
if (calendar.isSet(field)) {
lastPrecision = precision;
} else {
break;
}
}
return lastPrecision;
}
// Note: this could be a field on DateRangePrecision, but it's only used within this class so it's
// better not to expose it.
private static final ImmutableMap FIELD_BY_PRECISION =
ImmutableMap.builder()
.put(DateRangePrecision.YEAR, Calendar.YEAR)
.put(DateRangePrecision.MONTH, Calendar.MONTH)
.put(DateRangePrecision.DAY, Calendar.DAY_OF_MONTH)
.put(DateRangePrecision.HOUR, Calendar.HOUR_OF_DAY)
.put(DateRangePrecision.MINUTE, Calendar.MINUTE)
.put(DateRangePrecision.SECOND, Calendar.SECOND)
.put(DateRangePrecision.MILLISECOND, Calendar.MILLISECOND)
.build();
public static ZonedDateTime toZonedDateTime(Calendar calendar) {
int year = calendar.get(Calendar.YEAR);
if (calendar.get(Calendar.ERA) == 0) {
// BC era; 1 BC == 0 AD, 0 BD == -1 AD, etc
year -= 1;
if (year > 0) {
year = -year;
}
}
LocalDateTime localDateTime =
LocalDateTime.of(
year,
calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.DAY_OF_MONTH),
calendar.get(Calendar.HOUR_OF_DAY),
calendar.get(Calendar.MINUTE),
calendar.get(Calendar.SECOND));
localDateTime =
localDateTime.with(ChronoField.MILLI_OF_SECOND, calendar.get(Calendar.MILLISECOND));
return ZonedDateTime.of(localDateTime, ZoneOffset.UTC);
}
}