You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by cv...@apache.org on 2026/10/04 16:54:30 UTC

(beam) branch master updated: [Python] Round float seconds to the nearest microsecond in Duration (#40236)

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 <14...@users.noreply.github.com>
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)