From a3a61223b7ab382611ab6e149786d8d6abb98161 Mon Sep 17 00:00:00 2001 From: arisnguyenit97 Date: Sat, 25 May 2024 20:53:54 +0700 Subject: [PATCH] :sparkles: feat: add unify functions Date4j, Time4j and String4j #4 --- .../groovy/org/unify4j/common/Cookie4j.java | 4 +- .../groovy/org/unify4j/common/Date4j.java | 328 ++++++++++++++++++ .../groovy/org/unify4j/common/String4j.java | 15 + .../groovy/org/unify4j/common/Time4j.java | 72 +++- .../org/unify4j/text/TimeFormatText.java | 8 +- .../test/groovy/org/unify4j/Date4jTest.java | 17 + ...{UniqueIdTest.java => UniqueId4jTest.java} | 2 +- 7 files changed, 437 insertions(+), 9 deletions(-) create mode 100644 plugin/src/main/groovy/org/unify4j/common/Date4j.java create mode 100644 plugin/src/test/groovy/org/unify4j/Date4jTest.java rename plugin/src/test/groovy/org/unify4j/{UniqueIdTest.java => UniqueId4jTest.java} (99%) diff --git a/plugin/src/main/groovy/org/unify4j/common/Cookie4j.java b/plugin/src/main/groovy/org/unify4j/common/Cookie4j.java index a6c6207..32b1259 100644 --- a/plugin/src/main/groovy/org/unify4j/common/Cookie4j.java +++ b/plugin/src/main/groovy/org/unify4j/common/Cookie4j.java @@ -31,7 +31,9 @@ public static Map getCookies(HttpServletRequest request) { return Arrays.stream(request.getCookies()).collect(Collectors.toMap(Cookie::getName, // Use cookie's name as the map key Cookie::getValue, // Use cookie's value as the map value (cookie1, cookie2) -> { // Merge function for handling duplicate keys - logger.info("cookie duplicated key found by cookie-1: {} and cookie-2: {}", cookie1, cookie2); + if (logger.isInfoEnabled()) { + logger.info("cookie duplicated key found by cookie-1: {} and cookie-2: {}", cookie1, cookie2); + } return cookie1; // Keep the value of the first cookie in case of a duplicate key })); } diff --git a/plugin/src/main/groovy/org/unify4j/common/Date4j.java b/plugin/src/main/groovy/org/unify4j/common/Date4j.java new file mode 100644 index 0000000..178e13d --- /dev/null +++ b/plugin/src/main/groovy/org/unify4j/common/Date4j.java @@ -0,0 +1,328 @@ +package org.unify4j.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Date utility for parsing String dates with optional times, especially when the input String formats + * may be inconsistent. This will parse the following formats:
+ *
+ * 12-31-2023, 12/31/2023, 12.31.2023     mm is 1-12 or 01-12, dd is 1-31 or 01-31, and yyyy can be 0000 to 9999.
+ *
+ * 2023-12-31, 2023/12/31, 2023.12.31     mm is 1-12 or 01-12, dd is 1-31 or 01-31, and yyyy can be 0000 to 9999.
+ *
+ * January 6th, 2024                Month (3-4 digit abbreviation or full English name), white-space and optional comma,
+ *                                  day of month (1-31) with optional suffixes 1st, 3rd, 22nd, whitespace and
+ *                                  optional comma, and yyyy (0000-9999)
+ *
+ * 17th January 2024                day of month (1-31) with optional suffixes (e.g. 1st, 3rd, 22nd),
+ *                                  Month (3-4 digit abbreviation or full English name), whites space and optional comma,
+ *                                  and yyyy (0000-9999)
+ *
+ * 2024 January 31st                4 digit year, white space and optional comma, Month (3-4 digit abbreviation or full
+ *                                  English name), white space and optional command, and day of month with optional
+ *                                  suffixes (1st, 3rd, 22nd)
+ *
+ * Sat Jan 6 11:06:10 EST 2024      Unix/Linux style.  Day of week (3-letter or full name), Month (3-4 digit or full
+ *                                  English name), time hh:mm:ss, TimeZone (Java supported Timezone names), Year
+ * 
+ * All dates can be followed by a Time, or the time can precede the Date. Whitespace or a single letter T must separate the + * date and the time for the non-Unix time formats. The Time formats supported:
+ *
+ * hh:mm                            hours (00-23), minutes (00-59).  24 hour format.
+ *
+ * hh:mm:ss                         hours (00-23), minutes (00-59), seconds (00-59).  24 hour format.
+ *
+ * hh:mm:ss.sssss                   hh:mm:ss and fractional seconds. Variable fractional seconds supported.
+ *
+ * hh:mm:offset -or-                offset can be specified as +HH:mm, +HHmm, +HH, -HH:mm, -HHmm, -HH, or Z (GMT)
+ * hh:mm:ss.sss:offset              which will match: "12:34", "12:34:56", "12:34.789", "12:34:56.789", "12:34+01:00",
+ *                                  "12:34:56+1:00", "12:34-01", "12:34:56-1", "12:34Z", "12:34:56Z"
+ *
+ * hh:mm:zone -or-                  Zone can be specified as Z (Zulu = UTC), older short forms: GMT, EST, CST, MST,
+ * hh:mm:ss.sss:zone                PST, IST, JST, BST etc. as well as the long forms: "America/New_York", "Asia/Saigon",
+ *                                  etc. See ZoneId.getAvailableZoneIds().
+ * 
+ * DateUtilities will parse Epoch-based integer-based value. It is considered number of milliseconds since Jan, 1970 GMT. + *
+ * "0" to                           A string of numeric digits will be parsed and returned as the number of milliseconds
+ * "999999999999999999"             the Unix Epoch, January 1st, 1970 00:00:00 UTC.
+ * 
+ * On all patterns above (excluding the numeric epoch millis), if a day-of-week (e.g. Thu, Sunday, etc.) is included + * (front, back, or between date and time), it will be ignored, allowing for even more formats than listed here. + * The day-of-week is not be used to influence the Date calculation. + */ +@SuppressWarnings({"SpellCheckingInspection"}) +public class Date4j { + protected static final Logger logger = LoggerFactory.getLogger(Date4j.class); + protected static final Pattern allDigits = Pattern.compile("^\\d+$"); + protected static final String days = "monday|mon|tuesday|tues|tue|wednesday|wed|thursday|thur|thu|friday|fri|saturday|sat|sunday|sun"; + protected static final String mos = "January|Jan|February|Feb|March|Mar|April|Apr|May|June|Jun|July|Jul|August|Aug|September|Sept|Sep|October|Oct|November|Nov|December|Dec"; + protected static final String yr = "[+-]?\\d{4,5}\\b"; + protected static final String d1or2 = "\\d{1,2}"; + protected static final String d2 = "\\d{2}"; + protected static final String ord = "st|nd|rd|th"; + protected static final String sep = "[./-]"; + protected static final String ws = "\\s+"; + protected static final String wsOp = "\\s*"; + protected static final String wsOrComma = "[ ,]+"; + protected static final String tzUnix = "[A-Z]{1,3}"; + protected static final String tz_Hh_MM = "[+-]\\d{1,2}:\\d{2}"; + protected static final String tz_Hh_MM_SS = "[+-]\\d{1,2}:\\d{2}:\\d{2}"; + protected static final String tz_HHMM = "[+-]\\d{4}"; + protected static final String tz_Hh = "[+-]\\d{1,2}"; + protected static final String tzNamed = wsOp + "\\[?[A-Za-z][A-Za-z0-9~/._+-]+]?"; + protected static final String nano = "\\.\\d+"; + + // Patterns defined in BNF influenced style using above named elements + protected static final Pattern isoDatePattern = Pattern.compile( // Regex's using | (OR) + "(" + yr + ")(" + sep + ")(" + d1or2 + ")" + "\\2" + "(" + d1or2 + ")|" + // 2024/01/21 (yyyy/mm/dd -or- yyyy-mm-dd -or- yyyy.mm.dd) [optional time, optional day of week] \2 references 1st separator (ensures both same) + "(" + d1or2 + ")(" + sep + ")(" + d1or2 + ")" + "\\6(" + yr + ")"); // 01/21/2024 (mm/dd/yyyy -or- mm-dd-yyyy -or- mm.dd.yyyy) [optional time, optional day of week] \6 references 2nd 1st separator (ensures both same) + protected static final Pattern alphaMonthPattern = Pattern.compile("\\b(" + mos + ")\\b" + wsOrComma + "(" + d1or2 + ")(" + ord + ")?" + wsOrComma + "(" + yr + ")|" + // Jan 21st, 2024 (comma optional between all, day of week optional, time optional, ordinal text optional [st, nd, rd, th]) + "(" + d1or2 + ")(" + ord + ")?" + wsOrComma + "\\b(" + mos + ")\\b" + wsOrComma + "(" + yr + ")|" + // 21st Jan, 2024 + "(" + yr + ")" + wsOrComma + "\\b(" + mos + "\\b)" + wsOrComma + "(" + d1or2 + ")(" + ord + ")?", // 2024 Jan 21st + Pattern.CASE_INSENSITIVE); + protected static final Pattern unixDateTimePattern = Pattern.compile("\\b(" + days + ")\\b" + ws + "\\b(" + mos + ")\\b" + ws + "(" + d1or2 + ")" + ws + "(" + d2 + ":" + d2 + ":" + d2 + ")" + wsOp + "(" + tzUnix + ")?" + wsOp + "(" + yr + ")", Pattern.CASE_INSENSITIVE); + protected static final Pattern timePattern = Pattern.compile("(" + d2 + "):(" + d2 + ")(?::(" + d2 + ")(" + nano + ")?)?(" + tz_Hh_MM_SS + "|" + tz_Hh_MM + "|" + tz_HHMM + "|" + tz_Hh + "|Z)?(" + tzNamed + ")?", Pattern.CASE_INSENSITIVE); + protected static final Pattern dayPattern = Pattern.compile("\\b(" + days + ")\\b", Pattern.CASE_INSENSITIVE); + protected static final Map months = new ConcurrentHashMap<>(); + + static { + months.put("jan", 1); + months.put("january", 1); + months.put("feb", 2); + months.put("february", 2); + months.put("mar", 3); + months.put("march", 3); + months.put("apr", 4); + months.put("april", 4); + months.put("may", 5); + months.put("jun", 6); + months.put("june", 6); + months.put("jul", 7); + months.put("july", 7); + months.put("aug", 8); + months.put("august", 8); + months.put("sep", 9); + months.put("sept", 9); + months.put("september", 9); + months.put("oct", 10); + months.put("october", 10); + months.put("nov", 11); + months.put("november", 11); + months.put("dec", 12); + months.put("december", 12); + } + + /** + * If the date-time given does not include a timezone offset or name, then ZoneId.systemDefault() + * will be used. We recommend using parse(String, ZoneId, boolean) version, so you can control the default + * timezone used when one is not specified. + * + * @param s String containing a date. If there is excess content, it will throw an IllegalArgumentException. + * @return Date instance that represents the passed in date. See comments at top of class for supported + * formats. This API is intended to be super flexible in terms of what it can parse. If a null or empty String is + * passed in, null will be returned. + */ + public static Date parse(String s) { + if (String4j.isEmpty(s)) { + return null; + } + ZonedDateTime time = parse(s, ZoneId.systemDefault(), true); + if (time == null) { + return null; + } + Instant instant = Instant.from(time); + return Date.from(instant); + } + + /** + * Retrieve date-time from passed in String. The boolean ensureDateTimeAlone, if set true, ensures that + * no other non-date content existed in the String. + * + * @param s String containing a date. + * @param defaultZoneId ZoneId to use if no timezone offset or name is given. Cannot be null. + * @param ensureDateTimeAlone If true, if there is excess non-Date content, it will throw an IllegalArgument exception. + * @return ZonedDateTime instance converted from the passed in date String. See comments at top of class for supported + * formats. This function is intended to be super flexible in terms of what it can parse. If a null or empty String is + * passed in, null will be returned. + */ + public static ZonedDateTime parse(String s, ZoneId defaultZoneId, boolean ensureDateTimeAlone) { + if (String4j.isEmpty(s)) { + return null; + } + s = String4j.trimWhitespace(s); + Vi4j.throwIfNull(defaultZoneId, "ZoneId cannot be null. Use ZoneId.of(\"America/New_York\"), ZoneId.systemDefault(), etc."); + if (allDigits.matcher(s).matches()) { + return Instant.ofEpochMilli(Long.parseLong(s)).atZone(defaultZoneId); + } + String year, day, remains, tz = null; + int month; + // Determine which date pattern to use + Matcher matcher = isoDatePattern.matcher(s); + String remnant = matcher.replaceFirst(""); + if (remnant.length() < s.length()) { + if (matcher.group(1) != null) { + year = matcher.group(1); + month = Integer.parseInt(matcher.group(3)); + day = matcher.group(4); + } else { + year = matcher.group(8); + month = Integer.parseInt(matcher.group(5)); + day = matcher.group(7); + } + remains = remnant; + } else { + matcher = alphaMonthPattern.matcher(s); + remnant = matcher.replaceFirst(""); + if (remnant.length() < s.length()) { + String mon; + if (matcher.group(1) != null) { + mon = matcher.group(1); + day = matcher.group(2); + year = matcher.group(4); + remains = remnant; + } else if (matcher.group(7) != null) { + mon = matcher.group(7); + day = matcher.group(5); + year = matcher.group(8); + remains = remnant; + } else { + year = matcher.group(9); + mon = matcher.group(10); + day = matcher.group(11); + remains = remnant; + } + month = months.get(mon.trim().toLowerCase()); + } else { + matcher = unixDateTimePattern.matcher(s); + if (matcher.replaceFirst("").length() == s.length()) { + throw new IllegalArgumentException(String.format("Unable to parse %s as a date-time", s)); + } + year = matcher.group(6); + String mon = matcher.group(2); + month = months.get(mon.trim().toLowerCase()); + day = matcher.group(3); + tz = matcher.group(5); + remains = matcher.group(4); // leave optional time portion remaining + } + } + // For the remaining String, match the time portion (which could have appeared ahead of the date portion) + String hour = null, min = null, sec = "00", fracSec = "0"; + remains = remains.trim(); + matcher = timePattern.matcher(remains); + remnant = matcher.replaceFirst(""); + if (remnant.length() < remains.length()) { + hour = matcher.group(1); + min = matcher.group(2); + if (matcher.group(3) != null) { + sec = matcher.group(3); + } + if (matcher.group(4) != null) { + fracSec = "0" + matcher.group(4); + } + if (matcher.group(5) != null) { + tz = matcher.group(5).trim(); + } + if (matcher.group(6) != null) { + // to make round trip of ZonedDateTime equivalent we need to use the original Zone as ZoneId + // is a much broader definition handling multiple possible dates, and we want this to + // be equivalent to the original zone that was used if one was present. + tz = String4j.removeBrackets(matcher.group(6).trim()); + } + } + if (ensureDateTimeAlone) { + verifyNoExtraneous(remnant); + } + ZoneId zoneId = String4j.isEmpty(tz) ? defaultZoneId : Time4j.parseTimeZone(tz); + return parseDateTime(s, zoneId, year, month, day, hour, min, sec, fracSec); + } + + /** + * Constructs a ZonedDateTime object from the provided date and time components. + * Parses the year, month, day, hour, minute, second, and fractional second components to create + * a ZonedDateTime instance in the specified time zone (zoneId). + * If any component is invalid or out of range, throws an IllegalArgumentException with an appropriate message. + * + * @param dateTime The original date-time string for error reporting purposes. + * @param zoneId The ZoneId representing the time zone of the resulting ZonedDateTime. + * @param year The year component of the date. + * @param month The month component of the date (1-12). + * @param day The day component of the date (1-31). + * @param hour The hour component of the time (0-23). + * @param min The minute component of the time (0-59). + * @param sec The second component of the time (0-59). + * @param fractionalSecondText The fractional second component of the time as a string representation. + * @return A ZonedDateTime object representing the specified date and time. + * @throws IllegalArgumentException if any component is invalid or out of range. + */ + private static ZonedDateTime parseDateTime(String dateTime, ZoneId zoneId, String year, int month, String day, String hour, String min, String sec, String fractionalSecondText) { + int y = Integer.parseInt(year); + int d = Integer.parseInt(day); + if (month < 1 || month > 12) { + throw new IllegalArgumentException(String.format("Month must be between 1 and 12 inclusive, date: %s", dateTime)); + } + if (d < 1 || d > 31) { + throw new IllegalArgumentException(String.format("Day must be between 1 and 31 inclusive, date: %s", dateTime)); + } + // Check if time portion is present. + if (hour == null) { // no [valid] time portion + // Construct ZonedDateTime without time components. + return ZonedDateTime.of(y, month, d, 0, 0, 0, 0, zoneId); + } else { + // Parse time components to integers. + int h = Integer.parseInt(hour); + int mn = Integer.parseInt(min); + int s = Integer.parseInt(sec); + // Convert fractional second text to nanoseconds. + long nanoOfSec = Time4j.fromFractionToNanos(fractionalSecondText); + // Validate hour component. + if (h > 23) { + throw new IllegalArgumentException(String.format("Hour must be between 0 and 23 inclusive, time: %s", dateTime)); + } + // Validate minute component. + if (mn > 59) { + throw new IllegalArgumentException(String.format("Minute must be between 0 and 59 inclusive, time: %s", dateTime)); + } + // Validate second component. + if (s > 59) { + throw new IllegalArgumentException(String.format("Second must be between 0 and 59 inclusive, time: %s", dateTime)); + } + return ZonedDateTime.of(y, month, d, h, mn, s, (int) nanoOfSec, zoneId); + } + } + + /** + * Verifies that the provided string contains no extraneous non-date-related content. + * This function removes any day-of-week substrings and verifies that the remaining content + * is either empty or consists solely of the characters 'T' or ','. + * If any other characters are found, an IllegalArgumentException is thrown. + * + * @param remnant The string to verify for non-date-related content. + * @throws IllegalArgumentException if extraneous non-date content is found in the string. + */ + private static void verifyNoExtraneous(String remnant) { + if (String4j.isEmpty(remnant)) { + return; + } + // Remove any day-of-week substrings (e.g., mon, tue, wed, ...). + if (String4j.length(remnant) > 0) { + Matcher day = dayPattern.matcher(remnant); + remnant = day.replaceFirst("").trim(); + } + // Ensure that the remaining content is either empty, "T", or ",". + if (String4j.length(remnant) > 0) { + remnant = remnant.replaceAll("[T,]", "").trim(); + Vi4j.throwIfNullOrEmpty(remnant, String.format("Issue parsing date-time, other characters present: %s", remnant)); + } + } +} diff --git a/plugin/src/main/groovy/org/unify4j/common/String4j.java b/plugin/src/main/groovy/org/unify4j/common/String4j.java index d268794..dab293f 100644 --- a/plugin/src/main/groovy/org/unify4j/common/String4j.java +++ b/plugin/src/main/groovy/org/unify4j/common/String4j.java @@ -799,4 +799,19 @@ public static boolean hasContent(String s) { public static int length(CharSequence cs) { return cs == null ? 0 : cs.length(); } + + /** + * Removes the leading and trailing brackets from a string, if present. + * This function checks if the input string is empty or null using the String4j utility, + * and then uses a regular expression to replace any brackets at the start or end of the string. + * + * @param input the string from which brackets are to be removed + * @return the input string with leading and trailing brackets removed, or the original string if it is empty or null + */ + public static String removeBrackets(String input) { + if (String4j.isEmpty(input)) { + return input; + } + return input.replaceAll("^\\[|]$", ""); + } } diff --git a/plugin/src/main/groovy/org/unify4j/common/Time4j.java b/plugin/src/main/groovy/org/unify4j/common/Time4j.java index 715abf9..473eee8 100644 --- a/plugin/src/main/groovy/org/unify4j/common/Time4j.java +++ b/plugin/src/main/groovy/org/unify4j/common/Time4j.java @@ -19,6 +19,21 @@ public class Time4j { protected static final Logger logger = LoggerFactory.getLogger(Time4j.class); + /** + * Converts a fractional second string representation to nanoseconds. + * Parses the given fractionalSecondText to a double, representing a fraction of a second. + * Multiplies the fractional value by 1,000,000,000 (nanoseconds in a second) and returns the result + * as a long integer, representing the equivalent nanoseconds. + * + * @param fractionalSecondText The string representation of the fractional part of a second. + * @return The equivalent nanoseconds of the given fractional second. + */ + public static long fromFractionToNanos(String fractionalSecondText) { + double fractionalSecond = Double.parseDouble(fractionalSecondText); + // Multiply the fractional value by 1,000,000,000 (nanoseconds in a second) and cast it to a long. + return (long) (fractionalSecond * 1_000_000_000); + } + /** * Converts milliseconds to seconds. * @@ -1102,8 +1117,23 @@ public static boolean isWithinRange(Date date, Date from, Date to) { * @return A string describing the elapsed time in a human-readable format. */ public static String since(Date date) { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime then = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); + return since(new Date(), date); + } + + /** + * Provides a human-readable string representing the time elapsed since the given date. + *

+ * This function calculates the duration between the provided date and the current date, + * and returns a string describing this duration in a human-readable format such as "just now", + * "X minutes ago", "X hours ago", etc. + * + * @param source The reference date to calculate the duration from. Typically, this would be the current date. + * @param target The date from which the elapsed time is calculated. + * @return A string describing the elapsed time in a human-readable format. + */ + public static String since(Date source, Date target) { + LocalDateTime now = transform(source); + LocalDateTime then = LocalDateTime.ofInstant(target.toInstant(), ZoneId.systemDefault()); Duration duration = Duration.between(then, now); long seconds = duration.getSeconds(); long minutes = duration.toMinutes(); @@ -1112,7 +1142,6 @@ public static String since(Date date) { long weeks = days / 7; long months = days / 30; long years = days / 365; - if (seconds < 60) { return "just now"; } else if (minutes < 60) { @@ -1129,4 +1158,41 @@ public static String since(Date date) { return years + " year" + (years > 1 ? "s" : "") + " ago"; } } + + /** + * Retrieves the ZoneId corresponding to the given time zone identifier. + * If the provided time zone identifier is empty or null, the system default ZoneId is returned. + * If the time zone identifier starts with a plus (+) or minus (-) sign, it is treated as an offset + * and converted to a ZoneId using the GMT time zone. + * If the time zone identifier is a valid time zone identifier, it is converted to a ZoneId directly. + * If the time zone identifier is not valid or cannot be recognized, an attempt is made to retrieve + * the corresponding TimeZone instance, and its ZoneId is returned. If the TimeZone instance has + * a raw offset of 0 (indicating an unknown or invalid time zone), an exception is thrown. + * + * @param tz The time zone identifier string. + * @return The corresponding ZoneId for the given time zone identifier. + * @throws IllegalArgumentException if the time zone identifier is invalid or cannot be recognized. + */ + public static ZoneId parseTimeZone(String tz) { + if (String4j.isEmpty(tz)) { + return ZoneId.systemDefault(); + } + if (tz.startsWith("-") || tz.startsWith("+")) { + ZoneOffset offset = ZoneOffset.of(tz); // If the time zone identifier starts with '+' or '-', treat it as an offset and convert it to a ZoneId. + return ZoneId.ofOffset("GMT", offset); + } else { + try { + return ZoneId.of(tz); // Attempt to parse the time zone identifier directly as a ZoneId. + } catch (Exception e) { + // If the time zone identifier is not recognized as a valid ZoneId, attempt to retrieve the corresponding TimeZone. + TimeZone timezone = TimeZone.getTimeZone(tz); + if (timezone.getRawOffset() == 0) { + // If the retrieved TimeZone has a raw offset of 0, indicating an unknown or invalid time zone, throw an exception. + throw e; + } + // Otherwise, convert the retrieved TimeZone to a ZoneId and return it. + return timezone.toZoneId(); + } + } + } } diff --git a/plugin/src/main/groovy/org/unify4j/text/TimeFormatText.java b/plugin/src/main/groovy/org/unify4j/text/TimeFormatText.java index e5b9337..12acf18 100755 --- a/plugin/src/main/groovy/org/unify4j/text/TimeFormatText.java +++ b/plugin/src/main/groovy/org/unify4j/text/TimeFormatText.java @@ -16,13 +16,13 @@ public class TimeFormatText { public static final String MEDIUM_EPOCH_PATTERN = "MMM d, y, h:mm:ss a"; /** - * Format: June 15, 2019 at 10:54:25 PM GMT+5 + * Format: June 15, 2019, at 10:54:25 PM GMT+5 * Description: Long pattern for date and time including full month name, day, year, hour, minutes, seconds, AM/PM indicator, and timezone. */ public static final String LONG_EPOCH_PATTERN = "MMMM d, y, h:mm:ss a z"; /** - * Format: Saturday, June 15, 2019 at 10:54:25 PM GMT+05:30 + * Format: Saturday, June 15, 2019, at 10:54:25 PM GMT+05:30 * Description: Completed pattern for date and time including day of the week, full month name, day, year, hour, minutes, seconds, AM/PM indicator, and timezone. */ public static final String COMPLETED_EPOCH_PATTERN = "EEEE, MMMM d, y, h:mm:ss a zzzz"; @@ -40,13 +40,13 @@ public class TimeFormatText { public static final String MEDIUM_DATE_EPOCH_PATTERN = "MMM d, y"; /** - * Format: June 15, 2019 + * Format: June 15, 2019, * Description: Long pattern for date only including full month name, day, and year. */ public static final String LONG_DATE_EPOCH_PATTERN = "MMMM d, y"; /** - * Format: Saturday, June 15, 2019 + * Format: Saturday, June 15, 2019, * Description: Completed pattern for date only including day of the week, full month name, day, and year. */ public static final String COMPLETED_DATE_EPOCH_PATTERN = "EEEE, MMMM d, y"; diff --git a/plugin/src/test/groovy/org/unify4j/Date4jTest.java b/plugin/src/test/groovy/org/unify4j/Date4jTest.java new file mode 100644 index 0000000..8ab3a9a --- /dev/null +++ b/plugin/src/test/groovy/org/unify4j/Date4jTest.java @@ -0,0 +1,17 @@ +package org.unify4j; + +import org.junit.Test; +import org.unify4j.common.Date4j; +import org.unify4j.common.Time4j; +import org.unify4j.text.TimeFormatText; + +import java.util.Date; + +public class Date4jTest { + + @Test + public void testParseDate() { + Date date = Date4j.parse("2024/05/25 19:54:23"); + System.out.println(Time4j.since(date) + " * " + Time4j.format(date, TimeFormatText.SPREADSHEET_BIBLIOGRAPHY_EPOCH_PATTERN)); + } +} diff --git a/plugin/src/test/groovy/org/unify4j/UniqueIdTest.java b/plugin/src/test/groovy/org/unify4j/UniqueId4jTest.java similarity index 99% rename from plugin/src/test/groovy/org/unify4j/UniqueIdTest.java rename to plugin/src/test/groovy/org/unify4j/UniqueId4jTest.java index ea85d13..53a74dc 100644 --- a/plugin/src/test/groovy/org/unify4j/UniqueIdTest.java +++ b/plugin/src/test/groovy/org/unify4j/UniqueId4jTest.java @@ -16,7 +16,7 @@ import static org.junit.Assert.assertEquals; import static org.unify4j.common.UniqueId4j.*; -public class UniqueIdTest { +public class UniqueId4jTest { protected static final int bucketSize = 200000; @Test