Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,3 +137,13 @@ cel_android_library(
name = "proto_time_utils_android",
exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"],
)

java_library(
name = "date_time_helpers",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers"],
)

cel_android_library(
name = "date_time_helpers_android",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
)
1 change: 1 addition & 0 deletions common/src/main/java/dev/cel/common/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,6 +205,7 @@ java_library(
tags = [
],
deps = [
"//common/internal:date_time_helpers",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
Expand Down
10 changes: 10 additions & 0 deletions common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,10 @@
import com.google.protobuf.Struct;
import com.google.protobuf.Timestamp;
import com.google.protobuf.Value;
import dev.cel.common.internal.DateTimeHelpers;
import dev.cel.common.internal.ProtoTimeUtils;
import dev.cel.common.values.CelByteString;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
Expand DownExpand Up@@ -118,6 +120,14 @@ public static Value adaptValueToJsonValue(Object value) {
String duration = ProtoTimeUtils.toString((Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof Instant) {
// Instant's toString follows RFC 3339
return json.setStringValue(value.toString()).build();
}
if (value instanceof java.time.Duration) {
String duration = DateTimeHelpers.toString((java.time.Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof FieldMask) {
String fieldMaskStr = toJsonString((FieldMask) value);
return json.setStringValue(fieldMaskStr).build();
Expand Down
30 changes: 30 additions & 0 deletions common/src/main/java/dev/cel/common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -195,9 +195,11 @@ java_library(
deps = [
":well_known_proto",
"//common:error_codes",
"//common:options",
"//common:proto_json_adapter",
"//common:runtime_exception",
"//common/annotations",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
"@maven//:com_google_errorprone_error_prone_annotations",
Expand DownExpand Up@@ -429,3 +431,31 @@ cel_android_library(
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)

java_library(
name = "date_time_helpers",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
],
)

cel_android_library(
name = "date_time_helpers_android",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven_android//:com_google_guava_guava",
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)
261 changes: 261 additions & 0 deletions common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dev.cel.common.internal;

import com.google.common.base.Strings;
import com.google.protobuf.Timestamp;
import dev.cel.common.CelErrorCode;
import dev.cel.common.CelRuntimeException;
import dev.cel.common.annotations.Internal;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Locale;

/** Collection of utility methods for CEL datetime handlings. */
@Internal
@SuppressWarnings("JavaInstantGetSecondsGetNano") // Intended within CEL.
public final class DateTimeHelpers {
public static final String UTC = "UTC";

// Timestamp for "0001-01-01T00:00:00Z"
private static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
// Timestamp for "9999-12-31T23:59:59Z"
private static final long TIMESTAMP_SECONDS_MAX = 253402300799L;

private static final long DURATION_SECONDS_MIN = -315576000000L;
private static final long DURATION_SECONDS_MAX = 315576000000L;
private static final int NANOS_PER_SECOND = 1000000000;

/**
* Constructs a new {@link LocalDateTime} instance
*
* @param ts Timestamp protobuf object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return If an Invalid timezone is supplied.
*/
public static LocalDateTime newLocalDateTime(Timestamp ts, String tz) {
return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos())
.atZone(timeZone(tz))
.toLocalDateTime();
}

/**
* Constructs a new {@link LocalDateTime} instance from a Java Instant.
*
* @param instant Instant object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return A new {@link LocalDateTime} instance.
*/
public static LocalDateTime newLocalDateTime(Instant instant, String tz) {
return instant.atZone(timeZone(tz)).toLocalDateTime();
}

/**
* Parse from RFC 3339 date string to {@link java.time.Instant}.
*
* <p>Example of accepted format: "1972-01-01T10:00:20.021-05:00"
*/
public static Instant parse(String text) {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(text);
Instant instant = offsetDateTime.toInstant();
checkValid(instant);

return instant;
}

/** Adds a duration to an instant. */
public static Instant add(Instant ts, Duration dur) {
Instant newInstant = ts.plus(dur);
checkValid(newInstant);

return newInstant;
}

/** Adds two durations */
public static Duration add(Duration d1, Duration d2) {
Duration newDuration = d1.plus(d2);
checkValid(newDuration);

return newDuration;
}

/** Subtracts a duration to an instant. */
public static Instant subtract(Instant ts, Duration dur) {
Instant newInstant = ts.minus(dur);
checkValid(newInstant);

return newInstant;
}

/** Subtract a duration from another. */
public static Duration subtract(Duration d1, Duration d2) {
Duration newDuration = d1.minus(d2);
checkValid(newDuration);

return newDuration;
}

/**
* Formats a {@link Duration} into a minimal seconds-based representation.
*
* <p>Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation
*/
public static String toString(Duration duration) {
if (duration.isZero()) {
return "0s";
}

long totalNanos = duration.toNanos();
StringBuilder sb = new StringBuilder();

if (totalNanos < 0) {
sb.append('-');
totalNanos = -totalNanos;
}

long seconds = totalNanos / 1_000_000_000;
int nanos = (int) (totalNanos % 1_000_000_000);

sb.append(seconds);

// Follows ProtoTimeUtils.toString(Duration) implementation
if (nanos > 0) {
sb.append('.');
if (nanos % 1_000_000 == 0) {
// Millisecond precision (3 digits)
int millis = nanos / 1_000_000;
sb.append(String.format(Locale.US, "%03d", millis));
} else if (nanos % 1_000 == 0) {
// Microsecond precision (6 digits)
int micros = nanos / 1_000;
sb.append(String.format(Locale.US, "%06d", micros));
} else {
// Nanosecond precision (9 digits)
sb.append(String.format(Locale.US, "%09d", nanos));
}
}

sb.append('s');
return sb.toString();
}

/**
* Get the DateTimeZone Instance.
*
* @param tz the ID of the datetime zone
* @return the ZoneId object
*/
private static ZoneId timeZone(String tz) {
try {
return ZoneId.of(tz);
} catch (DateTimeException e) {
// If timezone is not a string name (for example, 'US/Central'), it should be a numerical
// offset from UTC in the format [+/-]HH:MM.
try {
int ind = tz.indexOf(":");
if (ind == -1) {
throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT);
}

int hourOffset = Integer.parseInt(tz.substring(0, ind));
int minOffset = Integer.parseInt(tz.substring(ind + 1));
// Ensures that the offset are properly formatted in [+/-]HH:MM to conform with
// ZoneOffset's format requirements.
// Example: "-9:30" -> "-09:30" and "9:30" -> "+09:30"
String formattedOffset =
((hourOffset < 0) ? "-" : "+")
+ String.format(Locale.US, "%02d:%02d", Math.abs(hourOffset), minOffset);

return ZoneId.of(formattedOffset);

} catch (DateTimeException e2) {
throw new CelRuntimeException(e2, CelErrorCode.BAD_FORMAT);
}
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */
private static void checkValid(Instant instant) {
long seconds = instant.getEpochSecond();

if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Timestamp is not valid. "
+ "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. "
+ "Nanos (%s) must be in range [0, +999,999,999].",
seconds, instant.getNano()));
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */
private static void checkValid(Duration duration) {
long seconds = duration.getSeconds();
int nanos = duration.getNano();
if (!isDurationValid(seconds, nanos)) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Duration is not valid. "
+ "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
+ "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
+ "Nanos must have the same sign as seconds",
seconds, nanos));
}
}

/**
* Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code
* seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
* value must be in the range [-999,999,999, +999,999,999].
*
* <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
* and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
* value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
*/
private static boolean isDurationValid(long seconds, int nanos) {
if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
return false;
}
if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
return false;
}
if (seconds < 0 || nanos < 0) {
if (seconds > 0 || nanos > 0) {
return false;
}
}
return true;
}

private DateTimeHelpers() {}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ public final class ProtoAdapter {

public ProtoAdapter(DynamicProto dynamicProto, CelOptions celOptions) {
this.dynamicProto = checkNotNull(dynamicProto);
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions.enableUnsignedLongs());
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions);
this.celOptions = celOptions;
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,3 +137,13 @@ cel_android_library(
name = "proto_time_utils_android",
exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"],
)

java_library(
name = "date_time_helpers",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers"],
)

cel_android_library(
name = "date_time_helpers_android",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
)
1 change: 1 addition & 0 deletions common/src/main/java/dev/cel/common/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,6 +205,7 @@ java_library(
tags = [
],
deps = [
"//common/internal:date_time_helpers",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
Expand Down
10 changes: 10 additions & 0 deletions common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,10 @@
import com.google.protobuf.Struct;
import com.google.protobuf.Timestamp;
import com.google.protobuf.Value;
import dev.cel.common.internal.DateTimeHelpers;
import dev.cel.common.internal.ProtoTimeUtils;
import dev.cel.common.values.CelByteString;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
Expand DownExpand Up@@ -118,6 +120,14 @@ public static Value adaptValueToJsonValue(Object value) {
String duration = ProtoTimeUtils.toString((Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof Instant) {
// Instant's toString follows RFC 3339
return json.setStringValue(value.toString()).build();
}
if (value instanceof java.time.Duration) {
String duration = DateTimeHelpers.toString((java.time.Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof FieldMask) {
String fieldMaskStr = toJsonString((FieldMask) value);
return json.setStringValue(fieldMaskStr).build();
Expand Down
30 changes: 30 additions & 0 deletions common/src/main/java/dev/cel/common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -195,9 +195,11 @@ java_library(
deps = [
":well_known_proto",
"//common:error_codes",
"//common:options",
"//common:proto_json_adapter",
"//common:runtime_exception",
"//common/annotations",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
"@maven//:com_google_errorprone_error_prone_annotations",
Expand DownExpand Up@@ -429,3 +431,31 @@ cel_android_library(
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)

java_library(
name = "date_time_helpers",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
],
)

cel_android_library(
name = "date_time_helpers_android",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven_android//:com_google_guava_guava",
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)
261 changes: 261 additions & 0 deletions common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dev.cel.common.internal;

import com.google.common.base.Strings;
import com.google.protobuf.Timestamp;
import dev.cel.common.CelErrorCode;
import dev.cel.common.CelRuntimeException;
import dev.cel.common.annotations.Internal;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Locale;

/** Collection of utility methods for CEL datetime handlings. */
@Internal
@SuppressWarnings("JavaInstantGetSecondsGetNano") // Intended within CEL.
public final class DateTimeHelpers {
public static final String UTC = "UTC";

// Timestamp for "0001-01-01T00:00:00Z"
private static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
// Timestamp for "9999-12-31T23:59:59Z"
private static final long TIMESTAMP_SECONDS_MAX = 253402300799L;

private static final long DURATION_SECONDS_MIN = -315576000000L;
private static final long DURATION_SECONDS_MAX = 315576000000L;
private static final int NANOS_PER_SECOND = 1000000000;

/**
* Constructs a new {@link LocalDateTime} instance
*
* @param ts Timestamp protobuf object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return If an Invalid timezone is supplied.
*/
public static LocalDateTime newLocalDateTime(Timestamp ts, String tz) {
return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos())
.atZone(timeZone(tz))
.toLocalDateTime();
}

/**
* Constructs a new {@link LocalDateTime} instance from a Java Instant.
*
* @param instant Instant object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return A new {@link LocalDateTime} instance.
*/
public static LocalDateTime newLocalDateTime(Instant instant, String tz) {
return instant.atZone(timeZone(tz)).toLocalDateTime();
}

/**
* Parse from RFC 3339 date string to {@link java.time.Instant}.
*
* <p>Example of accepted format: "1972-01-01T10:00:20.021-05:00"
*/
public static Instant parse(String text) {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(text);
Instant instant = offsetDateTime.toInstant();
checkValid(instant);

return instant;
}

/** Adds a duration to an instant. */
public static Instant add(Instant ts, Duration dur) {
Instant newInstant = ts.plus(dur);
checkValid(newInstant);

return newInstant;
}

/** Adds two durations */
public static Duration add(Duration d1, Duration d2) {
Duration newDuration = d1.plus(d2);
checkValid(newDuration);

return newDuration;
}

/** Subtracts a duration to an instant. */
public static Instant subtract(Instant ts, Duration dur) {
Instant newInstant = ts.minus(dur);
checkValid(newInstant);

return newInstant;
}

/** Subtract a duration from another. */
public static Duration subtract(Duration d1, Duration d2) {
Duration newDuration = d1.minus(d2);
checkValid(newDuration);

return newDuration;
}

/**
* Formats a {@link Duration} into a minimal seconds-based representation.
*
* <p>Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation
*/
public static String toString(Duration duration) {
if (duration.isZero()) {
return "0s";
}

long totalNanos = duration.toNanos();
StringBuilder sb = new StringBuilder();

if (totalNanos < 0) {
sb.append('-');
totalNanos = -totalNanos;
}

long seconds = totalNanos / 1_000_000_000;
int nanos = (int) (totalNanos % 1_000_000_000);

sb.append(seconds);

// Follows ProtoTimeUtils.toString(Duration) implementation
if (nanos > 0) {
sb.append('.');
if (nanos % 1_000_000 == 0) {
// Millisecond precision (3 digits)
int millis = nanos / 1_000_000;
sb.append(String.format(Locale.US, "%03d", millis));
} else if (nanos % 1_000 == 0) {
// Microsecond precision (6 digits)
int micros = nanos / 1_000;
sb.append(String.format(Locale.US, "%06d", micros));
} else {
// Nanosecond precision (9 digits)
sb.append(String.format(Locale.US, "%09d", nanos));
}
}

sb.append('s');
return sb.toString();
}

/**
* Get the DateTimeZone Instance.
*
* @param tz the ID of the datetime zone
* @return the ZoneId object
*/
private static ZoneId timeZone(String tz) {
try {
return ZoneId.of(tz);
} catch (DateTimeException e) {
// If timezone is not a string name (for example, 'US/Central'), it should be a numerical
// offset from UTC in the format [+/-]HH:MM.
try {
int ind = tz.indexOf(":");
if (ind == -1) {
throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT);
}

int hourOffset = Integer.parseInt(tz.substring(0, ind));
int minOffset = Integer.parseInt(tz.substring(ind + 1));
// Ensures that the offset are properly formatted in [+/-]HH:MM to conform with
// ZoneOffset's format requirements.
// Example: "-9:30" -> "-09:30" and "9:30" -> "+09:30"
String formattedOffset =
((hourOffset < 0) ? "-" : "+")
+ String.format(Locale.US, "%02d:%02d", Math.abs(hourOffset), minOffset);

return ZoneId.of(formattedOffset);

} catch (DateTimeException e2) {
throw new CelRuntimeException(e2, CelErrorCode.BAD_FORMAT);
}
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */
private static void checkValid(Instant instant) {
long seconds = instant.getEpochSecond();

if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Timestamp is not valid. "
+ "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. "
+ "Nanos (%s) must be in range [0, +999,999,999].",
seconds, instant.getNano()));
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */
private static void checkValid(Duration duration) {
long seconds = duration.getSeconds();
int nanos = duration.getNano();
if (!isDurationValid(seconds, nanos)) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Duration is not valid. "
+ "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
+ "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
+ "Nanos must have the same sign as seconds",
seconds, nanos));
}
}

/**
* Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code
* seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
* value must be in the range [-999,999,999, +999,999,999].
*
* <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
* and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
* value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
*/
private static boolean isDurationValid(long seconds, int nanos) {
if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
return false;
}
if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
return false;
}
if (seconds < 0 || nanos < 0) {
if (seconds > 0 || nanos > 0) {
return false;
}
}
return true;
}

private DateTimeHelpers() {}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ public final class ProtoAdapter {

public ProtoAdapter(DynamicProto dynamicProto, CelOptions celOptions) {
this.dynamicProto = checkNotNull(dynamicProto);
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions.enableUnsignedLongs());
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions);
this.celOptions = celOptions;
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,3 +137,13 @@ cel_android_library(
name = "proto_time_utils_android",
exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"],
)

java_library(
name = "date_time_helpers",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers"],
)

cel_android_library(
name = "date_time_helpers_android",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
)
1 change: 1 addition & 0 deletions common/src/main/java/dev/cel/common/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,6 +205,7 @@ java_library(
tags = [
],
deps = [
"//common/internal:date_time_helpers",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
Expand Down
10 changes: 10 additions & 0 deletions common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,10 @@
import com.google.protobuf.Struct;
import com.google.protobuf.Timestamp;
import com.google.protobuf.Value;
import dev.cel.common.internal.DateTimeHelpers;
import dev.cel.common.internal.ProtoTimeUtils;
import dev.cel.common.values.CelByteString;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
Expand DownExpand Up@@ -118,6 +120,14 @@ public static Value adaptValueToJsonValue(Object value) {
String duration = ProtoTimeUtils.toString((Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof Instant) {
// Instant's toString follows RFC 3339
return json.setStringValue(value.toString()).build();
}
if (value instanceof java.time.Duration) {
String duration = DateTimeHelpers.toString((java.time.Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof FieldMask) {
String fieldMaskStr = toJsonString((FieldMask) value);
return json.setStringValue(fieldMaskStr).build();
Expand Down
30 changes: 30 additions & 0 deletions common/src/main/java/dev/cel/common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -195,9 +195,11 @@ java_library(
deps = [
":well_known_proto",
"//common:error_codes",
"//common:options",
"//common:proto_json_adapter",
"//common:runtime_exception",
"//common/annotations",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
"@maven//:com_google_errorprone_error_prone_annotations",
Expand DownExpand Up@@ -429,3 +431,31 @@ cel_android_library(
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)

java_library(
name = "date_time_helpers",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
],
)

cel_android_library(
name = "date_time_helpers_android",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven_android//:com_google_guava_guava",
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)
261 changes: 261 additions & 0 deletions common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dev.cel.common.internal;

import com.google.common.base.Strings;
import com.google.protobuf.Timestamp;
import dev.cel.common.CelErrorCode;
import dev.cel.common.CelRuntimeException;
import dev.cel.common.annotations.Internal;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Locale;

/** Collection of utility methods for CEL datetime handlings. */
@Internal
@SuppressWarnings("JavaInstantGetSecondsGetNano") // Intended within CEL.
public final class DateTimeHelpers {
public static final String UTC = "UTC";

// Timestamp for "0001-01-01T00:00:00Z"
private static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
// Timestamp for "9999-12-31T23:59:59Z"
private static final long TIMESTAMP_SECONDS_MAX = 253402300799L;

private static final long DURATION_SECONDS_MIN = -315576000000L;
private static final long DURATION_SECONDS_MAX = 315576000000L;
private static final int NANOS_PER_SECOND = 1000000000;

/**
* Constructs a new {@link LocalDateTime} instance
*
* @param ts Timestamp protobuf object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return If an Invalid timezone is supplied.
*/
public static LocalDateTime newLocalDateTime(Timestamp ts, String tz) {
return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos())
.atZone(timeZone(tz))
.toLocalDateTime();
}

/**
* Constructs a new {@link LocalDateTime} instance from a Java Instant.
*
* @param instant Instant object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return A new {@link LocalDateTime} instance.
*/
public static LocalDateTime newLocalDateTime(Instant instant, String tz) {
return instant.atZone(timeZone(tz)).toLocalDateTime();
}

/**
* Parse from RFC 3339 date string to {@link java.time.Instant}.
*
* <p>Example of accepted format: "1972-01-01T10:00:20.021-05:00"
*/
public static Instant parse(String text) {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(text);
Instant instant = offsetDateTime.toInstant();
checkValid(instant);

return instant;
}

/** Adds a duration to an instant. */
public static Instant add(Instant ts, Duration dur) {
Instant newInstant = ts.plus(dur);
checkValid(newInstant);

return newInstant;
}

/** Adds two durations */
public static Duration add(Duration d1, Duration d2) {
Duration newDuration = d1.plus(d2);
checkValid(newDuration);

return newDuration;
}

/** Subtracts a duration to an instant. */
public static Instant subtract(Instant ts, Duration dur) {
Instant newInstant = ts.minus(dur);
checkValid(newInstant);

return newInstant;
}

/** Subtract a duration from another. */
public static Duration subtract(Duration d1, Duration d2) {
Duration newDuration = d1.minus(d2);
checkValid(newDuration);

return newDuration;
}

/**
* Formats a {@link Duration} into a minimal seconds-based representation.
*
* <p>Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation
*/
public static String toString(Duration duration) {
if (duration.isZero()) {
return "0s";
}

long totalNanos = duration.toNanos();
StringBuilder sb = new StringBuilder();

if (totalNanos < 0) {
sb.append('-');
totalNanos = -totalNanos;
}

long seconds = totalNanos / 1_000_000_000;
int nanos = (int) (totalNanos % 1_000_000_000);

sb.append(seconds);

// Follows ProtoTimeUtils.toString(Duration) implementation
if (nanos > 0) {
sb.append('.');
if (nanos % 1_000_000 == 0) {
// Millisecond precision (3 digits)
int millis = nanos / 1_000_000;
sb.append(String.format(Locale.US, "%03d", millis));
} else if (nanos % 1_000 == 0) {
// Microsecond precision (6 digits)
int micros = nanos / 1_000;
sb.append(String.format(Locale.US, "%06d", micros));
} else {
// Nanosecond precision (9 digits)
sb.append(String.format(Locale.US, "%09d", nanos));
}
}

sb.append('s');
return sb.toString();
}

/**
* Get the DateTimeZone Instance.
*
* @param tz the ID of the datetime zone
* @return the ZoneId object
*/
private static ZoneId timeZone(String tz) {
try {
return ZoneId.of(tz);
} catch (DateTimeException e) {
// If timezone is not a string name (for example, 'US/Central'), it should be a numerical
// offset from UTC in the format [+/-]HH:MM.
try {
int ind = tz.indexOf(":");
if (ind == -1) {
throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT);
}

int hourOffset = Integer.parseInt(tz.substring(0, ind));
int minOffset = Integer.parseInt(tz.substring(ind + 1));
// Ensures that the offset are properly formatted in [+/-]HH:MM to conform with
// ZoneOffset's format requirements.
// Example: "-9:30" -> "-09:30" and "9:30" -> "+09:30"
String formattedOffset =
((hourOffset < 0) ? "-" : "+")
+ String.format(Locale.US, "%02d:%02d", Math.abs(hourOffset), minOffset);

return ZoneId.of(formattedOffset);

} catch (DateTimeException e2) {
throw new CelRuntimeException(e2, CelErrorCode.BAD_FORMAT);
}
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */
private static void checkValid(Instant instant) {
long seconds = instant.getEpochSecond();

if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Timestamp is not valid. "
+ "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. "
+ "Nanos (%s) must be in range [0, +999,999,999].",
seconds, instant.getNano()));
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */
private static void checkValid(Duration duration) {
long seconds = duration.getSeconds();
int nanos = duration.getNano();
if (!isDurationValid(seconds, nanos)) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Duration is not valid. "
+ "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
+ "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
+ "Nanos must have the same sign as seconds",
seconds, nanos));
}
}

/**
* Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code
* seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
* value must be in the range [-999,999,999, +999,999,999].
*
* <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
* and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
* value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
*/
private static boolean isDurationValid(long seconds, int nanos) {
if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
return false;
}
if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
return false;
}
if (seconds < 0 || nanos < 0) {
if (seconds > 0 || nanos > 0) {
return false;
}
}
return true;
}

private DateTimeHelpers() {}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ public final class ProtoAdapter {

public ProtoAdapter(DynamicProto dynamicProto, CelOptions celOptions) {
this.dynamicProto = checkNotNull(dynamicProto);
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions.enableUnsignedLongs());
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions);
this.celOptions = celOptions;
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,3 +137,13 @@ cel_android_library(
name = "proto_time_utils_android",
exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"],
)

java_library(
name = "date_time_helpers",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers"],
)

cel_android_library(
name = "date_time_helpers_android",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
)
1 change: 1 addition & 0 deletions common/src/main/java/dev/cel/common/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,6 +205,7 @@ java_library(
tags = [
],
deps = [
"//common/internal:date_time_helpers",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
Expand Down
10 changes: 10 additions & 0 deletions common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,10 @@
import com.google.protobuf.Struct;
import com.google.protobuf.Timestamp;
import com.google.protobuf.Value;
import dev.cel.common.internal.DateTimeHelpers;
import dev.cel.common.internal.ProtoTimeUtils;
import dev.cel.common.values.CelByteString;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
Expand DownExpand Up@@ -118,6 +120,14 @@ public static Value adaptValueToJsonValue(Object value) {
String duration = ProtoTimeUtils.toString((Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof Instant) {
// Instant's toString follows RFC 3339
return json.setStringValue(value.toString()).build();
}
if (value instanceof java.time.Duration) {
String duration = DateTimeHelpers.toString((java.time.Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof FieldMask) {
String fieldMaskStr = toJsonString((FieldMask) value);
return json.setStringValue(fieldMaskStr).build();
Expand Down
30 changes: 30 additions & 0 deletions common/src/main/java/dev/cel/common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -195,9 +195,11 @@ java_library(
deps = [
":well_known_proto",
"//common:error_codes",
"//common:options",
"//common:proto_json_adapter",
"//common:runtime_exception",
"//common/annotations",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
"@maven//:com_google_errorprone_error_prone_annotations",
Expand DownExpand Up@@ -429,3 +431,31 @@ cel_android_library(
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)

java_library(
name = "date_time_helpers",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
],
)

cel_android_library(
name = "date_time_helpers_android",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven_android//:com_google_guava_guava",
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)
261 changes: 261 additions & 0 deletions common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dev.cel.common.internal;

import com.google.common.base.Strings;
import com.google.protobuf.Timestamp;
import dev.cel.common.CelErrorCode;
import dev.cel.common.CelRuntimeException;
import dev.cel.common.annotations.Internal;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Locale;

/** Collection of utility methods for CEL datetime handlings. */
@Internal
@SuppressWarnings("JavaInstantGetSecondsGetNano") // Intended within CEL.
public final class DateTimeHelpers {
public static final String UTC = "UTC";

// Timestamp for "0001-01-01T00:00:00Z"
private static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
// Timestamp for "9999-12-31T23:59:59Z"
private static final long TIMESTAMP_SECONDS_MAX = 253402300799L;

private static final long DURATION_SECONDS_MIN = -315576000000L;
private static final long DURATION_SECONDS_MAX = 315576000000L;
private static final int NANOS_PER_SECOND = 1000000000;

/**
* Constructs a new {@link LocalDateTime} instance
*
* @param ts Timestamp protobuf object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return If an Invalid timezone is supplied.
*/
public static LocalDateTime newLocalDateTime(Timestamp ts, String tz) {
return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos())
.atZone(timeZone(tz))
.toLocalDateTime();
}

/**
* Constructs a new {@link LocalDateTime} instance from a Java Instant.
*
* @param instant Instant object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return A new {@link LocalDateTime} instance.
*/
public static LocalDateTime newLocalDateTime(Instant instant, String tz) {
return instant.atZone(timeZone(tz)).toLocalDateTime();
}

/**
* Parse from RFC 3339 date string to {@link java.time.Instant}.
*
* <p>Example of accepted format: "1972-01-01T10:00:20.021-05:00"
*/
public static Instant parse(String text) {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(text);
Instant instant = offsetDateTime.toInstant();
checkValid(instant);

return instant;
}

/** Adds a duration to an instant. */
public static Instant add(Instant ts, Duration dur) {
Instant newInstant = ts.plus(dur);
checkValid(newInstant);

return newInstant;
}

/** Adds two durations */
public static Duration add(Duration d1, Duration d2) {
Duration newDuration = d1.plus(d2);
checkValid(newDuration);

return newDuration;
}

/** Subtracts a duration to an instant. */
public static Instant subtract(Instant ts, Duration dur) {
Instant newInstant = ts.minus(dur);
checkValid(newInstant);

return newInstant;
}

/** Subtract a duration from another. */
public static Duration subtract(Duration d1, Duration d2) {
Duration newDuration = d1.minus(d2);
checkValid(newDuration);

return newDuration;
}

/**
* Formats a {@link Duration} into a minimal seconds-based representation.
*
* <p>Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation
*/
public static String toString(Duration duration) {
if (duration.isZero()) {
return "0s";
}

long totalNanos = duration.toNanos();
StringBuilder sb = new StringBuilder();

if (totalNanos < 0) {
sb.append('-');
totalNanos = -totalNanos;
}

long seconds = totalNanos / 1_000_000_000;
int nanos = (int) (totalNanos % 1_000_000_000);

sb.append(seconds);

// Follows ProtoTimeUtils.toString(Duration) implementation
if (nanos > 0) {
sb.append('.');
if (nanos % 1_000_000 == 0) {
// Millisecond precision (3 digits)
int millis = nanos / 1_000_000;
sb.append(String.format(Locale.US, "%03d", millis));
} else if (nanos % 1_000 == 0) {
// Microsecond precision (6 digits)
int micros = nanos / 1_000;
sb.append(String.format(Locale.US, "%06d", micros));
} else {
// Nanosecond precision (9 digits)
sb.append(String.format(Locale.US, "%09d", nanos));
}
}

sb.append('s');
return sb.toString();
}

/**
* Get the DateTimeZone Instance.
*
* @param tz the ID of the datetime zone
* @return the ZoneId object
*/
private static ZoneId timeZone(String tz) {
try {
return ZoneId.of(tz);
} catch (DateTimeException e) {
// If timezone is not a string name (for example, 'US/Central'), it should be a numerical
// offset from UTC in the format [+/-]HH:MM.
try {
int ind = tz.indexOf(":");
if (ind == -1) {
throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT);
}

int hourOffset = Integer.parseInt(tz.substring(0, ind));
int minOffset = Integer.parseInt(tz.substring(ind + 1));
// Ensures that the offset are properly formatted in [+/-]HH:MM to conform with
// ZoneOffset's format requirements.
// Example: "-9:30" -> "-09:30" and "9:30" -> "+09:30"
String formattedOffset =
((hourOffset < 0) ? "-" : "+")
+ String.format(Locale.US, "%02d:%02d", Math.abs(hourOffset), minOffset);

return ZoneId.of(formattedOffset);

} catch (DateTimeException e2) {
throw new CelRuntimeException(e2, CelErrorCode.BAD_FORMAT);
}
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */
private static void checkValid(Instant instant) {
long seconds = instant.getEpochSecond();

if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Timestamp is not valid. "
+ "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. "
+ "Nanos (%s) must be in range [0, +999,999,999].",
seconds, instant.getNano()));
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */
private static void checkValid(Duration duration) {
long seconds = duration.getSeconds();
int nanos = duration.getNano();
if (!isDurationValid(seconds, nanos)) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Duration is not valid. "
+ "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
+ "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
+ "Nanos must have the same sign as seconds",
seconds, nanos));
}
}

/**
* Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code
* seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
* value must be in the range [-999,999,999, +999,999,999].
*
* <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
* and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
* value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
*/
private static boolean isDurationValid(long seconds, int nanos) {
if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
return false;
}
if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
return false;
}
if (seconds < 0 || nanos < 0) {
if (seconds > 0 || nanos > 0) {
return false;
}
}
return true;
}

private DateTimeHelpers() {}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ public final class ProtoAdapter {

public ProtoAdapter(DynamicProto dynamicProto, CelOptions celOptions) {
this.dynamicProto = checkNotNull(dynamicProto);
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions.enableUnsignedLongs());
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions);
this.celOptions = celOptions;
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,3 +137,13 @@ cel_android_library(
name = "proto_time_utils_android",
exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"],
)

java_library(
name = "date_time_helpers",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers"],
)

cel_android_library(
name = "date_time_helpers_android",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
)
1 change: 1 addition & 0 deletions common/src/main/java/dev/cel/common/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,6 +205,7 @@ java_library(
tags = [
],
deps = [
"//common/internal:date_time_helpers",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
Expand Down
10 changes: 10 additions & 0 deletions common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,10 @@
import com.google.protobuf.Struct;
import com.google.protobuf.Timestamp;
import com.google.protobuf.Value;
import dev.cel.common.internal.DateTimeHelpers;
import dev.cel.common.internal.ProtoTimeUtils;
import dev.cel.common.values.CelByteString;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
Expand DownExpand Up@@ -118,6 +120,14 @@ public static Value adaptValueToJsonValue(Object value) {
String duration = ProtoTimeUtils.toString((Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof Instant) {
// Instant's toString follows RFC 3339
return json.setStringValue(value.toString()).build();
}
if (value instanceof java.time.Duration) {
String duration = DateTimeHelpers.toString((java.time.Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof FieldMask) {
String fieldMaskStr = toJsonString((FieldMask) value);
return json.setStringValue(fieldMaskStr).build();
Expand Down
30 changes: 30 additions & 0 deletions common/src/main/java/dev/cel/common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -195,9 +195,11 @@ java_library(
deps = [
":well_known_proto",
"//common:error_codes",
"//common:options",
"//common:proto_json_adapter",
"//common:runtime_exception",
"//common/annotations",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
"@maven//:com_google_errorprone_error_prone_annotations",
Expand DownExpand Up@@ -429,3 +431,31 @@ cel_android_library(
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)

java_library(
name = "date_time_helpers",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
],
)

cel_android_library(
name = "date_time_helpers_android",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven_android//:com_google_guava_guava",
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)
261 changes: 261 additions & 0 deletions common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dev.cel.common.internal;

import com.google.common.base.Strings;
import com.google.protobuf.Timestamp;
import dev.cel.common.CelErrorCode;
import dev.cel.common.CelRuntimeException;
import dev.cel.common.annotations.Internal;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Locale;

/** Collection of utility methods for CEL datetime handlings. */
@Internal
@SuppressWarnings("JavaInstantGetSecondsGetNano") // Intended within CEL.
public final class DateTimeHelpers {
public static final String UTC = "UTC";

// Timestamp for "0001-01-01T00:00:00Z"
private static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
// Timestamp for "9999-12-31T23:59:59Z"
private static final long TIMESTAMP_SECONDS_MAX = 253402300799L;

private static final long DURATION_SECONDS_MIN = -315576000000L;
private static final long DURATION_SECONDS_MAX = 315576000000L;
private static final int NANOS_PER_SECOND = 1000000000;

/**
* Constructs a new {@link LocalDateTime} instance
*
* @param ts Timestamp protobuf object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return If an Invalid timezone is supplied.
*/
public static LocalDateTime newLocalDateTime(Timestamp ts, String tz) {
return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos())
.atZone(timeZone(tz))
.toLocalDateTime();
}

/**
* Constructs a new {@link LocalDateTime} instance from a Java Instant.
*
* @param instant Instant object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return A new {@link LocalDateTime} instance.
*/
public static LocalDateTime newLocalDateTime(Instant instant, String tz) {
return instant.atZone(timeZone(tz)).toLocalDateTime();
}

/**
* Parse from RFC 3339 date string to {@link java.time.Instant}.
*
* <p>Example of accepted format: "1972-01-01T10:00:20.021-05:00"
*/
public static Instant parse(String text) {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(text);
Instant instant = offsetDateTime.toInstant();
checkValid(instant);

return instant;
}

/** Adds a duration to an instant. */
public static Instant add(Instant ts, Duration dur) {
Instant newInstant = ts.plus(dur);
checkValid(newInstant);

return newInstant;
}

/** Adds two durations */
public static Duration add(Duration d1, Duration d2) {
Duration newDuration = d1.plus(d2);
checkValid(newDuration);

return newDuration;
}

/** Subtracts a duration to an instant. */
public static Instant subtract(Instant ts, Duration dur) {
Instant newInstant = ts.minus(dur);
checkValid(newInstant);

return newInstant;
}

/** Subtract a duration from another. */
public static Duration subtract(Duration d1, Duration d2) {
Duration newDuration = d1.minus(d2);
checkValid(newDuration);

return newDuration;
}

/**
* Formats a {@link Duration} into a minimal seconds-based representation.
*
* <p>Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation
*/
public static String toString(Duration duration) {
if (duration.isZero()) {
return "0s";
}

long totalNanos = duration.toNanos();
StringBuilder sb = new StringBuilder();

if (totalNanos < 0) {
sb.append('-');
totalNanos = -totalNanos;
}

long seconds = totalNanos / 1_000_000_000;
int nanos = (int) (totalNanos % 1_000_000_000);

sb.append(seconds);

// Follows ProtoTimeUtils.toString(Duration) implementation
if (nanos > 0) {
sb.append('.');
if (nanos % 1_000_000 == 0) {
// Millisecond precision (3 digits)
int millis = nanos / 1_000_000;
sb.append(String.format(Locale.US, "%03d", millis));
} else if (nanos % 1_000 == 0) {
// Microsecond precision (6 digits)
int micros = nanos / 1_000;
sb.append(String.format(Locale.US, "%06d", micros));
} else {
// Nanosecond precision (9 digits)
sb.append(String.format(Locale.US, "%09d", nanos));
}
}

sb.append('s');
return sb.toString();
}

/**
* Get the DateTimeZone Instance.
*
* @param tz the ID of the datetime zone
* @return the ZoneId object
*/
private static ZoneId timeZone(String tz) {
try {
return ZoneId.of(tz);
} catch (DateTimeException e) {
// If timezone is not a string name (for example, 'US/Central'), it should be a numerical
// offset from UTC in the format [+/-]HH:MM.
try {
int ind = tz.indexOf(":");
if (ind == -1) {
throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT);
}

int hourOffset = Integer.parseInt(tz.substring(0, ind));
int minOffset = Integer.parseInt(tz.substring(ind + 1));
// Ensures that the offset are properly formatted in [+/-]HH:MM to conform with
// ZoneOffset's format requirements.
// Example: "-9:30" -> "-09:30" and "9:30" -> "+09:30"
String formattedOffset =
((hourOffset < 0) ? "-" : "+")
+ String.format(Locale.US, "%02d:%02d", Math.abs(hourOffset), minOffset);

return ZoneId.of(formattedOffset);

} catch (DateTimeException e2) {
throw new CelRuntimeException(e2, CelErrorCode.BAD_FORMAT);
}
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */
private static void checkValid(Instant instant) {
long seconds = instant.getEpochSecond();

if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Timestamp is not valid. "
+ "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. "
+ "Nanos (%s) must be in range [0, +999,999,999].",
seconds, instant.getNano()));
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */
private static void checkValid(Duration duration) {
long seconds = duration.getSeconds();
int nanos = duration.getNano();
if (!isDurationValid(seconds, nanos)) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Duration is not valid. "
+ "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
+ "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
+ "Nanos must have the same sign as seconds",
seconds, nanos));
}
}

/**
* Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code
* seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
* value must be in the range [-999,999,999, +999,999,999].
*
* <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
* and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
* value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
*/
private static boolean isDurationValid(long seconds, int nanos) {
if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
return false;
}
if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
return false;
}
if (seconds < 0 || nanos < 0) {
if (seconds > 0 || nanos > 0) {
return false;
}
}
return true;
}

private DateTimeHelpers() {}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ public final class ProtoAdapter {

public ProtoAdapter(DynamicProto dynamicProto, CelOptions celOptions) {
this.dynamicProto = checkNotNull(dynamicProto);
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions.enableUnsignedLongs());
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions);
this.celOptions = celOptions;
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,3 +137,13 @@ cel_android_library(
name = "proto_time_utils_android",
exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"],
)

java_library(
name = "date_time_helpers",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers"],
)

cel_android_library(
name = "date_time_helpers_android",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
)
1 change: 1 addition & 0 deletions common/src/main/java/dev/cel/common/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,6 +205,7 @@ java_library(
tags = [
],
deps = [
"//common/internal:date_time_helpers",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
Expand Down
10 changes: 10 additions & 0 deletions common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,10 @@
import com.google.protobuf.Struct;
import com.google.protobuf.Timestamp;
import com.google.protobuf.Value;
import dev.cel.common.internal.DateTimeHelpers;
import dev.cel.common.internal.ProtoTimeUtils;
import dev.cel.common.values.CelByteString;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
Expand DownExpand Up@@ -118,6 +120,14 @@ public static Value adaptValueToJsonValue(Object value) {
String duration = ProtoTimeUtils.toString((Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof Instant) {
// Instant's toString follows RFC 3339
return json.setStringValue(value.toString()).build();
}
if (value instanceof java.time.Duration) {
String duration = DateTimeHelpers.toString((java.time.Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof FieldMask) {
String fieldMaskStr = toJsonString((FieldMask) value);
return json.setStringValue(fieldMaskStr).build();
Expand Down
30 changes: 30 additions & 0 deletions common/src/main/java/dev/cel/common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -195,9 +195,11 @@ java_library(
deps = [
":well_known_proto",
"//common:error_codes",
"//common:options",
"//common:proto_json_adapter",
"//common:runtime_exception",
"//common/annotations",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
"@maven//:com_google_errorprone_error_prone_annotations",
Expand DownExpand Up@@ -429,3 +431,31 @@ cel_android_library(
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)

java_library(
name = "date_time_helpers",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
],
)

cel_android_library(
name = "date_time_helpers_android",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven_android//:com_google_guava_guava",
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)
261 changes: 261 additions & 0 deletions common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dev.cel.common.internal;

import com.google.common.base.Strings;
import com.google.protobuf.Timestamp;
import dev.cel.common.CelErrorCode;
import dev.cel.common.CelRuntimeException;
import dev.cel.common.annotations.Internal;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Locale;

/** Collection of utility methods for CEL datetime handlings. */
@Internal
@SuppressWarnings("JavaInstantGetSecondsGetNano") // Intended within CEL.
public final class DateTimeHelpers {
public static final String UTC = "UTC";

// Timestamp for "0001-01-01T00:00:00Z"
private static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
// Timestamp for "9999-12-31T23:59:59Z"
private static final long TIMESTAMP_SECONDS_MAX = 253402300799L;

private static final long DURATION_SECONDS_MIN = -315576000000L;
private static final long DURATION_SECONDS_MAX = 315576000000L;
private static final int NANOS_PER_SECOND = 1000000000;

/**
* Constructs a new {@link LocalDateTime} instance
*
* @param ts Timestamp protobuf object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return If an Invalid timezone is supplied.
*/
public static LocalDateTime newLocalDateTime(Timestamp ts, String tz) {
return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos())
.atZone(timeZone(tz))
.toLocalDateTime();
}

/**
* Constructs a new {@link LocalDateTime} instance from a Java Instant.
*
* @param instant Instant object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return A new {@link LocalDateTime} instance.
*/
public static LocalDateTime newLocalDateTime(Instant instant, String tz) {
return instant.atZone(timeZone(tz)).toLocalDateTime();
}

/**
* Parse from RFC 3339 date string to {@link java.time.Instant}.
*
* <p>Example of accepted format: "1972-01-01T10:00:20.021-05:00"
*/
public static Instant parse(String text) {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(text);
Instant instant = offsetDateTime.toInstant();
checkValid(instant);

return instant;
}

/** Adds a duration to an instant. */
public static Instant add(Instant ts, Duration dur) {
Instant newInstant = ts.plus(dur);
checkValid(newInstant);

return newInstant;
}

/** Adds two durations */
public static Duration add(Duration d1, Duration d2) {
Duration newDuration = d1.plus(d2);
checkValid(newDuration);

return newDuration;
}

/** Subtracts a duration to an instant. */
public static Instant subtract(Instant ts, Duration dur) {
Instant newInstant = ts.minus(dur);
checkValid(newInstant);

return newInstant;
}

/** Subtract a duration from another. */
public static Duration subtract(Duration d1, Duration d2) {
Duration newDuration = d1.minus(d2);
checkValid(newDuration);

return newDuration;
}

/**
* Formats a {@link Duration} into a minimal seconds-based representation.
*
* <p>Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation
*/
public static String toString(Duration duration) {
if (duration.isZero()) {
return "0s";
}

long totalNanos = duration.toNanos();
StringBuilder sb = new StringBuilder();

if (totalNanos < 0) {
sb.append('-');
totalNanos = -totalNanos;
}

long seconds = totalNanos / 1_000_000_000;
int nanos = (int) (totalNanos % 1_000_000_000);

sb.append(seconds);

// Follows ProtoTimeUtils.toString(Duration) implementation
if (nanos > 0) {
sb.append('.');
if (nanos % 1_000_000 == 0) {
// Millisecond precision (3 digits)
int millis = nanos / 1_000_000;
sb.append(String.format(Locale.US, "%03d", millis));
} else if (nanos % 1_000 == 0) {
// Microsecond precision (6 digits)
int micros = nanos / 1_000;
sb.append(String.format(Locale.US, "%06d", micros));
} else {
// Nanosecond precision (9 digits)
sb.append(String.format(Locale.US, "%09d", nanos));
}
}

sb.append('s');
return sb.toString();
}

/**
* Get the DateTimeZone Instance.
*
* @param tz the ID of the datetime zone
* @return the ZoneId object
*/
private static ZoneId timeZone(String tz) {
try {
return ZoneId.of(tz);
} catch (DateTimeException e) {
// If timezone is not a string name (for example, 'US/Central'), it should be a numerical
// offset from UTC in the format [+/-]HH:MM.
try {
int ind = tz.indexOf(":");
if (ind == -1) {
throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT);
}

int hourOffset = Integer.parseInt(tz.substring(0, ind));
int minOffset = Integer.parseInt(tz.substring(ind + 1));
// Ensures that the offset are properly formatted in [+/-]HH:MM to conform with
// ZoneOffset's format requirements.
// Example: "-9:30" -> "-09:30" and "9:30" -> "+09:30"
String formattedOffset =
((hourOffset < 0) ? "-" : "+")
+ String.format(Locale.US, "%02d:%02d", Math.abs(hourOffset), minOffset);

return ZoneId.of(formattedOffset);

} catch (DateTimeException e2) {
throw new CelRuntimeException(e2, CelErrorCode.BAD_FORMAT);
}
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */
private static void checkValid(Instant instant) {
long seconds = instant.getEpochSecond();

if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Timestamp is not valid. "
+ "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. "
+ "Nanos (%s) must be in range [0, +999,999,999].",
seconds, instant.getNano()));
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */
private static void checkValid(Duration duration) {
long seconds = duration.getSeconds();
int nanos = duration.getNano();
if (!isDurationValid(seconds, nanos)) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Duration is not valid. "
+ "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
+ "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
+ "Nanos must have the same sign as seconds",
seconds, nanos));
}
}

/**
* Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code
* seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
* value must be in the range [-999,999,999, +999,999,999].
*
* <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
* and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
* value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
*/
private static boolean isDurationValid(long seconds, int nanos) {
if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
return false;
}
if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
return false;
}
if (seconds < 0 || nanos < 0) {
if (seconds > 0 || nanos > 0) {
return false;
}
}
return true;
}

private DateTimeHelpers() {}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ public final class ProtoAdapter {

public ProtoAdapter(DynamicProto dynamicProto, CelOptions celOptions) {
this.dynamicProto = checkNotNull(dynamicProto);
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions.enableUnsignedLongs());
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions);
this.celOptions = celOptions;
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,3 +137,13 @@ cel_android_library(
name = "proto_time_utils_android",
exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"],
)

java_library(
name = "date_time_helpers",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers"],
)

cel_android_library(
name = "date_time_helpers_android",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
)
1 change: 1 addition & 0 deletions common/src/main/java/dev/cel/common/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,6 +205,7 @@ java_library(
tags = [
],
deps = [
"//common/internal:date_time_helpers",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
Expand Down
10 changes: 10 additions & 0 deletions common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,10 @@
import com.google.protobuf.Struct;
import com.google.protobuf.Timestamp;
import com.google.protobuf.Value;
import dev.cel.common.internal.DateTimeHelpers;
import dev.cel.common.internal.ProtoTimeUtils;
import dev.cel.common.values.CelByteString;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
Expand DownExpand Up@@ -118,6 +120,14 @@ public static Value adaptValueToJsonValue(Object value) {
String duration = ProtoTimeUtils.toString((Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof Instant) {
// Instant's toString follows RFC 3339
return json.setStringValue(value.toString()).build();
}
if (value instanceof java.time.Duration) {
String duration = DateTimeHelpers.toString((java.time.Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof FieldMask) {
String fieldMaskStr = toJsonString((FieldMask) value);
return json.setStringValue(fieldMaskStr).build();
Expand Down
30 changes: 30 additions & 0 deletions common/src/main/java/dev/cel/common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -195,9 +195,11 @@ java_library(
deps = [
":well_known_proto",
"//common:error_codes",
"//common:options",
"//common:proto_json_adapter",
"//common:runtime_exception",
"//common/annotations",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
"@maven//:com_google_errorprone_error_prone_annotations",
Expand DownExpand Up@@ -429,3 +431,31 @@ cel_android_library(
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)

java_library(
name = "date_time_helpers",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
],
)

cel_android_library(
name = "date_time_helpers_android",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven_android//:com_google_guava_guava",
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)
261 changes: 261 additions & 0 deletions common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dev.cel.common.internal;

import com.google.common.base.Strings;
import com.google.protobuf.Timestamp;
import dev.cel.common.CelErrorCode;
import dev.cel.common.CelRuntimeException;
import dev.cel.common.annotations.Internal;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Locale;

/** Collection of utility methods for CEL datetime handlings. */
@Internal
@SuppressWarnings("JavaInstantGetSecondsGetNano") // Intended within CEL.
public final class DateTimeHelpers {
public static final String UTC = "UTC";

// Timestamp for "0001-01-01T00:00:00Z"
private static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
// Timestamp for "9999-12-31T23:59:59Z"
private static final long TIMESTAMP_SECONDS_MAX = 253402300799L;

private static final long DURATION_SECONDS_MIN = -315576000000L;
private static final long DURATION_SECONDS_MAX = 315576000000L;
private static final int NANOS_PER_SECOND = 1000000000;

/**
* Constructs a new {@link LocalDateTime} instance
*
* @param ts Timestamp protobuf object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return If an Invalid timezone is supplied.
*/
public static LocalDateTime newLocalDateTime(Timestamp ts, String tz) {
return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos())
.atZone(timeZone(tz))
.toLocalDateTime();
}

/**
* Constructs a new {@link LocalDateTime} instance from a Java Instant.
*
* @param instant Instant object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return A new {@link LocalDateTime} instance.
*/
public static LocalDateTime newLocalDateTime(Instant instant, String tz) {
return instant.atZone(timeZone(tz)).toLocalDateTime();
}

/**
* Parse from RFC 3339 date string to {@link java.time.Instant}.
*
* <p>Example of accepted format: "1972-01-01T10:00:20.021-05:00"
*/
public static Instant parse(String text) {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(text);
Instant instant = offsetDateTime.toInstant();
checkValid(instant);

return instant;
}

/** Adds a duration to an instant. */
public static Instant add(Instant ts, Duration dur) {
Instant newInstant = ts.plus(dur);
checkValid(newInstant);

return newInstant;
}

/** Adds two durations */
public static Duration add(Duration d1, Duration d2) {
Duration newDuration = d1.plus(d2);
checkValid(newDuration);

return newDuration;
}

/** Subtracts a duration to an instant. */
public static Instant subtract(Instant ts, Duration dur) {
Instant newInstant = ts.minus(dur);
checkValid(newInstant);

return newInstant;
}

/** Subtract a duration from another. */
public static Duration subtract(Duration d1, Duration d2) {
Duration newDuration = d1.minus(d2);
checkValid(newDuration);

return newDuration;
}

/**
* Formats a {@link Duration} into a minimal seconds-based representation.
*
* <p>Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation
*/
public static String toString(Duration duration) {
if (duration.isZero()) {
return "0s";
}

long totalNanos = duration.toNanos();
StringBuilder sb = new StringBuilder();

if (totalNanos < 0) {
sb.append('-');
totalNanos = -totalNanos;
}

long seconds = totalNanos / 1_000_000_000;
int nanos = (int) (totalNanos % 1_000_000_000);

sb.append(seconds);

// Follows ProtoTimeUtils.toString(Duration) implementation
if (nanos > 0) {
sb.append('.');
if (nanos % 1_000_000 == 0) {
// Millisecond precision (3 digits)
int millis = nanos / 1_000_000;
sb.append(String.format(Locale.US, "%03d", millis));
} else if (nanos % 1_000 == 0) {
// Microsecond precision (6 digits)
int micros = nanos / 1_000;
sb.append(String.format(Locale.US, "%06d", micros));
} else {
// Nanosecond precision (9 digits)
sb.append(String.format(Locale.US, "%09d", nanos));
}
}

sb.append('s');
return sb.toString();
}

/**
* Get the DateTimeZone Instance.
*
* @param tz the ID of the datetime zone
* @return the ZoneId object
*/
private static ZoneId timeZone(String tz) {
try {
return ZoneId.of(tz);
} catch (DateTimeException e) {
// If timezone is not a string name (for example, 'US/Central'), it should be a numerical
// offset from UTC in the format [+/-]HH:MM.
try {
int ind = tz.indexOf(":");
if (ind == -1) {
throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT);
}

int hourOffset = Integer.parseInt(tz.substring(0, ind));
int minOffset = Integer.parseInt(tz.substring(ind + 1));
// Ensures that the offset are properly formatted in [+/-]HH:MM to conform with
// ZoneOffset's format requirements.
// Example: "-9:30" -> "-09:30" and "9:30" -> "+09:30"
String formattedOffset =
((hourOffset < 0) ? "-" : "+")
+ String.format(Locale.US, "%02d:%02d", Math.abs(hourOffset), minOffset);

return ZoneId.of(formattedOffset);

} catch (DateTimeException e2) {
throw new CelRuntimeException(e2, CelErrorCode.BAD_FORMAT);
}
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */
private static void checkValid(Instant instant) {
long seconds = instant.getEpochSecond();

if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Timestamp is not valid. "
+ "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. "
+ "Nanos (%s) must be in range [0, +999,999,999].",
seconds, instant.getNano()));
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */
private static void checkValid(Duration duration) {
long seconds = duration.getSeconds();
int nanos = duration.getNano();
if (!isDurationValid(seconds, nanos)) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Duration is not valid. "
+ "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
+ "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
+ "Nanos must have the same sign as seconds",
seconds, nanos));
}
}

/**
* Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code
* seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
* value must be in the range [-999,999,999, +999,999,999].
*
* <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
* and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
* value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
*/
private static boolean isDurationValid(long seconds, int nanos) {
if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
return false;
}
if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
return false;
}
if (seconds < 0 || nanos < 0) {
if (seconds > 0 || nanos > 0) {
return false;
}
}
return true;
}

private DateTimeHelpers() {}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ public final class ProtoAdapter {

public ProtoAdapter(DynamicProto dynamicProto, CelOptions celOptions) {
this.dynamicProto = checkNotNull(dynamicProto);
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions.enableUnsignedLongs());
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions);
this.celOptions = celOptions;
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,3 +137,13 @@ cel_android_library(
name = "proto_time_utils_android",
exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"],
)

java_library(
name = "date_time_helpers",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers"],
)

cel_android_library(
name = "date_time_helpers_android",
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
)
1 change: 1 addition & 0 deletions common/src/main/java/dev/cel/common/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,6 +205,7 @@ java_library(
tags = [
],
deps = [
"//common/internal:date_time_helpers",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
Expand Down
10 changes: 10 additions & 0 deletions common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,8 +26,10 @@
import com.google.protobuf.Struct;
import com.google.protobuf.Timestamp;
import com.google.protobuf.Value;
import dev.cel.common.internal.DateTimeHelpers;
import dev.cel.common.internal.ProtoTimeUtils;
import dev.cel.common.values.CelByteString;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
Expand DownExpand Up@@ -118,6 +120,14 @@ public static Value adaptValueToJsonValue(Object value) {
String duration = ProtoTimeUtils.toString((Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof Instant) {
// Instant's toString follows RFC 3339
return json.setStringValue(value.toString()).build();
}
if (value instanceof java.time.Duration) {
String duration = DateTimeHelpers.toString((java.time.Duration) value);
return json.setStringValue(duration).build();
}
if (value instanceof FieldMask) {
String fieldMaskStr = toJsonString((FieldMask) value);
return json.setStringValue(fieldMaskStr).build();
Expand Down
30 changes: 30 additions & 0 deletions common/src/main/java/dev/cel/common/internal/BUILD.bazel
Original file line numberDiff line numberDiff line change
Expand Up@@ -195,9 +195,11 @@ java_library(
deps = [
":well_known_proto",
"//common:error_codes",
"//common:options",
"//common:proto_json_adapter",
"//common:runtime_exception",
"//common/annotations",
"//common/internal:proto_time_utils",
"//common/values",
"//common/values:cel_byte_string",
"@maven//:com_google_errorprone_error_prone_annotations",
Expand DownExpand Up@@ -429,3 +431,31 @@ cel_android_library(
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)

java_library(
name = "date_time_helpers",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
],
)

cel_android_library(
name = "date_time_helpers_android",
srcs = ["DateTimeHelpers.java"],
tags = [
],
deps = [
"//common:error_codes",
"//common:runtime_exception",
"//common/annotations",
"@maven_android//:com_google_guava_guava",
"@maven_android//:com_google_protobuf_protobuf_javalite",
],
)
261 changes: 261 additions & 0 deletions common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dev.cel.common.internal;

import com.google.common.base.Strings;
import com.google.protobuf.Timestamp;
import dev.cel.common.CelErrorCode;
import dev.cel.common.CelRuntimeException;
import dev.cel.common.annotations.Internal;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Locale;

/** Collection of utility methods for CEL datetime handlings. */
@Internal
@SuppressWarnings("JavaInstantGetSecondsGetNano") // Intended within CEL.
public final class DateTimeHelpers {
public static final String UTC = "UTC";

// Timestamp for "0001-01-01T00:00:00Z"
private static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
// Timestamp for "9999-12-31T23:59:59Z"
private static final long TIMESTAMP_SECONDS_MAX = 253402300799L;

private static final long DURATION_SECONDS_MIN = -315576000000L;
private static final long DURATION_SECONDS_MAX = 315576000000L;
private static final int NANOS_PER_SECOND = 1000000000;

/**
* Constructs a new {@link LocalDateTime} instance
*
* @param ts Timestamp protobuf object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return If an Invalid timezone is supplied.
*/
public static LocalDateTime newLocalDateTime(Timestamp ts, String tz) {
return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos())
.atZone(timeZone(tz))
.toLocalDateTime();
}

/**
* Constructs a new {@link LocalDateTime} instance from a Java Instant.
*
* @param instant Instant object
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
* <ul>
* <li>UTC
* <li>America/Los_Angeles
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
* </ul>
*
* @return A new {@link LocalDateTime} instance.
*/
public static LocalDateTime newLocalDateTime(Instant instant, String tz) {
return instant.atZone(timeZone(tz)).toLocalDateTime();
}

/**
* Parse from RFC 3339 date string to {@link java.time.Instant}.
*
* <p>Example of accepted format: "1972-01-01T10:00:20.021-05:00"
*/
public static Instant parse(String text) {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(text);
Instant instant = offsetDateTime.toInstant();
checkValid(instant);

return instant;
}

/** Adds a duration to an instant. */
public static Instant add(Instant ts, Duration dur) {
Instant newInstant = ts.plus(dur);
checkValid(newInstant);

return newInstant;
}

/** Adds two durations */
public static Duration add(Duration d1, Duration d2) {
Duration newDuration = d1.plus(d2);
checkValid(newDuration);

return newDuration;
}

/** Subtracts a duration to an instant. */
public static Instant subtract(Instant ts, Duration dur) {
Instant newInstant = ts.minus(dur);
checkValid(newInstant);

return newInstant;
}

/** Subtract a duration from another. */
public static Duration subtract(Duration d1, Duration d2) {
Duration newDuration = d1.minus(d2);
checkValid(newDuration);

return newDuration;
}

/**
* Formats a {@link Duration} into a minimal seconds-based representation.
*
* <p>Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation
*/
public static String toString(Duration duration) {
if (duration.isZero()) {
return "0s";
}

long totalNanos = duration.toNanos();
StringBuilder sb = new StringBuilder();

if (totalNanos < 0) {
sb.append('-');
totalNanos = -totalNanos;
}

long seconds = totalNanos / 1_000_000_000;
int nanos = (int) (totalNanos % 1_000_000_000);

sb.append(seconds);

// Follows ProtoTimeUtils.toString(Duration) implementation
if (nanos > 0) {
sb.append('.');
if (nanos % 1_000_000 == 0) {
// Millisecond precision (3 digits)
int millis = nanos / 1_000_000;
sb.append(String.format(Locale.US, "%03d", millis));
} else if (nanos % 1_000 == 0) {
// Microsecond precision (6 digits)
int micros = nanos / 1_000;
sb.append(String.format(Locale.US, "%06d", micros));
} else {
// Nanosecond precision (9 digits)
sb.append(String.format(Locale.US, "%09d", nanos));
}
}

sb.append('s');
return sb.toString();
}

/**
* Get the DateTimeZone Instance.
*
* @param tz the ID of the datetime zone
* @return the ZoneId object
*/
private static ZoneId timeZone(String tz) {
try {
return ZoneId.of(tz);
} catch (DateTimeException e) {
// If timezone is not a string name (for example, 'US/Central'), it should be a numerical
// offset from UTC in the format [+/-]HH:MM.
try {
int ind = tz.indexOf(":");
if (ind == -1) {
throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT);
}

int hourOffset = Integer.parseInt(tz.substring(0, ind));
int minOffset = Integer.parseInt(tz.substring(ind + 1));
// Ensures that the offset are properly formatted in [+/-]HH:MM to conform with
// ZoneOffset's format requirements.
// Example: "-9:30" -> "-09:30" and "9:30" -> "+09:30"
String formattedOffset =
((hourOffset < 0) ? "-" : "+")
+ String.format(Locale.US, "%02d:%02d", Math.abs(hourOffset), minOffset);

return ZoneId.of(formattedOffset);

} catch (DateTimeException e2) {
throw new CelRuntimeException(e2, CelErrorCode.BAD_FORMAT);
}
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */
private static void checkValid(Instant instant) {
long seconds = instant.getEpochSecond();

if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Timestamp is not valid. "
+ "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. "
+ "Nanos (%s) must be in range [0, +999,999,999].",
seconds, instant.getNano()));
}
}

/** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */
private static void checkValid(Duration duration) {
long seconds = duration.getSeconds();
int nanos = duration.getNano();
if (!isDurationValid(seconds, nanos)) {
throw new IllegalArgumentException(
Strings.lenientFormat(
"Duration is not valid. "
+ "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
+ "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
+ "Nanos must have the same sign as seconds",
seconds, nanos));
}
}

/**
* Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code
* seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
* value must be in the range [-999,999,999, +999,999,999].
*
* <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
* and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
* value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
*/
private static boolean isDurationValid(long seconds, int nanos) {
if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
return false;
}
if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
return false;
}
if (seconds < 0 || nanos < 0) {
if (seconds > 0 || nanos > 0) {
return false;
}
}
return true;
}

private DateTimeHelpers() {}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ public final class ProtoAdapter {

public ProtoAdapter(DynamicProto dynamicProto, CelOptions celOptions) {
this.dynamicProto = checkNotNull(dynamicProto);
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions.enableUnsignedLongs());
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions);
this.celOptions = celOptions;
}

Expand Down
Loading
Loading