This is an automated email from the ASF dual-hosted git repository.

claudevdm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 464db0171e9 [Python] Round float seconds to the nearest microsecond in 
Duration (#40236)
464db0171e9 is described below

commit 464db0171e93f8eb10d9270e22872baeccfd47a0
Author: Anish Mehta <[email protected]>
AuthorDate: Sun Oct 4 22:24:21 2026 +0530

    [Python] Round float seconds to the nearest microsecond in Duration (#40236)
---
 CHANGES.md                                      |  1 +
 sdks/python/apache_beam/utils/timestamp.py      |  5 ++++-
 sdks/python/apache_beam/utils/timestamp_test.py | 10 ++++++++++
 3 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/CHANGES.md b/CHANGES.md
index 788ffd08154..dbe5a054455 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -89,6 +89,7 @@
 * (Java) BigQueryIO now treats a 404 when deleting a temporary table or 
dataset as success, so a replayed work item whose earlier attempt already 
deleted it no longer retries forever 
([#24997](https://github.com/apache/beam/issues/24997)).
 * (Java) IcebergIO now writes rows containing `EnumerationType` (proto enum) 
fields as strings, instead of throwing `Unsupported Beam logical type Enum` 
([#40299](https://github.com/apache/beam/issues/40299)).
 * (Python) Fixed stateful DoFns with side inputs sometimes taking the timer 
key coder from a side input instead of the main input, which could make the 
worker fail to decode timer keys with `Unknown type tag` 
([#40374](https://github.com/apache/beam/issues/40374)).
+* (Python) `Duration` built from float seconds now rounds to the nearest 
microsecond instead of truncating, which could lose a microsecond 
([#40263](https://github.com/apache/beam/issues/40263)).
 * Fixed X (Java/Python) ([#X](https://github.com/apache/beam/issues/X)).
 
 ## Security Fixes
diff --git a/sdks/python/apache_beam/utils/timestamp.py 
b/sdks/python/apache_beam/utils/timestamp.py
index c09cc93439c..81d8430d3af 100644
--- a/sdks/python/apache_beam/utils/timestamp.py
+++ b/sdks/python/apache_beam/utils/timestamp.py
@@ -497,7 +497,10 @@ class Duration(object):
       self,
       seconds: Union[int, float] = 0,
       micros: Union[int, float] = 0) -> None:
-    self.micros = int(seconds * 1000000) + int(micros)
+    # Round rather than truncate, as the Timestamp constructor does: the float
+    # multiplication can land just below the exact integer (for example
+    # 2.000002 * 1e6 == 2000001.9999999998) and int() would drop a microsecond.
+    self.micros = round(seconds * 1000000) + int(micros)
 
   @staticmethod
   def of(seconds: DurationTypes) -> 'Duration':
diff --git a/sdks/python/apache_beam/utils/timestamp_test.py 
b/sdks/python/apache_beam/utils/timestamp_test.py
index ff37ff84b5d..ebdd42d3702 100644
--- a/sdks/python/apache_beam/utils/timestamp_test.py
+++ b/sdks/python/apache_beam/utils/timestamp_test.py
@@ -411,6 +411,16 @@ class DurationTest(unittest.TestCase):
     with self.assertRaises(TypeError):
       Duration.of(Timestamp(10))
 
+  def test_constructor_float_rounds_to_nearest(self):
+    # Same float-to-micros conversion as Timestamp: truncating dropped a
+    # microsecond whenever seconds * 1e6 landed just below the integer.
+    self.assertEqual(Duration(2.000002).micros, 2000002)
+    self.assertEqual(Duration(-2.000002).micros, -2000002)
+    self.assertEqual(Duration.of(1.000001).micros, 1000001)
+    self.assertEqual(Duration(1.5).micros, 1500000)
+    for micros in (1056803002554, 544368929943, 33956373746):
+      self.assertEqual(Duration(micros / 1000000).micros, micros)
+
   def test_precision(self):
     self.assertEqual(Duration(10000000) % 0.1, 0)
     self.assertEqual(Duration(10000000) % 0.05, 0)

Reply via email to