org.tentackle.fx.translate.DateStringTranslator Maven / Gradle / Ivy
/*
* Tentackle - https://tentackle.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.tentackle.fx.translate;
import javafx.scene.Node;
import org.tentackle.bind.BindingException;
import org.tentackle.common.DateHelper;
import org.tentackle.common.StringHelper;
import org.tentackle.fx.FxFxBundle;
import org.tentackle.fx.FxRuntimeException;
import org.tentackle.fx.FxTextComponent;
import org.tentackle.fx.FxUtilities;
import org.tentackle.fx.ValueTranslatorService;
import org.tentackle.fx.bind.FxComponentBinding;
import org.tentackle.log.Logger;
import org.tentackle.misc.FormatHelper;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Calendar;
import java.util.Date;
import java.util.function.Function;
import java.util.function.Supplier;
import static java.util.Calendar.DATE;
import static java.util.Calendar.DAY_OF_MONTH;
import static java.util.Calendar.DAY_OF_WEEK;
import static java.util.Calendar.DAY_OF_YEAR;
import static java.util.Calendar.HOUR;
import static java.util.Calendar.HOUR_OF_DAY;
import static java.util.Calendar.MINUTE;
import static java.util.Calendar.MONTH;
import static java.util.Calendar.SECOND;
import static java.util.Calendar.WEEK_OF_YEAR;
import static java.util.Calendar.YEAR;
/**
* Date translator.
*
* The date can be entered in the specified format or as a shortcut.
* The following shortcuts are defined:
*
* 5/29/
: expands to May 29 of the current year (midnight).
* 0529
: dto.
* 05290"
: if the year is 4-digits in length the century will be added
* which is closest to the current date, i.e.:
* 5/29/99
: will be expanded to "05/29/1999" and _not_ "05/29/2099".
* 7:00
: today at 7:00am
*
*
* Furthermore, the date can determined in relation to the current time:
*
*
* /.,-=* or the date-delimiter of the current locale
: current date
* :;'"
: current time
* +3d
: today 0:00:00 plus 3 days
* -2y
: today 2 years ago.
* 17
: the unit is determined according to the model's type. If {@link Time} the unit
* defaults to the most significant format-pattern (usually hours). Else if {@link Date} or {@link Timestamp},
* the unit defaults to 'd'.
* For dates this means the 17th of the current month.
* If the type is a {@link Time}, it means 5pm.
* +14
: same as above but the value will be added (or subtracted if negative)
* to (from) the current time. For date fields, for example, this is again shorter than "+14d".
* 4m
: the unit according to the letter following the number will be set _and_
* the next "smaller" unit set to its minimum.
* In this example, the time (if it is a time field) will be set to 4 minutes and 0 seconds.
* Likewise, "6y" would mean "January 1st, 2006". Consequently, "8h" is an even shorter
* way to express "today at 8am" than "8:00".
*
*
* The units are the same as described in {@link SimpleDateFormat} with some
* minor differences:
*
* "y" or "Y"
: year(s)
* "M"
: month(s). In date fields without minutes a lowercase "m" works as well.
* "w or W"
: week(s) or calendar week. For example: "-2w" means "two weeks ago"
* but "30w" means the first day of week 30.
* "d oder D"
: day(s)
* "h oder H"
: hour(s). Notice that "-24h" means "24 hours ago" and is not
* the dame as "-1d" which means "yesterday 0am".
* "m"
: minute(s)
* "s oder S"
: second(s)
*
*
*
* The shortcuts (except the units) are locale dependent. In German, for example, the
* shortcuts are as follows:
*
* "29.5."
: adds the current year
* "2905"
: dto.
* "290506"
: the closest year if the year is 4-digit, i.e.:
* "29.5.99"
: becomes "29.05.1999" and not "29.05.2099".
* "7:00"
: today 7 am
*
*
* @author harald
*/
@ValueTranslatorService(modelClass = Date.class, viewClass = String.class)
public class DateStringTranslator extends ValueStringTranslator {
/**
* Component property for a reference date supplier of type Supplier<Date>.
* '@' as input will be replaced by the supplier's date.
* Example:
*
* blahField.getProperties.put(REFERENCE_DATE_SUPPLIER, (Supplier<Date>) () -> otherDate);
*
* @see #connectAsReferenceDateSuppliers(FxTextComponent, FxTextComponent)
*/
public static final String REFERENCE_DATE_SUPPLIER = "referenceDateSupplier";
/**
* Number of years after the current year to automatically determine the century.
* If missing, the value 50 is used. Supposed we're in 2020 and the user enters 99,
* then the year would be expanded to 1999 and 30 would be expanded to 2030, accordingly.
* For special date fields, for example birthdates, the value should be 0,
* since it's rather unlikely to mean a birthdate in the future.
* A negative value disables this feature.
*
* Example:
*
* bkahField.getProperties().put(DateStringTranslator.YEAR_WINDOW, 30);
*
*/
public static final String YEAR_WINDOW = "yearWindow";
/**
* Same as YEAR_WINDOW, but for months.
* Valid values are 0 - 11.
* Example: If we're in April and enter 1225, it will be expanded to 12/25/<previous year>
* Again, for special date fields, for example the booking part of a financial accounting system, values of 1 or 2 may be
* appropriate.
* A negative value disables this feature, which is the default.
*
* Example:
*
* bkahField.getProperties().put(DateStringTranslator.MONTH_WINDOW, 2);
*
*/
public static final String MONTH_WINDOW = "monthWindow";
/**
* Same as YEAR_WINDOW, but for days.
* Valid values are 0 - 30.
* A negative value disables this feature, which is the default.
*
* Example:
*
* bkahField.getProperties().put(DateStringTranslator.DAY_WINDOW, 1);
*
*/
public static final String DAY_WINDOW = "dayWindow";
private static final Logger LOGGER = Logger.get(DateStringTranslator.class);
/**
* Determines how to handle information loss when a timestamp is edited
* by a date field without a time format.
* Primarily this is the log-level, but the level also controls what to log
* and/or when to throw an exception:
*
*
* - FINER: log with stacktrace and throw a {@link FxRuntimeException}
* - FINE: log with stacktrace
* - SEVERE: check disabled
* - all other levels: just log without stacktrace
*
*
* The default is INFO.
*
* The check can be turned off if the level of the logger does not
* cover the given check level.
*/
public static Logger.Level informationLossLogLevel = Logger.Level.INFO;
/**
* Connects two fields as reference date suppliers.
* Each field provides the reference date for the other one.
* Replaces code like:
*
* fromField.getProperties().put(DateStringTranslator.REFERENCE_DATE_SUPPLIER, (Supplier) () -> until);
* untilField.getProperties().put(DateStringTranslator.REFERENCE_DATE_SUPPLIER, (Supplier) () -> from);
*
* by
*
* DateStringTranslator.connectAsReferenceDateSuppliers(fromField, untilField);
*
* Throws {@link BindingException} if one of the fields is not bound.
*
* @param a the first component
* @param b the second component
*/
@SuppressWarnings("rawtypes")
public static void connectAsReferenceDateSuppliers(FxTextComponent a, FxTextComponent b) {
FxComponentBinding bindingA = a.getBinding();
FxComponentBinding bindingB = b.getBinding();
if (bindingA == null || bindingB == null) {
throw new BindingException("both components must be bound to the model");
}
((Node) a).getProperties().put(REFERENCE_DATE_SUPPLIER, (Supplier) bindingB::getModelValue);
((Node) b).getProperties().put(REFERENCE_DATE_SUPPLIER, (Supplier) bindingA::getModelValue);
}
private static final String LEGACY_DATE_DELIMITERS = ".,/*=-";
private static final String LEGACY_TIME_DELIMITERS = ":;\"'";
private String pattern; // the cached pattern
private String dateDelimiters; // delimiters used in the format string, in the order of appearance
private SimpleDateFormat format; // the cached format
private Date lastDate; // last processed date
/**
* Creates a translator.
*
* @param component the text component
*/
public DateStringTranslator(FxTextComponent component) {
super(component);
}
@Override
public Function toViewFunction() {
return this::format;
}
@Override
public Function toModelFunction() {
return s -> toType(parse(s));
}
/**
* Formats the given date.
*
* @param value the date value
* @return the formatted string
*/
protected String format(Date value) {
lastDate = value;
if (value != null) {
boolean infoLoss = false;
if (value instanceof Timestamp || value instanceof Time) {
if (!FormatHelper.isFormattingTime(getFormat())) {
infoLoss = true;
}
}
else {
if (!FormatHelper.isFormattingDate(getFormat())) {
infoLoss = true;
}
}
if (infoLoss &&
informationLossLogLevel != null &&
informationLossLogLevel != Logger.Level.SEVERE &&
LOGGER.isLoggable(informationLossLogLevel)) {
String msg = "possible information loss while formatting " + value.getClass().getName() + " '" + value +
"' with format " + getFormat().toPattern() + " in:\n" +
FxUtilities.getInstance().dumpComponentHierarchy((Node) getComponent());
FxRuntimeException uix = informationLossLogLevel == Logger.Level.FINE ||
informationLossLogLevel == Logger.Level.FINER ?
new FxRuntimeException(msg) : null;
LOGGER.log(informationLossLogLevel, uix == null ? msg : "", uix);
if (informationLossLogLevel == Logger.Level.FINER && uix != null) {
throw uix;
}
}
return getFormat().format(value);
}
return null;
}
/**
* Converts the date to the desired type.
*
* @param date the date
* @return the desired type
*/
protected Date toType(Date date) {
if (date != null) {
Class> type = getComponent().getType();
if (org.tentackle.common.Timestamp.class.isAssignableFrom(type)) {
org.tentackle.common.Timestamp ts = new org.tentackle.common.Timestamp(date.getTime());
if (getComponent().isUTC()) {
ts.setUTC(true);
}
return ts;
}
else if (org.tentackle.common.Time.class.isAssignableFrom(type)) {
return new org.tentackle.common.Time(date.getTime());
}
else if (org.tentackle.common.Date.class.isAssignableFrom(type)) {
return new org.tentackle.common.Date(date.getTime());
}
else if (java.sql.Timestamp.class.isAssignableFrom(type) || LocalDateTime.class.isAssignableFrom(type)) {
return new java.sql.Timestamp(date.getTime());
}
else if (java.sql.Time.class.isAssignableFrom(type) || LocalTime.class.isAssignableFrom(type)) {
return new java.sql.Time(date.getTime());
}
else if (java.sql.Date.class.isAssignableFrom(type) || LocalDate.class.isAssignableFrom(type)) {
return new java.sql.Date(date.getTime());
}
}
return date;
}
/**
* Parses a string.
*
* @param str the string
* @return the date
*/
protected Date parse(String str) {
if (str != null) {
Date referenceDate = null;
// decode format string, retry twice
for (int loop=0; loop < 3; loop++) {
getComponent().setError(null);
str = str.replace(getComponent().getFiller(), ' ').trim();
int slen = str.length();
if (slen == 0) {
return null;
}
if (str.charAt(0) == '@') {
str = str.substring(1, slen);
slen--;
@SuppressWarnings("rawtypes")
Supplier dateSupplier = getReferenceDateSupplier();
if (dateSupplier != null) {
Object refDate = dateSupplier.get();
if (refDate instanceof Date) {
referenceDate = (Date) refDate;
}
else if (refDate instanceof LocalDate) {
referenceDate = java.sql.Date.valueOf((LocalDate) refDate);
}
else if (refDate instanceof LocalTime) {
referenceDate = java.sql.Time.valueOf((LocalTime) refDate);
}
else if (refDate instanceof LocalDateTime) {
referenceDate = java.sql.Timestamp.valueOf((LocalDateTime) refDate);
}
if (slen == 0) {
str = "+0";
slen = 2;
}
}
else {
referenceDate = null;
}
}
if (str.charAt(0) == '$') {
str = str.substring(1, slen);
slen--;
referenceDate = lastDate;
}
getFormat(); // make sure format is initialized and up to date
boolean withDate = FormatHelper.isFormattingDate(format);
boolean withTime = FormatHelper.isFormattingTime(format);
if (slen == 1) {
// only one char: check for shortcut
char c = str.charAt(0);
if (withDate && LEGACY_DATE_DELIMITERS.indexOf(c) >= 0) {
// current date at 0:00:00
Calendar cal = DateHelper.today();
DateHelper.setMidnight(cal);
return parse(format(toType(cal.getTime()))); // start over
}
if (withTime && LEGACY_TIME_DELIMITERS.indexOf(c) >= 0) {
// current date and time
return parse(format(toType(new Date()))); // start over
}
// else not allowed
}
if (slen > 0 &&
(str.indexOf('-') == 0 || str.indexOf('+') == 0 ||
(slen <= 2 && StringHelper.isAllDigits(str)) ||
"sSmMhHdDwWyY".indexOf(str.charAt(slen-1)) >= 0)) {
/*
* current +/-Nt expression, i.e. current time plus or minus
* some seconds, minutes, hours, days, weeks, months or years.
* E.g.: +1d
* The type defaults to 'd' if the type is Date or Timestamp
* and to the most significant value according to the format
* if the type is Time (usually 'h').
* The + can also be ommitted for 1 or 2-digit numbers and means
* 'set' instead of 'add'.
* I.e. 17 means 17th of current month (if date-format) or
* 12h means 12:00
*/
boolean setValue = Character.isDigit(str.charAt(0)); // true = set instead of add
try {
Calendar cal = DateHelper.today();
if (referenceDate != null) {
cal.setTime(referenceDate);
}
char type = str.charAt(slen-1);
int value;
if (Character.isDigit(type)) {
// missing type: determine according to model type and/or format
if (Time.class.isAssignableFrom(getComponent().getType()) ||
LocalTime.class.isAssignableFrom(getComponent().getType())) {
if (pattern.indexOf('H') >= 0 || pattern.indexOf('h') >= 0) {
type = 'h';
}
else if (pattern.indexOf('m') >= 0) {
type = 'm';
}
else if (pattern.indexOf('s') >= 0) {
type = 's';
}
else {
type = 'h';
}
}
else { // date or timestamp
type = 'd';
}
value = Integer.parseInt(str.charAt(0) == '+' ? str.substring(1) : str);
}
else {
value = Integer.parseInt(str.substring(str.charAt(0) == '+' ? 1 : 0, slen-1));
}
if (setValue) {
switch (type) {
case 's':
case 'S':
setCalendarValue(cal, SECOND, value);
break;
case 'm':
if (pattern.indexOf('m') == -1) {
// meant month (m entered instead of M)
setCalendarValue(cal, MONTH, value - 1);
}
else {
setCalendarValue(cal, MINUTE, value);
cal.set(SECOND, 0);
}
break;
case 'h':
case 'H':
setCalendarValue(cal, HOUR_OF_DAY, value);
cal.set(MINUTE, 0);
cal.set(SECOND, 0);
break;
case 'd':
case 'D':
int month = deriveMonthFromDay(value);
if (month == -1) {
setCalendarValue(cal, YEAR, DateHelper.getCurrentYear() - 1);
month = 11;
}
else if (month == 12) {
setCalendarValue(cal, YEAR, DateHelper.getCurrentYear() + 1);
month = 0;
}
setCalendarValue(cal, MONTH, month);
setCalendarValue(cal, DAY_OF_MONTH, value);
DateHelper.setMidnight(cal);
break;
case 'w':
case 'W':
setCalendarValue(cal, WEEK_OF_YEAR, value);
cal.set(DAY_OF_WEEK, cal.getFirstDayOfWeek());
DateHelper.setMidnight(cal);
break;
case 'M':
setCalendarValue(cal, MONTH, value - 1);
cal.set(DAY_OF_MONTH, 1);
DateHelper.setMidnight(cal);
break;
case 'y':
case 'Y':
if (value < 100) {
value = derive4DigitYearFrom2DigitYear(value);
}
setCalendarValue(cal, YEAR, value);
cal.set(DAY_OF_YEAR, 1);
DateHelper.setMidnight(cal);
break;
}
}
else {
switch (type) {
case 's':
case 'S':
cal.add(SECOND, value);
break;
case 'm':
if (pattern.indexOf('m') == -1) {
// meant month (m entered instead of M)
cal.add(MONTH, value);
}
else {
cal.add(MINUTE, value);
}
break;
case 'h':
case 'H':
cal.add(HOUR, value);
break;
case 'd':
case 'D':
cal.add(DATE, value);
DateHelper.setMidnight(cal);
break;
case 'w':
case 'W':
cal.add(WEEK_OF_YEAR, value);
DateHelper.setMidnight(cal);
break;
case 'M':
cal.add(MONTH, value);
DateHelper.setMidnight(cal);
break;
case 'y':
case 'Y':
cal.add(YEAR, value);
DateHelper.setMidnight(cal);
break;
}
}
// start over
return parse(format(toType(cal.getTime())));
}
catch (ParseException e) {
getComponent().setErrorOffset(e.getErrorOffset());
getComponent().setError(e.getMessage());
getComponent().setErrorTemporary(true);
return null; // start over
}
catch (RuntimeException e) {
// fall through...
}
}
try {
// parse input
Date date = format.parse(str);
Calendar cal = DateHelper.today();
cal.setTime(date);
// cut time information if format does not contain time
if (!withTime) {
DateHelper.setMidnight(cal);
date = cal.getTime();
}
// expand 2-digit year to 4-digits, e.g. 66 to 1966 and 02 to 2002
int year = cal.get(YEAR);
if (year < 100) {
// user entered 66 instead of 1966
year = derive4DigitYearFrom2DigitYear(year);
cal.set(YEAR, year);
date = cal.getTime();
} // else user entered a 4-digit year
return date;
}
catch (ParseException e) {
getComponent().setErrorOffset(e.getErrorOffset());
getComponent().setError(MessageFormat.format(withDate ?
FxFxBundle.getString("INVALID DATE: {0}") :
FxFxBundle.getString("INVALID TIME: {0}"),
str));
getComponent().setErrorTemporary(true);
char errorChar = 0;
if (e.getErrorOffset() > 0 && e.getErrorOffset() == slen) {
errorChar = str.charAt(e.getErrorOffset() - 1);
}
// check for user entered 1.1. and meant 1.1.
if (errorChar > 0 && dateDelimiters.indexOf(errorChar) >= 0) {
// last char was a date-delimiter: try appending current year
str += DateHelper.getCurrentYear();
}
else if (errorChar == ':') {
// last char was a time-delimiter: try appending 00
str += "00";
}
else if ((slen > 2 && Character.isDigit(errorChar) && Character.isDigit(str.charAt(slen - 2)) &&
(str.charAt(slen - 3) == ':' || str.charAt(slen - 3) == ' ')) ||
(slen > 1 && Character.isDigit(errorChar) &&
(str.charAt(slen - 2) == ':' || str.charAt(slen - 2) == ' '))) {
// ends with ":NN", ":N", " NN" or " N" -> add another ":00"
str += ":00";
}
else { // check for user omitted the delimiters at all, e.g. 0105
StringBuilder nBuf = new StringBuilder(); // new generated input
int dlen = dateDelimiters.length(); // length of delimiters
int spos = 0; // index in user input
int dpos = 0; // index in delimiters of format
while (spos < slen) {
char c = str.charAt(spos);
if (dateDelimiters.indexOf(c) >= 0 || LEGACY_DATE_DELIMITERS.indexOf(c) >= 0) {
break; // some delimiter, real error
}
if (dpos < dlen && spos > 0 && spos % 2 == 0) {
// insert delimiter
nBuf.append(dateDelimiters.charAt(dpos++));
}
nBuf.append(c);
spos++;
}
if (spos == slen) { // delimiters inserted
if (slen % 2 == 0 && dpos < dlen) {
nBuf.append(dateDelimiters.charAt(dpos));
}
if (nBuf.length() == 6) { // day + month. and year missing?
nBuf.append(deriveYearFromInput(nBuf.toString()));
}
str = nBuf.toString();
}
else {
// try if time entered only: add current date.
// the colon is international (at least in western countries)
boolean timeOnly = true;
int colonCount = 0;
for (int i=0; i < slen; i++) {
char c = str.charAt(i);
if (c == ':') {
colonCount++;
}
else if (!Character.isDigit(c)) {
timeOnly = false;
break;
}
}
if (timeOnly) {
try {
Calendar cal = DateHelper.today();
cal.setTime(colonCount == 1 ? FormatHelper.parseShortTime(str) : FormatHelper.parseTime(str));
int hour = cal.get(HOUR_OF_DAY);
int minute = cal.get(MINUTE);
int second = cal.get(SECOND);
cal.setTime(new Date()); // today
cal.set(HOUR_OF_DAY, hour);
cal.set(MINUTE, minute);
cal.set(SECOND, second);
getComponent().setError(null);
return cal.getTime();
}
catch (ParseException ex) {
// did not work
}
}
else {
// try appending 00:00:00 if only date entered (there is a small chance ;-)
String newstr = str + " 00:00:00";
try {
// just parse
format.parse(newstr);
// worked!
str = newstr;
// start over
}
catch (ParseException ex) {
// try to replace legacy delimiters to the (first) date delimiter
if (dateDelimiters.length() > 0) {
StringBuilder buf = new StringBuilder(str);
String delimStr = dateDelimiters.substring(0, 1);
for (int i=0; i < buf.length(); i++) {
char c = buf.charAt(i);
if (LEGACY_DATE_DELIMITERS.indexOf(c) >= 0) {
buf.replace(i, i+1, delimStr);
}
}
newstr = buf.toString();
if (!newstr.equals(str)) {
try {
// just parse
format.parse(newstr);
// worked!
str = newstr;
// start over
}
catch (ParseException ex2) {
// nice try but didn't work out
}
}
}
}
}
}
}
}
} // start over
}
return null;
}
/**
* Gets the default pattern according to the type.
*
* @return the pattern
*/
protected String getDefaultPattern() {
String pat;
// derive from type
Class> type = getComponent().getType();
if (Time.class.isAssignableFrom(type) || LocalTime.class.isAssignableFrom(type)) {
pat = FormatHelper.getTimePattern();
}
else if (Timestamp.class.isAssignableFrom(type) || LocalDateTime.class.isAssignableFrom(type)) {
pat = FormatHelper.getTimestampPattern();
}
else {
pat = FormatHelper.getDatePattern();
}
return pat;
}
/**
* Gets the date format.
*
* @return the date format
*/
protected SimpleDateFormat getFormat() {
String pat = getComponent().getPattern();
if (pat == null) {
pat = getDefaultPattern();
}
if (format == null || !pat.equals(pattern)) {
pattern = pat;
format = new SimpleDateFormat(pattern);
format.setLenient(getComponent().isLenient());
}
// extract date-delimiters
StringBuilder buf = new StringBuilder();
String f = format.toPattern();
for (int i=0; i < f.length(); i++) {
char c = f.charAt(i);
if (c != ':' && !Character.isLetterOrDigit(c)) {
buf.append(c);
}
}
dateDelimiters = buf.toString();
return format;
}
/**
* Sets the calendar value and checks whether the value is valid if date format is not lenient.
*
* @param cal the gregorian calendar object
* @param field the field index
* @param value the value
* @throws ParseException if value out of bounds (if not lenient)
*/
protected void setCalendarValue(Calendar cal, int field, int value) throws ParseException {
// check the bounds
int min = cal.getActualMinimum(field);
int max = cal.getActualMaximum(field);
if (value < min || value > max) {
if (field == MONTH) {
value++;
min++;
max++;
}
throw new ParseException(
MessageFormat.format(FxFxBundle.getString("INVALID {0}: {1} MUST BE BETWEEN {2} AND {3}"),
FormatHelper.calendarFieldToString(field, false), value, min, max),
0);
}
cal.set(field, value);
}
/**
* Gets the reference date supplier.
*
* @return the date supplier, null if none
*/
@SuppressWarnings("rawtypes")
protected Supplier getReferenceDateSupplier() {
return (Supplier) ((Node) getComponent()).getProperties().get(REFERENCE_DATE_SUPPLIER);
}
/**
* Gets the year window.
*
* @return the year window (0-99, all other values disable this feature)
*/
protected int getYearWindow() {
Integer val = (Integer) ((Node) getComponent()).getProperties().get(YEAR_WINDOW);
return val == null ? getDefaultYearWindow() : val;
}
/**
* Gets the default value for {@link #getYearWindow()}.
*
* @return the default (50 years)
*/
protected int getDefaultYearWindow() {
return 50;
}
/**
* Converts a short 2-digit year to a 4-digit year.
*
* @param year2 the 2-digit year
* @return the 4-digit year
* @see #getYearWindow()
*/
protected int derive4DigitYearFrom2DigitYear(int year2) {
return DateHelper.derive4DigitYearFrom2DigitYear(year2, DateHelper.getCurrentYear(), getYearWindow());
}
/**
* Gets the month window.
*
* @return the month window (0-11, all other values disable this feature)
*/
protected int getMonthWindow() {
Integer val = (Integer) ((Node) getComponent()).getProperties().get(MONTH_WINDOW);
return val == null ? getDefaultMonthWindow() : val;
}
/**
* Gets the default month window for {@link #getMonthWindow()}.
*
* @return the default (-1 == disabled)
*/
protected int getDefaultMonthWindow() {
return -1;
}
/**
* Derives the year from the user's input, if only month and day are given.
*
* @param input in the format NN.NN. where NN is a number and . is a date delimiter
* @return the default year according to the month window
* @see #getMonthWindow()
*/
protected int deriveYearFromInput(String input) {
try {
String fmt = format.toPattern();
int dayNdx = fmt.indexOf('d');
int monthNdx = fmt.indexOf('M');
int month = monthNdx < dayNdx ? Integer.parseInt(input.substring(0, 2)) : Integer.parseInt(input.substring(3, 5)); // 1 - 12
return DateHelper.deriveYearFromMonth((month - 1) % 12, DateHelper.getCurrentMonth(), DateHelper.getCurrentYear(), getMonthWindow());
}
catch (RuntimeException rx) {
// is okay, take current year
}
return DateHelper.getCurrentYear();
}
/**
* Gets the day window.
*
* @return the day window, (0-30, all other values disable this feature)
*/
protected int getDayWindow() {
Integer val = (Integer) ((Node) getComponent()).getProperties().get(DAY_WINDOW);
return val == null ? getDefaultDayWindow() : val;
}
/**
* Gets the default day window for {@link #getDayWindow()}.
*
* @return the default (-1 == disabled)
*/
protected int getDefaultDayWindow() {
return -1;
}
/**
* Derives the the month from a day input.
*
* @param day the day (1-31)
* @return the closest month (0-11 for current year, -1 for december of last year, 12 for january of next year)
* @see #getDayWindow()
*/
protected int deriveMonthFromDay(int day) {
return DateHelper.deriveMonthFromDay(day, DateHelper.getCurrentDay(), DateHelper.getCurrentMonth(), getDayWindow());
}
}