Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions src/surrealdb/data/types/duration.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
from dataclasses import dataclass
from math import floor
from typing import Union
import re

Check failure on line 4 in src/surrealdb/data/types/duration.py

View workflow job for this annotation

GitHub Actions / Check format

Ruff (I001)

src/surrealdb/data/types/duration.py:1:1: I001 Import block is un-sorted or un-formatted

UNITS = {
"ns": 1,
"us": int(1e3),
"µs": int(1e3), # Microsecond (µ symbol)
"us": int(1e3), # Microsecond (us)
"ms": int(1e6),
"s": int(1e9),
"m": int(60 * 1e9),
"h": int(3600 * 1e9),
"d": int(86400 * 1e9),
"w": int(604800 * 1e9),
"y": int(365 * 86400 * 1e9), # Year (365 days)
}


Expand All @@ -23,18 +26,21 @@
if isinstance(value, int):
return Duration(nanoseconds + value * UNITS["s"])
else:
# Check for multi-character units first
for unit in ["ns", "us", "ms"]:
if value.endswith(unit):
num = int(value[: -len(unit)])
return Duration(num * UNITS[unit])
# Check for single-character units
unit = value[-1]
num = int(value[:-1])
if unit in UNITS:
return Duration(num * UNITS[unit])
else:
raise ValueError(f"Unknown duration unit: {unit}")
# Support compound durations: "1h30m", "2d3h15m", etc.
pattern = r'(\d+)(ns|µs|us|ms|[smhdwy])'
matches = re.findall(pattern, value.lower())

if not matches:
raise ValueError(f"Invalid duration format: {value}")

total_ns = nanoseconds
for num_str, unit in matches:
num = int(num_str)
if unit not in UNITS:
raise ValueError(f"Unknown duration unit: {unit}")
total_ns += num * UNITS[unit]

return Duration(total_ns)

def get_seconds_and_nano(self) -> tuple[int, int]:
sec = floor(self.elapsed / UNITS["s"])
Expand Down Expand Up @@ -78,8 +84,12 @@
def weeks(self) -> int:
return self.elapsed // UNITS["w"]

@property
def years(self) -> int:
return self.elapsed // UNITS["y"]

def to_string(self) -> str:
for unit in ["w", "d", "h", "m", "s", "ms", "us", "ns"]:
for unit in ["y", "w", "d", "h", "m", "s", "ms", "us", "ns"]:
value = self.elapsed // UNITS[unit]
if value > 0:
return f"{value}{unit}"
Expand Down
35 changes: 32 additions & 3 deletions tests/unit_tests/data_types/test_duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,43 @@ def test_duration_parse_str_weeks() -> None:
assert duration.elapsed == 604800 * 1_000_000_000


def test_duration_parse_str_years() -> None:
"""Test Duration.parse with string input in years."""
duration = Duration.parse("2y")
assert duration.elapsed == 2 * 365 * 86400 * 1_000_000_000


def test_duration_parse_str_milliseconds() -> None:
"""Test Duration.parse with string input in milliseconds."""
duration = Duration.parse("500ms")
assert duration.elapsed == 500 * 1_000_000


def test_duration_parse_str_microseconds() -> None:
"""Test Duration.parse with string input in microseconds."""
duration = Duration.parse("100us")
assert duration.elapsed == 100 * 1_000
"""Test Duration.parse with string input in microseconds (both us and µs variants)."""
duration_us = Duration.parse("100us")
duration_mu = Duration.parse("100µs")

# Both should equal 100 microseconds in nanoseconds
assert duration_us.elapsed == 100 * 1_000
assert duration_mu.elapsed == 100 * 1_000

# Both variants should produce identical results
assert duration_us.elapsed == duration_mu.elapsed


def test_duration_parse_str_compound() -> None:
"""Test Duration.parse with comprehensive compound duration including all units."""
duration = Duration.parse("1y2w3d4h5m6s7ms8us9ns")
assert duration.elapsed == (1 * 365 * 86400 * 1_000_000_000) \
+ (2 * 604800 * 1_000_000_000) \
+ (3 * 86400 * 1_000_000_000) \
+ (4 * 3600 * 1_000_000_000) \
+ (5 * 60 * 1_000_000_000) \
+ (6 * 1_000_000_000) \
+ (7 * 1_000_000) \
+ (8 * 1_000) \
+ 9


def test_duration_parse_str_nanoseconds() -> None:
Expand Down Expand Up @@ -115,6 +142,7 @@ def test_duration_properties() -> None:
assert duration.hours == total_ns // (3600 * 1_000_000_000)
assert duration.days == total_ns // (86400 * 1_000_000_000)
assert duration.weeks == total_ns // (604800 * 1_000_000_000)
assert duration.years == total_ns // (365 * 86400 * 1_000_000_000)


def test_duration_to_string() -> None:
Expand All @@ -128,6 +156,7 @@ def test_duration_to_string() -> None:
assert Duration(3600 * 1_000_000_000).to_string() == "1h"
assert Duration(86400 * 1_000_000_000).to_string() == "1d"
assert Duration(604800 * 1_000_000_000).to_string() == "1w"
assert Duration(365 * 86400 * 1_000_000_000).to_string() == "1y"

# Test compound duration (should use largest unit)
compound = Duration(3600 * 1_000_000_000 + 30 * 60 * 1_000_000_000) # 1h30m
Expand Down
Loading