From c7ce945b21cb2c9ac3db60d69a7a3d659a7d161a Mon Sep 17 00:00:00 2001 From: Rishesh Agarwal Date: Sat, 24 Oct 2026 07:18:21 -0700 Subject: [PATCH] Add timestamp date accessors to the CEL C runtime. PiperOrigin-RevId: 1006285901 --- cel-c/internal/runtime/instr.h | 6 + cel-c/internal/runtime/interpretable.cc | 126 ++++- cel-c/internal/runtime/interpreter.cc | 22 +- cel-c/internal/runtime_test.cc | 629 ++++++++++++++++++++++++ cel-c/internal/timestampconv.cc | 5 + cel-c/internal/timestampconv.h | 2 + cel-c/internal/timestampconv_test.cc | 10 + 7 files changed, 794 insertions(+), 6 deletions(-) diff --git a/cel-c/internal/runtime/instr.h b/cel-c/internal/runtime/instr.h index ae9692b..9ca6b99 100644 --- a/cel-c/internal/runtime/instr.h +++ b/cel-c/internal/runtime/instr.h @@ -82,6 +82,12 @@ typedef enum CEL_ATTRIBUTE_CLOSED_ENUM { _cel_InstrKind_kCallGetMinutes, _cel_InstrKind_kCallGetSeconds, _cel_InstrKind_kCallGetMilliseconds, + _cel_InstrKind_kCallGetFullYear, + _cel_InstrKind_kCallGetMonth, + _cel_InstrKind_kCallGetDate, + _cel_InstrKind_kCallGetDayOfMonth, + _cel_InstrKind_kCallGetDayOfWeek, + _cel_InstrKind_kCallGetDayOfYear, _cel_InstrKind_kCallContainsString, _cel_InstrKind_kCallStartsWithString, _cel_InstrKind_kCallEndsWithString, diff --git a/cel-c/internal/runtime/interpretable.cc b/cel-c/internal/runtime/interpretable.cc index 93519c5..bf30522 100644 --- a/cel-c/internal/runtime/interpretable.cc +++ b/cel-c/internal/runtime/interpretable.cc @@ -2779,16 +2779,45 @@ static inline void _cel_Interpretable_CallSize( } typedef enum { + // Accessors supported by both durations and timestamps. _cel_TimeAccessor_kHours, _cel_TimeAccessor_kMinutes, _cel_TimeAccessor_kSeconds, _cel_TimeAccessor_kMilliseconds, + // Accessors supported only by timestamps. + _cel_TimeAccessor_kFullYear, + _cel_TimeAccessor_kMonth, + _cel_TimeAccessor_kDate, + _cel_TimeAccessor_kDayOfMonth, + _cel_TimeAccessor_kDayOfWeek, + _cel_TimeAccessor_kDayOfYear, } _cel_TimeAccessor; +// Returns true if `accessor` has an overload for `google.protobuf.Duration`. +CEL_ATTRIBUTE_NODISCARD +static inline bool _cel_TimeAccessor_SupportsDuration( + _cel_TimeAccessor accessor) { + switch (accessor) { + case _cel_TimeAccessor_kHours: + case _cel_TimeAccessor_kMinutes: + case _cel_TimeAccessor_kSeconds: + case _cel_TimeAccessor_kMilliseconds: + return true; + case _cel_TimeAccessor_kFullYear: + case _cel_TimeAccessor_kMonth: + case _cel_TimeAccessor_kDate: + case _cel_TimeAccessor_kDayOfMonth: + case _cel_TimeAccessor_kDayOfWeek: + case _cel_TimeAccessor_kDayOfYear: + return false; + } + CEL_UNREACHABLE(); +} + // Returns the requested component of a duration, matching cel-cpp. Hours, // minutes and seconds are the total number of whole units in the duration, // truncated towards zero. Milliseconds is the sub-second component, in the -// range [-999, 999]. +// range [-999, 999]. `accessor` must support durations. CEL_ATTRIBUTE_NODISCARD static inline int64_t _cel_Interpretable_DurationComponent( cel_Duration d, _cel_TimeAccessor accessor) { @@ -2806,10 +2835,20 @@ static inline int64_t _cel_Interpretable_DurationComponent( return sec; case _cel_TimeAccessor_kMilliseconds: return nsec / INT32_C(1000000); + case _cel_TimeAccessor_kFullYear: + case _cel_TimeAccessor_kMonth: + case _cel_TimeAccessor_kDate: + case _cel_TimeAccessor_kDayOfMonth: + case _cel_TimeAccessor_kDayOfWeek: + case _cel_TimeAccessor_kDayOfYear: + break; } CEL_UNREACHABLE(); } +// Returns the requested component of a civil time, matching the CEL +// specification. Note that `getMonth`, `getDayOfMonth` and `getDayOfYear` are +// zero-based, whereas `getDate` is one-based. CEL_ATTRIBUTE_NODISCARD static inline int64_t _cel_Interpretable_CivilTimeComponent( const _cel_CivilTime* cel_nonnull civil_time, _cel_TimeAccessor accessor) { @@ -2822,6 +2861,18 @@ static inline int64_t _cel_Interpretable_CivilTimeComponent( return civil_time->second; case _cel_TimeAccessor_kMilliseconds: return civil_time->millisecond; + case _cel_TimeAccessor_kFullYear: + return civil_time->year; + case _cel_TimeAccessor_kMonth: + return civil_time->month - 1; + case _cel_TimeAccessor_kDate: + return civil_time->day; + case _cel_TimeAccessor_kDayOfMonth: + return civil_time->day - 1; + case _cel_TimeAccessor_kDayOfWeek: + return civil_time->day_of_week; + case _cel_TimeAccessor_kDayOfYear: + return civil_time->day_of_year - 1; } CEL_UNREACHABLE(); } @@ -2832,6 +2883,12 @@ static inline int64_t _cel_Interpretable_CivilTimeComponent( // (google.protobuf.Timestamp) -> int // (google.protobuf.Timestamp, string) -> int // +// And `getFullYear`, `getMonth`, `getDate`, `getDayOfMonth`, `getDayOfWeek` and +// `getDayOfYear`: +// +// (google.protobuf.Timestamp) -> int +// (google.protobuf.Timestamp, string) -> int +// // For timestamps, the optional string is the time zone, otherwise UTC is used. static inline void _cel_Interpretable_CallTimeAccessor( _cel_Interpretable* cel_nonnull interp, @@ -2848,9 +2905,10 @@ static inline void _cel_Interpretable_CallTimeAccessor( case cel_ValueKind_kError: break; case cel_ValueKind_kDuration: - if (tz != cel_nullptr) { - // There is no time zone overload for durations. - if (cel_Value_IsError(tz)) { + if (tz != cel_nullptr || !_cel_TimeAccessor_SupportsDuration(accessor)) { + // There is no time zone overload for durations, and not all accessors + // support durations. + if (tz != cel_nullptr && cel_Value_IsError(tz)) { *top = *tz; } else { cel_Value_SetError(top, @@ -3252,6 +3310,12 @@ static inline void _cel_Interpretable_ComputedGoto( &&INSTR_CALL_GET_MINUTES, &&INSTR_CALL_GET_SECONDS, &&INSTR_CALL_GET_MILLISECONDS, + &&INSTR_CALL_GET_FULL_YEAR, + &&INSTR_CALL_GET_MONTH, + &&INSTR_CALL_GET_DATE, + &&INSTR_CALL_GET_DAY_OF_MONTH, + &&INSTR_CALL_GET_DAY_OF_WEEK, + &&INSTR_CALL_GET_DAY_OF_YEAR, &&INSTR_CALL_CONTAINS_STRING, &&INSTR_CALL_STARTS_WITH_STRING, &&INSTR_CALL_ENDS_WITH_STRING, @@ -3438,6 +3502,30 @@ static inline void _cel_Interpretable_ComputedGoto( _cel_Interpretable_CallTimeAccessor(interp, &instr->data, _cel_TimeAccessor_kMilliseconds); DISPATCH_NEXT_INSTR(); +INSTR_CALL_GET_FULL_YEAR: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kFullYear); + DISPATCH_NEXT_INSTR(); +INSTR_CALL_GET_MONTH: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kMonth); + DISPATCH_NEXT_INSTR(); +INSTR_CALL_GET_DATE: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kDate); + DISPATCH_NEXT_INSTR(); +INSTR_CALL_GET_DAY_OF_MONTH: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kDayOfMonth); + DISPATCH_NEXT_INSTR(); +INSTR_CALL_GET_DAY_OF_WEEK: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kDayOfWeek); + DISPATCH_NEXT_INSTR(); +INSTR_CALL_GET_DAY_OF_YEAR: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kDayOfYear); + DISPATCH_NEXT_INSTR(); INSTR_CALL_CONTAINS_STRING: _cel_Interpretable_CallContainsString(interp, &instr->data); DISPATCH_NEXT_INSTR(); @@ -3696,6 +3784,36 @@ static inline void _cel_Interpretable_Switch( _cel_TimeAccessor_kMilliseconds); ++instr; break; + case _cel_InstrKind_kCallGetFullYear: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kFullYear); + ++instr; + break; + case _cel_InstrKind_kCallGetMonth: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kMonth); + ++instr; + break; + case _cel_InstrKind_kCallGetDate: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kDate); + ++instr; + break; + case _cel_InstrKind_kCallGetDayOfMonth: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kDayOfMonth); + ++instr; + break; + case _cel_InstrKind_kCallGetDayOfWeek: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kDayOfWeek); + ++instr; + break; + case _cel_InstrKind_kCallGetDayOfYear: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kDayOfYear); + ++instr; + break; case _cel_InstrKind_kCallContainsString: _cel_Interpretable_CallContainsString(interp, &instr->data); ++instr; diff --git a/cel-c/internal/runtime/interpreter.cc b/cel-c/internal/runtime/interpreter.cc index 474fa45..d956b15 100644 --- a/cel-c/internal/runtime/interpreter.cc +++ b/cel-c/internal/runtime/interpreter.cc @@ -1565,8 +1565,8 @@ static void _cel_Interpreter_PreVisitCallArgExpr( // Returns the instruction kind for the duration/timestamp accessor `fn_name`, // or `_cel_InstrKind_kUnreachable` if `fn_name` is not an accessor. Accessors -// accept (google.protobuf.Duration), (google.protobuf.Timestamp) or -// (google.protobuf.Timestamp, string). +// accept (google.protobuf.Timestamp) or (google.protobuf.Timestamp, string), +// and some additionally accept (google.protobuf.Duration). CEL_ATTRIBUTE_NODISCARD static _cel_InstrKind _cel_Interpreter_TimeAccessorKind( cel_StringView fn_name) { @@ -1582,6 +1582,24 @@ static _cel_InstrKind _cel_Interpreter_TimeAccessorKind( if (cel_StringView_Equals(fn_name, cel_StringView_From("getMilliseconds"))) { return _cel_InstrKind_kCallGetMilliseconds; } + if (cel_StringView_Equals(fn_name, cel_StringView_From("getFullYear"))) { + return _cel_InstrKind_kCallGetFullYear; + } + if (cel_StringView_Equals(fn_name, cel_StringView_From("getMonth"))) { + return _cel_InstrKind_kCallGetMonth; + } + if (cel_StringView_Equals(fn_name, cel_StringView_From("getDate"))) { + return _cel_InstrKind_kCallGetDate; + } + if (cel_StringView_Equals(fn_name, cel_StringView_From("getDayOfMonth"))) { + return _cel_InstrKind_kCallGetDayOfMonth; + } + if (cel_StringView_Equals(fn_name, cel_StringView_From("getDayOfWeek"))) { + return _cel_InstrKind_kCallGetDayOfWeek; + } + if (cel_StringView_Equals(fn_name, cel_StringView_From("getDayOfYear"))) { + return _cel_InstrKind_kCallGetDayOfYear; + } return _cel_InstrKind_kUnreachable; } diff --git a/cel-c/internal/runtime_test.cc b/cel-c/internal/runtime_test.cc index 2326624..9e6f388 100644 --- a/cel-c/internal/runtime_test.cc +++ b/cel-c/internal/runtime_test.cc @@ -1299,6 +1299,431 @@ TEST_F(RuntimeTest, CallGetMillisecondsTimestampWithTimeZone) { EXPECT_EQ(cel_Value_GetInt(&value), 0); } +TEST_F(RuntimeTest, CallGetFullYearTimestamp) { + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getFullYear())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 2025); + + prog = Parse(R"cel(getFullYear(timestamp("2025-12-01T10:20:30.456Z")))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 2025); + + // Timestamps before the Unix epoch. + prog = Parse(R"cel(timestamp("1969-12-31T23:59:59Z").getFullYear())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1969); + + // Timestamp bounds. + prog = Parse(R"cel(timestamp("0001-01-01T00:00:00Z").getFullYear())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1); + + prog = Parse( + R"cel(timestamp("9999-12-31T23:59:59.999999999Z").getFullYear())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 9999); +} + +TEST_F(RuntimeTest, CallGetMonthTimestamp) { + // Months are zero-based. + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getMonth())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 11); + + prog = Parse(R"cel(getMonth(timestamp("2025-01-31T23:59:59Z")))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + // Timestamps before the Unix epoch. + prog = Parse(R"cel(timestamp("1969-12-31T23:59:59Z").getMonth())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 11); + + // Timestamp bounds. + prog = Parse(R"cel(timestamp("0001-01-01T00:00:00Z").getMonth())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); +} + +TEST_F(RuntimeTest, CallGetDateTimestamp) { + // Dates are one-based. + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getDate())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1); + + prog = Parse(R"cel(getDate(timestamp("2025-01-31T23:59:59Z")))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 31); + + // Leap day. + prog = Parse(R"cel(timestamp("2024-02-29T12:00:00Z").getDate())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 29); + + // Timestamps before the Unix epoch. + prog = Parse(R"cel(timestamp("1969-12-31T23:59:59Z").getDate())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 31); +} + +TEST_F(RuntimeTest, CallGetDayOfMonthTimestamp) { + // Days of the month are zero-based. + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getDayOfMonth())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + prog = Parse(R"cel(getDayOfMonth(timestamp("2025-01-31T23:59:59Z")))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 30); + + // Leap day. + prog = Parse(R"cel(timestamp("2024-02-29T12:00:00Z").getDayOfMonth())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 28); + + // Timestamps before the Unix epoch. + prog = Parse(R"cel(timestamp("1969-12-31T23:59:59Z").getDayOfMonth())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 30); +} + +TEST_F(RuntimeTest, CallGetDayOfWeekTimestamp) { + // Monday. + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getDayOfWeek())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1); + + prog = Parse(R"cel(getDayOfWeek(timestamp("2025-12-01T10:20:30.456Z")))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1); + + // Days of the week start on Sunday. + prog = Parse(R"cel(timestamp("2025-11-30T23:59:59Z").getDayOfWeek())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + // Saturday. + prog = Parse(R"cel(timestamp("2025-12-06T00:00:00Z").getDayOfWeek())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 6); + + // Thursday. + prog = Parse(R"cel(timestamp("1970-01-01T00:00:00Z").getDayOfWeek())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 4); + + // Timestamps before the Unix epoch. + prog = Parse(R"cel(timestamp("1969-12-31T23:59:59Z").getDayOfWeek())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 3); + + // Timestamp bounds. + prog = Parse(R"cel(timestamp("0001-01-01T00:00:00Z").getDayOfWeek())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1); + + prog = Parse( + R"cel(timestamp("9999-12-31T23:59:59.999999999Z").getDayOfWeek())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 5); +} + +TEST_F(RuntimeTest, CallGetDayOfYearTimestamp) { + // Days of the year are zero-based. + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getDayOfYear())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 334); + + prog = Parse(R"cel(getDayOfYear(timestamp("2025-01-01T00:00:00Z")))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + prog = Parse(R"cel(timestamp("2025-12-31T23:59:59Z").getDayOfYear())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 364); + + // Leap years. + prog = Parse(R"cel(timestamp("2024-03-01T00:00:00Z").getDayOfYear())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 60); + + prog = Parse(R"cel(timestamp("2024-12-31T23:59:59Z").getDayOfYear())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 365); + + // Timestamps before the Unix epoch. + prog = Parse(R"cel(timestamp("1969-12-31T23:59:59Z").getDayOfYear())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 364); +} + +TEST_F(RuntimeTest, CallDateAccessorsTimestampWithTimeZone) { + // Empty and UTC time zones match the single argument overloads. + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T02:00:00Z").getDate(""))cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1); + + prog = Parse(R"cel(timestamp("2025-12-01T02:00:00Z").getDate("UTC"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1); + + prog = Parse(R"cel(getDate(timestamp("2025-12-01T02:00:00Z"), "UTC"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1); + + // IANA time zones crossing the day and month boundary. + prog = Parse( + R"cel(timestamp("2025-12-01T02:00:00Z").getFullYear("America/Los_Angeles"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 2025); + + prog = Parse( + R"cel(timestamp("2025-12-01T02:00:00Z").getMonth("America/Los_Angeles"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 10); + + prog = Parse( + R"cel(timestamp("2025-12-01T02:00:00Z").getDate("America/Los_Angeles"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 30); + + prog = Parse( + R"cel(timestamp("2025-12-01T02:00:00Z").getDayOfMonth("America/Los_Angeles"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 29); + + prog = Parse( + R"cel(timestamp("2025-12-01T02:00:00Z").getDayOfWeek("America/Los_Angeles"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + prog = Parse( + R"cel(timestamp("2025-12-01T02:00:00Z").getDayOfYear("America/Los_Angeles"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 333); + + // IANA time zones crossing the year boundary backwards, into a leap year. + prog = Parse( + R"cel(timestamp("2025-01-01T03:00:00Z").getFullYear("America/New_York"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 2024); + + prog = Parse( + R"cel(timestamp("2025-01-01T03:00:00Z").getMonth("America/New_York"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 11); + + prog = Parse( + R"cel(timestamp("2025-01-01T03:00:00Z").getDate("America/New_York"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 31); + + prog = Parse( + R"cel(timestamp("2025-01-01T03:00:00Z").getDayOfWeek("America/New_York"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 2); + + prog = Parse( + R"cel(timestamp("2025-01-01T03:00:00Z").getDayOfYear("America/New_York"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 365); + + // IANA time zones crossing the year boundary forwards. + prog = Parse( + R"cel(timestamp("2025-12-31T20:00:00Z").getFullYear("Asia/Tokyo"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 2026); + + prog = Parse( + R"cel(timestamp("2025-12-31T20:00:00Z").getMonth("Asia/Tokyo"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + prog = + Parse(R"cel(timestamp("2025-12-31T20:00:00Z").getDate("Asia/Tokyo"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1); + + prog = Parse( + R"cel(timestamp("2025-12-31T20:00:00Z").getDayOfMonth("Asia/Tokyo"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + prog = Parse( + R"cel(timestamp("2025-12-31T20:00:00Z").getDayOfWeek("Asia/Tokyo"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 4); + + prog = Parse( + R"cel(timestamp("2025-12-31T20:00:00Z").getDayOfYear("Asia/Tokyo"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + // Fixed UTC offsets. + prog = Parse(R"cel(timestamp("2025-12-01T20:00:00Z").getDate("+05:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 2); + + prog = Parse( + R"cel(timestamp("2025-12-01T20:00:00Z").getDayOfWeek("+05:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 2); + + prog = Parse(R"cel(timestamp("2025-12-01T02:00:00Z").getDate("-08:00"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 30); + + prog = Parse(R"cel(timestamp("2024-02-28T20:00:00Z").getDate("+05:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 29); + + prog = Parse( + R"cel(timestamp("2024-02-28T20:00:00Z").getDayOfYear("+05:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 59); + + // Time zones before the Unix epoch. + prog = + Parse(R"cel(timestamp("1970-01-01T02:00:00Z").getFullYear("-05:00"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1969); + + prog = Parse( + R"cel(timestamp("1970-01-01T02:00:00Z").getDayOfWeek("-05:00"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 3); +} + TEST_F(RuntimeTest, CallTimeAccessorsNoSuchOverload) { _cel_ProgramPtr prog = Parse(R"cel(getHours(1))cel"); _cel_TestingVariableResolver var_resolver; @@ -1468,6 +1893,210 @@ TEST_F(RuntimeTest, CallTimeAccessorsPropagatesError) { cel_StringView_From("cel: divide by zero")); } +TEST_F(RuntimeTest, CallDateAccessorsNoSuchOverload) { + // Durations do not support the date accessors. + _cel_ProgramPtr prog = Parse(R"cel(duration("1h").getFullYear())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(getMonth(duration("1h")))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(duration("1h").getDate())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(duration("1h").getDayOfMonth())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(duration("1h").getDayOfWeek())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(duration("1h").getDayOfYear())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(duration("1h").getDayOfYear("UTC"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(getFullYear(1))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel("foo".getMonth())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(getDate(1.0, "UTC"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + // The time zone must be a string. + prog = Parse(R"cel(timestamp("2025-12-01T10:20:30Z").getDayOfWeek(1))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); +} + +TEST_F(RuntimeTest, CallDateAccessorsInvalidTimeZone) { + _cel_ProgramPtr prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30Z").getFullYear("Invalid/Zone"))cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: invalid time zone")); + + prog = Parse(R"cel(timestamp("2025-12-01T10:20:30Z").getMonth("foo"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: invalid time zone")); + + prog = Parse(R"cel(timestamp("2025-12-01T10:20:30Z").getDate("+aa:bb"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: invalid time zone")); + + prog = Parse(R"cel(timestamp("2025-12-01T10:20:30Z").getDayOfMonth(":"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: invalid time zone")); + + prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30Z").getDayOfWeek("foo"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: invalid time zone")); + + prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30Z").getDayOfYear("Invalid/Zone"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: invalid time zone")); +} + +TEST_F(RuntimeTest, CallDateAccessorsPropagatesError) { + _cel_ProgramPtr prog = Parse(R"cel(getFullYear(1 / 0))cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel((1 / 0).getMonth())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel((1 / 0).getDate())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel((1 / 0).getDayOfMonth())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel((1 / 0).getDayOfWeek())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel((1 / 0).getDayOfYear())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + // Errors in either argument of the time zone overload propagate. + prog = Parse(R"cel((1 / 0).getDayOfYear("UTC"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel(timestamp("2025-12-01T10:20:30Z").getMonth(1 / 0))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel(duration("1h").getDate(1 / 0))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel(true.getDayOfWeek(1 / 0))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); +} + // TODO: Add addition tests for duration/timestamp TEST_F(RuntimeTest, IntSub) { diff --git a/cel-c/internal/timestampconv.cc b/cel-c/internal/timestampconv.cc index b6deb82..7c75b42 100644 --- a/cel-c/internal/timestampconv.cc +++ b/cel-c/internal/timestampconv.cc @@ -20,6 +20,7 @@ #include "absl/strings/match.h" #include "absl/strings/str_replace.h" #include "absl/strings/string_view.h" +#include "absl/time/civil_time.h" #include "absl/time/time.h" #include "cel-c/arena.h" #include "cel-c/internal/config.h" @@ -115,6 +116,10 @@ bool _cel_Timestamp_ToCivilTime(cel_Timestamp ts, cel_StringView tz, out->second = info.cs.second(); out->millisecond = static_cast(absl::ToInt64Milliseconds(info.subsecond)); + // `absl::Weekday` enumerates Monday through Sunday as 0 through 6, so rotate + // it to make Sunday 0. + out->day_of_week = (static_cast(absl::GetWeekday(info.cs)) + 1) % 7; + out->day_of_year = absl::GetYearDay(absl::CivilDay(info.cs)); return true; } diff --git a/cel-c/internal/timestampconv.h b/cel-c/internal/timestampconv.h index 99293d6..56c2138 100644 --- a/cel-c/internal/timestampconv.h +++ b/cel-c/internal/timestampconv.h @@ -55,6 +55,8 @@ typedef struct { int32_t minute; // [0, 59] int32_t second; // [0, 59] int32_t millisecond; // [0, 999] + int32_t day_of_week; // [0, 6], where 0 is Sunday + int32_t day_of_year; // [1, 366] } _cel_CivilTime; // _cel_Timestamp_ToCivilTime diff --git a/cel-c/internal/timestampconv_test.cc b/cel-c/internal/timestampconv_test.cc index 627a367..72d58c9 100644 --- a/cel-c/internal/timestampconv_test.cc +++ b/cel-c/internal/timestampconv_test.cc @@ -177,6 +177,8 @@ TEST_F(CelTimestampConvTest, ToCivilTime) { EXPECT_EQ(civil_time.minute, 20); EXPECT_EQ(civil_time.second, 30); EXPECT_EQ(civil_time.millisecond, 456); + EXPECT_EQ(civil_time.day_of_week, 1); + EXPECT_EQ(civil_time.day_of_year, 335); ASSERT_TRUE(_cel_Timestamp_ToCivilTime( ts, cel_StringView_From("America/Los_Angeles"), &civil_time)); @@ -187,6 +189,8 @@ TEST_F(CelTimestampConvTest, ToCivilTime) { EXPECT_EQ(civil_time.minute, 20); EXPECT_EQ(civil_time.second, 30); EXPECT_EQ(civil_time.millisecond, 456); + EXPECT_EQ(civil_time.day_of_week, 1); + EXPECT_EQ(civil_time.day_of_year, 335); ASSERT_TRUE(_cel_Timestamp_ToCivilTime(ts, cel_StringView_From("+05:45"), &civil_time)); @@ -197,6 +201,8 @@ TEST_F(CelTimestampConvTest, ToCivilTime) { EXPECT_EQ(civil_time.minute, 5); EXPECT_EQ(civil_time.second, 30); EXPECT_EQ(civil_time.millisecond, 456); + EXPECT_EQ(civil_time.day_of_week, 1); + EXPECT_EQ(civil_time.day_of_year, 335); // A time zone that crosses the date boundary. ASSERT_TRUE(_cel_Timestamp_ToCivilTime(ts, cel_StringView_From("-11:00"), @@ -208,6 +214,8 @@ TEST_F(CelTimestampConvTest, ToCivilTime) { EXPECT_EQ(civil_time.minute, 20); EXPECT_EQ(civil_time.second, 30); EXPECT_EQ(civil_time.millisecond, 456); + EXPECT_EQ(civil_time.day_of_week, 0); + EXPECT_EQ(civil_time.day_of_year, 334); // 1969-12-31T23:59:58.5Z ts = cel_Timestamp_FromUnix(-2, 500000000); @@ -220,6 +228,8 @@ TEST_F(CelTimestampConvTest, ToCivilTime) { EXPECT_EQ(civil_time.minute, 59); EXPECT_EQ(civil_time.second, 58); EXPECT_EQ(civil_time.millisecond, 500); + EXPECT_EQ(civil_time.day_of_week, 3); + EXPECT_EQ(civil_time.day_of_year, 365); } TEST_F(CelTimestampConvTest, ToCivilTimeInvalidTimeZone) {