001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.svl.functions;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021
022import java.time.*;
023import java.time.format.*;
024
025import org.apache.juneau.commons.svl.*;
026
027/**
028 * Date/time functions for the {@code #{...}} script catalog.
029 *
030 * <p>
031 * All functions exchange epoch milliseconds as the integration currency. Formatting and parsing
032 * use {@link DateTimeFormatter} patterns for the explicit-pattern variants;
033 * the no-pattern variants use ISO-8601 ({@code Instant.toString()} on the way out, the ISO
034 * detection chain on the way in).
035 */
036public final class DateFunctions {
037
038   private DateFunctions() {}
039
040   /** All function classes in this category. */
041   @SuppressWarnings({
042      "java:S2386", // ALL is an immutable compile-time registry; exposed as an array for the cross-package/varargs functions(...) API, so visibility cannot be reduced.
043      "unchecked" // Cast is safe: type verified by caller context.
044   })
045   public static final Class<? extends VarFunction>[] ALL = new Class[] {
046      Now.class, ParseDate.class, FormatDate.class
047   };
048
049   /** {@code #{now()}} — current epoch milliseconds (UTC). */
050   public static class Now extends TypedFunction {
051      @Override public String name() { return "now"; }
052      public String invoke() { return String.valueOf(System.currentTimeMillis()); }
053   }
054
055   /**
056    * {@code #{parseDate(s)}} / {@code #{parseDate(s, format)}} — parses an ISO-8601 string or
057    * a {@link DateTimeFormatter}-pattern string to epoch milliseconds.
058    *
059    * <p>
060    * The single-arg form tries, in order: {@link Instant#parse}, {@link OffsetDateTime#parse},
061    * {@link ZonedDateTime#parse}, {@link LocalDateTime#parse} (assumed UTC),
062    * {@link LocalDate#parse} (start-of-day UTC).
063    */
064   public static class ParseDate extends TypedFunction {
065      @Override public String name() { return "parseDate"; }
066
067      public String invoke(String s) {
068         if (isEmpty(s)) return "0";
069         return String.valueOf(parseIso(s));
070      }
071
072      public String invoke(String s, String format) {
073         if (isEmpty(s)) return "0";
074         if (isEmpty(format)) return invoke(s);
075         var fmt = DateTimeFormatter.ofPattern(format);
076         try {
077            return String.valueOf(LocalDateTime.parse(s, fmt).toInstant(ZoneOffset.UTC).toEpochMilli());
078         } catch (@SuppressWarnings("unused") DateTimeParseException e) {
079            // Try as a date-only pattern.
080         }
081         return String.valueOf(LocalDate.parse(s, fmt).atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli());
082      }
083
084      @SuppressWarnings({
085         "java:S1166", // Exceptions swallowed intentionally — this is a try-each-format chain.
086         "java:S3776" // Cognitive complexity: small ISO-format dispatch.
087      })
088      private static long parseIso(String s) {
089         try { return Instant.parse(s).toEpochMilli(); } catch (@SuppressWarnings("unused") DateTimeParseException e) { /* fall through */ }
090         try { return OffsetDateTime.parse(s).toInstant().toEpochMilli(); } catch (@SuppressWarnings("unused") DateTimeParseException e) { /* fall through */ }
091         try { return ZonedDateTime.parse(s).toInstant().toEpochMilli(); } catch (@SuppressWarnings("unused") DateTimeParseException e) { /* fall through */ }
092         try { return LocalDateTime.parse(s).toInstant(ZoneOffset.UTC).toEpochMilli(); } catch (@SuppressWarnings("unused") DateTimeParseException e) { /* fall through */ }
093         return LocalDate.parse(s).atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli();
094      }
095   }
096
097   /**
098    * {@code #{formatDate(epoch)}} — renders {@code epoch} as ISO-8601 ({@link Instant#toString()}).
099    *
100    * <p>
101    * {@code #{formatDate(epoch, format)}} — renders {@code epoch} per a
102    * {@link DateTimeFormatter} pattern in UTC.
103    */
104   public static class FormatDate extends TypedFunction {
105      @Override public String name() { return "formatDate"; }
106
107      public String invoke(long epoch) {
108         return Instant.ofEpochMilli(epoch).toString();
109      }
110
111      public String invoke(long epoch, String format) {
112         if (ie(format)) return invoke(epoch);
113         var fmt = DateTimeFormatter.ofPattern(format).withZone(ZoneOffset.UTC);
114         return fmt.format(Instant.ofEpochMilli(epoch));
115      }
116   }
117}