Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/activity_parser/parse_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ def split_left_right_balance(
is_right = (present & right_flag) != 0
percent = (present & percent_mask).astype(float) / percent_scale
right = percent.where(is_right, 100 - percent)
# Discard an out-of-range balance.
right = right.where(right.between(0, 100))

df = df.copy()
df["left_balance"] = (100 - right).reindex(df.index)
Expand Down
25 changes: 18 additions & 7 deletions tests/test_parse_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,25 +273,35 @@ def test_split_left_right_balance_noop_without_column():
assert "right_balance" not in out.columns


def test_split_left_right_balance_discards_out_of_range_percentage():
# An out-of-range percentage isn't a valid balance; discard it.
df = pd.DataFrame({"left_right_balance": [110]})
out = split_record_balance(df)
assert out["right_balance"].isna().all()
assert out["left_balance"].isna().all()


def test_split_left_right_balance_decodes_enum_values():
# Confirm the enum strings are recovered rather than crashing on cast.
df = pd.DataFrame({"left_right_balance": ["right", "mask", 180]})
out = split_record_balance(df)
assert out["right_balance"].tolist() == pytest.approx([0.0, -27.0, 52.0])
assert out["left_balance"].tolist() == pytest.approx([100.0, 127.0, 48.0])
nan = float("nan")
assert out["right_balance"].tolist() == pytest.approx([0.0, nan, 52.0], nan_ok=True)
assert out["left_balance"].tolist() == pytest.approx([100.0, nan, 48.0], nan_ok=True)


def test_split_left_right_balance_decodes_enum_values_100():
# Same FIT profile issue, but for the lap/session/segment_lap uint16 layout.
# Same enum-string handling, for the lap/session/segment_lap uint16 layout.
df = pd.DataFrame({"left_right_balance": ["right", "mask", 37968]})
out = split_left_right_balance(
df,
right_flag=LEFT_RIGHT_BALANCE_RIGHT_FLAG_100,
percent_mask=LEFT_RIGHT_BALANCE_PERCENT_MASK_100,
percent_scale=100.0,
)
assert out["right_balance"].tolist() == pytest.approx([0.0, -63.83, 52.0])
assert out["left_balance"].tolist() == pytest.approx([100.0, 163.83, 48.0])
nan = float("nan")
assert out["right_balance"].tolist() == pytest.approx([0.0, nan, 52.0], nan_ok=True)
assert out["left_balance"].tolist() == pytest.approx([100.0, nan, 48.0], nan_ok=True)


def test_gz_round_trip(tmp_path):
Expand Down Expand Up @@ -452,8 +462,9 @@ def test_activity_parser_left_right_balance_column_curation():
def test_parse_fit_left_right_balance_enum_quirk_end_to_end():
# Regression test: this used to crash ~12.7% of a real archive's FIT files.
records, _, _ = parse_fit(io.BytesIO(synthetic_fit.left_right_balance_enum_quirk()))
assert records["right_balance"].tolist() == pytest.approx([0.0, -27.0, 52.0])
assert records["left_balance"].tolist() == pytest.approx([100.0, 127.0, 48.0])
nan = float("nan")
assert records["right_balance"].tolist() == pytest.approx([0.0, nan, 52.0], nan_ok=True)
assert records["left_balance"].tolist() == pytest.approx([100.0, nan, 48.0], nan_ok=True)


def test_parse_fit_coercion_skips_column_with_non_numeric_value():
Expand Down