fix(ondemand): beyond_next_return compares UTC with UTC (#175 acceptance, BUG-1818)
Round 2 read coverage.end.local, but _parse_moment cannot parse the +hh:mm suffix, so that branch never ran and the comparison always fell back to utc + profile.tz. Convert each Mudda end to UTC with the solar-return instant's own offset (tajika_mudda.TIME_BASIS) and compare it with coverage.end.utc. Adds tests for a UTC+8 birth and a New York birth (±3 h around the next return, plus real rows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
This commit is contained in:
@@ -250,15 +250,16 @@ def run(body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
elif abs(float(dasha_year['days']) - NOMINAL_YEAR_DAYS) > 1e-9:
|
||||
gaps.append({'field': 'nominal_year', 'reason': 'dasha_year_days_not_365_25'})
|
||||
|
||||
# 原值:用 coverage.end 的 utc 去比(mudda 段是返照地点的本地时刻,两边差一个时区偏移)
|
||||
# 新值:两边都用本地时刻(coverage.end.local 与 raw_periods 同一地点、同一时区)
|
||||
# 原因:验收退回(BUG-1818):beyond_next_return 的时区基准必须一致
|
||||
coverage_end = _parse_moment((coverage_interval.get('end') or {}).get('local'))
|
||||
if coverage_end is None:
|
||||
# 没有 local 时才退回 utc + 返照地点偏移,仍在同一基准上比较。
|
||||
coverage_end_utc = _parse_moment((coverage_interval.get('end') or {}).get('utc'))
|
||||
if coverage_end_utc is not None:
|
||||
coverage_end = coverage_end_utc + timedelta(hours=float(profile['tz']))
|
||||
# 原值:mudda 段终点(返照地点本地时刻)直接与 coverage.end.utc 比,差一个时区偏移;
|
||||
# 第 2 轮改成读 coverage.end.local,但带 ±hh:mm 的串 _parse_moment 解析不了,实际总走 utc + profile.tz 的回退。
|
||||
# 新值:两边都换成 UTC 再比:段终点减去返照时刻的本地偏移(tajika_mudda.TIME_BASIS:
|
||||
# “naive local wall time in the UTC offset of the solar-return instant”),对 coverage.end.utc。
|
||||
# 原因:验收退回(BUG-1818):beyond_next_return 必须同一时间基准(UTC 对 UTC),夏令时两端不同也不受影响。
|
||||
coverage_end_utc = _parse_moment((coverage_interval.get('end') or {}).get('utc'))
|
||||
if isinstance(moment_local, datetime) and isinstance(moment_utc, datetime):
|
||||
mudda_offset = moment_local.replace(tzinfo=None) - moment_utc.replace(tzinfo=None)
|
||||
else:
|
||||
mudda_offset = timedelta(hours=float(profile['tz']))
|
||||
raw_periods: List[Dict[str, Any]] = []
|
||||
for item in (mudda.get('raw_periods') or []):
|
||||
if not isinstance(item, dict):
|
||||
@@ -266,7 +267,8 @@ def run(body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
period = dict(item)
|
||||
period_end = _parse_moment(period.get('end'))
|
||||
period['beyond_next_return'] = bool(
|
||||
coverage_end is not None and period_end is not None and period_end > coverage_end
|
||||
coverage_end_utc is not None and period_end is not None
|
||||
and period_end - mudda_offset > coverage_end_utc
|
||||
)
|
||||
raw_periods.append(period)
|
||||
|
||||
|
||||
@@ -248,3 +248,56 @@ def test_beyond_next_return_uses_the_same_local_time_basis_in_the_west(monkeypat
|
||||
envelope = ondemand_calc_api(_body(return_location={'mode': 'current_location', **NEW_YORK}))
|
||||
assert envelope['calculation_status'] in {'executed', 'partial'}
|
||||
assert envelope['raw']['mudda_raw'][0]['beyond_next_return'] is beyond
|
||||
|
||||
|
||||
# 验收补充(Claude,2026-11-09):东八区出生与西半球出生各一组,越界标记用 UTC 对 UTC。
|
||||
# 段终点按「返照时刻的本地偏移」写成本地时刻(tajika_mudda.TIME_BASIS),再挪 ±3 小时。
|
||||
NEW_YORK_BIRTH = {
|
||||
'year': 1990, 'month': 7, 'day': 4, 'hour': 9, 'minute': 30,
|
||||
'lat': 40.7128, 'lon': -74.0060, 'tz': -4.0, 'timezone_id': 'America/New_York',
|
||||
}
|
||||
|
||||
|
||||
def _patch_engine_utc_based(monkeypatch, shift_hours: float) -> None:
|
||||
real = solar_return._solar_return_module()
|
||||
real_report = real.solar_return_full_report
|
||||
|
||||
def patched(*args, **kwargs):
|
||||
result = real_report(*args, **kwargs)
|
||||
moment = result['solar_return']
|
||||
offset = moment['dt_local'].replace(tzinfo=None) - moment['dt_ut'].replace(tzinfo=None)
|
||||
end_utc = _moment(result['annual_interval']['solar_return_interval']['end']['utc'])
|
||||
mudda = dict(result.get('mudda_dasha') or {})
|
||||
mudda['raw_periods'] = [{
|
||||
'order': 1, 'lord': 'Rahu', 'start': '2026-01-01 00:00:00',
|
||||
'end': (end_utc + offset + timedelta(hours=shift_hours)).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
}]
|
||||
mudda['periods'] = []
|
||||
return dict(result, mudda_dasha=mudda)
|
||||
|
||||
monkeypatch.setattr(real, 'solar_return_full_report', patched)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('shift_hours,beyond', [(3.0, True), (-3.0, False)])
|
||||
def test_utc_plus_8_birth_marks_beyond_on_utc(monkeypatch, shift_hours, beyond):
|
||||
assert float(NEUTRAL_TZ) == 8.0
|
||||
_patch_engine_utc_based(monkeypatch, shift_hours)
|
||||
envelope = ondemand_calc_api(_body())
|
||||
assert envelope['raw']['solar_return']['utc_offset_hours'] == 8.0
|
||||
assert envelope['raw']['mudda_raw'][0]['beyond_next_return'] is beyond
|
||||
|
||||
|
||||
@pytest.mark.parametrize('shift_hours,beyond', [(3.0, True), (-3.0, False)])
|
||||
def test_western_hemisphere_birth_marks_beyond_on_utc(monkeypatch, shift_hours, beyond):
|
||||
_patch_engine_utc_based(monkeypatch, shift_hours)
|
||||
envelope = ondemand_calc_api(_body(birth=dict(NEW_YORK_BIRTH)))
|
||||
assert envelope['calculation_status'] in {'executed', 'partial'}
|
||||
assert envelope['raw']['solar_return']['utc_offset_hours'] < 0
|
||||
assert envelope['raw']['mudda_raw'][0]['beyond_next_return'] is beyond
|
||||
|
||||
|
||||
def test_western_hemisphere_birth_real_rows_only_last_is_beyond():
|
||||
envelope = ondemand_calc_api(_body(birth=dict(NEW_YORK_BIRTH)))
|
||||
flags = [row['beyond_next_return'] for row in envelope['raw']['mudda_raw']]
|
||||
assert flags[-1] is True
|
||||
assert not any(flags[:-1])
|
||||
|
||||
Reference in New Issue
Block a user