/** * rain-episode.test.ts * * "연속적인 비라면 비온 날짜부터 누적강수량을 표시하면 좋겠는데" (2026-08-16 요청). * * 하루 누적은 기상청 실측으로 되지만, 초단기실황은 `resultCode 10 최근 1일 간의 자료만 * 제공합니다`로 딱 잘린다. 며칠짜리 ASOS 시간자료는 이 키로 403(미승인)이라, 사상 누적만 * Open-Meteo로 낸다. findRainEpisode는 그 시간별 배열에서 "이번 비가 언제 시작됐나"를 * 거꾸로 걸어 찾는 순수 함수다. * * 판정 기준 두 개: 6시간 이상 그치면 다른 사상, 시간당 0.1mm 미만은 비 온 시간이 아님. */ import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import { findRainEpisode } from '../src/tools/weather'; // "..##...#" 같은 문자열을 시간별 배열로. '#'=1.0mm, 숫자=그 값, '.'=0 function series(start: string, pattern: string): Array<{ time: string; mm: number }> { const t0 = new Date(start + ':00:00Z').getTime(); return [...pattern].map((c, i) => ({ time: new Date(t0 + i * 3600e3).toISOString().slice(0, 16).replace('T', 'T'), mm: c === '#' ? 1.0 : c === '.' ? 0 : Number(c) / 10, })); } describe('findRainEpisode — 이어지는 비의 시작 시각 찾기', () => { test('4시간 소강은 같은 사상으로 이어붙인다', () => { // 2026-08-16 서울 실제 패턴: 8/15 12시 시작, 중간 최대 4시간 소강 후 8/16 새벽까지. const ep = findRainEpisode(series('2026-08-15T00', '............##...##....#########...')); assert.ok(ep); assert.equal(ep.startTime.slice(0, 13), '2026-08-15T12'); assert.equal(ep.truncated, false); }); test('6시간 이상 그치면 그 이전은 다른 비다', () => { // 앞쪽 '##'은 8시간 공백 건너편 — 끌어오면 안 된다. const ep = findRainEpisode(series('2026-08-16T00', '##........####.')); assert.ok(ep); assert.equal(ep.startTime.slice(0, 13), '2026-08-16T10'); assert.equal(ep.totalMm, 4); assert.equal(ep.wetHours, 4); }); test('이미 6시간 넘게 그쳤으면 진행 중이 아니므로 null', () => { // 그친 지 한참인 비의 합계를 "누적"으로 띄우면 지금 오는 비의 양으로 읽힌다. assert.equal(findRainEpisode(series('2026-08-16T00', '####........')), null); }); test('그친 지 5시간이면 아직 진행 중으로 본다 (경계)', () => { const ep = findRainEpisode(series('2026-08-16T00', '####.....')); assert.ok(ep); assert.equal(ep.wetHours, 4); }); test('0.1mm 미만 흔적은 비 온 시간으로 세지 않는다', () => { // '1'=0.1mm는 젖은 시간, 하지만 0.0x는 없으니 여기선 '.'로 표현된 0만 마른 시간. const ep = findRainEpisode(series('2026-08-16T00', '...1#..')); assert.ok(ep); assert.equal(ep.startTime.slice(0, 13), '2026-08-16T03'); assert.equal(ep.wetHours, 2); assert.equal(ep.totalMm, 1.1); }); test('비가 전혀 없으면 null', () => { assert.equal(findRainEpisode(series('2026-08-16T00', '............')), null); }); test('빈 배열은 null', () => { assert.equal(findRainEpisode([]), null); }); test('조회창 첫 시각까지 끊기지 않으면 truncated로 표시한다', () => { // 7일 창을 꽉 채운 비 — 그 이전은 안 봤으므로 합계는 하한값이고, 출력에 '≥'가 붙는다. const ep = findRainEpisode(series('2026-08-10T00', '########')); assert.ok(ep); assert.equal(ep.truncated, true); }); test('gapHours를 낮추면 같은 데이터가 더 짧은 사상으로 쪼개진다', () => { const pattern = '##...##'; assert.equal(findRainEpisode(series('2026-08-16T00', pattern))!.startTime.slice(11, 13), '00'); const tight = findRainEpisode(series('2026-08-16T00', pattern), { gapHours: 3 }); assert.equal(tight!.startTime.slice(11, 13), '05'); }); });