|
45 | 45 | import java.text.ParseException; |
46 | 46 | import java.text.ParsePosition; |
47 | 47 | import java.text.SimpleDateFormat; |
| 48 | +import java.time.DateTimeException; |
48 | 49 | import java.time.LocalDate; |
49 | 50 | import java.time.LocalDateTime; |
50 | 51 | import java.time.LocalTime; |
| 52 | +import java.time.Year; |
| 53 | +import java.time.YearMonth; |
51 | 54 | import java.time.ZoneId; |
52 | 55 | import java.time.ZonedDateTime; |
53 | 56 | import java.time.format.DateTimeFormatter; |
| 57 | +import java.time.temporal.Temporal; |
54 | 58 | import java.util.Arrays; |
55 | 59 | import java.util.Calendar; |
56 | 60 | import java.util.Date; |
|
62 | 66 | import java.util.TimeZone; |
63 | 67 | import java.util.concurrent.ConcurrentHashMap; |
64 | 68 | import java.util.concurrent.TimeUnit; |
| 69 | +import java.util.regex.Matcher; |
65 | 70 | import java.util.regex.Pattern; |
66 | 71 |
|
67 | 72 | import static java.util.Calendar.AM_PM; |
@@ -96,6 +101,9 @@ private DateUtil() |
96 | 101 |
|
97 | 102 | // SimpleDataFormat does not support microseconds, it can only support up to milliseconds |
98 | 103 | private static final Pattern NON_SIMPLE_PRECISION_TIME_PATTERN = Pattern.compile(".*([0-5][0-9]):([0-5][0-9])\\.(\\d{4,6}).*"); |
| 104 | + private static final Pattern YEAR_PATTERN = Pattern.compile("\\d{4}"); |
| 105 | + // The scanner reads a trailing number as the day, so "2024-02" would be February 2nd of an unknown month |
| 106 | + private static final Pattern YEAR_MONTH_PATTERN = Pattern.compile("(\\d{4})[-/](\\d{1,2})"); |
99 | 107 | private static final String[] SIMPLE_TIME_FORMATS_WITH_AMPM = {"hh:mm:ss.SSS a", "hh:mm:ss a", "hh:mm a"}; |
100 | 108 | private static final String[] SIMPLE_TIME_FORMATS_NO_AMPM = { |
101 | 109 | ISO_LONG_TIME_FORMAT_STRING, |
@@ -809,6 +817,46 @@ private static long parseDate(String s, MonthDayOption md, @Nullable String extr |
809 | 817 | } |
810 | 818 | } |
811 | 819 |
|
| 820 | + /** Parses a date that may stop at the month or year, such as "2024", "2024-02" or "Feb 2024", using the site's month/day setting. */ |
| 821 | + public static @Nullable Temporal parsePartialDate(@NotNull String s) |
| 822 | + { |
| 823 | + MonthDayOption monthDayOption = LookAndFeelProperties.getInstance(ContainerManager.getRoot()).getDateParsingMode().getDayMonth(); |
| 824 | + |
| 825 | + return parsePartialDate(s, monthDayOption); |
| 826 | + } |
| 827 | + |
| 828 | + /** |
| 829 | + * Returns a LocalDate, YearMonth or Year, or null when s isn't one. Fields are taken as written, so any time is |
| 830 | + * ignored and an explicit zone can't shift the date into the server's zone. |
| 831 | + */ |
| 832 | + public static @Nullable Temporal parsePartialDate(@NotNull String s, @NotNull MonthDayOption md) |
| 833 | + { |
| 834 | + s = s.trim(); |
| 835 | + if (YEAR_PATTERN.matcher(s).matches()) |
| 836 | + return Year.of(Integer.parseInt(s)); |
| 837 | + |
| 838 | + try |
| 839 | + { |
| 840 | + Matcher m = YEAR_MONTH_PATTERN.matcher(s); |
| 841 | + if (m.matches()) |
| 842 | + return YearMonth.of(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2))); |
| 843 | + |
| 844 | + CalendarParts parts = new CalendarParts(); |
| 845 | + ParseDateTimeEN.getInstance(_localeDefault, _timezoneDefault, DateTimeOption.DateTime, md, false).scan(s, new ParsePosition(0), parts); |
| 846 | + if (!parts.isSet(YEAR, MONTH)) |
| 847 | + return null; |
| 848 | + if (parts.isSet(DAY_OF_MONTH)) |
| 849 | + return LocalDate.of(parts.getYear(), parts.getMonth() + 1, parts.getDayOfMonth()); |
| 850 | + if (parts.isHourSet() || parts.anySet(MINUTE, SECOND)) |
| 851 | + return null; |
| 852 | + return YearMonth.of(parts.getYear(), parts.getMonth() + 1); |
| 853 | + } |
| 854 | + catch (ConversionException | DateTimeException e) |
| 855 | + { |
| 856 | + return null; |
| 857 | + } |
| 858 | + } |
| 859 | + |
812 | 860 | public static @Nullable Date parseSimpleTime(@NotNull String s) |
813 | 861 | { |
814 | 862 | Date duration = null; |
@@ -1826,6 +1874,23 @@ public void testDateTimeCH() |
1826 | 1874 | assertEquals(dateExpected, DateUtil.parseDateTime("3.2.2001", MonthDayOption.DAY_MONTH)); |
1827 | 1875 | } |
1828 | 1876 |
|
| 1877 | + @Test |
| 1878 | + public void testParsePartialDate() |
| 1879 | + { |
| 1880 | + assertEquals(Year.of(2024), parsePartialDate("2024", MonthDayOption.MONTH_DAY)); |
| 1881 | + YearMonth feb = YearMonth.of(2024, 2); |
| 1882 | + for (String s : List.of("2024-02", "2024/2", "Feb 2024", "Feb. 2024", "february 2024")) |
| 1883 | + assertEquals(s, feb, parsePartialDate(s, MonthDayOption.MONTH_DAY)); |
| 1884 | + |
| 1885 | + assertEquals(LocalDate.of(2024, 2, 3), parsePartialDate("2/3/2024", MonthDayOption.MONTH_DAY)); |
| 1886 | + assertEquals(LocalDate.of(2024, 3, 2), parsePartialDate("2/3/2024", MonthDayOption.DAY_MONTH)); |
| 1887 | + assertEquals(LocalDate.of(2024, 2, 3), parsePartialDate("Feb 3, 2024 11:38 pm", MonthDayOption.MONTH_DAY)); |
| 1888 | + assertEquals(LocalDate.of(2024, 2, 3), parsePartialDate("2024-02-03T23:30:00Z", MonthDayOption.MONTH_DAY)); |
| 1889 | + |
| 1890 | + for (String s : List.of("2024-13", "2023-02-29", "31/12/2023", "Feb", "86", "Feb 2024 11:38", "11:38", "Sodium")) |
| 1891 | + assertNull(s, parsePartialDate(s, MonthDayOption.MONTH_DAY)); |
| 1892 | + } |
| 1893 | + |
1829 | 1894 | private void assertXmlDateMatches(long millis) |
1830 | 1895 | { |
1831 | 1896 | Calendar parsedXml = new GregorianCalendar(); |
|
0 commit comments