1414
1515package dev .cel .common .internal ;
1616
17+ import static dev .cel .common .internal .ProtoTimeUtils .DURATION_SECONDS_MAX ;
18+ import static dev .cel .common .internal .ProtoTimeUtils .DURATION_SECONDS_MIN ;
19+ import static dev .cel .common .internal .ProtoTimeUtils .MAX_NANOS ;
20+ import static dev .cel .common .internal .ProtoTimeUtils .NANOS_PER_SECOND ;
21+ import static dev .cel .common .internal .ProtoTimeUtils .TIMESTAMP_SECONDS_MAX ;
22+ import static dev .cel .common .internal .ProtoTimeUtils .TIMESTAMP_SECONDS_MIN ;
23+
1724import com .google .common .base .Strings ;
1825import com .google .protobuf .Timestamp ;
1926import dev .cel .common .annotations .Internal ;
3239public final class DateTimeHelpers {
3340 public static final String UTC = "UTC" ;
3441
35- // Timestamp for "0001-01-01T00:00:00Z"
36- private static final long TIMESTAMP_SECONDS_MIN = -62135596800L ;
37- // Timestamp for "9999-12-31T23:59:59Z"
38- private static final long TIMESTAMP_SECONDS_MAX = 253402300799L ;
39-
40- private static final long DURATION_SECONDS_MIN = -315576000000L ;
41- private static final long DURATION_SECONDS_MAX = 315576000000L ;
42- private static final int NANOS_PER_SECOND = 1000000000 ;
42+ private static final Duration DURATION_MIN = Duration .ofSeconds (DURATION_SECONDS_MIN , -MAX_NANOS );
43+ private static final Duration DURATION_MAX = Duration .ofSeconds (DURATION_SECONDS_MAX , MAX_NANOS );
4344
4445 /**
4546 * Constructs a new {@link LocalDateTime} instance
@@ -79,6 +80,14 @@ public static LocalDateTime newLocalDateTime(Instant instant, String tz) {
7980 return instant .atZone (timeZone (tz )).toLocalDateTime ();
8081 }
8182
83+ /** Creates an {@link Instant} from epoch seconds, validating CEL timestamp bounds. */
84+ // CEL's timestamp(int) conversion accepts epoch seconds as an int64.
85+ @ SuppressWarnings ("GoodTime-ApiWithNumericTimeUnit" )
86+ public static Instant fromSecondsToInstant (long seconds ) {
87+ checkSecondsValid (seconds );
88+ return Instant .ofEpochSecond (seconds );
89+ }
90+
8291 /**
8392 * Parse from RFC 3339 date string to {@link java.time.Instant}.
8493 *
@@ -92,6 +101,75 @@ public static Instant parse(String text) {
92101 return instant ;
93102 }
94103
104+ /** Parses a CEL duration string (following Go {@code time.ParseDuration} syntax). */
105+ public static Duration parseDuration (String text ) {
106+ if (text .isEmpty ()) {
107+ throw invalidDurationFormat ();
108+ }
109+ int len = text .length ();
110+ int i = 0 ;
111+ char first = text .charAt (0 );
112+ boolean negative = (first == '-' );
113+ if (negative || first == '+' ) {
114+ i ++;
115+ }
116+ if (i == len ) {
117+ throw invalidDurationFormat ();
118+ }
119+ if (len - i == 1 && text .charAt (i ) == '0' ) {
120+ return Duration .ZERO ;
121+ }
122+ long totalSeconds = 0 ;
123+ long totalNanos = 0 ;
124+ while (i < len ) {
125+ int intStart = i ;
126+ int intEnd = scanAsciiDigits (text , intStart );
127+ int fracStart = intEnd ;
128+ int fracEnd = intEnd ;
129+ if (intEnd < len && text .charAt (intEnd ) == '.' ) {
130+ fracStart = intEnd + 1 ;
131+ fracEnd = scanAsciiDigits (text , fracStart );
132+ }
133+ if (intStart == intEnd && fracStart == fracEnd ) {
134+ throw invalidDurationFormat ();
135+ }
136+ i = fracEnd ;
137+ int unitStart = i ;
138+ while (i < len && text .charAt (i ) != '.' && !isAsciiDigit (text .charAt (i ))) {
139+ i ++;
140+ }
141+ long unitNanos = parseUnitNanos (text .substring (unitStart , i ));
142+
143+ long segSeconds = 0 ;
144+ long segNanos = 0 ;
145+ for (int p = intStart ; p < intEnd ; p ++) {
146+ int digit = text .charAt (p ) - '0' ;
147+ segSeconds *= 10 ;
148+ segNanos = segNanos * 10 + digit * unitNanos ;
149+ segSeconds += segNanos / NANOS_PER_SECOND ;
150+ segNanos %= NANOS_PER_SECOND ;
151+ if (segSeconds > DURATION_SECONDS_MAX ) {
152+ throw outOfRangeDuration (text );
153+ }
154+ }
155+
156+ long fracNanos = 0 ;
157+ for (int p = fracEnd - 1 ; p >= fracStart ; p --) {
158+ fracNanos = ((text .charAt (p ) - '0' ) * unitNanos + fracNanos ) / 10 ;
159+ }
160+
161+ totalNanos += segNanos + fracNanos ;
162+ totalSeconds += segSeconds + totalNanos / NANOS_PER_SECOND ;
163+ totalNanos %= NANOS_PER_SECOND ;
164+ if (totalSeconds > DURATION_SECONDS_MAX ) {
165+ throw outOfRangeDuration (text );
166+ }
167+ }
168+ return negative
169+ ? Duration .ofSeconds (-totalSeconds , -totalNanos )
170+ : Duration .ofSeconds (totalSeconds , totalNanos );
171+ }
172+
95173 /** Adds a duration to an instant. */
96174 public static Instant add (Instant ts , Duration dur ) {
97175 Instant newInstant = ts .plus (dur );
@@ -124,48 +202,63 @@ public static Duration subtract(Duration d1, Duration d2) {
124202 return newDuration ;
125203 }
126204
127- /**
128- * Formats a {@link Duration} into a minimal seconds-based representation.
129- *
130- * <p>Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation
131- */
205+ /** Calculates the difference between two instants. */
206+ public static Duration between (Instant d1 , Instant d2 ) {
207+ Duration diff = Duration .between (d1 , d2 );
208+ checkValid (diff );
209+ return diff ;
210+ }
211+
212+ /** Formats a {@link Duration} into a minimal seconds-based representation. */
132213 public static String toString (Duration duration ) {
133- if (duration .isZero ()) {
134- return "0s" ;
214+ checkValid (duration );
215+ long seconds = duration .getSeconds ();
216+ int nanos = duration .getNano ();
217+ if (seconds < 0 && nanos > 0 ) {
218+ seconds ++;
219+ nanos -= NANOS_PER_SECOND ;
135220 }
221+ return ProtoTimeUtils .formatDuration (seconds , nanos );
222+ }
136223
137- long totalNanos = duration .toNanos ();
138- StringBuilder sb = new StringBuilder ();
139-
140- if (totalNanos < 0 ) {
141- sb .append ('-' );
142- totalNanos = -totalNanos ;
224+ private static int scanAsciiDigits (String text , int i ) {
225+ while (i < text .length () && isAsciiDigit (text .charAt (i ))) {
226+ i ++;
143227 }
228+ return i ;
229+ }
144230
145- long seconds = totalNanos / 1_000_000_000 ;
146- int nanos = (int ) (totalNanos % 1_000_000_000 );
147-
148- sb .append (seconds );
149-
150- // Follows ProtoTimeUtils.toString(Duration) implementation
151- if (nanos > 0 ) {
152- sb .append ('.' );
153- if (nanos % 1_000_000 == 0 ) {
154- // Millisecond precision (3 digits)
155- int millis = nanos / 1_000_000 ;
156- sb .append (String .format (Locale .US , "%03d" , millis ));
157- } else if (nanos % 1_000 == 0 ) {
158- // Microsecond precision (6 digits)
159- int micros = nanos / 1_000 ;
160- sb .append (String .format (Locale .US , "%06d" , micros ));
161- } else {
162- // Nanosecond precision (9 digits)
163- sb .append (String .format (Locale .US , "%09d" , nanos ));
164- }
231+ private static boolean isAsciiDigit (char c ) {
232+ return c >= '0' && c <= '9' ;
233+ }
234+
235+ private static long parseUnitNanos (String unit ) {
236+ switch (unit ) {
237+ case "ns" :
238+ return 1L ;
239+ case "us" :
240+ case "\u00b5 s" :
241+ case "\u03bc s" :
242+ return 1_000L ;
243+ case "ms" :
244+ return 1_000_000L ;
245+ case "s" :
246+ return NANOS_PER_SECOND ;
247+ case "m" :
248+ return 60L * NANOS_PER_SECOND ;
249+ case "h" :
250+ return 3600L * NANOS_PER_SECOND ;
251+ default :
252+ throw invalidDurationFormat ();
165253 }
254+ }
255+
256+ private static IllegalArgumentException invalidDurationFormat () {
257+ return new IllegalArgumentException ("invalid duration format" );
258+ }
166259
167- sb . append ( 's' );
168- return sb . toString ( );
260+ private static IllegalArgumentException outOfRangeDuration ( String text ) {
261+ return new IllegalArgumentException ( "Duration is out of valid range: " + text );
169262 }
170263
171264 /**
@@ -205,55 +298,28 @@ private static ZoneId timeZone(String tz) {
205298
206299 /** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */
207300 public static void checkValid (Instant instant ) {
208- long seconds = instant .getEpochSecond ();
209-
210- if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX ) {
211- throw new IllegalArgumentException (
212- Strings .lenientFormat (
213- "Timestamp is not valid. "
214- + "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. "
215- + "Nanos (%s) must be in range [0, +999,999,999]." ,
216- seconds , instant .getNano ()));
217- }
301+ checkSecondsValid (instant .getEpochSecond ());
218302 }
219303
220304 /** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */
221- private static void checkValid (Duration duration ) {
222- long seconds = duration .getSeconds ();
223- int nanos = duration .getNano ();
224- if (!isDurationValid (seconds , nanos )) {
305+ public static void checkValid (Duration duration ) {
306+ if (duration .compareTo (DURATION_MIN ) < 0 || duration .compareTo (DURATION_MAX ) > 0 ) {
225307 throw new IllegalArgumentException (
226308 Strings .lenientFormat (
227- "Duration is not valid. "
228- + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
229- + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
230- + "Nanos must have the same sign as seconds" ,
231- seconds , nanos ));
309+ "Duration is out of valid range [-315,576,000,000.999999999s,"
310+ + " +315,576,000,000.999999999s]: %s" ,
311+ duration ));
232312 }
233313 }
234314
235- /**
236- * Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code
237- * seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
238- * value must be in the range [-999,999,999, +999,999,999].
239- *
240- * <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
241- * and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
242- * value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
243- */
244- private static boolean isDurationValid (long seconds , int nanos ) {
245- if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX ) {
246- return false ;
247- }
248- if (nanos < -999999999L || nanos >= NANOS_PER_SECOND ) {
249- return false ;
250- }
251- if (seconds < 0 || nanos < 0 ) {
252- if (seconds > 0 || nanos > 0 ) {
253- return false ;
254- }
315+ private static void checkSecondsValid (long seconds ) {
316+ if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX ) {
317+ throw new IllegalArgumentException (
318+ Strings .lenientFormat (
319+ "Timestamp is not valid. Seconds (%s) must be in range [-62,135,596,800,"
320+ + " +253,402,300,799]." ,
321+ seconds ));
255322 }
256- return true ;
257323 }
258324
259325 private DateTimeHelpers () {}
0 commit comments