/**
* Copyright Intellectual Reserve, 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 org.gedcomx.date;
/**
* Static utility functions for handling GedcomX Dates
* @author John Clark.
*/
public class GedcomxDateUtil {
/**
* Parse a String representation of a Formal GedcomX Date
* @param date The GedcomX Date
* @return A GedcomxDate
*/
public static GedcomxDate parse(String date) {
if(date == null || date.equals("")) {
throw new GedcomxDateException("Invalid Date");
}
if(date.charAt(0) == 'R') {
return new GedcomxDateRecurring(date);
} else if(date.contains("/")) {
return new GedcomxDateRange(date);
} else if(date.charAt(0) == 'A') {
return new GedcomxDateApproximate(date);
} else {
return new GedcomxDateSimple(date);
}
}
/**
* Calculates the Duration between two dates
* @param startDate The start date
* @param endDate The end date
* @return The duration
*/
public static GedcomxDateDuration getDuration(GedcomxDateSimple startDate, GedcomxDateSimple endDate) {
if(startDate == null || endDate == null) {
throw new GedcomxDateException("Start and End must be simple dates");
}
Date start = new Date(startDate, true);
Date end = new Date(endDate, true);
boolean hasTime = false;
StringBuilder duration = new StringBuilder();
zipDates(start, end);
// Build the duration backwards so we can grab the correct diff
// Also we need to roll everything up so we don't generate an invalid max year
if(end.seconds != null) {
while(end.seconds-start.seconds < 0) {
end.minutes -= 1;
end.seconds += 60;
}
if(end.seconds-start.seconds > 0) {
hasTime = true;
duration.insert(0,'S').insert(0,String.format("%02d", end.seconds - start.seconds));
}
}
if(end.minutes != null) {
while(end.minutes-start.minutes < 0) {
end.hours -= 1;
end.minutes += 60;
}
if(end.minutes-start.minutes > 0) {
hasTime = true;
duration.insert(0,'M').insert(0,String.format("%02d", end.minutes-start.minutes));
}
}
if(end.hours != null) {
while(end.hours-start.hours < 0) {
end.day -= 1;
end.hours += 24;
}
if(end.hours-start.hours > 0) {
hasTime = true;
duration.insert(0,'H').insert(0,String.format("%02d", end.hours-start.hours));
}
}
if(hasTime) {
duration.insert(0,'T');
}
if(end.day != null) {
while(end.day-start.day < 0) {
end.day += daysInMonth(end.month == 1 ? 12 : end.month - 1, end.year);
end.month -= 1;
if(end.month < 1) {
end.year -= 1;
end.month += 12;
}
}
if(end.day-start.day > 0) {
duration.insert(0,'D').insert(0,String.format("%02d", end.day-start.day));
}
}
if(end.month != null) {
while(end.month-start.month < 0) {
end.year -= 1;
end.month += 12;
}
if(end.month-start.month > 0) {
duration.insert(0,'M').insert(0,String.format("%02d", end.month-start.month));
}
}
if(end.year-start.year > 0) {
duration.insert(0,'Y').insert(0,String.format("%04d", end.year-start.year));
}
String finalDuration = duration.toString();
if(end.year-start.year < 0 || duration.toString().equals("")) {
throw new GedcomxDateException("Start Date must be less than End Date");
}
return new GedcomxDateDuration("P"+finalDuration);
}
/**
* Add a duration to a simple date
* @param startDate The date to start from
* @param duration The duration to add
* @return a new simple date
*/
public static GedcomxDateSimple addDuration(GedcomxDateSimple startDate, GedcomxDateDuration duration) {
if(startDate == null) {
throw new GedcomxDateException("Invalid Start Date");
}
if(duration == null) {
throw new GedcomxDateException("Invalid Duration");
}
Date end = new Date(startDate, false);
StringBuilder endString = new StringBuilder();
// Initialize all the values we need in end based on the duration
zipDuration(end, duration);
// Add Timezone offset to endString
if(startDate.getTzHours() != null) {
endString.append(startDate.getTzHours() >= 0 ? "+" : "-").append(String.format("%02d", Math.abs(startDate.getTzHours())));
endString.append(":").append(String.format("%02d", startDate.getTzMinutes()));
}
if(end.seconds != null) {
if (duration.getSeconds() != null) {
end.seconds += duration.getSeconds();
}
while (end.seconds >= 60) {
end.seconds -= 60;
end.minutes += 1;
}
endString.insert(0, String.format("%02d", end.seconds)).insert(0, ":");
}
if(end.minutes != null) {
if (duration.getMinutes() != null) {
end.minutes += duration.getMinutes();
}
while (end.minutes >= 60) {
end.minutes -= 60;
end.hours += 1;
}
endString.insert(0, String.format("%02d", end.minutes)).insert(0, ":");
}
if(end.hours != null) {
if (duration.getHours() != null) {
end.hours += duration.getHours();
}
while (end.hours >= 24) {
end.hours -= 24;
end.day += 1;
}
endString.insert(0, String.format("%02d", end.hours)).insert(0, "T");
}
if(end.day != null) {
if (duration.getDays() != null) {
end.day += duration.getDays();
}
while (end.day >= GedcomxDateUtil.daysInMonth(end.month, end.year)) {
end.day -= GedcomxDateUtil.daysInMonth(end.month, end.year);
end.month += 1;
if(end.month > 12) {
end.month -= 12;
end.year += 1;
}
}
endString.insert(0, String.format("%02d", end.day)).insert(0, "-");
}
if(end.month != null) {
if (duration.getMonths() != null) {
end.month += duration.getMonths();
}
while (end.month > 12) {
end.month -= 12;
end.year += 1;
}
endString.insert(0, String.format("%02d", end.month)).insert(0, "-");
}
if(duration.getYears() != null) {
end.year += duration.getYears();
}
// After adding months to this year we could have bumped into or out of a non leap year
// TODO fix this
if(end.year > 9999) {
throw new GedcomxDateException("New date out of range");
}
if(end.year != null) {
endString.insert(0, String.format("%04d", Math.abs(end.year))).insert(0,end.year >= 0 ? "+" : "-");
}
return new GedcomxDateSimple(endString.toString());
}
/**
* Multiple a duration by a fixed number
* @param duration The duration to multiply
* @param multiplier The amount to multiply by
* @return The new, multiplied duration
*/
public static GedcomxDateDuration multiplyDuration(GedcomxDateDuration duration, int multiplier) {
if(duration == null) {
throw new GedcomxDateException("Invalid Duration");
}
if(multiplier <= 0) {
throw new GedcomxDateException("Invalid Multiplier");
}
StringBuilder newDuration = new StringBuilder("P");
if(duration.getYears() != null) {
newDuration.append(duration.getYears()*multiplier).append('Y');
}
if(duration.getMonths() != null) {
newDuration.append(duration.getMonths()*multiplier).append('M');
}
if(duration.getDays() != null) {
newDuration.append(duration.getDays()*multiplier).append('D');
}
if(duration.getHours() != null || duration.getMinutes() != null || duration.getSeconds() != null) {
newDuration.append('T');
if(duration.getHours() != null) {
newDuration.append(duration.getHours()*multiplier).append('H');
}
if(duration.getMinutes() != null) {
newDuration.append(duration.getMinutes()*multiplier).append('M');
}
if(duration.getSeconds() != null) {
newDuration.append(duration.getSeconds()*multiplier).append('S');
}
}
return new GedcomxDateDuration(newDuration.toString());
}
/**
* Find the number of days in a month within a given year
* @param month The month
* @param year The year
* @return The number of days in the month
*/
public static int daysInMonth(Integer month, Integer year) {
switch(month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
return 31;
case 4:
case 6:
case 9:
case 11:
return 30;
case 2:
boolean leapYear;
if(year % 4 != 0) {
leapYear = false;
} else if(year % 100 != 0) {
leapYear = true;
} else if(year % 400 != 0) {
leapYear = false;
} else {
leapYear = true;
}
if(leapYear) {
return 29;
} else {
return 28;
}
default:
throw new GedcomxDateException("Unknown Month");
}
}
/**
* Ensures that both start and end have values where the other has values.
* For example, if start has minutes but end does not, this function
* will initialize minutes in end.
* @param start The start date
* @param end The end date
*/
protected static void zipDates(Date start, Date end) {
if(start.month == null && end.month != null) {
start.month = 1;
}
if(start.month != null && end.month == null) {
end.month = 1;
}
if(start.day == null && end.day != null) {
start.day = 1;
}
if(start.day != null && end.day == null) {
end.day = 1;
}
if(start.hours == null && end.hours != null) {
start.hours = 0;
}
if(start.hours != null && end.hours == null) {
end.hours = 0;
}
if(start.minutes == null && end.minutes != null) {
start.minutes = 0;
}
if(start.minutes != null && end.minutes == null) {
end.minutes = 0;
}
if(start.seconds == null && end.seconds != null) {
start.seconds = 0;
}
if(start.seconds != null && end.seconds == null) {
end.seconds = 0;
}
}
/**
* Ensures that date has its properties initialized based on what the duration has.
* For example, if date does not have minutes and duration does, this will
* initialize minutes in the date.
* @param date The start date
* @param duration The duration
*/
protected static void zipDuration(Date date, GedcomxDateDuration duration) {
boolean seconds = false;
boolean minutes = false;
boolean hours = false;
boolean days = false;
boolean months = false;
if(duration.getSeconds() != null) {
seconds = true;
minutes = true;
hours = true;
days = true;
months = true;
} else if(duration.getMinutes() != null) {
minutes = true;
hours = true;
days = true;
months = true;
} else if(duration.getHours() != null) {
hours = true;
days = true;
months = true;
} else if(duration.getDays() != null) {
days = true;
months = true;
} else if(duration.getMonths() != null) {
months = true;
} else {
return;
}
if(seconds && date.seconds == null) {
date.seconds = 0;
}
if(minutes && date.minutes == null) {
date.minutes = 0;
}
if(hours && date.hours == null) {
date.hours = 0;
}
if(days && date.day == null) {
date.day = 1;
}
if(months && date.month == null) {
date.month = 1;
}
}
/**
* A simplified representation of a date.
* Used as a bag-o-properties when performing caluclations
*/
protected static class Date {
public Integer year = null;
public Integer month = null;
public Integer day = null;
public Integer hours = null;
public Integer minutes = null;
public Integer seconds = null;
public Date() {}
public Date(GedcomxDateSimple simple, boolean adjustTimezone) {
year = simple.getYear();
month = simple.getMonth();
day = simple.getDay();
hours = simple.getHours();
minutes = simple.getMinutes();
seconds = simple.getSeconds();
if(adjustTimezone) {
if(hours != null && simple.getTzHours() != null) {
hours += simple.getTzHours();
}
if(minutes != null && simple.getTzMinutes() != null) {
minutes += simple.getTzMinutes();
}
}
}
}
}