| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394 |
- // 教龄换算单测(PHP `utils.php:924-935` `calculatingAge()` 口径)。
- //
- // 三条口径各有一组断言,**任一条写错都会让课酬"稳定地跨错档且不报错"**:
- //
- // ① 除数固定 365(不是 365.25、不是年份相减)
- // ② 四舍五入到 1 位小数(不是 3 位、不是截断)
- // ③ 空 / 非法 / 未来日期 → 0
- //
- // 为什么用固定 `now`:真实 `time.Now()` 会让断言依赖运行时刻(跨零点跑两次结果不同),
- // 而教龄恰好要在"整数年边界"上稳定 —— 边界值必须可复现。
- package service
- import (
- "testing"
- "time"
- )
- // dateUTC 构造 UTC 日期(只取年月日,与 `t_teacher_information.teacher_exp` 的 DATE 语义一致)。
- func dateUTC(year int, month time.Month, day int) time.Time {
- return time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
- }
- // ptrTime 时间指针(可空列用)。
- func ptrTime(t time.Time) *time.Time { return &t }
- // TestCalculatingAgeYearsMatchesPHP 教龄换算逐值断言(固定 now,边界可复现)。
- func TestCalculatingAgeYearsMatchesPHP(t *testing.T) {
- now := dateUTC(2026, time.November, 9)
- cases := []struct {
- name string
- exp *time.Time
- want float64
- }{
- {"nil 起算日 → 0", nil, 0},
- {"零值起算日 → 0", ptrTime(time.Time{}), 0},
- {"恰好 365 天 → 1.0", ptrTime(now.AddDate(0, 0, -365)), 1.0},
- {"恰好 730 天 → 2.0", ptrTime(now.AddDate(0, 0, -730)), 2.0},
- // ① 除数口径:313 天 = 0.8575 年 → 0.9。若按"年份相减"会得 **1.0**(2025-12-31 → 2026-11-09)
- {"313 天(跨年但不足一年)→ 0.9 而非 1.0", ptrTime(dateUTC(2025, time.December, 31)), 0.9},
- // ② 小数位口径:10 天 = 0.0274 年 → **0.0**(按 3 位会是 0.027,按截断也是 0.0 但 100 天会分叉)
- {"10 天 → 0.0(1 位小数舍入)", ptrTime(now.AddDate(0, 0, -10)), 0},
- {"100 天 → 0.3(0.274 → 0.3)", ptrTime(now.AddDate(0, 0, -100)), 0.3},
- {"400 天 → 1.1(1.0959 → 1.1)", ptrTime(now.AddDate(0, 0, -400)), 1.1},
- // ③ 未来日期 → 0(PHP 的负时间戳分支)
- {"未来日期 → 0", ptrTime(now.AddDate(0, 0, 30)), 0},
- }
- for _, tc := range cases {
- got := calculatingAgeYears(tc.exp, now)
- if got != tc.want {
- t.Errorf("%s:calculatingAgeYears = %v,期望 %v", tc.name, got, tc.want)
- }
- }
- }
- // ⚠ **本测试覆盖不到的一处,写在最前面免得被误解为"已覆盖"**:
- // 除数取 `365` 还是 `365.25`,在 `round(x,1)` 之下**对现实教龄区间不可观测**——
- // 两者每天只差 1.88e-6 年,要拉出 0.05 年的差异需 26000+ 天(70 年以上)才可能跨舍入边界。
- // 我们仍写 `365` 的唯一理由是**逐字照抄 PHP**(`Utils.php:930`),不是因为有断言能抓到;
- // 若有人改成 365.25,现有用例**不会红**——记在这里,不假装有防线。
- // 真正可观测、且已被用例钉住的是:① 不得用年份相减(见下一条)、② 必须舍入到 1 位、③ 空/未来 → 0。
- // TestCalculatingAgeYearsUses365NotCalendar 专门钉住"不得用年份相减"。
- //
- // 同一个起算日,两种算法给出**不同档位**(0.9 vs 1.0),而 `teacher_exp_min=1.000` 的规则
- // 只在后者命中 ⇒ 写错口径会**稳定地跨错档**(且课酬算错、无任何报错)。
- func TestCalculatingAgeYearsUses365NotCalendar(t *testing.T) {
- now := dateUTC(2026, time.November, 9)
- exp := dateUTC(2025, time.December, 31)
- // 日历相减:2026-2025 = 1
- calendarYears := float64(now.Year() - exp.Year())
- got := calculatingAgeYears(ptrTime(exp), now)
- if got == calendarYears {
- t.Fatalf("教龄 = %v,与「年份相减」结果相同:说明没按 365 天口径算(本例应为 0.9,年份相减是 1.0)", got)
- }
- if got != 0.9 {
- t.Fatalf("教龄 = %v,期望 0.9(313 天 / 365,四舍五入 1 位)", got)
- }
- }
- // TestRoundToDecimal 舍入辅助的边界(0.5 进位按 PHP round 的四舍五入)。
- func TestRoundToDecimal(t *testing.T) {
- cases := []struct{ in, want float64 }{
- {0.5479, 0.5},
- {0.55, 0.6},
- {1.0959, 1.1},
- {2.04, 2.0},
- {9.96, 10.0},
- }
- for _, tc := range cases {
- if got := roundToDecimal(tc.in, 1); got != tc.want {
- t.Errorf("roundToDecimal(%v,1) = %v,期望 %v", tc.in, got, tc.want)
- }
- }
- }
|