/*******************************************************************************
* Copyright (c) 2012, Directors of the Tyndale STEP Project
* 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 the Tyndale House, Cambridge (www.TyndaleHouse.com)
* 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 HOLDER 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 com.tyndalehouse.step.core.data.common;
import static com.tyndalehouse.step.core.utils.StringUtils.isEmpty;
import static com.tyndalehouse.step.core.utils.StringUtils.split;
import static java.lang.Integer.parseInt;
import org.joda.time.LocalDateTime;
import com.tyndalehouse.step.core.exceptions.StepInternalException;
/**
* This class is the way dates are represented in the databased and they should be parsed back into this
* object on their way out!
*
* The date field indicates when the event (or start of the event) took place, the precision type indicates
* whether how much of the date can be trusted...
*
* This means we can store dates such as (01/03/1900, MONTH), meaning March 1900 (and not 1st March 1900).
*
* @author CJBurrell
*/
public class PartialDate {
private static final String DATE_DELIMITER = "[ ]?-[ ]?";
private static final int NO_PARTS = 0;
private static final int YEAR = 1;
private static final int YEAR_AND_MONTH = 2;
private static final int YEAR_MONTH_AND_DAY = 3;
/**
* The date to be represented (whether fully accurate or not)
*/
private final LocalDateTime localDateTime;
/**
* The precision specifier which tells us just quite how accurate the date is (year, month, day)
*
* @see com.tyndalehouse.step.dataloader.common.PrecisionType
*/
private final PrecisionType precision;
/**
* Public constructor to give us a partial date.
*
* @param ldt date partial representation of a date
* @param precision precision indicating how much of the date can be trusted day/month/year or month/year
* or just year
*/
public PartialDate(final LocalDateTime ldt, final PrecisionType precision) {
this.localDateTime = ldt;
this.precision = precision;
}
/**
* Date is specified in yy-mm-dd or yyyy-mm-dd and gets parsed in to a date. the mm and dd are optional
* which is what determines the precision of the date.
*
* @param date date to be parsed as a string
* @return a PartialDate
*/
public static PartialDate parseDate(final String date) {
// check for null value
if (date == null) {
return new PartialDate(null, PrecisionType.NONE);
}
// if passed in empty, return null and be done with empty strings!
final String trimmedDate = date.trim();
if (isEmpty(trimmedDate)) {
return new PartialDate(null, PrecisionType.NONE);
}
final boolean negativeDate = date.charAt(0) == '-';
final String parseableDate = negativeDate ? trimmedDate.substring(1) : trimmedDate;
return getPartialDateFromArray(split(parseableDate.trim(), DATE_DELIMITER), negativeDate);
}
/**
* Depending on the number of parts, it creates a Partial date with year/month/day resolution
*
* @param parts the array of parts each representing part of the date
* @param negativeDate true if the date is BC
* @return the newly created PartialDate
*/
private static PartialDate getPartialDateFromArray(final String[] parts, final boolean negativeDate) {
final LocalDateTime translatedTime;
PrecisionType p;
final int multiplier = negativeDate ? -1 : 1;
try {
// length of field determines how much of the date has been specified
switch (parts.length) {
case NO_PARTS:
throw new StepInternalException("There weren't enough parts to this date");
case YEAR:
// only the year is specified, so use 1st of Jan Year
translatedTime = new LocalDateTime(multiplier * parseInt(parts[0]), 1, 1, 0, 0);
p = PrecisionType.YEAR;
break;
case YEAR_AND_MONTH:
translatedTime = new LocalDateTime(multiplier * parseInt(parts[0]), parseInt(parts[1]),
1, 0, 0);
p = PrecisionType.MONTH;
break;
case YEAR_MONTH_AND_DAY:
translatedTime = new LocalDateTime(multiplier * parseInt(parts[0]), parseInt(parts[1]),
parseInt(parts[2]), 0, 0);
p = PrecisionType.DAY;
break;
default:
throw new StepInternalException("Too many parts to the date: ");
}
} catch (final NumberFormatException nfe) {
throw new StepInternalException("Could not parse date into year, month or day.", nfe);
}
return new PartialDate(translatedTime, p);
}
/**
* @return gets the internal date
*/
public LocalDateTime getDate() {
return this.localDateTime;
}
/**
* @return the precision accuracy
*/
public PrecisionType getPrecision() {
return this.precision;
}
}