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}