diff --git a/CHANGELOG.md b/CHANGELOG.md index ca6775c6..567937d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Bug Fixes +1. Avoid floating-point timestamp rounding across microsecond, millisecond, and second boundaries. + 1. [#706](https://github.com/influxdata/influxdb-client-python/pull/706): Use logger instead logging. 1. [#710](https://github.com/influxdata/influxdb-client-python/pull/710): Stop `default_tags` from one client leaking into other clients' `WriteApi` (shared mutable default `PointSettings`). 1. [#713](https://github.com/influxdata/influxdb-client-python/pull/713): Replace invalid tuple type annotations `(str, str, str)` with `Tuple[str, str, str]` in batching callbacks, examples and docs. diff --git a/influxdb_client/client/write/point.py b/influxdb_client/client/write/point.py index cc95d204..031e45a4 100644 --- a/influxdb_client/client/write/point.py +++ b/influxdb_client/client/write/point.py @@ -351,11 +351,16 @@ def _convert_timestamp(timestamp, precision=DEFAULT_WRITE_PRECISION): if precision is None or precision == WritePrecision.NS: return ns elif precision == WritePrecision.US: - return ns / 1e3 + divisor = 1000 elif precision == WritePrecision.MS: - return ns / 1e6 + divisor = 1000000 elif precision == WritePrecision.S: - return ns / 1e9 + divisor = 1000000000 + else: + raise ValueError(timestamp) + + # Keep truncation toward zero without rounding through a float. + return ns // divisor if ns >= 0 else -(-ns // divisor) raise ValueError(timestamp) diff --git a/tests/test_PointTimestampBoundaries.py b/tests/test_PointTimestampBoundaries.py new file mode 100644 index 00000000..89bfe954 --- /dev/null +++ b/tests/test_PointTimestampBoundaries.py @@ -0,0 +1,31 @@ +"""Timestamp boundary regressions through the public Point API.""" + +import pandas as pd +import pytest + +from influxdb_client import Point, WritePrecision +from influxdb_client.client.util import date_utils +from influxdb_client.client.util.date_utils_pandas import PandasDateTimeHelper + + +@pytest.mark.parametrize("precision, divisor", [ + (WritePrecision.US, 1000), + (WritePrecision.MS, 1000000), + (WritePrecision.S, 1000000000), +]) +@pytest.mark.parametrize("sign", [1, -1]) +@pytest.mark.parametrize("on_boundary", [False, True]) +def test_point_timestamp_boundary(monkeypatch, precision, divisor, sign, on_boundary): + monkeypatch.setattr(date_utils, "date_helper", PandasDateTimeHelper()) + magnitude = 1700000000000000000 + divisor - (0 if on_boundary else 1) + stamp = pd.Timestamp(sign * magnitude, unit="ns", tz="UTC") + point = Point("sensor").field("value", 1).time(stamp, precision) + assert point.to_line_protocol() == f"sensor value=1i {sign * (magnitude // divisor)}" + + +@pytest.mark.parametrize("sign", [1, -1]) +def test_point_nanosecond_timestamp_unchanged(monkeypatch, sign): + monkeypatch.setattr(date_utils, "date_helper", PandasDateTimeHelper()) + ns = sign * 1700000000000000999 + point = Point("sensor").field("value", 1).time(pd.Timestamp(ns, unit="ns", tz="UTC")) + assert point.to_line_protocol() == f"sensor value=1i {ns}"