88import logging
99import re
1010from 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
154158class 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