package routines; import java.lang.Math; import java.text.ParseException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.ArrayList; import java.util.Locale; /* * user specification: the function's comment should contain keys as follows: 1. write about the function's comment.but * it must be before the "{talendTypes}" key. * * 2. {talendTypes} 's value must be talend Type, it is required . its value should be one of: String, char | Character, * long | Long, int | Integer, boolean | Boolean, byte | Byte, Date, double | Double, float | Float, Object, short | * Short * * 3. {Category} define a category for the Function. it is required. its value is user-defined . * * 4. {param} 's format is: {param} [()] [ : ] * * 's value should be one of: string, int, list, double, object, boolean, long, char, date. 's value is the * Function's parameter name. the {param} is optional. so if you the Function without the parameters. the {param} don't * added. you can have many parameters for the Function. * * 5. {example} gives a example for the Function. it is optional. */ public class LcagDateTime { private static LcagLogger log = LcagLogger.getLogger(LcagDateTime.class); public static String MDM_DATE_FORMAT_STR = "yyyy-MM-dd"; public static String MDM_DATETIME_FORMAT_STR = "yyyy-MM-dd'T'HH:mm:ss"; private static final String DATETIME_FORMAT_ISO8601_UTC_STR = "yyyy-MM-dd'T'HH:mm:ss'Z'"; public static final String TALEND_INTERNAL_DATE_FORMAT_STR = "yyyy-MM-dd"; // Used as file prefix for all relevant MDM job files public static final String MDM_FILE_TIMESTAMP_FORMAT_STR = "yyyy-MM-dd_HH-mm-ss-SSS'Z'"; // Used as file prefix for all files sent to MDM consumers public static final String MDM_CONSUMER_FILE_TIMESTAMP_FORMAT_STR = "yyyy-MM-dd_HH-mm-ss-SSS"; // Used in MDM messages sent to consumer queue. public static final String MDM_CONSUMER_TIMESTAMP_FORMAT_STR = DATETIME_FORMAT_ISO8601_UTC_STR; // Used in Talend UI XML trigger messages and as Default format for Date to String Conversions public static final String TALEND_INTERNAL_DATETIME_FORMAT_STR = "yyyy-MM-dd HH:mm:ss"; // Deprecated: Do not use these in Jobs or other code, use Standard MDM formats defined in next paragraph public static SimpleDateFormat DATETIME_FORMAT_ISO8601_UTC = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); public static SimpleDateFormat DATETIME_FORMAT_ISO8601_UTC_FILE = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss'Z'"); public static SimpleDateFormat DATE_FORMAT_ISO8601_UTC = new SimpleDateFormat("yyyy-MM-dd'Z'"); public static SimpleDateFormat DATE_FORMAT_ISO8601 = new SimpleDateFormat("yyyy-MM-dd"); public static final SimpleDateFormat MDM_FILE_TIMESTAMP_FORMAT = new SimpleDateFormat(MDM_FILE_TIMESTAMP_FORMAT_STR); // Used as file prefix for all files sent to MDM consumers public static final SimpleDateFormat MDM_CONSUMER_FILE_TIMESTAMP_FORMAT = new SimpleDateFormat(MDM_CONSUMER_FILE_TIMESTAMP_FORMAT_STR); // Used in MDM messages sent to consumer queue. public static final SimpleDateFormat MDM_CONSUMER_TIMESTAMP_FORMAT = new SimpleDateFormat(MDM_CONSUMER_TIMESTAMP_FORMAT_STR); // Used in Talend UI XML trigger messages and as Default format for Date to String Conversions public static final SimpleDateFormat TALEND_INTERNAL_DATETIME_FORMAT = new SimpleDateFormat(TALEND_INTERNAL_DATETIME_FORMAT_STR); public static final SimpleDateFormat TALEND_INTERNAL_DATE_FORMAT = new SimpleDateFormat(TALEND_INTERNAL_DATE_FORMAT_STR); // Standard MDM Date format to use in all Jobs public static SimpleDateFormat MDM_DATE_FORMAT = new SimpleDateFormat(MDM_DATE_FORMAT_STR); // Standard MDM Timestamp format to use in all Jobs for LastUpdateTimestamp public static SimpleDateFormat MDM_DATETIME_FORMAT = new SimpleDateFormat(MDM_DATETIME_FORMAT_STR); public final static class PeriodCompareResult { // Set default values for result when comparing to null dates: public boolean EXTENDS_BEFORE = true; /* A extends B directly before B */ public boolean EXTENDS_AFTER = true; /* extends B directly after B */ public boolean EXTENDS = true; /* extends before or after directly adjactent */ public boolean OVERLAP = false; /* fully or partially overlap with each other */ public boolean OVERLAPS_WITH_START = false; /* A overlaps with start of B */ public boolean OVERLAPS_WITH_END = false; /* A overlaps with end of B */ public boolean CONTAINS = false; /* A fully contains B */ public boolean CONTAINS_AT_START = false; /* A fully contains B starting at start of A */ public boolean CONTAINS_AT_END = false; /* A fully contains B at end at A */ public boolean IS_CONTAINED = false; /* B fully contains A */ public boolean HAS_GAP_BEFORE = false; public boolean HAS_GAP_AFTER = false; public boolean HAVE_GAP = false; /* There is a gap between A and B */ public boolean EQUAL = false; public boolean EARLIER = false; /* period completely earlier */ public boolean LATER = false; /* period completely later */ public long NO_DAYS = 0; /* amount of days overlapping or in-between a gap */ public PeriodCompareResult() {} { } public String toString() { List result = new ArrayList(); if (this.EQUAL) { result.add("equals"); } if (this.EXTENDS_BEFORE) { result.add("extends before"); } if (this.EXTENDS_AFTER) { result.add("extends after"); } if (this.EXTENDS) { result.add("extends"); } if (this.OVERLAPS_WITH_START) { result.add("overlaps with start of"); } if (this.OVERLAPS_WITH_END) { result.add("overlaps with end of"); } if (this.OVERLAP) { result.add("overlaps"); } if (this.CONTAINS) { result.add("contains"); } if (this.CONTAINS_AT_START) { result.add("contains at start of"); } if (this.CONTAINS_AT_END) { result.add("contains at end of"); } if (this.IS_CONTAINED) { result.add("is contained in"); } if (this.HAVE_GAP) { result.add("has a gap with"); } if (this.HAS_GAP_BEFORE) { result.add("has gap before"); } if (this.HAS_GAP_AFTER) { result.add("has gap after"); } if (this.EARLIER) { result.add("is earlier than"); } if (this.LATER) { result.add("is later than"); } return result.toString(); } } /** * addDays: Add a number of days to a given date * * * {talendTypes} Date * * {Category} LCAG-DateTime * * {param} Date("date") input: date to increase * {param} int("days") input: number of days to add (can be negative * * {example} asDurationString("+12:04") = "PT12H04M". */ public static Date addDays(Date date, int days) { Calendar cal = Calendar.getInstance(); cal.setTime(date); cal.add(Calendar.DATE, days); //minus number would decrement the days return cal.getTime(); } /** * asDurationString: returns a XML duration format string. * * * {talendTypes} String * * {Category} LCAG-DateTime * * {param} string("aTime") input: The time duration string in MDS format (+/-HH:MM) to be converted into a XML duration format string. * * {example} asDurationString("+12:04") = "PT12H04M". */ public static String asDurationString(String aTime) { boolean inFuture = true; String result = null; int hours = 0, minutes = 0; if (aTime.startsWith("+")) { aTime = aTime.replaceFirst("+", ""); } else if (aTime.startsWith("-")) { inFuture = false; aTime = aTime.replaceFirst("-", ""); } String values[] = aTime.split(":"); if (values.length == 2) { hours = Integer.parseInt(values[0]); minutes = Integer.parseInt(values[1]); result = String.format("%sPT%02H%02M", inFuture ? "" : "-", hours, minutes); } return result; } /** * asDuration: Return a Java Duration object from a MDS formatted String * * * {talendTypes} String * * {Category} LCAG-DateTime * * {param} string("aTime") input: The time duration string in MDS format (+/-HH:MM) to be converted into a XML duration format string. * * {example} asDuration("+12:04") = "PT12H04M". */ public static Duration asDuration(String aTime) { String durationString = asDurationString(aTime); Duration result = Duration.parse(durationString); return result; } /** * * utcOffsetToDouble: convert UTC offset String to floating point number * * {talendTypes} Double * * {Category} LCAG-DateTime * * {param} String("offset") input: UTC offset string +04:30 * */ public static Double utcOffset2Double(String offset) { Double result = null; if (offset != null) { String sign = offset.substring(0,1); String hours = offset.substring(1,3); String mins = offset.substring(4); result = new Double( ( sign.equals("-") ? -1.0 : 1.0 ) * ( Integer.parseInt(hours) + Integer.parseInt(mins) / 60.0 ) ); } return result; } /** * toDate: Returns a Date object from a "YYYY-MM-DD" formated String 'obj' or obj itself if it is already a Date object * * * {talendTypes} Date * * {Category} LCAG-DateTime * * {param} Object("obj") input: The obj to convert to Date * * {example} toDate("2017-01-31"), toDate(new Date()) */ public static Date toDate(Object obj) throws ParseException { Date result = null; if (obj != null) { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); String dateString = null; if (obj instanceof Date) { result = (Date)obj; dateString = df.format(result); // to strip the time part from date at next parse } else { dateString = obj.toString(); } if ( ! StringUtil.isEmpty(dateString) ) { result = df.parse(dateString); log.trace(String.format("toDate(%s) = %s\n", dateString, df.format(result))); } else { log.debug("LcagDateTime.toDate called with empty (%s) string argument", dateString); } } return result; } /** * toDate: Converts a Date string to a Date object if any String representation in the formatList is applicable * * @param aDate Date string to convert * @param formatList list of String formats to try for conversion * @return Date object or null if no conversion is applicable * * {talendTypes} Object * * {Category} LCAG-DateTime * * {param} String("aDate") aDate: Date string to convert * {param} Object("formatList") formatList...: variable number of String formats to try for conversion * * {examples} */ public static Date toDate(String aDate, String... formatList) { Date result = null; if (aDate == null) return null; // try all formats until first one succeeds: for (String fmt: formatList) { SimpleDateFormat df = new SimpleDateFormat(fmt); try { result = df.parse(aDate); break; } catch(Exception e) {} } return result; } /** * countDays: Returns the number of days with the Date period [d1 - d2]. If given by "YYYY-MM-DD" string it is converted first. * * * {talendTypes} Long * * {Category} LCAG-DateTime * * {param} Object("d1") input: The first date to use * {param} Object("d2") input: The second date to use * * {example} countDays("2017-01-01", "2017-01-03") = 3 */ public static long countDays(Object d1, Object d2) throws ParseException { long result = 0; if (d1 != null && d2 != null) { Date date1 = toDate(d1); Date date2 = toDate(d2); result = Math.round((date2.getTime() - date1.getTime()) / (60 * 60 * 24 * 1000.0)); result += 1; } return result; } /** * getStartOfToday: Converts a Date string to a Date object if any String representation in the formatList is applicable * * @return Date object for today, 00:00:00 * * {talendTypes} Date * * {Category} LCAG-DateTime * * {examples} */ public static Date getStartOfToday() { Date result = new Date(); String s = DATE_FORMAT_ISO8601_UTC.format(new Date()); String resultDate = s.replace("Z", "T00:00:00Z"); try { result = DATETIME_FORMAT_ISO8601_UTC.parse(resultDate); } catch(Exception e) { result = null; } return result; } /** * isEarlierToday: return true if the date passed is at a earlier time today * * @return boolean true if the date passed is at a earlier time today * * {talendTypes} boolean * * {Category} LCAG-DateTime * * {examples} */ public static boolean isEarlierToday(Date aDate) { Date now = new Date(); Date todayStart = getStartOfToday(); boolean result = (aDate.before(now) && aDate.after(todayStart)); return result; } /** * isEarlierToday: return true if the date passed in seconds since 1970 is at a earlier time today * * @return boolean true if the date passed is at a earlier time today * * {talendTypes} boolean * * {Category} LCAG-DateTime * * {examples} */ public static boolean isEarlierToday(long aDateInSecs) { Date aDate = new Date(); aDate.setTime(aDateInSecs); boolean result = isEarlierToday(aDate); return result; } /** * compareDatePeriods: Compares two date ranges and returns a PeriodCompareResult object that lists the results: * .EQUAL, if Period A is the same as B * .EXTENDS, A extends B if B extends the range of A without overlapping * .CONTAINS, A contains B completely (e.g. B1, B2) * .OVERLAP, A overlaps B if range A overlaps B somehow, i.e. A intersects with B * .EARLIER, A is completely earlier than B, i.e. both the “from date” and the “to date of A are before or equal to the start of B * .LATER, A is completely later than B * * {talendTypes} PeriodCompareResult * * {Category} LCAG-DateTime * * {param} Date("fromTimeA") input: Start date of period A * {param} Date("toTimeA") input: End date of period A * {param} Date("fromTimeB") input: Start date of period B * {param} Date("toTimeA") input: Start date of period B * * {example} compareDatePeriods(fromDateA, toDateA, fromDateB, toDateB).EXTENDS returns true if A extends B, i.e. if A is adjacent to B * @throws ParseException */ public static LcagDateTime.PeriodCompareResult compareDatePeriods(Object fromA, Object toA, Object fromB, Object toB) throws ParseException { Date fromDateA = toDate(fromA); Date toDateA = toDate(toA); Date fromDateB; Date toDateB; PeriodCompareResult result = new LcagDateTime.PeriodCompareResult(); if (fromA == null || toA == null || fromB == null || toB == null) return result; fromDateB = toDate(fromB); toDateB = toDate(toB); Date dayBeforeA = addDays(fromDateA, -1); Date dayAfterA = addDays(toDateA, 1); result.EQUAL = (fromDateA.compareTo(fromDateB) == 0 && toDateA.compareTo(toDateB) == 0); result.EXTENDS_AFTER = (dayBeforeA.compareTo(toDateB) == 0 && fromDateB.compareTo(fromDateA) < 0); /* B4 */ result.EXTENDS_BEFORE = (dayAfterA.compareTo(fromDateB) == 0 && toDateB.compareTo(toDateA) > 0); /* B5 */ result.EXTENDS = result.EXTENDS_BEFORE || result.EXTENDS_AFTER; result.CONTAINS = (fromDateB.compareTo(fromDateA) >= 0 && toDateB.compareTo(toDateA) <= 0); /* fully B1,B2 */ result.CONTAINS_AT_START = (fromDateB.compareTo(fromDateA) == 0 && toDateB.compareTo(toDateA) <= 0); result.CONTAINS_AT_END = (fromDateB.compareTo(fromDateA) >= 0 && toDateB.compareTo(toDateA) == 0); result.IS_CONTAINED = (fromDateA.compareTo(fromDateB) >= 0) && (toDateA.compareTo(toDateB) <= 0); /* B2, B3*/ result.EARLIER = /* B5, B9 */ (dayAfterA.compareTo(fromDateB) <= 0); result.LATER = /* B4, B8 */ (dayBeforeA.compareTo(toDateB) >= 0); result.OVERLAPS_WITH_END = (fromDateA.compareTo(toDateB) <= 0) /* B1,B2,B3,B5,B6,B7,B9 */ && (toDateA.compareTo(toDateB) > 0 /* B1,B4,B6,B8*/); /* => Result: B1, B6 */ result.OVERLAPS_WITH_START = (fromDateA.compareTo(fromDateB) < 0) /* B1,B5,B7,B9 */ && (toDateA.compareTo(fromDateB) >= 0 /* B1,B2,B3,B4,B6,B7,B8 */); /* => Result: B1, B7 */ // Lastly, also consider partial overlapping between A and B: result.OVERLAP = result.CONTAINS || result.IS_CONTAINED || result.OVERLAPS_WITH_START || result.OVERLAPS_WITH_END; /* B1,B2,B3, B6, B7 */ result.HAS_GAP_BEFORE = toDateB.before(dayBeforeA); result.HAS_GAP_AFTER = fromDateB.after(dayAfterA); result.HAVE_GAP = result.HAS_GAP_BEFORE || result.HAS_GAP_AFTER; if (result.CONTAINS) { result.NO_DAYS = countDays(fromDateB, toDateB); } else if (result.IS_CONTAINED) { result.NO_DAYS = countDays(fromDateA, toDateA); } else if (result.OVERLAPS_WITH_START) { result.NO_DAYS = countDays(fromDateB, toDateA); } else if (result.OVERLAPS_WITH_END) { result.NO_DAYS = countDays(fromDateA, toDateB); } else if (result.HAS_GAP_BEFORE) { result.NO_DAYS = countDays(toDateB, fromDateA) - 2; } else if (result.HAS_GAP_AFTER) { result.NO_DAYS = countDays(toDateA, fromDateB) - 2; } SimpleDateFormat df = new SimpleDateFormat("dd. MM YYYY"); String logText = String.format("[%1$td.%1$tm.%1$tY - %2$td.%2$tm.%2$tY] %3$s [%4$td.%4$tm.%4$tY - %5$td.%5$tm.%5$tY]", fromDateA, toDateA, result.toString(), fromDateB, toDateB); log.info(logText); return result; } /** * dateInbetween: returns a comparison of three datetime objects, i.e. whether one date located between the other two dates * * * {talendTypes} Boolean * * {Category} LCAG-DateTime * * {param} Object("from") input: The first date to use * {param} Object("inbetween") input: The date that is used for comparison * {param} Object("to") input: The last date to use * */ public static boolean dateInbetween(Date from, Date inbetween, Date to) { boolean result = false; if (from != null && inbetween != null && to != null) { result = (from.equals(inbetween) || from.before(inbetween)) && (to.equals(inbetween) || to.after(inbetween)) ; } return result; } /** * secureParseDate: returns a Date object parsed from String daterepresentation and, hopefully, does return * * * {talendTypes} Date * * {Category} LCAG-DateTime * * {param} Object("pattern") input: the pattern describing how to encode a datetime value YYYY-MM-dd HH:mm:ss * {param} Object("daterepresentation") input: the date that is parsed * */ public static Date secureParseDate(String pattern, String daterepresentation) { Date result = null; if (daterepresentation != null) if (! daterepresentation.equals("")) { try { result = TalendDate.parseDate(pattern, daterepresentation); } catch(Exception e) {} } return result; } /** * Converts a double value to a UTC time offset String * (Used in MAD timezone conversion) * * @param d output: Talend or Dom4J or String xml document to be changed * @return Time offset as String [+-]HH:MM * * {talendTypes} String * {Category} LCAG-DateTime * * {param} Double(d) d: input: xpath lookup string * * {example} double2UtcOffset(6) = "+06:00", double2UtcOffset(6.5) = "+06:30" */ public static String double2UtcOffset(Double d) { String result = null; if ( d == null) return result; if ( d <= -24.0 || d >= 24.0 ) throw new IllegalArgumentException(String.format("Argument must be > -24 and < 24, %f given", d)); if (d != null) { String sign = "+"; if ( d < 0) { sign = "-"; d = -d; } int hours = (int)Math.floor(d); int minutes = (int)Math.round((d - hours) * 60.0); if ( minutes % 15 > 0 ) { log.warn("double2UtcOffset: %.2f is an UNUSUAL offset (not a a multiple of 15 mins) !", d); } result = sign + String.format("%02d:%02d", hours, minutes); } return result; } /** * Checks if the first 2 letters of a string represent a IATA Country Code * of a Country that has more than one time zone. * * Note: List of Countries is hard-coded * * @param aCode input: Code to check * @return true if Code is valid as Multi-Timezone Country Code * * {talendTypes} String * {Category} LCAG-DateTime * * {param} String(aCode) aCode: input: Code to check * * {example} isMultiTimezoneCountry("DE:01") = false, * isMultiTimezoneCountry("DE0") = false * isMultiTimezoneCountry("AU2A") = true * isMultiTimezoneCountry("US:02") = true */ public static boolean isMultiTimezoneCountry(String aCode) { // List of Countries that have multiple Timezones according to IATA Airline Coding Directory (ACD): final List multiTzCountries = java.util.Arrays.asList( "AU", "BR", "CA", "CD", "CL", "EC", "ES", "FM", "GL", "ID", "JO", "KI", "KZ", "MX", "NZ", "PF", "PG", "PT", "RU", "UM", "US", "ZZ"); if ( aCode == null || aCode.length() < 2) { throw new IllegalArgumentException(String.format("Illegal Code length of '%s': Must start with 2 letter Country Code !", aCode)); } String countryCode = aCode.substring(0, 2); boolean result = multiTzCountries.contains(countryCode); return result; } /** * Converts a Timezone Code in MDS format (should by IATA) into MAD format. * Note: 1. Does not transform special values like for countries MX, BR,... * 2. Includes a hard coded list of Country Codes having multiple Timezone Codes * (Created for jobs updating Timezone) * * @param aCode input: MDS Timezone Code * @return MAD Timezone Code format * * {talendTypes} String * {Category} LCAG-DateTime * * {param} String(aCode) aCode: input: MDS Timezone Code * * {example} convertMDS_TimezoneCodeToMAD("DE:01") = "DE0", * convertMDS_TimezoneCodeToMAD("PF:01") = "PF1" (PF is multi-tz country) * convertMDS_TimezoneCodeToMAD("MX:05") = "MX5" (but MAD uses MX1B) */ public static String convertMDS_TimezoneCodeToMAD(String aCode) { if ( aCode == null || aCode.length() < 5) { throw new IllegalArgumentException(String.format("Illegal MDS Code '%s': Must have at least 5 characters!", aCode)); } String countryCode = aCode.substring(0,2); String tzNo = aCode.substring(3); String result = countryCode; if ( tzNo.startsWith("0") ) { if ("01".equals(tzNo) && ! isMultiTimezoneCountry(aCode) ) { // In MAD countries that have just one TZ always use "0" as last character result += "0"; } else { result += tzNo.substring(1); // drop leading "0" } } else { result += tzNo; } return result; } /** * Converts a Timezone Code in MAD format (should by IATA) into MDS format. * Note: Does not transform special values like for countries MX, BR,... * * @param aCode input: MAD Timezone Code * @return MDS Timezone Code format * * {talendTypes} String * {Category} LCAG-DateTime * * {param} String(aCode) aCode: input: MAD Timezone Code * * {example} convertMAD_TimezoneCodeToMDS("DE0") = "DE:01", convertMAD_TimezoneCodeToMDS("PF1") = "PF:01" * convertMAD_TimezoneCodeToMDS("MX1A") = "MX:1A" (but MDS uses MX:05) */ public static String convertMAD_TimezoneCodeToMDS(String aCode) { if ( aCode == null || aCode.length() < 3) { throw new IllegalArgumentException(String.format("Illegal MAD Code '%s': Must have at least 3 characters!", aCode)); } String countryCode = aCode.substring(0,2); String tzNo = aCode.substring(2); String result = countryCode + ":"; if ("0".equals(tzNo)) { // Code of a Country that has only one Timezone result += "01"; } else if ( tzNo.length() == 1) { result += "0" + tzNo; } else { result += tzNo; } return result; } /** * Convert a Local Date with Time to UTC using the local time offset to UTC * * {talendTypes} OffsetDateTime * * {Category} LCAG-DateTime * * {param} LocalDateTime localDateTime : input: Date with time to convert * {param} String("+04:30") offsetTime : input: Time offset to use, e.g. -04:00, +04:00, 04:00 * * {example} convertLocalTimeToUTC(aDate, "-03:00") */ public static OffsetDateTime convertLocalTimeToUTC(LocalDateTime localDateTime, String offsetTime) { String[] hoursMins = offsetTime.split(":"); if (hoursMins.length != 2) { throw new IllegalArgumentException(String.format("Unsupport Offset Time ('%s'), required format is [+-]HH:MM", offsetTime) ); } if (localDateTime == null) { localDateTime = LocalDateTime.now(); } if (! offsetTime.startsWith("-") && ! offsetTime.startsWith("+")) offsetTime = "+" + offsetTime; DateTimeFormatter df = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm"); String dateString = df.format(localDateTime) + ":00:000" + offsetTime; DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss:SSSXXXXX"); OffsetDateTime localOffsetDateTime = OffsetDateTime.parse(dateString, DATE_TIME_FORMATTER); OffsetDateTime utcOffsetDateTime = localOffsetDateTime.withOffsetSameInstant(ZoneOffset.UTC); return utcOffsetDateTime; } /** * Convert a Date with Time and Time Zone Offset to UTC. * * {talendTypes} OffsetDateTime * * {Category} LCAG-DateTime * * {param} OffsetDateTime dateTime : input: Date with time to convert */ public static OffsetDateTime convertToUTC(OffsetDateTime dateTime) { if (dateTime == null) { dateTime = OffsetDateTime.now(); } OffsetDateTime utcOffsetDateTime = dateTime.withOffsetSameInstant(ZoneOffset.UTC); return utcOffsetDateTime; } /** * Convert a UTC Time to another timzone given by its offset to UTC * * {talendTypes} OffsetDateTime * * {Category} LCAG-DateTime * * {param} Date localDateTime : input: Date with time to convert * {param} String("-HH:mm") offsetTime : input: Time offset to use, e.g. -04:00, +04:00, 04:00 * * {example} convertUTCTimeToLocal(aDate, "+03:30") */ public static OffsetDateTime convertUTCTimeToLocal(Date localDateTime, String offsetTime) { String[] hoursMins = offsetTime.split(":"); if (hoursMins.length != 2) { throw new IllegalArgumentException(String.format("Unsupport Offset Time ('%s'), required format is [+-]HH:MM", offsetTime) ); } if (localDateTime == null) { localDateTime = new Date(); } int hours = Integer.parseInt(hoursMins[0]); int minutes = Integer.parseInt(hoursMins[1]); if (hours < 0) minutes = -minutes; if (! offsetTime.startsWith("-") && ! offsetTime.startsWith("+")) offsetTime = "+" + offsetTime; SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy'T'HH:mm"); String dateString = df.format(localDateTime) + ":00:000+00:00"; DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/uuuu'T'HH:mm:ss:SSSXXXXX"); OffsetDateTime utcOffsetDateTime = OffsetDateTime.parse(dateString, DATE_TIME_FORMATTER); OffsetDateTime localOffsetDateTime = utcOffsetDateTime.withOffsetSameInstant(ZoneOffset.ofHoursMinutes(hours, minutes)); return localOffsetDateTime; } /** * Adjusts a given date in MDS notation to a regular date. Interprets MDS special time 23:59 as 00:00 same day and * 00:01 as 00:00 same day. MDS uses 00:01 for DST start time at the same day and 23:59 for DST end time at the same * day. * Example: the DST in Iran (IR:01) ends on Sep, 22th 2021 00:00 UTC, but MDS uses Sep, 22th, 23:59! * * {talendTypes} OffsetDateTime * * {Category} LCAG-DateTime * * {param} OffsetDateTime aDate : input: Date to use adjust * {returns} Date assembled from input parameters * {example} adjustMDS_Date(aDate whith value 2021-02-28 23:59) returns a Date for 2021-03-01 00:00:00.000) */ public static OffsetDateTime adjustMDS_Date(OffsetDateTime aDate) { int hours = aDate.getHour(); int minutes = aDate.getMinute(); OffsetDateTime result = aDate.withSecond(0).withNano(0); if ( hours == 0 && minutes == 1 ) { result = result.withMinute(0); } else if ( hours == 23 && minutes == 59 ) { result = result.plusMinutes(1); } return result; } /** * Assembles a DateTime object from a local Date and a local time string HH:mm that uses MDS special times 23:59 (00:00 next day) and * 00:01 (00:00 same day). * * {talendTypes} Date * * {Category} LCAG-DateTime * * {param} LocalDate localDate : input: Date part to use * {param} LocalTime localTime : input: Local Time to append to the date part * {param} String offsetTime : input: Used to define the time zone of the LocalDate and LocalTime * {returns} Date assembled from input parameters * {example} assembleMDS_DateTime(aDate, "23:59", "+00:00") returns the same day at 00:00 as aDate * @throws ParseException */ public static OffsetDateTime assembleMDS_DateTime(LocalDate localDate, LocalTime localTime, String offsetTime) throws ParseException { if (! offsetTime.startsWith("-") && ! offsetTime.startsWith("+")) offsetTime = "+" + offsetTime; OffsetDateTime dateTime = OffsetDateTime.of(localDate, localTime, ZoneOffset.of(offsetTime)); OffsetDateTime result = adjustMDS_Date(dateTime); return result; } /** * Created for DST enrichment: Calculates the UTC time from a given local MDS DST start/end time * using the offsetTime given * * {talendTypes} String * * {Category} LCAG-DateTime * * {param} Object resultDateFormat : input: Date format to return, either a format string or a SimpleDateFormat. * {param} LocalDateTime localDateTime: input: Date with time to convert * {param} String("+HH:mm") offsetTime : input: Time offset to use, e.g. -04:00, +04:00, 04:00 * * @throws ParseException */ public static String convertLocalTimeToUTC(Object resultDateFormat, LocalDateTime localDateTime, String offsetTime) throws ParseException { String result = null; OffsetDateTime resultDate = convertLocalTimeToUTC(localDateTime, offsetTime); result = formatDate(resultDateFormat, resultDate).toUpperCase(); return result; } /** * Created for CABS timezone export: Calculates the UTC time from a given local MDS DST start/end time * using the offsetTime given * * {talendTypes} String * * {Category} LCAG-DateTime * * {param} Object resultDateFormat : input: Date format to return, either a format string or a SimpleDateFormat. * {param} Date localDate : input: Date to convert * {param} Object("HH:mm") localTime : input: Local Time to convert as String, e.g. 02:00 or a Date object to extract this info from * {param} String("+HH:mm") offsetTime : input: Time offset to use, e.g. -04:00, +04:00, 04:00 * * {example} calculate_UTC_TimeFromLocal("ddMMMyy_HHmm", aDate, "23:59", "+04:30") * @throws ParseException */ public static String convertLocalDateTimeToUTC(Object resultDateFormat, Date localDate, Object localTime, String offsetTime) throws ParseException { String result = null; String localTimeString = null; String[] hoursMins = offsetTime.split(":"); if (hoursMins.length != 2) { throw new IllegalArgumentException(String.format("Unsupport Local Time ('%s'), required format is HH:mm", localTimeString) ); } LocalDate ld = localDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); LocalTime lt = null; if ( localTime instanceof Date) { lt = ((Date)localTime).toInstant().atZone(ZoneId.systemDefault()).toLocalTime(); } else if ( localTime instanceof String ) { localTimeString = (String)localTime + ":00"; lt = LocalTime.parse(localTimeString); } else { throw new IllegalArgumentException("Local time argument type not supported"); } OffsetDateTime localDateTime = assembleMDS_DateTime(ld, lt, offsetTime); OffsetDateTime odt = convertToUTC(localDateTime); result = formatDate(resultDateFormat, odt).toUpperCase(); return result; } /** * Formats a Date into a date/time string. * * @param aFormatter the pattern to format. * @param aDate the time value to be formatted into a time string. * @return the formatted time string. * * {talendTypes} String * * {Category} LCAG-DateTime * * {param} Object("yyyy-MM-dd HH:mm:ss") aFormatter : the pattern to format. May be a formatting String, a java DateFormat or a DateTimeFormatter * * {param} Object(aDate) aDate : the date und time value to be formatted into a date time string. May be of Type Date or OffsetDateTime * * {example} formatDate("yyyy-MM-dd", new Date()) */ public synchronized static String formatDate(Object aFormatter, Object aDate) { String result = null; DateFormat df = null; DateTimeFormatter formatter = null; if ( aFormatter instanceof String) { String fmt = (String)aFormatter; // Use the best suited formatter next! Otherwise you may get problems in time zone handling!! if ( aDate instanceof Date ) { df = new SimpleDateFormat(fmt.replaceAll("u", "y"), Locale.ENGLISH); } if ( aDate instanceof OffsetDateTime ) { formatter = DateTimeFormatter.ofPattern(fmt.replaceAll("y", "u"), Locale.ENGLISH); } } else if ( aFormatter instanceof DateFormat ) { df = (DateFormat)aFormatter; } else if ( aFormatter instanceof DateTimeFormatter ) { formatter = (DateTimeFormatter)aFormatter; } else { throw new IllegalArgumentException("Formatter argument type not supported"); } if (aDate instanceof Date) { Date d = (Date)aDate; if (df != null) { result = df.format(d); } else if (formatter != null) { result = formatter.format(d.toInstant()); } } else if (aDate instanceof Long) { Date d = new Date((Long)aDate); if (df != null) { result = df.format(d); } else if (df != null) { result = formatter.format(d.toInstant()); } } else if (aDate instanceof OffsetDateTime) { OffsetDateTime d = (OffsetDateTime)aDate; if (formatter != null) { result = formatter.format(d); } else if (df != null) { result = df.format(Date.from(d.toInstant())); } } else { throw new IllegalArgumentException("Date argument type not supported"); } return result; } }