All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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.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.log.Logger;
import org.tentackle.misc.DateHelper;
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.Date;
import java.util.GregorianCalendar;
import java.util.function.Function;
import java.util.function.Supplier;

import static java.util.Calendar.*;

/**
 * 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 { /** * Property for a reference date supplier of type Supplier<Date>.
* '@' as input will be replaced by that date. */ public static final String REFERENCE_DATE_SUPPLIER = "referenceDateSupplier"; 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; private static final String LEGACY_DATE_DELIMITERS = ".,/*=-"; private static final String LEGACY_TIME_DELIMITERS = ":;\"'"; private String pattern; // the cached pattern private String dateDelimiters; // usually . or / 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().setErrorOffset(null); 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({ "unchecked", "rawtypes" }) Supplier dateSupplier = (Supplier) ((Node) getComponent()).getProperties().get(REFERENCE_DATE_SUPPLIER); 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 GregorianCalendar cal = new GregorianCalendar(); 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 { GregorianCalendar cal = new GregorianCalendar(); 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': setGregorianValue(cal, SECOND, value); break; case 'm': if (pattern.indexOf('m') == -1) { // meant month (m entered instead of M) setGregorianValue(cal, MONTH, value - 1); } else { setGregorianValue(cal, MINUTE, value); cal.set(SECOND, 0); } break; case 'h': case 'H': setGregorianValue(cal, HOUR_OF_DAY, value); cal.set(MINUTE, 0); cal.set(SECOND, 0); break; case 'd': case 'D': setGregorianValue(cal, DAY_OF_MONTH, value); DateHelper.setMidnight(cal); break; case 'w': case 'W': setGregorianValue(cal, WEEK_OF_YEAR, value); cal.set(DAY_OF_WEEK, cal.getFirstDayOfWeek()); DateHelper.setMidnight(cal); break; case 'M': setGregorianValue(cal, MONTH, value - 1); cal.set(DAY_OF_MONTH, 1); DateHelper.setMidnight(cal); break; case 'y': case 'Y': if (value < 100) { value = convert2DigitYearTo4DigitYear(value); } setGregorianValue(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()); return null; // start over } catch (RuntimeException e) { // fall through... } } try { // parse input Date date = format.parse(str); GregorianCalendar cal = new GregorianCalendar(); 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 = convert2DigitYearTo4DigitYear(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(FxFxBundle.getString( withDate ? "INVALID DATE: {0}" : "INVALID TIME: {0}"), str)); 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 += new GregorianCalendar().get(YEAR); } 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 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(new GregorianCalendar().get(YEAR)); } 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 { GregorianCalendar cal = new GregorianCalendar(); 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().setErrorOffset(null); 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 gregorian 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 setGregorianValue(GregorianCalendar 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); } /** * Converts a short 2-digit year to a 4-digit year. * * @param year2 the 2-digit year * @return the 4-digit year */ protected int convert2DigitYearTo4DigitYear(int year2) { return DateHelper.convert2DigitYearTo4DigitYear(year2, new GregorianCalendar().get(YEAR)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy