/* * Copyright (c) 2017 OBiBa. All rights reserved. * * This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.obiba.magma.js.methods; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.GregorianCalendar; import javax.annotation.Nullable; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.Scriptable; import org.obiba.magma.MagmaDate; import org.obiba.magma.Value; import org.obiba.magma.ValueSequence; import org.obiba.magma.js.MagmaJsEvaluationRuntimeException; import org.obiba.magma.js.ScriptableValue; import org.obiba.magma.type.BooleanType; import org.obiba.magma.type.DateTimeType; import org.obiba.magma.type.DateType; import org.obiba.magma.type.IntegerType; import org.obiba.magma.type.TextType; /** * Methods of the {@code ScriptableValue} javascript class that deal with {@code ScriptableValue} of {@code DateType}. */ @SuppressWarnings("UnusedDeclaration") public class DateTimeMethods { private DateTimeMethods() { } /** * <pre> * $('Date').year() * </pre> */ public static Scriptable year(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.YEAR); } /** * Returns the month of a Date as an integer starting from 0 (January). * <p/> * <pre> * $('Date').month() * </pre> */ public static Scriptable month(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.MONTH); } /** * Returns the quarter of a Date as an integer starting from 0 (January-March) * <p/> * <pre> * $('Date').quarter() * </pre> */ public static Scriptable quarter(Context cx, Scriptable thisObj, Object[] args, Function funObj) { Value currentValue = ((ScriptableValue) thisObj).getValue(); if(currentValue.isSequence()) { if(currentValue.isNull()) { return new ScriptableValue(thisObj, IntegerType.get().nullSequence()); } Collection<Value> newValues = new ArrayList<>(); for(Value value : currentValue.asSequence().getValue()) { newValues.add(quarter(value)); } return new ScriptableValue(thisObj, IntegerType.get().sequenceOf(newValues)); } else { return new ScriptableValue(thisObj, quarter(currentValue)); } } private static Value quarter(Value value) { Calendar c = asCalendar(value); if(c != null) { int month = c.get(Calendar.MONTH); int quarter = 3; if(month < 3) { quarter = 0; } else if(month < 6) { quarter = 1; } else if(month < 9) { quarter = 2; } return IntegerType.get().valueOf(quarter); } return IntegerType.get().nullValue(); } /** * Returns the semester of a Date as an integer starting from 0 (January-June) * <p/> * <pre> * $('Date').semester() * </pre> */ public static Scriptable semester(Context cx, Scriptable thisObj, Object[] args, Function funObj) { Value currentValue = ((ScriptableValue) thisObj).getValue(); if(currentValue.isSequence()) { if(currentValue.isNull()) { return new ScriptableValue(thisObj, IntegerType.get().nullSequence()); } Collection<Value> newValues = new ArrayList<>(); for(Value value : currentValue.asSequence().getValue()) { newValues.add(semester(value)); } return new ScriptableValue(thisObj, IntegerType.get().sequenceOf(newValues)); } else { return new ScriptableValue(thisObj, semester(currentValue)); } } private static Value semester(Value value) { Calendar c = asCalendar(value); if(c != null) { int month = c.get(Calendar.MONTH); int semester = 1; if(month < 6) { semester = 0; } return IntegerType.get().valueOf(semester); } return IntegerType.get().nullValue(); } /** * Returns the day of week from a Date as an integer starting from 1 (Sunday). * <p/> * <pre> * $('Date').dayOfWeek() * </pre> */ public static Scriptable dayOfWeek(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.DAY_OF_WEEK); } /** * Returns a boolean value indicating whether the date denotes a weekday (between Monday and Friday inclusively) * <p/> * <pre> * $('Date').weekday() * </pre> */ public static Scriptable weekday(Context cx, Scriptable thisObj, Object[] args, Function funObj) { Value currentValue = ((ScriptableValue) thisObj).getValue(); if(currentValue.isSequence()) { if(currentValue.isNull()) { return new ScriptableValue(thisObj, BooleanType.get().nullSequence()); } Collection<Value> newValues = new ArrayList<>(); for(Value value : currentValue.asSequence().getValue()) { newValues.add(weekday(value)); } return new ScriptableValue(thisObj, BooleanType.get().sequenceOf(newValues)); } else { return new ScriptableValue(thisObj, weekday(currentValue)); } } private static Value weekday(Value value) { Calendar c = asCalendar(value); if(c != null) { int dow = c.get(Calendar.DAY_OF_WEEK); return BooleanType.get().valueOf(dow > Calendar.SUNDAY && dow < Calendar.SATURDAY); } return BooleanType.get().nullValue(); } /** * Returns a boolean value indicating whether the date denotes a weekend (either Sunday or Saturday) * <p/> * <pre> * $('Date').weekend() * </pre> */ public static Scriptable weekend(Context cx, Scriptable thisObj, Object[] args, Function funObj) { Value currentValue = ((ScriptableValue) thisObj).getValue(); if(currentValue.isSequence()) { if(currentValue.isNull()) { return new ScriptableValue(thisObj, BooleanType.get().nullSequence()); } Collection<Value> newValues = new ArrayList<>(); for(Value value : currentValue.asSequence().getValue()) { newValues.add(weekend(value)); } return new ScriptableValue(thisObj, BooleanType.get().sequenceOf(newValues)); } else { return new ScriptableValue(thisObj, weekend(currentValue)); } } private static Value weekend(Value value) { Calendar c = asCalendar(value); if(c != null) { int dow = c.get(Calendar.DAY_OF_WEEK); return BooleanType.get().valueOf(dow < Calendar.MONDAY || dow > Calendar.FRIDAY); } return BooleanType.get().nullValue(); } /** * Returns the day of month from a Date as an integer starting from 1 * <p/> * <pre> * $('Date').dayOfMonth() * </pre> */ public static Scriptable dayOfMonth(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.DAY_OF_MONTH); } /** * Returns the day of year from a Date as an integer starting from 1 * <p/> * <pre> * $('Date').dayOfYear() * </pre> */ public static Scriptable dayOfYear(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.DAY_OF_YEAR); } /** * Returns the week of year from a Date as an integer starting from 1 * <p/> * <pre> * $('Date').weekOfYear() * </pre> */ public static Scriptable weekOfYear(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.WEEK_OF_YEAR); } /** * Returns the week of month from a Date as an integer starting from 1 * <p/> * <pre> * $('Date').weekOfMonth() * </pre> */ public static Scriptable weekOfMonth(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.WEEK_OF_MONTH); } /** * Returns the hour of the day for the 24-hour clock. For example, at 10:04:15.250 PM the hour of the day is 22. * <p/> * <pre> * $('Date').hourOfDay() * </pre> */ public static Scriptable hourOfDay(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.HOUR_OF_DAY); } /** * Returns the hour of the day for the 12-hour clock (0 - 11). Noon and midnight are represented by 0, not by 12. For * example, at 10:04:15.250 PM the HOUR is 10. * <p/> * <pre> * $('Date').hour() * </pre> */ public static Scriptable hour(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.HOUR); } /** * Returns the minute within the hour. * <p/> * <pre> * $('Date').minute() * </pre> */ public static Scriptable minute(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.MINUTE); } /** * Returns the second within the minute. * <p/> * <pre> * $('Date').second() * </pre> */ public static Scriptable second(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.SECOND); } /** * Returns the millisecond within the second. * <p/> * <pre> * $('Date').millisecond() * </pre> */ public static Scriptable millisecond(Context cx, Scriptable thisObj, Object[] args, Function funObj) { return asScriptable(thisObj, thisObj, Calendar.MILLISECOND); } /** * Returns the number of milliseconds since January 1, 1970, 00:00:00 GMT (epoch time). * <p/> * <pre> * $('Date').time() * </pre> */ public static Scriptable time(Context cx, Scriptable thisObj, Object[] args, Function funObj) { Value currentValue = ((ScriptableValue) thisObj).getValue(); if(currentValue.isSequence()) { if(currentValue.isNull()) { return new ScriptableValue(thisObj, IntegerType.get().nullSequence()); } Collection<Value> newValues = new ArrayList<>(); for(Value value : currentValue.asSequence().getValue()) { newValues.add(timeValue(value)); } return new ScriptableValue(thisObj, TextType.get().sequenceOf(newValues)); } else { return new ScriptableValue(thisObj, timeValue(currentValue)); } } private static Value timeValue(Value value) { if(value.isNull()) return IntegerType.get().nullValue(); //noinspection ConstantConditions return IntegerType.get().valueOf(asDate(value).getTime()); } /** * Returns the text representation of the date formatted as specified by the provided pattern. * <p/> * <pre> * $('Date').format('dd/MM/yyyy') * </pre> * * @see java.text.SimpleDateFormat */ public static Scriptable format(Context cx, Scriptable thisObj, Object[] args, Function funObj) { Value currentValue = ((ScriptableValue) thisObj).getValue(); if(currentValue.isSequence()) { if(currentValue.isNull()) { return new ScriptableValue(thisObj, TextType.get().nullSequence()); } Collection<Value> newValues = new ArrayList<>(); for(Value value : currentValue.asSequence().getValue()) { newValues.add(format(value, args)); } return new ScriptableValue(thisObj, TextType.get().sequenceOf(newValues)); } else { return new ScriptableValue(thisObj, format(currentValue, args)); } } @SuppressWarnings({ "ChainOfInstanceofChecks", "PMD.NcssMethodCount" }) private static Value format(Value value, Object... args) { if(args == null || args.length == 0) { return TextType.get().nullValue(); } Date date = asDate(value); if(date == null) { return TextType.get().nullValue(); } SimpleDateFormat format = null; Object arg = args[0]; if(arg instanceof ScriptableValue) { ScriptableValue operand = (ScriptableValue) arg; if(operand.getValue().isSequence()) { throw new MagmaJsEvaluationRuntimeException("Argument to format() method must not be a sequence of values."); } if(operand.getValue().isNull()) { return TextType.get().nullValue(); } format = new SimpleDateFormat(arg.toString()); } else if(arg instanceof String) { format = new SimpleDateFormat((String) arg); } else { throw new MagmaJsEvaluationRuntimeException("Argument to format() method must be a String or a ScriptableValue."); } return TextType.get().valueOf(format.format(date)); } /** * Returns true if this Date value is after the specified date value(s) * <p/> * <pre> * $('Date').after($('OtherDate')) * $('Date').after($('OtherDate'), $('SomeOtherDate')) * </pre> */ public static Scriptable after(Context cx, Scriptable thisObj, Object[] args, @Nullable Function funObj) { if(args == null || args.length == 0) { return new ScriptableValue(thisObj, BooleanType.get().falseValue()); } Value currentValue = ((ScriptableValue) thisObj).getValue(); if(currentValue.isSequence()) { if(currentValue.isNull()) { return new ScriptableValue(thisObj, BooleanType.get().nullSequence()); } Collection<Value> newValues = new ArrayList<>(); for(Value value : currentValue.asSequence().getValue()) { newValues.add(after(value, args)); } return new ScriptableValue(thisObj, BooleanType.get().sequenceOf(newValues)); } return new ScriptableValue(thisObj, after(currentValue, args)); } private static Value after(Value value, Object... args) { Calendar thisCalendar = asCalendar(value); if(thisCalendar == null) { return BooleanType.get().nullValue(); } for(Object arg : args) { if(arg instanceof ScriptableValue) { ScriptableValue operand = (ScriptableValue) arg; if(operand.getValue().isSequence()) { throw new MagmaJsEvaluationRuntimeException("Operand to after() method must not be a sequence of values."); } Calendar c = asCalendar(operand.getValue()); if(c == null) { return BooleanType.get().nullValue(); } if(thisCalendar.before(c)) { return BooleanType.get().falseValue(); } } else { throw new MagmaJsEvaluationRuntimeException("Operand to after() method must be a ScriptableValue."); } } return BooleanType.get().trueValue(); } @Nullable private static Date asDate(Value value) { if(value.getValueType() == DateTimeType.get()) { if(!value.isNull()) { return (Date) value.getValue(); } } else if(value.getValueType() == DateType.get()) { if(!value.isNull()) { return ((MagmaDate) value.getValue()).asDate(); } } else { throw new MagmaJsEvaluationRuntimeException( "Invalid ValueType: expected '" + DateTimeType.get().getName() + "' or '" + DateType.get().getName() + "' got '" + value.getValueType().getName() + "'"); } return null; } /** * Converts a {@code Value} instance to a {@code Calendar} instance. If {@code Value#isNull()} returns true, this * method returns null. * * @param value * @return */ @Nullable private static Calendar asCalendar(Value value) { if(value.getValueType() == DateTimeType.get()) { if(!value.isNull()) { Date date = (Date) value.getValue(); Calendar c = GregorianCalendar.getInstance(); c.setTimeInMillis(date.getTime()); return c; } } else if(value.getValueType() == DateType.get()) { if(!value.isNull()) { return ((MagmaDate) value.getValue()).asCalendar(); } } else { throw new MagmaJsEvaluationRuntimeException( "Invalid ValueType: expected '" + DateTimeType.get().getName() + "' or '" + DateType.get().getName() + "' got '" + value.getValueType().getName() + "'"); } return null; } /** * Given a {@code ScriptableValue}, this method extracts a {@code field} from the Calendar. * * @param scope * @param sv * @param field * @return */ private static Scriptable asScriptable(Scriptable scope, Scriptable sv, int field) { Value currentValue = ((ScriptableValue) sv).getValue(); if(currentValue.isSequence()) { return asScriptable(scope, currentValue.asSequence(), field); } else { Calendar c = asCalendar(currentValue); if(c != null) { return asScriptable(scope, c.get(field)); } return new ScriptableValue(scope, IntegerType.get().nullValue()); } } private static Scriptable asScriptable(Scriptable scope, ValueSequence currentValue, int field) { if(currentValue.isNull()) { return new ScriptableValue(scope, IntegerType.get().nullSequence()); } Collection<Value> newValues = new ArrayList<>(); for(Value value : currentValue.asSequence().getValue()) { Calendar c = asCalendar(value); if(c != null) { newValues.add(IntegerType.get().valueOf(c.get(field))); } else { newValues.add(IntegerType.get().nullValue()); } } return new ScriptableValue(scope, IntegerType.get().sequenceOf(newValues)); } private static Scriptable asScriptable(Scriptable scope, int value) { return new ScriptableValue(scope, IntegerType.get().valueOf(value)); } /** * Adds days to a {@code ScriptableValue} of {@code DateType}. * <p/> * <pre> * $('Date').add(2) // Adds 2 days. * $('Date').add(-4) // Subtracts 4 days. * </pre> */ public static Scriptable add(Context cx, Scriptable thisObj, Object[] args, Function funObj) { if(args.length != 1) { throw new UnsupportedOperationException(".add() expects exactly one integer argument: days to add."); } Value currentValue = ((ScriptableValue) thisObj).getValue(); if(currentValue.isSequence()) { if(currentValue.isNull()) { return new ScriptableValue(thisObj, DateTimeType.get().nullSequence()); } Collection<Value> newValues = new ArrayList<>(); for(Value value : currentValue.asSequence().getValue()) { newValues.add(add(value, args)); } return new ScriptableValue(thisObj, DateTimeType.get().sequenceOf(newValues)); } return new ScriptableValue(thisObj, add(currentValue, args)); } private static Value add(Value cvalue, Object... args) { Calendar c = asCalendar(cvalue); if(c != null) { int argument = 0; if(args[0] instanceof ScriptableValue) { Value value = ((ScriptableValue) args[0]).getValue(); if(!value.isNull()) { argument = Integer.parseInt(value.getValue().toString()); } } else { argument = ((Number) args[0]).intValue(); } c.add(Calendar.DAY_OF_MONTH, argument); return DateTimeType.get().valueOf(c); } return cvalue; } }