This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 398ad2f80 feat(rust): make rust chrono optional (#3683)
398ad2f80 is described below
commit 398ad2f80c55bbe1f8d75def24009d1291019257
Author: Shawn Yang <[email protected]>
AuthorDate: Fri May 15 22:57:57 2026 +0800
feat(rust): make rust chrono optional (#3683)
## Why?
## What does this PR do?
## Related issues
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence of the final clean AI review results
from both fresh reviewers on the current PR diff or current HEAD after
the latest code changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
compiler/README.md | 30 +-
compiler/fory_compiler/frontend/fdl/parser.py | 1 +
compiler/fory_compiler/generators/rust.py | 28 +-
.../fory_compiler/tests/test_generated_code.py | 47 ++
docs/compiler/compiler-guide.md | 9 +-
docs/compiler/schema-idl.md | 70 +-
docs/guide/rust/basic-serialization.md | 34 +-
docs/specification/xlang_type_mapping.md | 4 +-
integration_tests/idl_tests/idl/example.fdl | 1 +
integration_tests/idl_tests/idl/optional_types.fdl | 2 +
integration_tests/idl_tests/rust/Cargo.lock | 4 -
integration_tests/idl_tests/rust/Cargo.toml | 2 +-
rust/fory-core/Cargo.toml | 9 +-
rust/fory-core/src/lib.rs | 6 +-
rust/fory-core/src/resolver/type_resolver.rs | 15 +-
rust/fory-core/src/row/row.rs | 51 +-
rust/fory-core/src/serializer/datetime.rs | 414 ++++++-----
rust/fory-core/src/serializer/skip.rs | 6 +-
rust/fory-core/src/type_id.rs | 10 +-
rust/fory-core/src/types/mod.rs | 2 +
rust/fory-core/src/types/temporal.rs | 763 +++++++++++++++++++++
rust/fory-core/src/util/mod.rs | 9 -
rust/fory-derive/Cargo.toml | 4 +-
rust/fory-derive/src/lib.rs | 6 +-
rust/fory-derive/src/object/util.rs | 4 +-
rust/fory/Cargo.toml | 5 +
rust/fory/src/lib.rs | 12 +-
rust/tests/Cargo.toml | 6 +-
rust/tests/tests/compatible/test_basic_type.rs | 58 +-
rust/tests/tests/test_complex_struct.rs | 10 +-
rust/tests/tests/test_cross_language.rs | 22 +-
rust/tests/tests/test_fory.rs | 13 +-
32 files changed, 1309 insertions(+), 348 deletions(-)
diff --git a/compiler/README.md b/compiler/README.md
index fb443ea4d..236c38b77 100644
--- a/compiler/README.md
+++ b/compiler/README.md
@@ -185,21 +185,21 @@ message Config { ... } // Registered as "package.Config"
### Primitive Types
-| FDL Type | Java | Python | Go | Rust
| C++ | C# | JavaScript |
-| ----------- | ----------- | ------------------- | ----------- |
----------------------- | ---------------------- | ---------------- |
------------------ |
-| `bool` | `boolean` | `bool` | `bool` | `bool`
| `bool` | `bool` | `boolean` |
-| `int8` | `byte` | `pyfory.Int8` | `int8` | `i8`
| `int8_t` | `sbyte` | `number` |
-| `int16` | `short` | `pyfory.Int16` | `int16` | `i16`
| `int16_t` | `short` | `number` |
-| `int32` | `int` | `pyfory.Int32` | `int32` | `i32`
| `int32_t` | `int` | `number` |
-| `int64` | `long` | `pyfory.Int64` | `int64` | `i64`
| `int64_t` | `long` | `bigint \| number` |
-| `float16` | `Float16` | `pyfory.Float16` | `float16` | `Float16`
| `fory::float16_t` | `Half` | `number` |
-| `bfloat16` | `BFloat16` | `pyfory.BFloat16` | `bfloat16` | `BFloat16`
| `fory::bfloat16_t` | `BFloat16` | `number` |
-| `float32` | `float` | `pyfory.Float32` | `float32` | `f32`
| `float` | `float` | `number` |
-| `float64` | `double` | `pyfory.Float64` | `float64` | `f64`
| `double` | `double` | `number` |
-| `string` | `String` | `str` | `string` | `String`
| `std::string` | `string` | `string` |
-| `bytes` | `byte[]` | `bytes` | `[]byte` | `Vec<u8>`
| `std::vector<uint8_t>` | `byte[]` | `Uint8Array` |
-| `date` | `LocalDate` | `datetime.date` | `time.Time` |
`chrono::NaiveDate` | `fory::Date` | `DateOnly` | `Date`
|
-| `timestamp` | `Instant` | `datetime.datetime` | `time.Time` |
`chrono::NaiveDateTime` | `fory::Timestamp` | `DateTimeOffset` | `Date`
|
+| FDL Type | Java | Python | Go | Rust
| C++ | C# | JavaScript |
+| ----------- | ----------- | ------------------- | ----------- |
----------------- | ---------------------- | ---------------- |
------------------ |
+| `bool` | `boolean` | `bool` | `bool` | `bool`
| `bool` | `bool` | `boolean` |
+| `int8` | `byte` | `pyfory.Int8` | `int8` | `i8`
| `int8_t` | `sbyte` | `number` |
+| `int16` | `short` | `pyfory.Int16` | `int16` | `i16`
| `int16_t` | `short` | `number` |
+| `int32` | `int` | `pyfory.Int32` | `int32` | `i32`
| `int32_t` | `int` | `number` |
+| `int64` | `long` | `pyfory.Int64` | `int64` | `i64`
| `int64_t` | `long` | `bigint \| number` |
+| `float16` | `Float16` | `pyfory.Float16` | `float16` | `Float16`
| `fory::float16_t` | `Half` | `number` |
+| `bfloat16` | `BFloat16` | `pyfory.BFloat16` | `bfloat16` | `BFloat16`
| `fory::bfloat16_t` | `BFloat16` | `number` |
+| `float32` | `float` | `pyfory.Float32` | `float32` | `f32`
| `float` | `float` | `number` |
+| `float64` | `double` | `pyfory.Float64` | `float64` | `f64`
| `double` | `double` | `number` |
+| `string` | `String` | `str` | `string` | `String`
| `std::string` | `string` | `string` |
+| `bytes` | `byte[]` | `bytes` | `[]byte` | `Vec<u8>`
| `std::vector<uint8_t>` | `byte[]` | `Uint8Array` |
+| `date` | `LocalDate` | `datetime.date` | `time.Time` | `fory::Date`
| `fory::Date` | `DateOnly` | `Date` |
+| `timestamp` | `Instant` | `datetime.datetime` | `time.Time` |
`fory::Timestamp` | `fory::Timestamp` | `DateTimeOffset` | `Date`
|
### Collection Types
diff --git a/compiler/fory_compiler/frontend/fdl/parser.py
b/compiler/fory_compiler/frontend/fdl/parser.py
index 2a8acf9eb..9ded037fe 100644
--- a/compiler/fory_compiler/frontend/fdl/parser.py
+++ b/compiler/fory_compiler/frontend/fdl/parser.py
@@ -54,6 +54,7 @@ KNOWN_FILE_OPTIONS: Set[str] = {
"enable_auto_type_id",
"go_nested_type_style",
"swift_namespace_style",
+ "rust_use_chrono_temporal_types",
"evolving",
}
diff --git a/compiler/fory_compiler/generators/rust.py
b/compiler/fory_compiler/generators/rust.py
index 64885b530..42fe2aae7 100644
--- a/compiler/fory_compiler/generators/rust.py
+++ b/compiler/fory_compiler/generators/rust.py
@@ -61,13 +61,35 @@ class RustGenerator(BaseGenerator):
PrimitiveKind.FLOAT64: "f64",
PrimitiveKind.STRING: "::std::string::String",
PrimitiveKind.BYTES: "::std::vec::Vec<u8>",
+ PrimitiveKind.DECIMAL: "::fory::Decimal",
+ PrimitiveKind.ANY: "::std::boxed::Box<dyn ::std::any::Any>",
+ }
+
+ FORY_TEMPORAL_MAP = {
+ PrimitiveKind.DATE: "::fory::Date",
+ PrimitiveKind.TIMESTAMP: "::fory::Timestamp",
+ PrimitiveKind.DURATION: "::fory::Duration",
+ }
+
+ CHRONO_TEMPORAL_MAP = {
PrimitiveKind.DATE: "::chrono::NaiveDate",
PrimitiveKind.TIMESTAMP: "::chrono::NaiveDateTime",
PrimitiveKind.DURATION: "::chrono::Duration",
- PrimitiveKind.DECIMAL: "::fory::Decimal",
- PrimitiveKind.ANY: "::std::boxed::Box<dyn ::std::any::Any>",
}
+ def use_chrono_temporal_types(self) -> bool:
+ return self.schema.get_option("rust_use_chrono_temporal_types") is True
+
+ def primitive_type_name(self, kind: PrimitiveKind) -> str:
+ if kind in self.FORY_TEMPORAL_MAP:
+ temporal_map = (
+ self.CHRONO_TEMPORAL_MAP
+ if self.use_chrono_temporal_types()
+ else self.FORY_TEMPORAL_MAP
+ )
+ return temporal_map[kind]
+ return self.PRIMITIVE_MAP[kind]
+
def generate(self) -> List[GeneratedFile]:
"""Generate Rust files for the schema."""
files = []
@@ -723,7 +745,7 @@ class RustGenerator(BaseGenerator):
if isinstance(field_type, PrimitiveType):
if field_type.kind == PrimitiveKind.ANY:
return "::std::boxed::Box<dyn ::std::any::Any>"
- base_type = self.PRIMITIVE_MAP[field_type.kind]
+ base_type = self.primitive_type_name(field_type.kind)
if nullable:
return f"::std::option::Option<{base_type}>"
return base_type
diff --git a/compiler/fory_compiler/tests/test_generated_code.py
b/compiler/fory_compiler/tests/test_generated_code.py
index b4fde196b..754612454 100644
--- a/compiler/fory_compiler/tests/test_generated_code.py
+++ b/compiler/fory_compiler/tests/test_generated_code.py
@@ -154,6 +154,53 @@ def test_generated_code_scalar_types_equivalent():
assert_all_languages_equal(schemas)
+def test_rust_generated_code_uses_fory_temporal_carriers():
+ schema = parse_fdl(
+ dedent(
+ """
+ package gen;
+
+ message TemporalTypes {
+ date day = 1;
+ timestamp instant = 2;
+ duration elapsed = 3;
+ }
+ """
+ )
+ )
+
+ rust_output = render_files(generate_files(schema, RustGenerator))
+ assert "pub day: ::fory::Date," in rust_output
+ assert "pub instant: ::fory::Timestamp," in rust_output
+ assert "pub elapsed: ::fory::Duration," in rust_output
+ assert "chrono::" not in rust_output
+
+
+def test_rust_generated_code_can_use_chrono_temporal_types():
+ schema = parse_fdl(
+ dedent(
+ """
+ package gen;
+ option rust_use_chrono_temporal_types = true;
+
+ message TemporalTypes {
+ date day = 1;
+ timestamp instant = 2;
+ duration elapsed = 3;
+ }
+ """
+ )
+ )
+
+ rust_output = render_files(generate_files(schema, RustGenerator))
+ assert "pub day: ::chrono::NaiveDate," in rust_output
+ assert "pub instant: ::chrono::NaiveDateTime," in rust_output
+ assert "pub elapsed: ::chrono::Duration," in rust_output
+ assert "::fory::Date" not in rust_output
+ assert "::fory::Timestamp" not in rust_output
+ assert "::fory::Duration" not in rust_output
+
+
def test_generated_code_integer_encoding_variants_equivalent():
fdl = dedent(
"""
diff --git a/docs/compiler/compiler-guide.md b/docs/compiler/compiler-guide.md
index a1c35e026..8cefb9abf 100644
--- a/docs/compiler/compiler-guide.md
+++ b/docs/compiler/compiler-guide.md
@@ -73,7 +73,14 @@ Compile options:
| `--emit-fdl` | Emit translated FDL (for non-FDL
inputs) | `false` |
| `--emit-fdl-path` | Write translated FDL to this path
(file or directory) | (stdout) |
-For both `go_nested_type_style` and `swift_namespace_style`, schema-level file
options are supported (`option ... = ...;`) and the CLI flag overrides the
schema option when both are present.
+Schema-level file options are supported for language-specific generation
choices.
+For `go_nested_type_style` and `swift_namespace_style`, the CLI flag overrides
+the schema option when both are present. Rust temporal codegen has no CLI flag:
+set `option rust_use_chrono_temporal_types = true;` in the schema to generate
+`chrono::NaiveDate`, `chrono::NaiveDateTime`, and `chrono::Duration` instead of
+the default `fory::Date`, `fory::Timestamp`, and `fory::Duration`. Crates that
+compile generated chrono-based Rust code must depend on `chrono` and enable
+Fory's `chrono` feature.
Scan options (with `--scan-generated`):
diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md
index b30ce9083..784bb9610 100644
--- a/docs/compiler/schema-idl.md
+++ b/docs/compiler/schema-idl.md
@@ -217,6 +217,29 @@ message Payment {
The CLI flag `--swift_namespace_style` overrides this schema option when both
are set.
+### Rust Chrono Temporal Types Option
+
+Rust generated code uses Fory's lightweight temporal carrier types by default:
+`fory::Date`, `fory::Timestamp`, and `fory::Duration`. Set
+`rust_use_chrono_temporal_types` when the generated Rust API should expose
+chrono temporal types instead:
+
+```protobuf
+package payment;
+option rust_use_chrono_temporal_types = true;
+
+message Event {
+ date business_day = 1;
+ timestamp created_at = 2;
+ duration timeout = 3;
+}
+```
+
+With this option, Rust code maps `date` to `chrono::NaiveDate`, `timestamp` to
+`chrono::NaiveDateTime`, and `duration` to `chrono::Duration`. The Rust crate
+that compiles the generated code must depend on `chrono` and enable Fory's
+`chrono` feature.
+
### Java Outer Classname Option
Generate all types as inner classes of a single outer wrapper class:
@@ -1150,27 +1173,38 @@ Underscore spellings for integer encoding are not FDL
type names.
##### Date
-| Language | Type | Notes |
-| ---------- | --------------------------- | ----------------------- |
-| Java | `java.time.LocalDate` | |
-| Python | `datetime.date` | |
-| Go | `time.Time` | Time portion ignored |
-| Rust | `chrono::NaiveDate` | Requires `chrono` crate |
-| C++ | `fory::serialization::Date` | |
-| JavaScript | `Date` | |
-| Dart | `LocalDate` | Fory package type |
+| Language | Type | Notes
|
+| ---------- | --------------------------- |
--------------------------------------------------------------------------- |
+| Java | `java.time.LocalDate` |
|
+| Python | `datetime.date` |
|
+| Go | `time.Time` | Time portion ignored
|
+| Rust | `fory::Date` | Set
`rust_use_chrono_temporal_types = true` to generate `chrono::NaiveDate` |
+| C++ | `fory::serialization::Date` |
|
+| JavaScript | `Date` |
|
+| Dart | `LocalDate` | Fory package type
|
##### Timestamp
-| Language | Type | Notes |
-| ---------- | -------------------------------- | ----------------------- |
-| Java | `java.time.Instant` | UTC-based |
-| Python | `datetime.datetime` | |
-| Go | `time.Time` | |
-| Rust | `chrono::NaiveDateTime` | Requires `chrono` crate |
-| C++ | `fory::serialization::Timestamp` | |
-| JavaScript | `Date` | |
-| Dart | `Timestamp` | Fory package type |
+| Language | Type | Notes
|
+| ---------- | -------------------------------- |
-------------------------------------------------------------------------------
|
+| Java | `java.time.Instant` | UTC-based
|
+| Python | `datetime.datetime` |
|
+| Go | `time.Time` |
|
+| Rust | `fory::Timestamp` | Set
`rust_use_chrono_temporal_types = true` to generate `chrono::NaiveDateTime` |
+| C++ | `fory::serialization::Timestamp` |
|
+| JavaScript | `Date` |
|
+| Dart | `Timestamp` | Fory package type
|
+
+##### Duration
+
+| Language | Type | Notes
|
+| -------- | ------------------------------- |
-------------------------------------------------------------------------- |
+| Java | `java.time.Duration` |
|
+| Python | `datetime.timedelta` |
|
+| Go | `time.Duration` |
|
+| Rust | `fory::Duration` | Set
`rust_use_chrono_temporal_types = true` to generate `chrono::Duration` |
+| C++ | `fory::serialization::Duration` |
|
+| Dart | `Duration` |
|
#### Any
diff --git a/docs/guide/rust/basic-serialization.md
b/docs/guide/rust/basic-serialization.md
index 2d2386f5a..63bb9a355 100644
--- a/docs/guide/rust/basic-serialization.md
+++ b/docs/guide/rust/basic-serialization.md
@@ -119,10 +119,36 @@ assert_eq!(person, decoded);
### Date and Time
-| Rust Type | Description |
-| ----------------------- | -------------------------- |
-| `chrono::NaiveDate` | Date without timezone |
-| `chrono::NaiveDateTime` | Timestamp without timezone |
+| Rust Type | Description |
+| ----------- | ------------------------------------------------------- |
+| `Date` | Date without timezone, stored as epoch days |
+| `Timestamp` | Point in time, stored as epoch seconds and nanos |
+| `Duration` | Signed duration, stored as seconds and normalized nanos |
+
+The built-in carriers expose dependency-free constructors, accessors,
conversions, and checked
+arithmetic:
+
+```rust
+use fory::{Date, Duration, Timestamp};
+
+let date = Date::from_epoch_days(19_782);
+assert_eq!(date.checked_add_days(1)?.epoch_days(), 19_783);
+
+let timestamp = Timestamp::from_epoch_millis(-1);
+assert_eq!(timestamp.to_epoch_millis()?, -1);
+
+let duration = Duration::from_parts(1, 1_500_000_000)?;
+assert_eq!(duration.to_millis()?, 2_500);
+let later = timestamp.checked_add_duration(duration)?;
+```
+
+`chrono::NaiveDate`, `chrono::NaiveDateTime`, and `chrono::Duration` are
supported when the Rust
+`chrono` feature is enabled:
+
+```toml
+[dependencies]
+fory = { version = "0.13", features = ["chrono"] }
+```
### Custom Types
diff --git a/docs/specification/xlang_type_mapping.md
b/docs/specification/xlang_type_mapping.md
index a2a49ebfb..96b1e6a72 100644
--- a/docs/specification/xlang_type_mapping.md
+++ b/docs/specification/xlang_type_mapping.md
@@ -89,8 +89,8 @@ FDL spells them as an encoding modifier plus a semantic
integer type.
| union | 33 | Union
| typing.Union | /
| `std::variant<Ts...>` | /
| tagged union enum
|
| none | 36 | null
| None | null
| `std::monostate` |
nil | `()`
|
| duration | 37 | Duration
| timedelta | Number
| duration |
Duration | Duration
|
-| timestamp | 38 | Instant
| datetime | Number
| std::chrono::nanoseconds |
Time | DateTime
|
-| date | 39 | LocalDate
| datetime.date | Date
| fory::serialization::Date |
fory.Date | chrono::NaiveDate
|
+| timestamp | 38 | Instant
| datetime | Number
| std::chrono::nanoseconds |
Time | Timestamp
|
+| date | 39 | LocalDate
| datetime.date | Date
| fory::serialization::Date |
fory.Date | Date
|
| decimal | 40 | BigDecimal
| Decimal | Decimal
| / |
fory.Decimal | fory::Decimal
|
| binary | 41 | byte[]
| bytes | /
| `uint8_t[n]/vector<T>` |
`[n]uint8/[]T` | `Vec<u8>`
|
| `array<bool>` (bool_array) | 43 | bool[]
| BoolArray / ndarray(np.bool\_) | BoolArray /
Type.boolArray() | `bool[n]`
| `[n]bool/[]T` | `Vec<bool>`
|
diff --git a/integration_tests/idl_tests/idl/example.fdl
b/integration_tests/idl_tests/idl/example.fdl
index 4ade5cd85..bf635448b 100644
--- a/integration_tests/idl_tests/idl/example.fdl
+++ b/integration_tests/idl_tests/idl/example.fdl
@@ -20,6 +20,7 @@ package example;
option go_package =
"github.com/apache/fory/integration_tests/idl_tests/go/example/generated;example";
option evolving = false;
+option rust_use_chrono_temporal_types = true;
enum ExampleState [id=1504] {
UNKNOWN = 0;
diff --git a/integration_tests/idl_tests/idl/optional_types.fdl
b/integration_tests/idl_tests/idl/optional_types.fdl
index fba9a28b3..fb83bfd63 100644
--- a/integration_tests/idl_tests/idl/optional_types.fdl
+++ b/integration_tests/idl_tests/idl/optional_types.fdl
@@ -17,6 +17,8 @@
package optional_types;
+option rust_use_chrono_temporal_types = true;
+
message AllOptionalTypes [id=120] {
optional bool bool_value = 1;
optional int8 int8_value = 2;
diff --git a/integration_tests/idl_tests/rust/Cargo.lock
b/integration_tests/idl_tests/rust/Cargo.lock
index 72f12773b..0eb6a8bd7 100644
--- a/integration_tests/idl_tests/rust/Cargo.lock
+++ b/integration_tests/idl_tests/rust/Cargo.lock
@@ -93,9 +93,6 @@ dependencies = [
"num-bigint",
"num_enum",
"paste",
- "proc-macro2",
- "quote",
- "syn 2.0.114",
"thiserror",
]
@@ -107,7 +104,6 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
- "thiserror",
]
[[package]]
diff --git a/integration_tests/idl_tests/rust/Cargo.toml
b/integration_tests/idl_tests/rust/Cargo.toml
index 66f3fbd28..d15105f2b 100644
--- a/integration_tests/idl_tests/rust/Cargo.toml
+++ b/integration_tests/idl_tests/rust/Cargo.toml
@@ -23,5 +23,5 @@ license = "Apache-2.0"
[dependencies]
chrono = "0.4"
-fory = { path = "../../../rust/fory" }
+fory = { path = "../../../rust/fory", features = ["chrono"] }
fory-core = { path = "../../../rust/fory-core" }
diff --git a/rust/fory-core/Cargo.toml b/rust/fory-core/Cargo.toml
index 6a96e6f8d..9fae8c6ff 100644
--- a/rust/fory-core/Cargo.toml
+++ b/rust/fory-core/Cargo.toml
@@ -28,16 +28,17 @@ keywords = ["serialization", "serde", "trait-object",
"zero-copy", "schema-evolu
categories = ["encoding"]
[dependencies]
-proc-macro2 = { default-features = false, version = "1.0" }
-syn = { default-features = false, version = "2.0", features = ["full", "fold"]
}
-quote = { default-features = false, version = "1.0" }
byteorder = { version = "1.4" }
-chrono = "0.4"
+chrono = { version = "0.4", default-features = false, features = ["std"],
optional = true }
thiserror = { default-features = false, version = "1.0" }
num_enum = "0.5.1"
paste = "1.0"
num-bigint = "0.4"
+[features]
+default = []
+chrono = ["dep:chrono"]
+tests = []
[[bench]]
name = "simd_bench"
diff --git a/rust/fory-core/src/lib.rs b/rust/fory-core/src/lib.rs
index 4d1ac219b..65716af4a 100644
--- a/rust/fory-core/src/lib.rs
+++ b/rust/fory-core/src/lib.rs
@@ -32,7 +32,7 @@
//! - **`serializer`**: Type-specific serialization implementations
//! - **`resolver`**: Type resolution and metadata management
//! - **`meta`**: Metadata handling for schema evolution
-//! - **`types`**: Runtime value carriers such as decimal, Float16, BFloat16,
and weak refs
+//! - **`types`**: Runtime value carriers such as temporal values, decimal,
Float16, BFloat16, and weak refs
//! - **`type_id`**: Type IDs and protocol header helpers
//! - **`error`**: Error handling and result types
//! - **`util`**: Utility functions and helpers
@@ -52,7 +52,7 @@
//! - Primitive types (bool, integers, floats, strings)
//! - Collections (Vec, HashMap, BTreeMap)
//! - Optional types (`Option<T>`)
-//! - Date/time types (chrono integration)
+//! - Date/time carriers with optional chrono integration
//! - Custom structs and enums
//! - Trait objects (Box, Rc, Arc)
//!
@@ -205,4 +205,4 @@ pub use crate::serializer::{read_data, write_data,
ForyDefault, Serializer, Stru
pub use crate::type_id::TypeId;
pub use crate::types::bfloat16::bfloat16 as BFloat16;
pub use crate::types::float16::float16 as Float16;
-pub use crate::types::{ArcWeak, Decimal, RcWeak};
+pub use crate::types::{ArcWeak, Date, Decimal, Duration, RcWeak, Timestamp};
diff --git a/rust/fory-core/src/resolver/type_resolver.rs
b/rust/fory-core/src/resolver/type_resolver.rs
index 3fcacda79..7983cc0c8 100644
--- a/rust/fory-core/src/resolver/type_resolver.rs
+++ b/rust/fory-core/src/resolver/type_resolver.rs
@@ -24,8 +24,10 @@ use crate::meta::{
use crate::resolver::RefMode;
use crate::serializer::{ForyDefault, Serializer, StructSerializer};
use crate::type_id::{get_ext_actual_type_id, is_enum_type_id};
+use crate::types::{Date, Duration, Timestamp};
use crate::TypeId;
-use chrono::{NaiveDate, NaiveDateTime};
+#[cfg(feature = "chrono")]
+use chrono::{Duration as ChronoDuration, NaiveDate, NaiveDateTime};
use std::collections::{HashSet, LinkedList};
use std::rc::Rc;
use std::vec;
@@ -771,8 +773,15 @@ impl TypeResolver {
self.register_internal_serializer::<usize>(TypeId::USIZE)?;
self.register_internal_serializer::<u128>(TypeId::U128)?;
self.register_internal_serializer::<String>(TypeId::STRING)?;
- self.register_internal_serializer::<NaiveDateTime>(TypeId::TIMESTAMP)?;
- self.register_internal_serializer::<NaiveDate>(TypeId::DATE)?;
+ #[cfg(feature = "chrono")]
+ {
+
self.register_internal_serializer::<ChronoDuration>(TypeId::DURATION)?;
+
self.register_internal_serializer::<NaiveDateTime>(TypeId::TIMESTAMP)?;
+ self.register_internal_serializer::<NaiveDate>(TypeId::DATE)?;
+ }
+ self.register_internal_serializer::<Duration>(TypeId::DURATION)?;
+ self.register_internal_serializer::<Timestamp>(TypeId::TIMESTAMP)?;
+ self.register_internal_serializer::<Date>(TypeId::DATE)?;
self.register_internal_serializer::<crate::types::Decimal>(TypeId::DECIMAL)?;
self.register_internal_serializer::<Vec<bool>>(TypeId::BOOL_ARRAY)?;
diff --git a/rust/fory-core/src/row/row.rs b/rust/fory-core/src/row/row.rs
index 147a2f110..8f6e5181c 100644
--- a/rust/fory-core/src/row/row.rs
+++ b/rust/fory-core/src/row/row.rs
@@ -15,10 +15,9 @@
// specific language governing permissions and limitations
// under the License.
-use crate::util::EPOCH;
+use crate::types::{Date, Duration, Timestamp};
use crate::{buffer::Writer, error::Error};
use byteorder::{ByteOrder, LittleEndian};
-use chrono::{DateTime, Days, NaiveDate, NaiveDateTime};
use std::collections::BTreeMap;
use std::marker::PhantomData;
@@ -133,40 +132,50 @@ impl<'a, T: Row<'a>, const N: usize> Row<'a> for [T; N] {
}
}
-impl Row<'_> for NaiveDate {
- type ReadResult = Result<NaiveDate, Error>;
+impl Row<'_> for Date {
+ type ReadResult = Result<Date, Error>;
fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> {
- let days_since_epoch = v.signed_duration_since(EPOCH).num_days();
- writer.write_u32(days_since_epoch as u32);
+ let days = i32::try_from(v.epoch_days()).map_err(|_| {
+ Error::invalid_data(format!(
+ "row date day count {} exceeds date32 range",
+ v.epoch_days()
+ ))
+ })?;
+ writer.write_i32(days);
Ok(())
}
fn cast(bytes: &[u8]) -> Self::ReadResult {
- let days = LittleEndian::read_u32(bytes);
- EPOCH
- .checked_add_days(Days::new(days.into()))
- .ok_or(Error::invalid_data(format!(
- "Date out of range, {days} days since epoch"
- )))
+ Ok(Date::from_epoch_days(i64::from(LittleEndian::read_i32(
+ bytes,
+ ))))
}
}
-impl Row<'_> for NaiveDateTime {
- type ReadResult = Result<NaiveDateTime, Error>;
+impl Row<'_> for Timestamp {
+ type ReadResult = Result<Timestamp, Error>;
fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> {
- writer.write_i64(v.and_utc().timestamp_millis());
+ writer.write_i64(v.to_epoch_micros()?);
Ok(())
}
fn cast(bytes: &[u8]) -> Self::ReadResult {
- let timestamp = LittleEndian::read_u64(bytes);
- DateTime::from_timestamp_millis(timestamp as i64)
- .map(|dt| dt.naive_utc())
- .ok_or(Error::invalid_data(format!(
- "Date out of range, timestamp:{timestamp}"
- )))
+ Ok(Timestamp::from_epoch_micros(LittleEndian::read_i64(bytes)))
+ }
+}
+
+impl Row<'_> for Duration {
+ type ReadResult = Result<Duration, Error>;
+
+ fn write(v: &Self, writer: &mut Writer) -> Result<(), Error> {
+ writer.write_i64(v.to_micros()?);
+ Ok(())
+ }
+
+ fn cast(bytes: &[u8]) -> Self::ReadResult {
+ Ok(Duration::from_micros(LittleEndian::read_i64(bytes)))
}
}
diff --git a/rust/fory-core/src/serializer/datetime.rs
b/rust/fory-core/src/serializer/datetime.rs
index 5823d0c7c..efa757245 100644
--- a/rust/fory-core/src/serializer/datetime.rs
+++ b/rust/fory-core/src/serializer/datetime.rs
@@ -23,19 +23,14 @@ use crate::serializer::util::read_basic_type_info;
use crate::serializer::ForyDefault;
use crate::serializer::Serializer;
use crate::type_id::TypeId;
-use crate::util::EPOCH;
-use chrono::{Duration as ChronoDuration, NaiveDate, NaiveDateTime, TimeDelta};
+use crate::types::{Date, Duration, Timestamp};
use std::mem;
-use std::time::Duration;
-impl Serializer for NaiveDateTime {
+impl Serializer for Timestamp {
#[inline(always)]
fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error>
{
- let dt = self.and_utc();
- let seconds = dt.timestamp();
- let nanos = dt.timestamp_subsec_nanos();
- context.writer.write_i64(seconds);
- context.writer.write_u32(nanos);
+ context.writer.write_i64(self.seconds());
+ context.writer.write_u32(self.subsec_nanos());
Ok(())
}
@@ -43,9 +38,7 @@ impl Serializer for NaiveDateTime {
fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
let seconds = context.reader.read_i64()?;
let nanos = context.reader.read_u32()?;
- #[allow(deprecated)]
- let result = NaiveDateTime::from_timestamp(seconds, nanos);
- Ok(result)
+ Timestamp::new(seconds, nanos)
}
#[inline(always)]
@@ -85,18 +78,22 @@ impl Serializer for NaiveDateTime {
}
}
-impl Serializer for NaiveDate {
+impl ForyDefault for Timestamp {
+ #[inline(always)]
+ fn fory_default() -> Self {
+ Timestamp::default()
+ }
+}
+
+impl Serializer for Date {
#[inline(always)]
fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error>
{
- let days_since_epoch = self.signed_duration_since(EPOCH).num_days();
+ let days = self.epoch_days();
if context.is_xlang() {
- context.writer.write_var_i64(days_since_epoch);
+ context.writer.write_var_i64(days);
} else {
- let native_days = i32::try_from(days_since_epoch).map_err(|_| {
- Error::invalid_data(format!(
- "date day count {} exceeds native i32 range",
- days_since_epoch
- ))
+ let native_days = i32::try_from(days).map_err(|_| {
+ Error::invalid_data(format!("date day count {} exceeds native
i32 range", days))
})?;
context.writer.write_i32(native_days);
}
@@ -110,18 +107,7 @@ impl Serializer for NaiveDate {
} else {
i64::from(context.reader.read_i32()?)
};
- let duration = TimeDelta::try_days(days).ok_or_else(|| {
- Error::invalid_data(format!(
- "date day count {} is out of chrono::TimeDelta range",
- days
- ))
- })?;
- EPOCH.checked_add_signed(duration).ok_or_else(|| {
- Error::invalid_data(format!(
- "date day count {} is out of chrono::NaiveDate range",
- days
- ))
- })
+ Ok(Date::from_epoch_days(days))
}
#[inline(always)]
@@ -161,62 +147,31 @@ impl Serializer for NaiveDate {
}
}
-impl ForyDefault for NaiveDateTime {
+impl ForyDefault for Date {
#[inline(always)]
fn fory_default() -> Self {
- NaiveDateTime::default()
- }
-}
-
-impl ForyDefault for NaiveDate {
- #[inline(always)]
- fn fory_default() -> Self {
- NaiveDate::default()
+ Date::default()
}
}
impl Serializer for Duration {
#[inline(always)]
fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error>
{
- let raw = self.as_secs();
- if raw > i64::MAX as u64 {
- return Err(Error::invalid_data(format!(
- "std::time::Duration seconds {} exceeds i64::MAX and cannot be
encoded with write_var_i64",
- raw
- )));
- }
- let secs = raw as i64;
- let nanos = self.subsec_nanos() as i32;
- context.writer.write_var_i64(secs);
- context.writer.write_i32(nanos);
+ context.writer.write_var_i64(self.seconds());
+ context.writer.write_i32(self.subsec_nanos() as i32);
Ok(())
}
#[inline(always)]
fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
- let secs = context.reader.read_var_i64()?;
- if secs < 0 {
- return Err(Error::invalid_data(format!(
- "negative duration seconds {} cannot be represented as
std::time::Duration; use chrono::Duration instead",
- secs
- )));
- }
+ let seconds = context.reader.read_var_i64()?;
let nanos = context.reader.read_i32()?;
- if !(0..=999_999_999).contains(&nanos) {
- // negative nanos will also be rejected, even though the xlang
spec actually allows it.
- // RFC 1040
(https://rust-lang.github.io/rfcs/1040-duration-reform.html#detailed-design)
explicitly forbids negative nanoseconds.
- // If supporting for negative nanoseconds is really needed, we can
implement **normalization** similar to chrono and Java.
- return Err(Error::invalid_data(format!(
- "duration nanoseconds {} out of valid range [0, 999_999_999]
for std::time::Duration",
- nanos
- )));
- }
- Ok(Duration::new(secs as u64, nanos as u32))
+ Duration::new(seconds, nanos)
}
#[inline(always)]
fn fory_reserved_space() -> usize {
- 9 + mem::size_of::<i32>() // max write_var_i64 payload is 9 bytes + 4
bytes for i32
+ 9 + mem::size_of::<i32>()
}
#[inline(always)]
@@ -254,82 +209,175 @@ impl Serializer for Duration {
impl ForyDefault for Duration {
#[inline(always)]
fn fory_default() -> Self {
- Duration::ZERO
+ Duration::default()
}
}
-impl Serializer for ChronoDuration {
- #[inline(always)]
- fn fory_write_data(&self, context: &mut WriteContext) -> Result<(), Error>
{
- let secs = self.num_seconds();
- let nanos = self.subsec_nanos();
- context.writer.write_var_i64(secs);
- context.writer.write_i32(nanos);
- Ok(())
- }
+#[cfg(feature = "chrono")]
+mod chrono_support {
+ use super::*;
+ use chrono::{Duration as ChronoDuration, NaiveDate, NaiveDateTime};
- #[inline(always)]
- fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
- let secs = context.reader.read_var_i64()?;
- let nanos = context.reader.read_i32()?;
- if !(-999_999_999..=999_999_999).contains(&nanos) {
- // chrono supports negative nanoseconds by applying normalization
internally.
- return Err(Error::invalid_data(format!(
- "duration nanoseconds {} out of valid range [-999_999_999,
999_999_999]",
- nanos
- )));
- }
- ChronoDuration::try_seconds(secs) // the maximum seconds chrono
supports is i64::MAX / 1_000, which is smaller than what the spec
allows(i64::MAX)
- .and_then(|d| d.checked_add(&ChronoDuration::nanoseconds(nanos as
i64)))
- .ok_or_else(|| {
- Error::invalid_data(format!(
- "duration seconds {} out of chrono::Duration valid range",
- secs
- ))
- })
- }
+ impl Serializer for NaiveDateTime {
+ #[inline(always)]
+ fn fory_write_data(&self, context: &mut WriteContext) -> Result<(),
Error> {
+ Timestamp::from(*self).fory_write_data(context)
+ }
- #[inline(always)]
- fn fory_reserved_space() -> usize {
- 9 + mem::size_of::<i32>() // max write_var_i64 payload is 9 bytes + 4
bytes for i32
- }
+ #[inline(always)]
+ fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
+ Timestamp::fory_read_data(context)?.try_into()
+ }
- #[inline(always)]
- fn fory_get_type_id(_: &TypeResolver) -> Result<TypeId, Error> {
- Ok(TypeId::DURATION)
- }
+ #[inline(always)]
+ fn fory_reserved_space() -> usize {
+ Timestamp::fory_reserved_space()
+ }
- #[inline(always)]
- fn fory_type_id_dyn(&self, _: &TypeResolver) -> Result<TypeId, Error> {
- Ok(TypeId::DURATION)
+ #[inline(always)]
+ fn fory_get_type_id(_: &TypeResolver) -> Result<TypeId, Error> {
+ Ok(TypeId::TIMESTAMP)
+ }
+
+ #[inline(always)]
+ fn fory_type_id_dyn(&self, _: &TypeResolver) -> Result<TypeId, Error> {
+ Ok(TypeId::TIMESTAMP)
+ }
+
+ #[inline(always)]
+ fn fory_static_type_id() -> TypeId {
+ TypeId::TIMESTAMP
+ }
+
+ #[inline(always)]
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
+ #[inline(always)]
+ fn fory_write_type_info(context: &mut WriteContext) -> Result<(),
Error> {
+ Timestamp::fory_write_type_info(context)
+ }
+
+ #[inline(always)]
+ fn fory_read_type_info(context: &mut ReadContext) -> Result<(), Error>
{
+ read_basic_type_info::<Self>(context)
+ }
}
- #[inline(always)]
- fn fory_static_type_id() -> TypeId {
- TypeId::DURATION
+ impl ForyDefault for NaiveDateTime {
+ #[inline(always)]
+ fn fory_default() -> Self {
+ NaiveDateTime::default()
+ }
}
- #[inline(always)]
- fn as_any(&self) -> &dyn std::any::Any {
- self
+ impl Serializer for NaiveDate {
+ #[inline(always)]
+ fn fory_write_data(&self, context: &mut WriteContext) -> Result<(),
Error> {
+ Date::from(*self).fory_write_data(context)
+ }
+
+ #[inline(always)]
+ fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
+ Date::fory_read_data(context)?.try_into()
+ }
+
+ #[inline(always)]
+ fn fory_reserved_space() -> usize {
+ Date::fory_reserved_space()
+ }
+
+ #[inline(always)]
+ fn fory_get_type_id(_: &TypeResolver) -> Result<TypeId, Error> {
+ Ok(TypeId::DATE)
+ }
+
+ #[inline(always)]
+ fn fory_type_id_dyn(&self, _: &TypeResolver) -> Result<TypeId, Error> {
+ Ok(TypeId::DATE)
+ }
+
+ #[inline(always)]
+ fn fory_static_type_id() -> TypeId {
+ TypeId::DATE
+ }
+
+ #[inline(always)]
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
+ #[inline(always)]
+ fn fory_write_type_info(context: &mut WriteContext) -> Result<(),
Error> {
+ Date::fory_write_type_info(context)
+ }
+
+ #[inline(always)]
+ fn fory_read_type_info(context: &mut ReadContext) -> Result<(), Error>
{
+ read_basic_type_info::<Self>(context)
+ }
}
- #[inline(always)]
- fn fory_write_type_info(context: &mut WriteContext) -> Result<(), Error> {
- context.writer.write_u8(TypeId::DURATION as u8);
- Ok(())
+ impl ForyDefault for NaiveDate {
+ #[inline(always)]
+ fn fory_default() -> Self {
+ NaiveDate::default()
+ }
}
- #[inline(always)]
- fn fory_read_type_info(context: &mut ReadContext) -> Result<(), Error> {
- read_basic_type_info::<Self>(context)
+ impl Serializer for ChronoDuration {
+ #[inline(always)]
+ fn fory_write_data(&self, context: &mut WriteContext) -> Result<(),
Error> {
+ Duration::try_from(*self)?.fory_write_data(context)
+ }
+
+ #[inline(always)]
+ fn fory_read_data(context: &mut ReadContext) -> Result<Self, Error> {
+ Duration::fory_read_data(context)?.try_into()
+ }
+
+ #[inline(always)]
+ fn fory_reserved_space() -> usize {
+ Duration::fory_reserved_space()
+ }
+
+ #[inline(always)]
+ fn fory_get_type_id(_: &TypeResolver) -> Result<TypeId, Error> {
+ Ok(TypeId::DURATION)
+ }
+
+ #[inline(always)]
+ fn fory_type_id_dyn(&self, _: &TypeResolver) -> Result<TypeId, Error> {
+ Ok(TypeId::DURATION)
+ }
+
+ #[inline(always)]
+ fn fory_static_type_id() -> TypeId {
+ TypeId::DURATION
+ }
+
+ #[inline(always)]
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
+ #[inline(always)]
+ fn fory_write_type_info(context: &mut WriteContext) -> Result<(),
Error> {
+ Duration::fory_write_type_info(context)
+ }
+
+ #[inline(always)]
+ fn fory_read_type_info(context: &mut ReadContext) -> Result<(), Error>
{
+ read_basic_type_info::<Self>(context)
+ }
}
-}
-impl ForyDefault for ChronoDuration {
- #[inline(always)]
- fn fory_default() -> Self {
- ChronoDuration::zero()
+ impl ForyDefault for ChronoDuration {
+ #[inline(always)]
+ fn fory_default() -> Self {
+ ChronoDuration::zero()
+ }
}
}
@@ -339,80 +387,72 @@ mod tests {
use crate::fory::Fory;
#[test]
- fn test_std_duration_serialization() {
+ fn test_temporal_carrier_serialization() {
let fory = Fory::default();
- // Test various durations
- let test_cases = vec![
- Duration::ZERO,
- Duration::new(0, 0),
- Duration::new(1, 0),
- Duration::new(0, 1),
- Duration::new(123, 456789),
- Duration::new(i64::MAX as u64, 999_999_999),
+ let timestamps = [
+ Timestamp::UNIX_EPOCH,
+ Timestamp::new(1, 0).unwrap(),
+ Timestamp::new(-1, 999_999_999).unwrap(),
];
-
- for duration in test_cases {
- let bytes = fory.serialize(&duration).unwrap();
- let deserialized: Duration = fory.deserialize(&bytes).unwrap();
- assert_eq!(
- duration, deserialized,
- "Failed for duration: {:?}",
- duration
- );
+ for timestamp in timestamps {
+ let bytes = fory.serialize(×tamp).unwrap();
+ let deserialized: Timestamp = fory.deserialize(&bytes).unwrap();
+ assert_eq!(timestamp, deserialized);
}
- }
-
- #[test]
- fn test_chrono_duration_serialization() {
- let fory = Fory::default();
- // Test various durations
- let test_cases = vec![
- ChronoDuration::zero(),
- ChronoDuration::new(0, 0).unwrap(),
- ChronoDuration::new(1, 0).unwrap(),
- ChronoDuration::new(0, 1).unwrap(),
- ChronoDuration::new(123, 456789).unwrap(),
- ChronoDuration::seconds(-1),
- ChronoDuration::nanoseconds(-1),
- ChronoDuration::microseconds(-456789),
- ChronoDuration::MAX,
- ChronoDuration::MIN,
+ let dates = [
+ Date::UNIX_EPOCH,
+ Date::from_epoch_days(-1),
+ Date::from_epoch_days(18_628),
];
+ for date in dates {
+ let bytes = fory.serialize(&date).unwrap();
+ let deserialized: Date = fory.deserialize(&bytes).unwrap();
+ assert_eq!(date, deserialized);
+ }
- for duration in test_cases {
+ let durations = [
+ Duration::ZERO,
+ Duration::new(1, 0).unwrap(),
+ Duration::new(0, -1).unwrap(),
+ Duration::new(-123, 456_789).unwrap(),
+ ];
+ for duration in durations {
let bytes = fory.serialize(&duration).unwrap();
- let deserialized: ChronoDuration =
fory.deserialize(&bytes).unwrap();
- assert_eq!(
- duration, deserialized,
- "Failed for duration: {:?}",
- duration
- );
+ let deserialized: Duration = fory.deserialize(&bytes).unwrap();
+ assert_eq!(duration, deserialized);
}
}
#[test]
- fn test_chrono_duration_out_of_range_is_error() {
- let fory = Fory::default();
- let too_large = Duration::new(i64::MAX as u64, 0);
- let bytes = fory.serialize(&too_large).unwrap();
- let result: Result<ChronoDuration, _> = fory.deserialize(&bytes);
- assert!(
- result.is_err(),
- "out-of-range seconds should not be deserialized into
chrono::Duration!"
+ fn test_duration_normalizes_negative_nanoseconds() {
+ assert_eq!(
+ Duration::new(0, -1).unwrap(),
+ Duration::from_normalized(-1, 999_999_999).unwrap()
);
}
+ #[cfg(feature = "chrono")]
#[test]
- fn test_negative_std_duration_read_is_error() {
+ fn test_chrono_temporal_feature_serialization() {
+ use chrono::{DateTime, Duration as ChronoDuration, NaiveDate,
NaiveDateTime};
+
let fory = Fory::default();
- let negative_duration = ChronoDuration::seconds(-1);
- let bytes = fory.serialize(&negative_duration).unwrap();
- let result: Result<Duration, _> = fory.deserialize(&bytes);
- assert!(
- result.is_err(),
- "negative duration should not be deserialized into
std::time::Duration!"
- );
+ let date = NaiveDate::from_ymd_opt(2024, 2, 3).unwrap();
+ let timestamp = DateTime::from_timestamp(100, 1).unwrap().naive_utc();
+ let duration = ChronoDuration::nanoseconds(-1);
+
+ let bytes = fory.serialize(&date).unwrap();
+ let deserialized: NaiveDate = fory.deserialize(&bytes).unwrap();
+ assert_eq!(date, deserialized);
+
+ let bytes = fory.serialize(×tamp).unwrap();
+ let deserialized: NaiveDateTime = fory.deserialize(&bytes).unwrap();
+ assert_eq!(timestamp, deserialized);
+
+ let bytes = fory.serialize(&duration).unwrap();
+ let deserialized: ChronoDuration = fory.deserialize(&bytes).unwrap();
+ assert_eq!(duration, deserialized);
}
}
diff --git a/rust/fory-core/src/serializer/skip.rs
b/rust/fory-core/src/serializer/skip.rs
index 944dadf34..4d8b90651 100644
--- a/rust/fory-core/src/serializer/skip.rs
+++ b/rust/fory-core/src/serializer/skip.rs
@@ -23,9 +23,9 @@ use crate::serializer::collection::{DECL_ELEMENT_TYPE,
HAS_NULL, IS_SAME_TYPE};
use crate::serializer::util;
use crate::serializer::Serializer;
use crate::type_id as types;
+use crate::types::{Date, Duration, Timestamp};
use crate::util::ENABLE_FORY_DEBUG_OUTPUT;
use crate::RefFlag;
-use chrono::{Duration, NaiveDate, NaiveDateTime};
use std::rc::Rc;
#[allow(unreachable_code)]
@@ -608,12 +608,12 @@ fn skip_value(
// ============ TIMESTAMP (TypeId = 36) ============
types::TIMESTAMP => {
- <NaiveDateTime as Serializer>::fory_read_data(context)?;
+ <Timestamp as Serializer>::fory_read_data(context)?;
}
// ============ DATE (TypeId = 37) ============
types::DATE => {
- <NaiveDate as Serializer>::fory_read_data(context)?;
+ <Date as Serializer>::fory_read_data(context)?;
}
// ============ DECIMAL (TypeId = 38) ============
diff --git a/rust/fory-core/src/type_id.rs b/rust/fory-core/src/type_id.rs
index 5c7ed103e..9b7db9e0d 100644
--- a/rust/fory-core/src/type_id.rs
+++ b/rust/fory-core/src/type_id.rs
@@ -184,7 +184,7 @@ pub const fn is_enum_type_id(type_id: TypeId) -> bool {
matches!(type_id, TypeId::ENUM | TypeId::NAMED_ENUM | TypeId::UNION)
}
-pub static BASIC_TYPES: [TypeId; 34] = [
+pub static BASIC_TYPES: [TypeId; 35] = [
TypeId::BOOL,
TypeId::INT8,
TypeId::INT16,
@@ -200,6 +200,7 @@ pub static BASIC_TYPES: [TypeId; 34] = [
TypeId::STRING,
TypeId::DATE,
TypeId::TIMESTAMP,
+ TypeId::DURATION,
TypeId::BOOL_ARRAY,
TypeId::BINARY,
TypeId::INT8_ARRAY,
@@ -272,7 +273,7 @@ pub static PRIMITIVE_ARRAY_TYPES: [u32; 19] = [
TypeId::USIZE_ARRAY as u32,
TypeId::ISIZE_ARRAY as u32,
];
-pub static BASIC_TYPE_NAMES: [&str; 20] = [
+pub static BASIC_TYPE_NAMES: [&str; 21] = [
"bool",
"i8",
"i16",
@@ -282,8 +283,9 @@ pub static BASIC_TYPE_NAMES: [&str; 20] = [
"f32",
"f64",
"String",
- "NaiveDate",
- "NaiveDateTime",
+ "Date",
+ "Timestamp",
+ "Duration",
"u8",
"u16",
"u32",
diff --git a/rust/fory-core/src/types/mod.rs b/rust/fory-core/src/types/mod.rs
index c204d91f1..673d004c6 100644
--- a/rust/fory-core/src/types/mod.rs
+++ b/rust/fory-core/src/types/mod.rs
@@ -18,7 +18,9 @@
pub mod bfloat16;
pub mod decimal;
pub mod float16;
+pub mod temporal;
pub mod weak;
pub use decimal::Decimal;
+pub use temporal::{Date, Duration, Timestamp};
pub use weak::{ArcWeak, RcWeak};
diff --git a/rust/fory-core/src/types/temporal.rs
b/rust/fory-core/src/types/temporal.rs
new file mode 100644
index 000000000..de3eb36be
--- /dev/null
+++ b/rust/fory-core/src/types/temporal.rs
@@ -0,0 +1,763 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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
+//
+// http://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.
+
+use crate::error::Error;
+
+const NANOS_PER_SECOND: i32 = 1_000_000_000;
+const NANOS_PER_SECOND_I128: i128 = NANOS_PER_SECOND as i128;
+const NANOS_PER_MILLI: i128 = 1_000_000;
+const NANOS_PER_MICRO: i128 = 1_000;
+const MILLIS_PER_SECOND: i64 = 1_000;
+const MICROS_PER_SECOND: i64 = 1_000_000;
+const SECONDS_PER_MINUTE: i64 = 60;
+const SECONDS_PER_HOUR: i64 = 60 * SECONDS_PER_MINUTE;
+const SECONDS_PER_DAY: i64 = 24 * SECONDS_PER_HOUR;
+
+/// Date without timezone, represented as signed days since Unix epoch.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct Date {
+ days: i64,
+}
+
+impl Date {
+ pub const UNIX_EPOCH: Self = Self { days: 0 };
+
+ /// Creates a date from signed days since 1970-01-01.
+ #[inline(always)]
+ pub const fn from_epoch_days(days: i64) -> Self {
+ Self { days }
+ }
+
+ /// Returns the signed day count from 1970-01-01.
+ #[inline(always)]
+ pub const fn epoch_days(self) -> i64 {
+ self.days
+ }
+
+ /// Returns a date offset by `days`.
+ #[inline(always)]
+ pub fn checked_add_days(self, days: i64) -> Result<Self, Error> {
+ self.days
+ .checked_add(days)
+ .map(Self::from_epoch_days)
+ .ok_or_else(|| {
+ Error::invalid_data(format!(
+ "date day count {} overflow adding {} days",
+ self.days, days
+ ))
+ })
+ }
+
+ /// Returns a date offset backward by `days`.
+ #[inline(always)]
+ pub fn checked_sub_days(self, days: i64) -> Result<Self, Error> {
+ self.days
+ .checked_sub(days)
+ .map(Self::from_epoch_days)
+ .ok_or_else(|| {
+ Error::invalid_data(format!(
+ "date day count {} overflow subtracting {} days",
+ self.days, days
+ ))
+ })
+ }
+
+ /// Returns the signed day distance from `earlier` to this date.
+ #[inline(always)]
+ pub fn days_since(self, earlier: Self) -> Result<i64, Error> {
+ self.days.checked_sub(earlier.days).ok_or_else(|| {
+ Error::invalid_data(format!(
+ "date day count {} overflow subtracting {}",
+ self.days, earlier.days
+ ))
+ })
+ }
+}
+
+/// Point in time, represented as seconds and nanoseconds since Unix epoch.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct Timestamp {
+ seconds: i64,
+ nanos: u32,
+}
+
+impl Timestamp {
+ pub const UNIX_EPOCH: Self = Self {
+ seconds: 0,
+ nanos: 0,
+ };
+
+ /// Creates a timestamp from epoch seconds and a nanosecond component.
+ #[inline(always)]
+ pub fn new(seconds: i64, nanos: u32) -> Result<Self, Error> {
+ if nanos >= NANOS_PER_SECOND as u32 {
+ return Err(Error::invalid_data(format!(
+ "timestamp nanoseconds {} out of valid range [0, 999_999_999]",
+ nanos
+ )));
+ }
+ Ok(Self { seconds, nanos })
+ }
+
+ /// Creates a timestamp from whole seconds since Unix epoch.
+ #[inline(always)]
+ pub const fn from_epoch_seconds(seconds: i64) -> Self {
+ Self { seconds, nanos: 0 }
+ }
+
+ /// Creates a timestamp from milliseconds since Unix epoch.
+ #[inline(always)]
+ pub fn from_epoch_millis(millis: i64) -> Self {
+ let seconds = millis.div_euclid(MILLIS_PER_SECOND);
+ let millis = millis.rem_euclid(MILLIS_PER_SECOND);
+ Self {
+ seconds,
+ nanos: (millis as u32) * 1_000_000,
+ }
+ }
+
+ /// Creates a timestamp from microseconds since Unix epoch.
+ #[inline(always)]
+ pub fn from_epoch_micros(micros: i64) -> Self {
+ let seconds = micros.div_euclid(MICROS_PER_SECOND);
+ let micros = micros.rem_euclid(MICROS_PER_SECOND);
+ Self {
+ seconds,
+ nanos: (micros as u32) * 1_000,
+ }
+ }
+
+ /// Creates a timestamp from nanoseconds since Unix epoch.
+ #[inline(always)]
+ pub fn from_epoch_nanos(nanos: i128) -> Result<Self, Error> {
+ let (seconds, nanos) = split_total_nanos(nanos, "timestamp")?;
+ Ok(Self { seconds, nanos })
+ }
+
+ /// Returns the whole seconds component.
+ #[inline(always)]
+ pub const fn seconds(self) -> i64 {
+ self.seconds
+ }
+
+ /// Returns the normalized nanosecond component.
+ #[inline(always)]
+ pub const fn subsec_nanos(self) -> u32 {
+ self.nanos
+ }
+
+ /// Returns milliseconds since Unix epoch, rounded down for
sub-millisecond values.
+ #[inline(always)]
+ pub fn to_epoch_millis(self) -> Result<i64, Error> {
+ total_unit(self.to_epoch_nanos(), NANOS_PER_MILLI, "timestamp")
+ }
+
+ /// Returns microseconds since Unix epoch, rounded down for
sub-microsecond values.
+ #[inline(always)]
+ pub fn to_epoch_micros(self) -> Result<i64, Error> {
+ total_unit(self.to_epoch_nanos(), NANOS_PER_MICRO, "timestamp")
+ }
+
+ /// Returns nanoseconds since Unix epoch.
+ #[inline(always)]
+ pub fn to_epoch_nanos(self) -> i128 {
+ i128::from(self.seconds) * NANOS_PER_SECOND_I128 +
i128::from(self.nanos)
+ }
+
+ /// Adds a signed duration to this timestamp.
+ #[inline(always)]
+ pub fn checked_add_duration(self, duration: Duration) -> Result<Self,
Error> {
+ Self::from_epoch_nanos(self.to_epoch_nanos() + duration.to_nanos())
+ }
+
+ /// Subtracts a signed duration from this timestamp.
+ #[inline(always)]
+ pub fn checked_sub_duration(self, duration: Duration) -> Result<Self,
Error> {
+ Self::from_epoch_nanos(self.to_epoch_nanos() - duration.to_nanos())
+ }
+
+ /// Returns the signed duration from `earlier` to this timestamp.
+ #[inline(always)]
+ pub fn duration_since(self, earlier: Self) -> Result<Duration, Error> {
+ Duration::from_nanos(self.to_epoch_nanos() - earlier.to_epoch_nanos())
+ }
+}
+
+/// Signed duration, represented as seconds and normalized nanoseconds.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct Duration {
+ seconds: i64,
+ nanos: u32,
+}
+
+impl Duration {
+ pub const ZERO: Self = Self {
+ seconds: 0,
+ nanos: 0,
+ };
+
+ /// Creates a duration from seconds and a nanosecond adjustment.
+ #[inline(always)]
+ pub fn new(seconds: i64, nanos: i32) -> Result<Self, Error> {
+ if !(-(NANOS_PER_SECOND - 1)..=(NANOS_PER_SECOND -
1)).contains(&nanos) {
+ return Err(Error::invalid_data(format!(
+ "duration nanoseconds {} out of valid range [-999_999_999,
999_999_999]",
+ nanos
+ )));
+ }
+ if nanos < 0 {
+ let seconds = seconds.checked_sub(1).ok_or_else(|| {
+ Error::invalid_data(
+ "duration seconds underflow while normalizing negative
nanoseconds",
+ )
+ })?;
+ return Ok(Self {
+ seconds,
+ nanos: (nanos + NANOS_PER_SECOND) as u32,
+ });
+ }
+ Ok(Self {
+ seconds,
+ nanos: nanos as u32,
+ })
+ }
+
+ /// Creates a duration from normalized seconds and nanoseconds.
+ #[inline(always)]
+ pub fn from_normalized(seconds: i64, nanos: u32) -> Result<Self, Error> {
+ if nanos >= NANOS_PER_SECOND as u32 {
+ return Err(Error::invalid_data(format!(
+ "duration nanoseconds {} out of valid range [0, 999_999_999]",
+ nanos
+ )));
+ }
+ Ok(Self { seconds, nanos })
+ }
+
+ /// Creates a duration from seconds and an arbitrary nanosecond adjustment.
+ #[inline(always)]
+ pub fn from_parts(seconds: i64, nanos: i64) -> Result<Self, Error> {
+ Self::from_nanos(i128::from(seconds) * NANOS_PER_SECOND_I128 +
i128::from(nanos))
+ }
+
+ /// Creates a duration from whole seconds.
+ #[inline(always)]
+ pub const fn from_secs(seconds: i64) -> Self {
+ Self { seconds, nanos: 0 }
+ }
+
+ /// Creates a duration from minutes.
+ #[inline(always)]
+ pub fn from_minutes(minutes: i64) -> Result<Self, Error> {
+ checked_duration_seconds(minutes, SECONDS_PER_MINUTE, "minutes")
+ }
+
+ /// Creates a duration from hours.
+ #[inline(always)]
+ pub fn from_hours(hours: i64) -> Result<Self, Error> {
+ checked_duration_seconds(hours, SECONDS_PER_HOUR, "hours")
+ }
+
+ /// Creates a duration from days.
+ #[inline(always)]
+ pub fn from_days(days: i64) -> Result<Self, Error> {
+ checked_duration_seconds(days, SECONDS_PER_DAY, "days")
+ }
+
+ /// Creates a duration from milliseconds.
+ #[inline(always)]
+ pub fn from_millis(millis: i64) -> Self {
+ let seconds = millis.div_euclid(MILLIS_PER_SECOND);
+ let millis = millis.rem_euclid(MILLIS_PER_SECOND);
+ Self {
+ seconds,
+ nanos: (millis as u32) * 1_000_000,
+ }
+ }
+
+ /// Creates a duration from microseconds.
+ #[inline(always)]
+ pub fn from_micros(micros: i64) -> Self {
+ let seconds = micros.div_euclid(MICROS_PER_SECOND);
+ let micros = micros.rem_euclid(MICROS_PER_SECOND);
+ Self {
+ seconds,
+ nanos: (micros as u32) * 1_000,
+ }
+ }
+
+ /// Creates a duration from nanoseconds.
+ #[inline(always)]
+ pub fn from_nanos(nanos: i128) -> Result<Self, Error> {
+ let (seconds, nanos) = split_total_nanos(nanos, "duration")?;
+ Ok(Self { seconds, nanos })
+ }
+
+ /// Returns the normalized seconds component.
+ #[inline(always)]
+ pub const fn seconds(self) -> i64 {
+ self.seconds
+ }
+
+ /// Returns the normalized nanosecond component.
+ #[inline(always)]
+ pub const fn subsec_nanos(self) -> u32 {
+ self.nanos
+ }
+
+ /// Returns whether this duration is zero.
+ #[inline(always)]
+ pub const fn is_zero(self) -> bool {
+ self.seconds == 0 && self.nanos == 0
+ }
+
+ /// Returns whether this duration is greater than zero.
+ #[inline(always)]
+ pub const fn is_positive(self) -> bool {
+ self.seconds > 0 || (self.seconds == 0 && self.nanos > 0)
+ }
+
+ /// Returns whether this duration is less than zero.
+ #[inline(always)]
+ pub const fn is_negative(self) -> bool {
+ self.seconds < 0
+ }
+
+ /// Returns whole milliseconds, rounded down for sub-millisecond values.
+ #[inline(always)]
+ pub fn to_millis(self) -> Result<i64, Error> {
+ total_unit(self.to_nanos(), NANOS_PER_MILLI, "duration")
+ }
+
+ /// Returns whole microseconds, rounded down for sub-microsecond values.
+ #[inline(always)]
+ pub fn to_micros(self) -> Result<i64, Error> {
+ total_unit(self.to_nanos(), NANOS_PER_MICRO, "duration")
+ }
+
+ /// Returns nanoseconds.
+ #[inline(always)]
+ pub fn to_nanos(self) -> i128 {
+ i128::from(self.seconds) * NANOS_PER_SECOND_I128 +
i128::from(self.nanos)
+ }
+
+ /// Adds two durations.
+ #[inline(always)]
+ pub fn checked_add(self, other: Self) -> Result<Self, Error> {
+ Self::from_nanos(self.to_nanos() + other.to_nanos())
+ }
+
+ /// Subtracts `other` from this duration.
+ #[inline(always)]
+ pub fn checked_sub(self, other: Self) -> Result<Self, Error> {
+ Self::from_nanos(self.to_nanos() - other.to_nanos())
+ }
+
+ /// Negates this duration.
+ #[inline(always)]
+ pub fn checked_neg(self) -> Result<Self, Error> {
+ Self::from_nanos(-self.to_nanos())
+ }
+
+ /// Returns the absolute duration.
+ #[inline(always)]
+ pub fn abs(self) -> Result<Self, Error> {
+ if self.is_negative() {
+ self.checked_neg()
+ } else {
+ Ok(self)
+ }
+ }
+}
+
+#[inline(always)]
+fn split_total_nanos(nanos: i128, value_name: &str) -> Result<(i64, u32),
Error> {
+ let total_nanos = nanos;
+ let seconds = total_nanos.div_euclid(NANOS_PER_SECOND_I128);
+ let nanos = total_nanos.rem_euclid(NANOS_PER_SECOND_I128) as u32;
+ let seconds = i64::try_from(seconds).map_err(|_| {
+ Error::invalid_data(format!(
+ "{} nanoseconds {} out of supported seconds range",
+ value_name, total_nanos
+ ))
+ })?;
+ Ok((seconds, nanos))
+}
+
+#[inline(always)]
+fn total_unit(nanos: i128, nanos_per_unit: i128, value_name: &str) ->
Result<i64, Error> {
+ let units = nanos.div_euclid(nanos_per_unit);
+ i64::try_from(units).map_err(|_| {
+ Error::invalid_data(format!(
+ "{} nanoseconds {} overflow conversion to requested unit",
+ value_name, nanos
+ ))
+ })
+}
+
+#[inline(always)]
+fn checked_duration_seconds(value: i64, multiplier: i64, unit: &str) ->
Result<Duration, Error> {
+ value
+ .checked_mul(multiplier)
+ .map(Duration::from_secs)
+ .ok_or_else(|| {
+ Error::invalid_data(format!(
+ "duration {} {} overflow second conversion",
+ value, unit
+ ))
+ })
+}
+
+impl TryFrom<std::time::Duration> for Duration {
+ type Error = Error;
+
+ #[inline(always)]
+ fn try_from(value: std::time::Duration) -> Result<Self, Self::Error> {
+ let seconds = i64::try_from(value.as_secs()).map_err(|_| {
+ Error::invalid_data(format!(
+ "std::time::Duration seconds {} exceed Fory duration range",
+ value.as_secs()
+ ))
+ })?;
+ Self::from_normalized(seconds, value.subsec_nanos())
+ }
+}
+
+impl TryFrom<Duration> for std::time::Duration {
+ type Error = Error;
+
+ #[inline(always)]
+ fn try_from(value: Duration) -> Result<Self, Self::Error> {
+ if value.is_negative() {
+ return Err(Error::invalid_data(format!(
+ "negative Fory duration {:?} cannot convert to
std::time::Duration",
+ value
+ )));
+ }
+ Ok(std::time::Duration::new(
+ u64::try_from(value.seconds()).map_err(|_| {
+ Error::invalid_data(format!(
+ "duration seconds {} exceed std::time::Duration range",
+ value.seconds()
+ ))
+ })?,
+ value.subsec_nanos(),
+ ))
+ }
+}
+
+impl TryFrom<std::time::SystemTime> for Timestamp {
+ type Error = Error;
+
+ #[inline(always)]
+ fn try_from(value: std::time::SystemTime) -> Result<Self, Self::Error> {
+ match value.duration_since(std::time::UNIX_EPOCH) {
+ Ok(duration) => {
+ let seconds = i64::try_from(duration.as_secs()).map_err(|_| {
+ Error::invalid_data(format!(
+ "SystemTime seconds {} exceed Fory timestamp range",
+ duration.as_secs()
+ ))
+ })?;
+ Self::new(seconds, duration.subsec_nanos())
+ }
+ Err(error) => {
+ let duration = error.duration();
+ let nanos = i128::from(duration.as_secs()) *
NANOS_PER_SECOND_I128
+ + i128::from(duration.subsec_nanos());
+ Self::from_epoch_nanos(-nanos)
+ }
+ }
+ }
+}
+
+impl TryFrom<Timestamp> for std::time::SystemTime {
+ type Error = Error;
+
+ #[inline(always)]
+ fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
+ let nanos = value.to_epoch_nanos();
+ if nanos >= 0 {
+ std::time::UNIX_EPOCH
+ .checked_add(std_duration_from_nanos(nanos)?)
+ .ok_or_else(|| {
+ Error::invalid_data(format!("timestamp {:?} exceeds
SystemTime range", value))
+ })
+ } else {
+ std::time::UNIX_EPOCH
+ .checked_sub(std_duration_from_nanos(-nanos)?)
+ .ok_or_else(|| {
+ Error::invalid_data(format!("timestamp {:?} exceeds
SystemTime range", value))
+ })
+ }
+ }
+}
+
+#[inline(always)]
+fn std_duration_from_nanos(nanos: i128) -> Result<std::time::Duration, Error> {
+ debug_assert!(nanos >= 0);
+ let total_nanos = nanos;
+ let seconds = total_nanos / NANOS_PER_SECOND_I128;
+ let nanos = (total_nanos % NANOS_PER_SECOND_I128) as u32;
+ let seconds = u64::try_from(seconds).map_err(|_| {
+ Error::invalid_data(format!(
+ "nanoseconds {} exceed std::time::Duration range",
+ total_nanos
+ ))
+ })?;
+ Ok(std::time::Duration::new(seconds, nanos))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn date_day_api() {
+ let date = Date::from_epoch_days(19_782);
+ assert_eq!(date.epoch_days(), 19_782);
+ assert_eq!(
+ date.checked_add_days(1).unwrap(),
+ Date::from_epoch_days(19_783)
+ );
+ assert_eq!(
+ date.checked_sub_days(1).unwrap(),
+ Date::from_epoch_days(19_781)
+ );
+ assert_eq!(
+ date.days_since(Date::UNIX_EPOCH).unwrap(),
+ date.epoch_days()
+ );
+ assert!(Date::from_epoch_days(i64::MAX).checked_add_days(1).is_err());
+ }
+
+ #[test]
+ fn timestamp_epoch_api() {
+ let millis = Timestamp::from_epoch_millis(-1);
+ assert_eq!(millis.seconds(), -1);
+ assert_eq!(millis.subsec_nanos(), 999_000_000);
+ assert_eq!(millis.to_epoch_millis().unwrap(), -1);
+
+ let nanos = Timestamp::from_epoch_nanos(-1).unwrap();
+ assert_eq!(nanos.seconds(), -1);
+ assert_eq!(nanos.subsec_nanos(), 999_999_999);
+ assert_eq!(nanos.to_epoch_nanos(), -1);
+ assert_eq!(nanos.to_epoch_micros().unwrap(), -1);
+
+ let duration = Duration::from_micros(1);
+ let timestamp = Timestamp::UNIX_EPOCH
+ .checked_add_duration(duration)
+ .unwrap();
+ assert_eq!(timestamp.to_epoch_micros().unwrap(), 1);
+ assert_eq!(
+ timestamp.duration_since(Timestamp::UNIX_EPOCH).unwrap(),
+ duration
+ );
+ assert_eq!(
+ Timestamp::UNIX_EPOCH
+ .checked_sub_duration(Duration::from_nanos(1).unwrap())
+ .unwrap(),
+ nanos
+ );
+
+ assert_eq!(Timestamp::from_epoch_seconds(10).seconds(), 10);
+ }
+
+ #[test]
+ fn duration_api() {
+ let nanos = Duration::from_nanos(-1).unwrap();
+ assert_eq!(nanos.seconds(), -1);
+ assert_eq!(nanos.subsec_nanos(), 999_999_999);
+ assert!(nanos.is_negative());
+ assert_eq!(nanos.to_nanos(), -1);
+ assert_eq!(nanos.to_micros().unwrap(), -1);
+
+ let duration = Duration::from_parts(1, 1_500_000_000).unwrap();
+ assert_eq!(duration, Duration::from_normalized(2,
500_000_000).unwrap());
+ assert_eq!(
+ Duration::from_millis(-1),
+ Duration::from_normalized(-1, 999_000_000).unwrap()
+ );
+ assert_eq!(
+ nanos.checked_neg().unwrap(),
+ Duration::from_normalized(0, 1).unwrap()
+ );
+ assert_eq!(
+ Duration::from_secs(1)
+ .checked_add(Duration::from_millis(500))
+ .unwrap(),
+ Duration::from_normalized(1, 500_000_000).unwrap()
+ );
+ assert_eq!(Duration::from_minutes(2).unwrap(),
Duration::from_secs(120));
+ assert_eq!(Duration::from_hours(2).unwrap(),
Duration::from_secs(7_200));
+ assert_eq!(
+ Duration::from_days(2).unwrap(),
+ Duration::from_secs(172_800)
+ );
+ assert_eq!(Duration::ZERO.abs().unwrap(), Duration::ZERO);
+ }
+
+ #[test]
+ fn std_time_conversions() {
+ let std_duration = std::time::Duration::new(3, 4);
+ let duration = Duration::try_from(std_duration).unwrap();
+ assert_eq!(duration, Duration::from_normalized(3, 4).unwrap());
+ let roundtrip: std::time::Duration = duration.try_into().unwrap();
+ assert_eq!(roundtrip, std_duration);
+
assert!(std::time::Duration::try_from(Duration::from_nanos(-1).unwrap()).is_err());
+
+ let before_epoch = Timestamp::from_epoch_nanos(-1).unwrap();
+ let system_time: std::time::SystemTime =
before_epoch.try_into().unwrap();
+ assert_eq!(Timestamp::try_from(system_time).unwrap(), before_epoch);
+
+ let after_epoch = Timestamp::from_epoch_nanos(1_500_000_001).unwrap();
+ let system_time: std::time::SystemTime =
after_epoch.try_into().unwrap();
+ assert_eq!(Timestamp::try_from(system_time).unwrap(), after_epoch);
+ }
+
+ #[cfg(feature = "chrono")]
+ #[test]
+ fn chrono_utc_conversion() {
+ use chrono::{DateTime, Utc};
+
+ let datetime = DateTime::<Utc>::from_timestamp(1, 2).unwrap();
+ let timestamp = Timestamp::from(datetime);
+ assert_eq!(timestamp, Timestamp::new(1, 2).unwrap());
+ let roundtrip: DateTime<Utc> = timestamp.try_into().unwrap();
+ assert_eq!(roundtrip, datetime);
+ }
+}
+
+#[cfg(feature = "chrono")]
+mod chrono_support {
+ use super::{Date, Duration, Timestamp};
+ use crate::error::Error;
+ use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeDelta, Utc};
+
+ fn epoch() -> NaiveDate {
+ NaiveDate::from_ymd_opt(1970, 1, 1).expect("1970-01-01 is a valid
chrono date")
+ }
+
+ impl From<NaiveDate> for Date {
+ #[inline(always)]
+ fn from(value: NaiveDate) -> Self {
+
Self::from_epoch_days(value.signed_duration_since(epoch()).num_days())
+ }
+ }
+
+ impl TryFrom<Date> for NaiveDate {
+ type Error = Error;
+
+ #[inline(always)]
+ fn try_from(value: Date) -> Result<Self, Self::Error> {
+ let duration =
TimeDelta::try_days(value.epoch_days()).ok_or_else(|| {
+ Error::invalid_data(format!(
+ "date day count {} is out of chrono::TimeDelta range",
+ value.epoch_days()
+ ))
+ })?;
+ epoch().checked_add_signed(duration).ok_or_else(|| {
+ Error::invalid_data(format!(
+ "date day count {} is out of chrono::NaiveDate range",
+ value.epoch_days()
+ ))
+ })
+ }
+ }
+
+ impl From<NaiveDateTime> for Timestamp {
+ #[inline(always)]
+ fn from(value: NaiveDateTime) -> Self {
+ let value = value.and_utc();
+ Self {
+ seconds: value.timestamp(),
+ nanos: value.timestamp_subsec_nanos(),
+ }
+ }
+ }
+
+ impl From<DateTime<Utc>> for Timestamp {
+ #[inline(always)]
+ fn from(value: DateTime<Utc>) -> Self {
+ Self {
+ seconds: value.timestamp(),
+ nanos: value.timestamp_subsec_nanos(),
+ }
+ }
+ }
+
+ impl TryFrom<Timestamp> for DateTime<Utc> {
+ type Error = Error;
+
+ #[inline(always)]
+ fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
+ DateTime::from_timestamp(value.seconds(),
value.subsec_nanos()).ok_or_else(|| {
+ Error::invalid_data(format!(
+ "timestamp seconds {} nanoseconds {} is out of
chrono::DateTime<Utc> range",
+ value.seconds(),
+ value.subsec_nanos()
+ ))
+ })
+ }
+ }
+
+ impl TryFrom<Timestamp> for NaiveDateTime {
+ type Error = Error;
+
+ #[inline(always)]
+ fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
+ DateTime::from_timestamp(value.seconds(), value.subsec_nanos())
+ .map(|value| value.naive_utc())
+ .ok_or_else(|| {
+ Error::invalid_data(format!(
+ "timestamp seconds {} nanoseconds {} is out of
chrono::NaiveDateTime range",
+ value.seconds(),
+ value.subsec_nanos()
+ ))
+ })
+ }
+ }
+
+ impl TryFrom<chrono::Duration> for Duration {
+ type Error = Error;
+
+ #[inline(always)]
+ fn try_from(value: chrono::Duration) -> Result<Self, Self::Error> {
+ Self::new(value.num_seconds(), value.subsec_nanos())
+ }
+ }
+
+ impl TryFrom<Duration> for chrono::Duration {
+ type Error = Error;
+
+ #[inline(always)]
+ fn try_from(value: Duration) -> Result<Self, Self::Error> {
+ chrono::Duration::try_seconds(value.seconds())
+ .and_then(|duration| {
+
duration.checked_add(&chrono::Duration::nanoseconds(i64::from(
+ value.subsec_nanos(),
+ )))
+ })
+ .ok_or_else(|| {
+ Error::invalid_data(format!(
+ "duration seconds {} out of chrono::Duration valid
range",
+ value.seconds()
+ ))
+ })
+ }
+ }
+}
diff --git a/rust/fory-core/src/util/mod.rs b/rust/fory-core/src/util/mod.rs
index 85d3be0dc..0d36492c0 100644
--- a/rust/fory-core/src/util/mod.rs
+++ b/rust/fory-core/src/util/mod.rs
@@ -24,15 +24,6 @@ pub use string_util::{
};
pub use sync::{Spinlock, SpinlockGuard};
-use chrono::NaiveDate;
-
-pub const EPOCH: NaiveDate = match NaiveDate::from_ymd_opt(1970, 1, 1) {
- None => {
- panic!("Unreachable code")
- }
- Some(epoch) => epoch,
-};
-
/// Set `ENABLE_FORY_DEBUG_OUTPUT=1` at compile time to enable debug output.
#[allow(unexpected_cfgs)]
pub const ENABLE_FORY_DEBUG_OUTPUT: bool = cfg!(fory_debug_output);
diff --git a/rust/fory-derive/Cargo.toml b/rust/fory-derive/Cargo.toml
index 1c0c9067e..15b4c47b7 100644
--- a/rust/fory-derive/Cargo.toml
+++ b/rust/fory-derive/Cargo.toml
@@ -40,8 +40,8 @@ syn = { default-features = false, version = "2.0", features =
[
"printing",
] }
quote = { default-features = false, version = "1.0" }
-thiserror = { default-features = false, version = "1.0" }
[features]
default = ["fields-loop-unroll"]
-fields-loop-unroll = []
\ No newline at end of file
+fields-loop-unroll = []
+tests = []
diff --git a/rust/fory-derive/src/lib.rs b/rust/fory-derive/src/lib.rs
index f8a726ef1..15ff08df7 100644
--- a/rust/fory-derive/src/lib.rs
+++ b/rust/fory-derive/src/lib.rs
@@ -133,8 +133,10 @@
//! - `Option<T>` for nullable values
//!
//! **Date/Time:**
-//! - `chrono::NaiveDate`
-//! - `chrono::NaiveDateTime`
+//! - `fory::Date`
+//! - `fory::Timestamp`
+//! - `fory::Duration`
+//! - `chrono::NaiveDate`, `chrono::NaiveDateTime`, and `chrono::Duration`
when the `chrono` feature is enabled
//!
//! **Custom Types:**
//! - Any type that implements `Serializer` (for `Fory`) or `Row` (for
`ForyRow`)
diff --git a/rust/fory-derive/src/object/util.rs
b/rust/fory-derive/src/object/util.rs
index 6239d5bc4..d7593fc6e 100644
--- a/rust/fory-derive/src/object/util.rs
+++ b/rust/fory-derive/src/object/util.rs
@@ -294,9 +294,11 @@ pub(crate) fn get_type_id_by_name(ty: &str) -> u32 {
// Check internal types
match unqualified_ty {
"String" => return TypeId::STRING as u32,
+ "Date" => return TypeId::DATE as u32,
+ "Timestamp" => return TypeId::TIMESTAMP as u32,
+ "Duration" => return TypeId::DURATION as u32,
"NaiveDate" => return TypeId::DATE as u32,
"NaiveDateTime" => return TypeId::TIMESTAMP as u32,
- "Duration" => return TypeId::DURATION as u32,
"Decimal" => return TypeId::DECIMAL as u32,
"bytes" => return TypeId::BINARY as u32,
_ => {}
diff --git a/rust/fory/Cargo.toml b/rust/fory/Cargo.toml
index a8c2645b9..52782f3b8 100644
--- a/rust/fory/Cargo.toml
+++ b/rust/fory/Cargo.toml
@@ -31,3 +31,8 @@ description = "Apache Fory: Blazingly fast multi-language
serialization framewor
[dependencies]
fory-core.workspace = true
fory-derive.workspace = true
+
+[features]
+default = []
+chrono = ["fory-core/chrono"]
+tests = []
diff --git a/rust/fory/src/lib.rs b/rust/fory/src/lib.rs
index af9e019fd..b8714853c 100644
--- a/rust/fory/src/lib.rs
+++ b/rust/fory/src/lib.rs
@@ -979,10 +979,12 @@
//! - `RefCell<T>` - Interior mutability with runtime borrow checking
//! - `Mutex<T>` - Thread-safe interior mutability
//!
-//! ### Date and Time (requires `chrono` feature)
+//! ### Date and Time
//!
-//! - `chrono::NaiveDate` - Date without timezone
-//! - `chrono::NaiveDateTime` - Timestamp without timezone
+//! - `Date` - Date without timezone, with epoch-day accessors and checked day
arithmetic
+//! - `Timestamp` - Point in time, with epoch unit conversions and checked
duration arithmetic
+//! - `Duration` - Signed duration, with normalized parts, total unit
conversions, and checked arithmetic
+//! - `chrono::NaiveDate`, `chrono::NaiveDateTime`, and `chrono::Duration`
when the `chrono` feature is enabled
//!
//! ### Custom Types
//!
@@ -1202,7 +1204,7 @@
pub use fory_core::{
error::Error, fory::Fory, fory::ForyBuilder, register_trait_type,
row::from_row, row::to_row,
- ArcWeak, BFloat16, Decimal, Float16, ForyDefault, RcWeak, ReadContext,
Reader, RefFlag,
- RefMode, Serializer, TypeId, TypeResolver, WriteContext, Writer,
+ ArcWeak, BFloat16, Date, Decimal, Duration, Float16, ForyDefault, RcWeak,
ReadContext, Reader,
+ RefFlag, RefMode, Serializer, Timestamp, TypeId, TypeResolver,
WriteContext, Writer,
};
pub use fory_derive::{ForyEnum, ForyRow, ForyStruct, ForyUnion};
diff --git a/rust/tests/Cargo.toml b/rust/tests/Cargo.toml
index 59e854670..c2f2fdc25 100644
--- a/rust/tests/Cargo.toml
+++ b/rust/tests/Cargo.toml
@@ -26,5 +26,9 @@ publish = false
fory-core = { path = "../fory-core" }
fory-derive = { path = "../fory-derive" }
-chrono = "0.4"
num-bigint = "0.4"
+
+[features]
+default = []
+chrono = ["fory-core/chrono"]
+tests = []
diff --git a/rust/tests/tests/compatible/test_basic_type.rs
b/rust/tests/tests/compatible/test_basic_type.rs
index d1e124411..5ebbe33a1 100644
--- a/rust/tests/tests/compatible/test_basic_type.rs
+++ b/rust/tests/tests/compatible/test_basic_type.rs
@@ -15,8 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-use chrono::{NaiveDate, NaiveDateTime};
-use fory_core::{fory::Fory, Reader};
+use fory_core::{fory::Fory, Date, Reader, Timestamp};
// primitive_val
const BOOL_VAL: bool = true;
@@ -29,10 +28,13 @@ const F64_VAL: f64 = 47.0;
// string
const STR_LATIN1_VAL: &str = "Çüéâäàåçêëèïî";
// time
-#[allow(deprecated)]
-const TIMESTAMP_VAL: NaiveDateTime = NaiveDateTime::from_timestamp(100, 0);
-#[allow(deprecated)]
-const LOCAL_DATE_VAL: NaiveDate = NaiveDate::from_ymd(2021, 11, 23);
+fn timestamp_val() -> Timestamp {
+ Timestamp::new(100, 0).unwrap()
+}
+
+fn local_date_val() -> Date {
+ Date::from_epoch_days(18_954)
+}
const BOOL_ARRAY: [bool; 1] = [true];
const INT8_ARRAY: [i8; 1] = [48];
@@ -53,8 +55,8 @@ fn serialize_non_null(fory: &Fory) -> Vec<u8> {
fory.serialize_to(&mut buf, &F64_VAL).unwrap();
fory.serialize_to(&mut buf, &STR_LATIN1_VAL.to_string())
.unwrap();
- fory.serialize_to(&mut buf, &LOCAL_DATE_VAL).unwrap();
- fory.serialize_to(&mut buf, &TIMESTAMP_VAL).unwrap();
+ fory.serialize_to(&mut buf, &local_date_val()).unwrap();
+ fory.serialize_to(&mut buf, ×tamp_val()).unwrap();
fory.serialize_to(&mut buf, &BOOL_ARRAY.to_vec()).unwrap();
fory.serialize_to(&mut buf, &INT8_ARRAY.to_vec()).unwrap();
fory.serialize_to(&mut buf, &INT16_ARRAY.to_vec()).unwrap();
@@ -78,8 +80,9 @@ fn serialize_nullable(fory: &Fory) -> Vec<u8> {
fory.serialize_to(&mut buf, &Some(F64_VAL)).unwrap();
fory.serialize_to(&mut buf, &Some(STR_LATIN1_VAL.to_string()))
.unwrap();
- fory.serialize_to(&mut buf, &Some(LOCAL_DATE_VAL)).unwrap();
- fory.serialize_to(&mut buf, &Some(TIMESTAMP_VAL)).unwrap();
+ fory.serialize_to(&mut buf, &Some(local_date_val()))
+ .unwrap();
+ fory.serialize_to(&mut buf, &Some(timestamp_val())).unwrap();
fory.serialize_to(&mut buf, &Some(BOOL_ARRAY.to_vec()))
.unwrap();
fory.serialize_to(&mut buf, &Some(INT8_ARRAY.to_vec()))
@@ -103,9 +106,8 @@ fn serialize_nullable(fory: &Fory) -> Vec<u8> {
fory.serialize_to(&mut buf, &Option::<f64>::None).unwrap();
fory.serialize_to(&mut buf, &Option::<String>::None)
.unwrap();
- fory.serialize_to(&mut buf, &Option::<NaiveDate>::None)
- .unwrap();
- fory.serialize_to(&mut buf, &Option::<NaiveDateTime>::None)
+ fory.serialize_to(&mut buf, &Option::<Date>::None).unwrap();
+ fory.serialize_to(&mut buf, &Option::<Timestamp>::None)
.unwrap();
fory.serialize_to(&mut buf, &Option::<Vec<bool>>::None)
.unwrap();
@@ -141,12 +143,12 @@ fn deserialize_non_null(fory: &Fory, bins: Vec<u8>,
auto_conv: bool) {
fory.deserialize_from::<String>(&mut reader).unwrap()
);
assert_eq!(
- LOCAL_DATE_VAL,
- fory.deserialize_from::<NaiveDate>(&mut reader).unwrap()
+ local_date_val(),
+ fory.deserialize_from::<Date>(&mut reader).unwrap()
);
assert_eq!(
- TIMESTAMP_VAL,
- fory.deserialize_from::<NaiveDateTime>(&mut reader).unwrap()
+ timestamp_val(),
+ fory.deserialize_from::<Timestamp>(&mut reader).unwrap()
);
assert_eq!(
@@ -211,12 +213,12 @@ fn deserialize_non_null(fory: &Fory, bins: Vec<u8>,
auto_conv: bool) {
fory.deserialize_from::<String>(&mut reader).unwrap()
);
assert_eq!(
- NaiveDate::default(),
- fory.deserialize_from::<NaiveDate>(&mut reader).unwrap()
+ Date::default(),
+ fory.deserialize_from::<Date>(&mut reader).unwrap()
);
assert_eq!(
- NaiveDateTime::default(),
- fory.deserialize_from::<NaiveDateTime>(&mut reader).unwrap()
+ Timestamp::default(),
+ fory.deserialize_from::<Timestamp>(&mut reader).unwrap()
);
assert_eq!(
@@ -286,13 +288,12 @@ fn deserialize_nullable(fory: &Fory, bins: Vec<u8>,
auto_conv: bool) {
.unwrap()
);
assert_eq!(
- Some(LOCAL_DATE_VAL),
- fory.deserialize_from::<Option<NaiveDate>>(&mut reader)
- .unwrap()
+ Some(local_date_val()),
+ fory.deserialize_from::<Option<Date>>(&mut reader).unwrap()
);
assert_eq!(
- Some(TIMESTAMP_VAL),
- fory.deserialize_from::<Option<NaiveDateTime>>(&mut reader)
+ Some(timestamp_val()),
+ fory.deserialize_from::<Option<Timestamp>>(&mut reader)
.unwrap()
);
assert_eq!(
@@ -366,12 +367,11 @@ fn deserialize_nullable(fory: &Fory, bins: Vec<u8>,
auto_conv: bool) {
);
assert_eq!(
None,
- fory.deserialize_from::<Option<NaiveDate>>(&mut reader)
- .unwrap()
+ fory.deserialize_from::<Option<Date>>(&mut reader).unwrap()
);
assert_eq!(
None,
- fory.deserialize_from::<Option<NaiveDateTime>>(&mut reader)
+ fory.deserialize_from::<Option<Timestamp>>(&mut reader)
.unwrap()
);
assert_eq!(
diff --git a/rust/tests/tests/test_complex_struct.rs
b/rust/tests/tests/test_complex_struct.rs
index 55e4a4f68..4e5fe5cd6 100644
--- a/rust/tests/tests/test_complex_struct.rs
+++ b/rust/tests/tests/test_complex_struct.rs
@@ -16,9 +16,9 @@
// under the License.
use fory_core::fory::Fory;
+use fory_core::{Date, Timestamp};
use fory_derive::{ForyEnum, ForyStruct};
// use std::any::Any;
-use chrono::{DateTime, NaiveDate, NaiveDateTime};
use std::collections::HashMap;
// RUSTFLAGS="-Awarnings" cargo expand -p tests --test test_complex_struct
@@ -83,8 +83,8 @@ fn complex_struct() {
// age: u16,
op: Option<String>,
op2: Option<String>,
- date: NaiveDate,
- time: NaiveDateTime,
+ date: Date,
+ time: Timestamp,
c5: f32,
c6: f64,
}
@@ -103,8 +103,8 @@ fn complex_struct() {
// age: 12,
op: Some("option".to_string()),
op2: None,
- date: NaiveDate::from_ymd_opt(2025, 12, 12).unwrap(),
- time: DateTime::from_timestamp(1689912359, 0).unwrap().naive_utc(),
+ date: Date::from_epoch_days(20_434),
+ time: Timestamp::new(1_689_912_359, 0).unwrap(),
c5: 2.0,
c6: 4.0,
};
diff --git a/rust/tests/tests/test_cross_language.rs
b/rust/tests/tests/test_cross_language.rs
index 8138878fb..1d100c322 100644
--- a/rust/tests/tests/test_cross_language.rs
+++ b/rust/tests/tests/test_cross_language.rs
@@ -15,14 +15,13 @@
// specific language governing permissions and limitations
// under the License.
-use chrono::{NaiveDate, NaiveDateTime};
use fory_core::buffer::{Reader, Writer};
use fory_core::error::Error;
use fory_core::resolver::TypeResolver;
use fory_core::serializer::{ForyDefault, Serializer};
use fory_core::type_id::TypeId;
use fory_core::util::murmurhash3_x64_128;
-use fory_core::{read_data, write_data, BFloat16, Decimal, Float16, Fory};
+use fory_core::{read_data, write_data, BFloat16, Date, Decimal, Float16, Fory,
Timestamp};
use fory_core::{ReadContext, WriteContext};
use fory_derive::{ForyEnum, ForyStruct, ForyUnion};
use num_bigint::BigInt;
@@ -116,14 +115,13 @@ fn test_buffer() {
}
#[test]
-#[allow(deprecated)]
-fn test_naive_date_uses_var_i64_day_count() {
+fn test_date_uses_var_i64_day_count() {
let fory = Fory::builder()
.xlang(true)
.compatible(false)
.track_ref(false)
.build();
- let day = NaiveDate::from_ymd_opt(1969, 12, 31).unwrap();
+ let day = Date::from_epoch_days(-1);
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &day).unwrap();
@@ -136,10 +134,9 @@ fn test_naive_date_uses_var_i64_day_count() {
}
#[test]
-#[allow(deprecated)]
-fn test_naive_date_uses_i32_day_count_in_native_mode() {
+fn test_date_uses_i32_day_count_in_native_mode() {
let fory = Fory::builder().xlang(false).track_ref(false).build();
- let day = NaiveDate::from_ymd_opt(1969, 12, 31).unwrap();
+ let day = Date::from_epoch_days(-1);
let mut buf = Vec::new();
fory.serialize_to(&mut buf, &day).unwrap();
@@ -330,10 +327,9 @@ macro_rules! assert_de {
#[test]
#[ignore]
-#[allow(deprecated)]
fn test_cross_language_serializer() {
- let day = NaiveDate::from_ymd_opt(2021, 11, 23).unwrap();
- let instant = NaiveDateTime::from_timestamp(100, 0);
+ let day = Date::from_epoch_days(18_954);
+ let instant = Timestamp::new(100, 0).unwrap();
let str_list = vec!["hello".to_string(), "world".to_string()];
let str_set = HashSet::from(["hello".to_string(), "world".to_string()]);
let str_map = HashMap::<String, String>::from([
@@ -361,8 +357,8 @@ fn test_cross_language_serializer() {
assert_de!(fory, reader, f32, -1f32);
assert_de!(fory, reader, f64, -1f64);
assert_de!(fory, reader, String, "str".to_string());
- assert_de!(fory, reader, NaiveDate, day);
- assert_de!(fory, reader, NaiveDateTime, instant);
+ assert_de!(fory, reader, Date, day);
+ assert_de!(fory, reader, Timestamp, instant);
assert_de!(fory, reader, Vec<bool>, [true, false]);
assert_de!(fory, reader, Vec<u8>, [1, i8::MAX as u8]);
assert_de!(fory, reader, Vec<i16>, [1, i16::MAX]);
diff --git a/rust/tests/tests/test_fory.rs b/rust/tests/tests/test_fory.rs
index 3b141a0ee..596ac1188 100644
--- a/rust/tests/tests/test_fory.rs
+++ b/rust/tests/tests/test_fory.rs
@@ -197,7 +197,7 @@ fn test_serialize_to_detailed() {
}
}
-use chrono::{DateTime, NaiveDateTime, Utc};
+use fory_core::Timestamp;
macro_rules! impl_value {
($record:ident, $value:ident, { $($field:ident : $ty:ty = $expr:expr),*
$(,)? }) => {
@@ -227,7 +227,7 @@ macro_rules! impl_value {
pub struct KeyValue {
feature_key: String,
count: u64,
- last_seen_event_time: DateTime<Utc>,
+ last_seen_event_time: Timestamp,
}
impl_value!(
@@ -235,7 +235,7 @@ impl_value!(
Value,
{
count: u64 = count,
- last_seen_event_time: NaiveDateTime = last_seen_event_time.naive_utc(),
+ last_seen_event_time: Timestamp = last_seen_event_time,
}
);
@@ -244,15 +244,12 @@ fn test_in_macro() {
let key_value = KeyValue {
feature_key: "test_key".to_string(),
count: 100,
- last_seen_event_time: Utc::now(),
+ last_seen_event_time: Timestamp::new(1_689_912_359, 123).unwrap(),
};
let (key, value) = key_value.clone().to_key_value();
assert_eq!(key, "test_key");
assert_eq!(value.count, 100);
- assert_eq!(
- value.last_seen_event_time,
- key_value.last_seen_event_time.naive_utc()
- );
+ assert_eq!(value.last_seen_event_time, key_value.last_seen_event_time);
}
#[test]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]