OOTD renders time deltas in human phrases that feel more intuitive than strict numeric time strings.
This repository is implemented as a Rust-first multi-binding project.
crates/ootd-core: pure Rust domain logic (between,from_duration)crates/ootd-ffi-c: C ABI layer for low-level interop (cbindgen, Java/Swift FFI path)bindings/python/rust: Python native extension crate (PyO3)bindings/node/rust: Node.js native addon (napi-rs)bindings/wasm/rust: Browser/WebAssembly binding (wasm-bindgen)bindings/python: Python package layout and tests (maturinproject)bindings/java: Java FFM wrapper and Gradle projectbindings/kotlin: Kotlin/JVM wrapper over Java FFM layerbindings/node: Node package scaffoldingbindings/wasm: wasm package scaffoldingbindings/swift: Swift package over C FFI (dlopen/dlsym)
enko
betweenrequires timezone-aware RFC3339 timestamps (Zor+/-hh:mmoffsets)- Naive datetime values are rejected by design
- Delta magnitude is computed on absolute instants (UTC-equivalent comparison)
- Daypart labels (
dawn/morning/...,새벽/아침/...) are anchored to theendtimestamp timezone offset - Mixed offsets are allowed;
startis converted to theendoffset before daypart labeling koonly: optionally enable native Korean numerals for시간and달units (1 -> 한,2 -> 두, ...)- Duration humanization uses policy buckets (not calendar-precise month/year lengths):
monthbucket basis:30dyearbucket basis:12 * 30d = 360d(policy consistency with month buckets)- first
1 yearlabel starts at350d(350d 00:00:00and later)
use ootd_core::{
between_rfc3339, between_rfc3339_with_options, from_duration, Direction, Locale, RenderOptions,
};
let phrase = between_rfc3339(
"2023-12-09T18:21:29Z",
"2024-01-25T13:31:43Z",
Locale::En,
)?;
assert_eq!(phrase, "a month and a half ago");
let mixed_en = between_rfc3339(
"2024-01-25T01:30:00+09:00",
"2024-01-25T13:00:00Z",
Locale::En,
)?;
assert_eq!(mixed_en, "yesterday afternoon");
let mixed_ko = between_rfc3339(
"2024-01-25T01:30:00+09:00",
"2024-01-25T13:00:00Z",
Locale::Ko,
)?;
assert_eq!(mixed_ko, "어제 낮");
let native_ko = between_rfc3339_with_options(
"2023-12-09T18:21:29Z",
"2024-01-25T13:31:43Z",
Locale::Ko,
RenderOptions {
ko_native_numerals: true,
},
)?;
assert_eq!(native_ko, "한 달 반 전");
let phrase = from_duration(90 * 60, Locale::Ko, Direction::Past)?;
assert_eq!(phrase, "1시간 반 전");
let err = from_duration(-1, Locale::En, Direction::Past);
assert!(err.is_err());Build/install locally:
cd bindings/python
maturin developUsage:
import ootd
from datetime import datetime, timezone, timedelta
start = datetime.now(timezone.utc) - timedelta(days=48)
end = datetime.now(timezone.utc)
print(ootd.between(start, end, "en"))
print(ootd.from_duration(90 * 60, False, "ko"))
print(ootd.from_duration(timedelta(minutes=90), False, "ko")) # timedelta 입력 허용
print(ootd.from_duration(90 * 60 + 0.9, False, "ko")) # float은 내부에서 int로 변환
print(ootd.from_duration(90 * 60, False, "ko", True)) # 한 시간 반 전
# raises ValueError: negative duration is not allowed: -1
# ootd.from_duration(-1, False, "en")Notes:
bindings/python/rust/build.rsauto-generatesbindings/python/ootd/__init__.pyiduring build.ootdis a pure-Python wrapper overootd._native, so monkeypatching is straightforward (ootd.between,ootd._between_impl, etc.).
cd bindings/node
npm install
npm run buildimport { between, fromDuration } from '@ootd/node'
// locale type: "en" | "ko"
// between input: RFC3339 string | Date | object with toISOString()
// fromDuration input: number | bigint | duration-like object(total/asSeconds/toMillis)
console.log(between('2023-12-09T18:21:29Z', '2024-01-25T13:31:43Z', 'en'))
console.log(between(new Date('2023-12-09T18:21:29Z'), new Date('2024-01-25T13:31:43Z'), 'en'))
console.log(fromDuration(90 * 60, false, 'ko'))
console.log(fromDuration({ asSeconds: () => 90 * 60 }, false, 'ko'))
console.log(fromDuration(90 * 60, false, 'ko', true)) // 한 시간 반 전
// throws Error: negative duration is not allowed: -1
// fromDuration(-1, false, 'en')Note: Date inputs are normalized via toISOString() (UTC Z). If you need a specific offset anchor for daypart labeling, pass explicit RFC3339 strings with that offset.
cd bindings/wasm
npm install
npm run buildimport { between, fromDuration } from '@ootd/wasm/pkg/ootd_wasm'
// locale type: "en" | "ko" (generated d.ts is patched after wasm build)
console.log(between('2023-12-09T18:21:29Z', '2024-01-25T13:31:43Z', 'en'))
console.log(fromDuration(90n * 60n, false, 'ko'))
console.log(fromDuration(90n * 60n, false, 'ko', true)) // 한 시간 반 전
// throws Error: negative duration is not allowed: -1
// fromDuration(-1n, false, 'en')Requires JDK 22+ (FFM/Panama target baseline).
Generate C header and optional Panama bindings:
./scripts/gen-c-header.sh
./scripts/gen-java-bindings.shBuild Java wrapper:
cd bindings/java
gradle test --no-daemonUsage:
import java.time.Duration;
import java.time.OffsetDateTime;
String phrase = Ootd.between("2023-12-09T18:21:29Z", "2024-01-25T13:31:43Z", OotdLocale.EN);
String phraseFromDateTime = Ootd.between(
OffsetDateTime.parse("2023-12-09T18:21:29Z"),
OffsetDateTime.parse("2024-01-25T13:31:43Z"),
OotdLocale.EN
);
String nativeKo = Ootd.between("2023-12-09T18:21:29Z", "2024-01-25T13:31:43Z", OotdLocale.KO, true);
String fromDurationObject = Ootd.fromDuration(Duration.ofMinutes(90), false, OotdLocale.EN);
// throws IllegalArgumentException for negative duration
// Ootd.fromDuration(-1, false, OotdLocale.EN);Requires JDK 22+ (toolchain/jvmTarget baseline).
Build/test:
cd bindings/kotlin
gradle test --no-daemonUsage:
import io.ootd.OotdLocale
import io.ootd.kotlin.OotdKotlin
import java.time.Duration
println(OotdKotlin.between("2023-12-09T18:21:29Z", "2024-01-25T13:31:43Z", OotdLocale.EN))
println(OotdKotlin.fromDuration(Duration.ofMinutes(90), false, OotdLocale.KO, true))Build native library first:
cargo build -p ootd-ffi-c
cd bindings/swift
swift run ootd-parityUsage:
import OOTD
let phrase = try OOTD.between(
startRFC3339: "2023-12-09T18:21:29Z",
endRFC3339: "2024-01-25T13:31:43Z",
locale: .en
)
let ko = try OOTD.fromDuration(
seconds: 90 * 60,
isFuture: false,
locale: .ko,
useNativeKoNumber: true
)- C header generation:
cbindgen(cbindgen.toml) - Java binding generation:
jextract(from generatedinclude/ootd.h) - Shared parity fixtures:
tests/parity_cases.json(between_cases,duration_cases)
GitHub Actions runs Rust checks/tests and validates multi-binding build commands.
LGPL-3.0 (see LICENSE.txt)