Skip to content

fix: correct SecondsTimedeltaProvider for negative fractional seconds - #453

Merged
zhPavel merged 2 commits into
reagento:developfrom
binggao1230:fix/timedelta-negative-fractional-seconds
Jul 12, 2026
Merged

fix: correct SecondsTimedeltaProvider for negative fractional seconds#453
zhPavel merged 2 commits into
reagento:developfrom
binggao1230:fix/timedelta-negative-fractional-seconds

Conversation

@binggao1230

Copy link
Copy Markdown

Bug

SecondsTimedeltaProvider returns the wrong timedelta when loading a
negative fractional number of seconds.

from datetime import timedelta
from adaptix import Retort

retort = Retort()
print(retort.load(-0.5, timedelta))   # timedelta(0, 0, 500000)  — should be -0.5 s
print(retort.load(-1.5, timedelta))   # timedelta(-1, 86399, 500000)  — off by 1 s

Root cause

The loader uses:

timedelta(seconds=int(data), microseconds=int(data % 1 * 10**6))

int() truncates toward zero, so for data = -0.5:

  • int(-0.5)0 (should be -1)
  • -0.5 % 10.5 (Python modulo is always non-negative)
  • result: timedelta(seconds=0, microseconds=500000)+0.5 s

Fix

Use math.floor() for the seconds component, which gives floor-division
semantics (always rounds toward negative infinity):

sec = math.floor(data)
timedelta(seconds=int(sec), microseconds=round((data - sec) * 10**6))

round() is used for the microseconds to avoid off-by-one float-precision
errors (e.g. -2.7 % 1 * 1e6 → 299999.99...). Decimal inputs are
handled correctly too since math.floor(Decimal(...)) returns an int.

Tests

Added four assertions for negative fractional float and Decimal inputs
to test_seconds_timedelta_provider. All 6 parametrized variants pass.


This pull request was prepared with the assistance of AI, under my direction and review.

zhPavel and others added 2 commits April 4, 2026 20:31
`timedelta(seconds=int(data), microseconds=int(data % 1 * 10**6))` uses
`int()` which truncates toward zero, so for `data = -0.5` it computes
`seconds=0, microseconds=500000` (+0.5 s) instead of the correct -0.5 s.

Replace with `math.floor()` to get the proper floor-division semantics:
`sec = math.floor(data)` and `microseconds=round((data - sec) * 10**6)`.
The `round()` avoids off-by-one microsecond errors from float arithmetic
(e.g. `-2.7 % 1 * 1e6 → 299999.9...`).  Decimal inputs are handled
correctly too since `math.floor(Decimal(...))` returns an int.

Reproducer:
    retort.load(-0.5, timedelta)  # returned timedelta(0, 0, 500000) (+0.5 s)
    retort.load(-1.5, timedelta)  # returned timedelta(-1, 86399, 500000) (-0.5 s)
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  src/adaptix/_internal/morphing
  concrete_provider.py 227
Project Total  

This report was generated by python-coverage-comment-action

@zhPavel
zhPavel changed the base branch from main to develop July 12, 2026 20:15
@zhPavel
zhPavel merged commit e25a512 into reagento:develop Jul 12, 2026
11 checks passed
@zhPavel

zhPavel commented Jul 12, 2026

Copy link
Copy Markdown
Member

Thank you for your contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants