blob: a8b423620391225a21e3d80302a049338ccbcb1d [file] [log] [blame]
/*
* 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 org.apache.cassandra.cql3.functions.types;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static com.google.common.base.Preconditions.checkArgument;
/**
* A date with no time components, no time zone, in the ISO 8601 calendar.
*
* <p>Note that ISO 8601 has a number of differences with the default gregorian calendar used in
* Java:
*
* <ul>
* <li>it uses a proleptic gregorian calendar, meaning that it's gregorian indefinitely back in
* the past (there is no gregorian change);
* <li>there is a year 0.
* </ul>
*
* <p>This class implements these differences, so that year/month/day fields match exactly the ones
* in CQL string literals.
*
* @since 2.2
*/
public final class LocalDate
{
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
private final long millisSinceEpoch;
private final int daysSinceEpoch;
// This gets initialized lazily if we ever need it. Once set, it is effectively immutable.
private volatile GregorianCalendar calendar;
private LocalDate(int daysSinceEpoch)
{
this.daysSinceEpoch = daysSinceEpoch;
this.millisSinceEpoch = TimeUnit.DAYS.toMillis(daysSinceEpoch);
}
/**
* Builds a new instance from a number of days since January 1st, 1970 GMT.
*
* @param daysSinceEpoch the number of days.
* @return the new instance.
*/
public static LocalDate fromDaysSinceEpoch(int daysSinceEpoch)
{
return new LocalDate(daysSinceEpoch);
}
/**
* Builds a new instance from a number of milliseconds since January 1st, 1970 GMT. Note that if
* the given number does not correspond to a whole number of days, it will be rounded towards 0.
*
* @param millisSinceEpoch the number of milliseconds since January 1st, 1970 GMT.
* @return the new instance.
* @throws IllegalArgumentException if the date is not in the range [-5877641-06-23;
* 5881580-07-11].
*/
public static LocalDate fromMillisSinceEpoch(long millisSinceEpoch)
throws IllegalArgumentException
{
long daysSinceEpoch = TimeUnit.MILLISECONDS.toDays(millisSinceEpoch);
checkArgument(
daysSinceEpoch >= Integer.MIN_VALUE && daysSinceEpoch <= Integer.MAX_VALUE,
"Date should be in the range [-5877641-06-23; 5881580-07-11]");
return new LocalDate((int) daysSinceEpoch);
}
/**
* Returns the number of days since January 1st, 1970 GMT.
*
* @return the number of days.
*/
public int getDaysSinceEpoch()
{
return daysSinceEpoch;
}
/**
* Returns the year.
*
* @return the year.
*/
public int getYear()
{
GregorianCalendar c = getCalendar();
int year = c.get(Calendar.YEAR);
if (c.get(Calendar.ERA) == GregorianCalendar.BC) year = -year + 1;
return year;
}
/**
* Returns the month.
*
* @return the month. It is 1-based, e.g. 1 for January.
*/
public int getMonth()
{
return getCalendar().get(Calendar.MONTH) + 1;
}
/**
* Returns the day in the month.
*
* @return the day in the month.
*/
public int getDay()
{
return getCalendar().get(Calendar.DAY_OF_MONTH);
}
/**
* Return a new {@link LocalDate} with the specified (signed) amount of time added to (or
* subtracted from) the given {@link Calendar} field, based on the calendar's rules.
*
* <p>Note that adding any amount to a field smaller than {@link Calendar#DAY_OF_MONTH} will
* remain without effect, as this class does not keep time components.
*
* <p>See {@link Calendar} javadocs for more information.
*
* @param field a {@link Calendar} field to modify.
* @param amount the amount of date or time to be added to the field.
* @return a new {@link LocalDate} with the specified (signed) amount of time added to (or
* subtracted from) the given {@link Calendar} field.
* @throws IllegalArgumentException if the new date is not in the range [-5877641-06-23;
* 5881580-07-11].
*/
public LocalDate add(int field, int amount)
{
GregorianCalendar newCalendar = isoCalendar();
newCalendar.setTimeInMillis(millisSinceEpoch);
newCalendar.add(field, amount);
LocalDate newDate = fromMillisSinceEpoch(newCalendar.getTimeInMillis());
newDate.calendar = newCalendar;
return newDate;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o instanceof LocalDate)
{
LocalDate that = (LocalDate) o;
return this.daysSinceEpoch == that.daysSinceEpoch;
}
return false;
}
@Override
public int hashCode()
{
return daysSinceEpoch;
}
@Override
public String toString()
{
return String.format("%d-%s-%s", getYear(), pad2(getMonth()), pad2(getDay()));
}
private static String pad2(int i)
{
String s = Integer.toString(i);
return s.length() == 2 ? s : '0' + s;
}
private GregorianCalendar getCalendar()
{
// Two threads can race and both create a calendar. This is not a problem.
if (calendar == null)
{
// Use a local variable to only expose after we're done mutating it.
GregorianCalendar tmp = isoCalendar();
tmp.setTimeInMillis(millisSinceEpoch);
calendar = tmp;
}
return calendar;
}
// This matches what Cassandra uses server side (from Joda Time's LocalDate)
private static GregorianCalendar isoCalendar()
{
GregorianCalendar calendar = new GregorianCalendar(UTC);
calendar.setGregorianChange(new Date(Long.MIN_VALUE));
return calendar;
}
}