같은 한국 저녁 6시라도 뉴욕에서 읽는 시각은 날짜에 따라 달라졌다. K-POP PULSE의 여름 시차 정정 기록을 읽고 나는 외워 둔 시차 대신 주어진 시각을 지역별로 계산하는 작은 프로그램을 만들었다. 원본 운영 코드 복원본이 아닌 새 공유물이다.
시간이 발표되지 않은 날짜에는 세계 시각을 만들지 않는다. 저장된 자정이 있어도 미고지 상태면 날짜만 남긴다. 지역 목록은 각 사이트에 맞춰 정하고, 새 코드와 입력 예제, 실제 CLI 검사를 함께 제공한다.
날짜와 시각의 발표 여부부터 받는다
입력의 date는 source_zone에서 공지된 날짜이고 time_announced는 시각 발표 여부다. 프로그램은 이 값을 실제 공지에서 판단하지 않으므로 운영자가 확인한 상태를 공급해야 한다. false면 세계 시각 목록은 비워 두고 발표된 날짜를 유지한다. 공지에 없는 시각을 채워 계산하지 않는다.
반대로 실제 자정이 발표됐으면 true와 근거 있는 시각을 넣는다. 그러면 전날의 미국 날짜처럼 변환 결과가 원래 날짜와 달라도 유지한다. 모두 자정이라고 숨기거나 날짜만 보고 같은 날로 맞추면 다른 사실을 전달할 수 있다.
지역 목록을 해당 사이트의 독자에 맞춘다
아래 설명용 예제는 기록에서 검토한 8월 19일 18시 한국 입력을 사용한 것이다. 현재 관측한 행사 상태가 아니다. UTC와 뉴욕, 런던을 보여 주지만 프로그램은 정해진 도시 세 개에 묶이지 않는다. 각 사이트에서 1~50개의 서로 다른 지역 키를 선택한다.
도시 표시 이름과 정렬은 화면에서 별도로 정할 수 있다. 계산용 키를 그대로 독자가 이해하기 좋은 이름으로 가정하지 않는다. 같은 지역도 날짜별 자료에 따라 오프셋이 달라질 수 있으므로 설치된 시간대 자료가 있어야 하고, 자료가 없으면 오류로 끝난다.
코드와 구현 살펴보기
설명용 입력. 실제 발표 상태와 시각을 확인해 공급한다.
{
"date": "2026-08-19",
"source_zone": "Asia/Seoul",
"time_announced": true,
"start_at": "2026-08-19T18:00:00+09:00",
"zones": [
"UTC",
"America/New_York",
"Europe/London"
]
}확정된 시각에는 오프셋이 있어야 한다
시간대가 없는 날짜시각은 받지 않는다. 알려진 순간을 명시한 오프셋으로 읽고 원래 지역에서 공지 날짜와 맞는지 확인한다. 그 뒤 UTC와 선택 지역의 날짜·시각·오프셋·약어를 반환한다. 단순한 시차 숫자를 연중 고정해서 빼는 계산을 하지 않는다.
시계가 되돌아가는 지역에서는 같은 1시 30분 표기가 서로 다른 순간을 가리킬 수 있다. 새 검사에서는 두 UTC 입력을 변환해 각각 다른 오프셋과 fold가 유지됨을 확인했다. 약어만 보고 같은 순간으로 합치지 않고 날짜와 오프셋을 함께 표시할 수 있도록 남긴다.
프로그램이 추측하지 않는 입력도 정한다
이 코드는 지역의 시계값만 받고 빠진 오프셋을 추측하지 않는다. 존재하지 않거나 모호한 현지 시각을 자동으로 결정하는 기능, 반복 일정, 지역별 자정 배포 정책도 구현하지 않는다. 모든 지역의 자정에 각각 나오는 상품은 하나의 순간을 세계 시각으로 바꾸는 모델과 다르게 설계해야 한다.
출처가 진짜인지, 공지가 바뀌었는지, 세계 표시가 독자에게 명확한지도 별도 책임이다. 이 프로그램은 입력 계약과 계산을 맡고 네트워크·저장·발행을 하지 않는다. 출력 문자열을 화면에 넣을 때는 해당 화면의 텍스트 처리 방식을 적용한다.
실행 방법과 검사 범위를 함께 가져간다
Python의 zoneinfo와 IANA 시간대 자료가 필요하며 사용하는 PC에서 지역 키가 열리는지 확인한다. 표준입력이나 --input 파일로 JSON을 넘기면 성공 JSON과 종료 0을 돌려준다. 잘못된 값이나 없는 시간대는 오류 JSON과 종료 2로 끝난다. 입력 한도는 256KiB다.
기록된 한 입력과 합성 여름·겨울·전날·미고지·반복 시각, 실제 파일과 표준입력 CLI를 포함한 24개 검사가 통과했다. 원본 사이트나 캘린더 앱·검색 출력을 수정한 결과로 넓히지 않는다. 공유 ZIP의 검사를 자신의 환경에서 다시 돌리고 실제 자료와 표시 화면을 연결해 확인한다.
값을 아는 경계 안에서만 계산한다
나는 계산 가능한 입력과 아직 모르는 상태를 같이 전달하는 모듈로 남기고 싶다. 시각이 없으면 날짜로 유용한 안내를 만들 수 있고, 시각이 발표되면 그 순간에 지역별 값을 더할 수 있다. 홈페이지마다 사용하는 도시와 언어가 달라도 이 정보의 경계는 읽을 수 있다.
아래 핵심 구현과 ZIP에는 입력 예제·전체 CLI·검사·설명이 있다. 새 필드와 표시를 붙일 때는 발표 여부, 원래 날짜와 오프셋이 함께 유지되는지도 검사하며 기능을 확장한다. 실제 공지와 계산 결과가 바뀌면 그 시점을 남겨 후속 문장이 어떤 값을 기준으로 했는지도 찾을 수 있게 한다.
코드와 구현 살펴보기
새 Python 구현. 표준입력 CLI 전체와 검사는 ZIP에 있다.
"""Project a supplied announced instant into named zones; preserve date-only uncertainty."""
import argparse,json,sys
from datetime import date,datetime,timezone
from pathlib import Path
from zoneinfo import ZoneInfo
def event_zone_times(record):
if type(record) is not dict or set(record)-{'date','source_zone','time_announced','start_at','zones'}:
raise ValueError('Unsupported event input')
if type(record.get('date')) is not str or type(record.get('time_announced')) is not bool:
raise ValueError('Explicit date and announcement state required')
day=date.fromisoformat(record['date'])
if day.isoformat()!=record['date']:raise ValueError('Use YYYY-MM-DD')
if type(record.get('source_zone')) is not str:raise ValueError('Source zone required')
source=ZoneInfo(record['source_zone'])
keys=record.get('zones')
if type(keys) is not list or not 1<=len(keys)<=50 or any(type(x) is not str for x in keys) or len(set(keys))!=len(keys):
raise ValueError('One to fifty distinct named zones required')
zones=[ZoneInfo(key) for key in keys]
supplied=record.get('start_at')
if supplied is not None and type(supplied) is not str:raise ValueError('Start must be a string or absent')
out={'source_date':day.isoformat(),'source_zone':source.key,'time_announced':record['time_announced'],'start_at_utc':None,'zone_times':[]}
if not record['time_announced']:return out
if supplied is None:raise ValueError('An announced time needs an explicit instant')
instant=datetime.fromisoformat(supplied.replace('Z','+00:00'))
if instant.tzinfo is None or instant.utcoffset() is None:raise ValueError('Explicit offset required')
if instant.astimezone(source).date()!=day:raise ValueError('Source date mismatch')
out['start_at_utc']=instant.astimezone(timezone.utc).isoformat()
for zone in zones:
local=instant.astimezone(zone)
out['zone_times'].append({'zone':zone.key,'date':local.date().isoformat(),'datetime':local.isoformat(),'abbreviation':local.tzname(),'fold':local.fold})
return out
def main():
parser=argparse.ArgumentParser();parser.add_argument('--input',type=Path);args=parser.parse_args()
try:
if args.input:
with args.input.open('rb') as f:raw=f.read(262145)
else:raw=sys.stdin.buffer.read(262145)
if len(raw)>262144:raise ValueError('Input limit exceeded')
result=event_zone_times(json.loads(raw.decode('utf-8')))
except (ValueError,TypeError,KeyError,OSError,OverflowError,RecursionError):
print(json.dumps({'ok':False,'error':'Invalid input or unavailable time-zone data'}));return 2
print(json.dumps({'ok':True,'event':result},ensure_ascii=False));return 0
if __name__=='__main__':raise SystemExit(main())