Skip to content

Commit 62499a1

Browse files
committed
Fixes
1 parent eb11c5f commit 62499a1

File tree

1 file changed

+58
-23
lines changed

1 file changed

+58
-23
lines changed

databricks/sdk/common.py

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import logging
99
import re
1010
from datetime import datetime, timedelta, timezone
11+
from decimal import Decimal
1112

1213
_LOG = logging.getLogger("databricks.sdk")
1314

@@ -56,14 +57,15 @@ def from_timedelta(cls, td: timedelta) -> "Duration":
5657
Returns:
5758
Duration: A new Duration instance with equivalent time span
5859
59-
Note:
60-
The conversion may lose precision as timedelta only supports microsecond precision
6160
"""
62-
total_seconds = int(td.total_seconds())
63-
# Get the microseconds part and convert to nanoseconds
64-
microseconds = td.microseconds
65-
nanoseconds = microseconds * 1000
66-
return cls(seconds=total_seconds, nanoseconds=nanoseconds)
61+
# Use Decimal for precise calculation of total seconds
62+
total_seconds = Decimal(str(td.total_seconds()))
63+
seconds = int(total_seconds)
64+
# Get the fractional part and convert to nanoseconds
65+
# This preserves more precision than using microsecond * 1000
66+
fractional = total_seconds - seconds
67+
nanoseconds = int(fractional * Decimal('1000000000'))
68+
return cls(seconds=seconds, nanoseconds=nanoseconds)
6769

6870
def to_timedelta(self) -> timedelta:
6971
"""Convert Duration to datetime.timedelta.
@@ -72,9 +74,11 @@ def to_timedelta(self) -> timedelta:
7274
timedelta: A new timedelta instance with equivalent time span
7375
7476
Note:
75-
The conversion may lose precision as timedelta only supports microsecond precision
77+
The conversion will lose nanosecond precision as timedelta
78+
only supports microsecond precision. Nanoseconds beyond
79+
microsecond precision will be truncated.
7680
"""
77-
# Convert nanoseconds to microseconds for timedelta
81+
# Convert nanoseconds to microseconds, truncating any extra precision
7882
microseconds = self.nanoseconds // 1000
7983
return timedelta(seconds=self.seconds, microseconds=microseconds)
8084

@@ -121,8 +125,8 @@ def parse(cls, duration_str: str) -> "Duration":
121125
raise ValueError("Duration string must end with 's'")
122126

123127
try:
124-
# Remove the 's' suffix and convert to float
125-
value = float(duration_str[:-1])
128+
# Remove the 's' suffix and convert to Decimal
129+
value = Decimal(duration_str[:-1])
126130
# Split into integer and fractional parts
127131
seconds = int(value)
128132
# Convert fractional part to nanoseconds
@@ -145,10 +149,10 @@ def to_string(self) -> str:
145149
if self.nanoseconds == 0:
146150
return f"{self.seconds}s"
147151

148-
# Convert to decimal representation
149-
total_seconds = self.seconds + (self.nanoseconds / 1_000_000_000)
152+
# Use Decimal for precise decimal arithmetic
153+
total = Decimal(self.seconds) + (Decimal(self.nanoseconds) / Decimal('1000000000'))
150154
# Format with up to 9 decimal places, removing trailing zeros
151-
return f"{total_seconds:.9f}".rstrip("0").rstrip(".") + "s"
155+
return f"{total:.9f}".rstrip('0').rstrip('.') + 's'
152156

153157

154158
class Timestamp:
@@ -200,16 +204,25 @@ def from_datetime(cls, dt: datetime) -> "Timestamp":
200204
Timestamp: A new Timestamp instance
201205
202206
Note:
203-
The datetime is converted to UTC if it isn't already
207+
The datetime is converted to UTC if it isn't already.
208+
Note that datetime only supports microsecond precision, so nanoseconds
209+
will be padded with zeros.
204210
"""
205211
# If datetime is naive (no timezone), assume UTC
206212
if dt.tzinfo is None:
207213
dt = dt.replace(tzinfo=timezone.utc)
208214
# Convert to UTC
209215
utc_dt = dt.astimezone(timezone.utc)
210-
# Use timestamp() to get seconds since epoch
211-
seconds = int(utc_dt.timestamp())
212-
nanos = utc_dt.microsecond * 1000
216+
217+
# Get seconds since epoch using Decimal for precise calculation
218+
# datetime.timestamp() returns float, so we need to handle it carefully
219+
ts = Decimal(str(utc_dt.timestamp()))
220+
seconds = int(ts)
221+
# Get the fractional part and convert to nanoseconds
222+
# This preserves more precision than using microsecond * 1000
223+
fractional = ts - seconds
224+
nanos = int(fractional * Decimal('1000000000'))
225+
213226
return cls(seconds=seconds, nanos=nanos)
214227

215228
def to_datetime(self) -> datetime:
@@ -219,11 +232,12 @@ def to_datetime(self) -> datetime:
219232
datetime: A new datetime instance in UTC timezone
220233
221234
Note:
222-
The returned datetime will have microsecond precision at most
235+
The returned datetime will have microsecond precision at most.
236+
Nanoseconds beyond microsecond precision will be truncated.
223237
"""
224238
# Create base datetime from seconds
225239
dt = datetime.fromtimestamp(self.seconds, tz=timezone.utc)
226-
# Add nanoseconds converted to microseconds
240+
# Convert nanoseconds to microseconds, truncating any extra precision
227241
microseconds = self.nanos // 1000
228242
return dt.replace(microsecond=microseconds)
229243

@@ -253,10 +267,16 @@ def parse(cls, timestamp_str: str) -> "Timestamp":
253267

254268
# Build the datetime string with a standardized offset format
255269
dt_str = f"{year}-{month}-{day}T{hour}:{minute}:{second}"
270+
271+
# Handle fractional seconds, truncating to microseconds for fromisoformat
272+
nanos = 0
256273
if frac:
257-
# Pad or truncate to 9 digits for nanoseconds
274+
# Pad to 9 digits for nanoseconds
258275
frac = (frac + "000000000")[:9]
259-
dt_str += f".{frac}"
276+
# Truncate to 6 digits (microseconds) for fromisoformat
277+
dt_str += f".{frac[:6]}"
278+
# Store full nanosecond precision separately
279+
nanos = int(frac)
260280

261281
# Handle timezone offset
262282
if offset == "Z":
@@ -267,8 +287,10 @@ def parse(cls, timestamp_str: str) -> "Timestamp":
267287
else:
268288
dt_str += offset
269289

290+
# Parse with microsecond precision
270291
dt = datetime.fromisoformat(dt_str)
271-
return cls.from_datetime(dt)
292+
# Create timestamp with full nanosecond precision
293+
return cls.from_datetime(dt).replace(nanos=nanos)
272294

273295
def to_string(self) -> str:
274296
"""Convert Timestamp to RFC3339 formatted string.
@@ -311,3 +333,16 @@ def __eq__(self, other: object) -> bool:
311333
if not isinstance(other, Timestamp):
312334
return NotImplemented
313335
return self.seconds == other.seconds and self.nanos == other.nanos
336+
337+
def replace(self, **kwargs) -> "Timestamp":
338+
"""Create a new Timestamp with the given fields replaced.
339+
340+
Args:
341+
**kwargs: Fields to replace (seconds, nanos)
342+
343+
Returns:
344+
A new Timestamp instance with the specified fields replaced
345+
"""
346+
seconds = kwargs.get('seconds', self.seconds)
347+
nanos = kwargs.get('nanos', self.nanos)
348+
return Timestamp(seconds=seconds, nanos=nanos)

0 commit comments

Comments
 (0)