버튼 튐을 시간표로 읽기
110분 안팎
학습 목표
시각과 버튼 값 목록에서 20ms 유지된 변화만 이벤트로 출력합니다.
개념
버튼 접점 로그가 여러 이벤트로 보이는 이유
기록기를 한 번 켜려고 눌렀는데 바로 꺼지면 먼저 원시 입력의 시간표를 봅니다. 접점이 닫히는 과정에서 짧은 레벨 변화가 여러 번 관측될 수 있습니다. 그 변화마다 모드를 반전하면 손가락 동작 하나에 여러 토글이 생깁니다. 이번 레슨은 시각과 레벨을 입력받는 작은 필터를 만듭니다. 접점의 물리 운동을 정밀 모사하는 대신 주어진 로그에서 언제 안정 전이를 인정하는지 명확히 정합니다.
원시 레벨, 후보 레벨, 안정 레벨은 같은 변수에 넣지 않습니다. 원시 레벨은 현재 행의 관측이고 후보는 최근에 유지 여부를 확인 중인 값이며 안정 레벨은 이미 확정한 값입니다. 후보가 바뀌면 후보 시작 시각도 바뀝니다. 안정 레벨은 확인 시간이 찼을 때만 바꿉니다. 둘을 구분해야 튐을 보고도 사용자 동작은 아직 발생시키지 않는 상태를 표현할 수 있습니다.
20ms 기준을 시간 차로 적용합니다
이 필터의 정책은 같은 후보가 관측된 뒤 20ms 이상 지나고 후보가 안정값과 다를 때 전이를 확정하는 것입니다. 후보 시작 시각을 since로 저장하고 현재 시각 t에서 t - since를 계산합니다. 반복 횟수 20회를 세는 방식이 아닙니다. 로그 간격이 1ms로 고정되어 있지 않으므로 호출 수와 경과 시간을 혼동하면 같은 fixture에서도 결과가 달라집니다. 실물 버튼의 임계는 파형과 응답 요구를 보고 별도로 선정합니다.
초기 안정 레벨과 후보 레벨은 모두 0입니다. 처음 관측한 1은 눌림 확정이 아니라 후보 변경입니다. 0ms에 1, 3ms에 0, 8ms에 1이면 마지막 후보의 시작 시각은 8입니다. 27ms에서는 차이가 19라 이벤트가 없고 28ms에서는 20이라 PRESSED가 됩니다. 최초 변화 시각 0을 끝까지 유지하면 20ms에 너무 일찍 확정하게 됩니다. 매번 후보 변화 시각을 재설정하는 이유를 시간표에 적습니다.
안정값이 1로 바뀐 뒤 100ms에서도 1이면 새 이벤트는 없습니다. 레벨이 눌림인 동안 계속 PRESSED를 출력하는 것은 에지 검출이 아닙니다. 에지는 확정된 상태가 바뀌는 순간이며 레벨은 계속 유지할 수 있는 값입니다. 해제도 같은 안정 검사를 거쳐 RELEASED를 냅니다. 애플리케이션은 다음 레슨에서 PRESSED만 토글에 사용하고 RELEASED는 다음 눌림을 가능하게 하는 상태 변화로 사용합니다.
짧은 펄스는 어떻게 처리할지도 정의합니다. 10ms에 1로 바뀌고 29ms에 0으로 돌아오면 눌림 후보가 19ms만 지속되어 안정 1로 확정되지 않습니다. 원래 안정값 0으로 돌아왔으므로 RELEASED도 출력하지 않습니다. 한 줄의 변경이 있다고 늘 이벤트가 나오는 것은 아닙니다. 이 입력은 빠른 탭을 무시하는 정책의 비용을 보여 주며 모든 짧은 입력을 노이즈라고 보장하는 실험은 아닙니다.
시간표의 빈 구간이 의미하는 것을 정합니다
fixture 행 사이에는 마지막 레벨이 유지된다고 가정합니다. 8ms와 28ms가 모두 1이고 중간 행이 없으면 모델에서는 20ms 유지라고 판단합니다. 실제 장치의 두 번 관측만으로 사이 파형 전체가 안정했음을 증명할 수는 없습니다. 샘플링 사이에 숨어 있는 변화가 있을 수 있기 때문입니다. 보고서에는 시간표 유지 가정과 실물 관측 한계를 적습니다. 로그 행 수가 적다는 이유로 물리 버튼 품질을 단정하지 않습니다.
전이는 확인한 행의 시각에 출력합니다. 후보가 8ms에 시작하고 다음 행이 35ms라면 PRESSED 35를 출력하며 PRESSED 28을 소급해서 만들지 않습니다. 조건이 성립한 최초 관측 시각과 이상적인 임계 도달 시각은 다릅니다. 이 차이는 폴링 지연을 설명하는 근거가 됩니다. 미래의 주기 작업을 붙일 때에는 호출 간격까지 고려해야 하지만 이번 필터는 입력 로그에 없는 행을 자동 생성하지 않습니다.
입력 종료도 시계를 전진시키지 않습니다. 마지막 행이 8ms의 후보 1이면 파일을 다 읽었다고 눌림을 확정하지 않습니다. 20ms 유지 확인 행이 아직 없기 때문입니다. EOF를 RELEASED로 해석하거나 마지막 후보에 20을 더한 이벤트를 출력하면 시험 계약을 어깁니다. 이벤트가 없는 유효 시간표에는 NONE 한 줄을 출력해 빈 결과와 입력 오류를 구분합니다.
브라우저 입출력 계약을 구현합니다
첫 줄에는 행 수 N을 0~1000 정수로 씁니다. 이후 N개의 줄에는 시각 t와 레벨 두 정수를 공백으로 구분해 씁니다. 시각은 0~4294967295 범위이고 앞 행보다 엄격히 커야 하며 레벨은 0 또는 1입니다. 이 Python 실습은 시계 순환을 받지 않습니다. N이 0이면 추가 줄 없이 NONE입니다. 마지막 줄 뒤 개행은 허용하지만 중간 빈 행과 여분 데이터 행은 오류입니다.
확정 이벤트마다 시각, 공백, PRESSED 또는 RELEASED를 한 줄에 출력합니다. 예를 들어 28 PRESSED가 눌림 확인 결과입니다. 레벨 숫자를 출력하거나 이벤트 개수만 출력하지 않습니다. 입력 문법이나 범위가 틀리면 ERROR 한 줄만 출력합니다. 오류 전까지의 이벤트를 먼저 찍지 않도록 전체 행을 검사한 뒤 필터를 실행합니다. 정수 파서와 입력 검증은 starter에 포함되어 후보 상태 처리에 집중할 수 있습니다.
구현 순서는 입력 전체 검증, 상태 초기화, 행별 후보 변경, 안정 확정, 이벤트 목록 출력입니다. 같은 행에서 후보를 바꾸고 곧바로 20ms 조건을 검사해도 시작 차이가 0이므로 정상 후보는 바로 확정되지 않습니다. 안정 확정 때 stable을 갱신하지 않으면 다음 동일 레벨 행에서도 이벤트를 반복합니다. events에 추가하기 전 안정값과 후보값이 다른지 검사하는 조건이 중복 방지 장치입니다.
오류 메시지와 오답 출력을 해석합니다
ValueError: invalid literal for int는 입력 숫자 변환 단계에서 발생하는 진단입니다. 제공 코드는 이를 잡아 ERROR로 변환합니다. SyntaxError가 발생하면 들여쓰기와 콜론, 괄호를 먼저 살펴봅니다. IndexError라면 N과 실제 행 수 검사를 삭제했는지 확인합니다. 실행 자체가 중단된 것과 정상 종료하며 기대 이벤트가 다른 것은 다른 문제이므로 파서와 필터의 수정 위치를 구분합니다.
28 PRESSED 뒤 100 PRESSED가 다시 나오면 시간 임계보다 안정 상태 갱신을 먼저 확인합니다. 해제 구간에서도 PRESSED만 나오면 새 안정 레벨에 따라 문자열을 선택했는지 봅니다. 27 PRESSED가 나오면 비교를 19 이상으로 썼거나 후보 시각을 1ms 앞당겼는지 확인합니다. 28ms 정답만 맞추는 상수를 넣지 않고 19/20 경계와 다른 시작 시각에서도 같은 규칙을 검증합니다.
블로킹 지연으로 20ms를 기다리면 이 필터가 입력 하나를 확인하는 동안 같은 실행 흐름의 다른 일이 지연됩니다. 이번 구현은 sleep 없이 현재 시각과 저장한 후보 시각을 비교하고 즉시 다음 행으로 넘어갑니다. PC 실행이 빨랐다는 관찰과 실제 장치의 시간 보장이 같지는 않습니다. 필터 함수가 다른 작업을 기다리지 않는 구조라는 점을 설명하고, 처리 시간 측정은 뒤의 자원 모듈에 남깁니다.
시험 사례를 통해 정책을 판단합니다
검사에는 튐 후 눌림, 19ms 미확정, 정확한 20ms 확정, 길게 누름, 안정 해제, 두 번째 눌림, 짧은 펄스, 늦은 관측을 넣습니다. 형식 오류와 역행 시각은 모두 ERROR를 기대합니다. 정상 시간표의 시작이 0이 아니어도 초기 안정 0이라는 정책은 동일합니다. 500ms 첫 행에서 1을 관측했다면 520ms에서 확인할 수 있습니다. 부팅 직후 눌림도 인정하는 정책을 문서와 시험에서 일치시킵니다.
완료 판단은 출력 문자열을 외우는 것이 아니라 새로운 시간표를 손으로 추적할 수 있는지입니다. 각 행에 candidate, since, stable, event 네 칸을 적고 출력과 비교합니다. 후보 변경과 안정 변경에 서로 다른 표시를 사용합니다. 카운터 기반 방식 등 다른 구현은 더 읽기 장에서 비교하며 이 레슨에서는 불규칙한 입력 간격에도 시간 차 기준을 지키는 구현을 제출합니다.
따라하기
후보 시각 추적
마지막 튐 이후 19/20ms 차이를 확인합니다.
candidate = stable = 0
since = 0
for t, level in [(0,1),(3,0),(8,1),(27,1),(28,1)]:
if level != candidate:
candidate, since = level, t
event = "NONE"
if candidate != stable and t - since >= 20:
stable = candidate
event = "PRESSED" if stable else "RELEASED"
print(t, candidate, since, stable, event)실행 결과
0 1 0 0 NONE 3 0 3 0 NONE 8 1 8 0 NONE 27 1 8 0 NONE 28 1 8 1 PRESSED
호출 수와 시간 차 비교
간격이 큰 두 관측의 확인 시각을 봅니다.
since = 8
for t in (19, 27, 35):
print(t, t - since, t - since >= 20)실행 결과
19 11 False 27 19 False 35 27 True
에지와 유지 상태 구분
stable을 확정한 뒤 동일한 상태에서는 재발행하지 않습니다.
stable = 0
for t, candidate in [(28,1),(100,1),(128,0),(170,1)]:
event = "NONE"
if candidate != stable:
stable = candidate
event = "PRESSED" if stable else "RELEASED"
print(t, stable, event)실행 결과
28 1 PRESSED 100 1 NONE 128 0 RELEASED 170 1 PRESSED
시간표 채점
브라우저 starter의 filter_events를 구현합니다. 첫 시험의 5개 행과 짧은 펄스·재눌림 시험을 실행하고 후보 시각을 손으로 적은 표와 비교합니다.
확인 문제
실습
filter_events의 후보·시작 시각·안정값 갱신을 구현합니다. 첫 줄 N(0~1000), 다음 N줄 t level입니다. t는 0~4294967295이며 엄격히 증가, level은 0·1입니다. 초기 안정 0, 후보 변경 후 20ms 이상 유지된 최초 관측에서 t PRESSED 또는 t RELEASED를 출력합니다. 행 사이 레벨 유지 가정이며 EOF는 시간을 추가하지 않습니다. 전이 없으면 NONE, 입력 오류는 ERROR 한 줄만 냅니다.
모범 답안
import sys
def read_rows(text):
lines = text.splitlines()
if not lines:
raise ValueError()
n = int(lines[0])
if not 0 <= n <= 1000 or len(lines) != n + 1:
raise ValueError()
rows = []
previous = -1
for line in lines[1:]:
parts = line.split()
if len(parts) != 2:
raise ValueError()
t, level = map(int, parts)
if not 0 <= t <= 4294967295 or t <= previous or level not in (0, 1):
raise ValueError()
rows.append((t, level))
previous = t
return rows
def filter_events(rows):
stable = candidate = 0
since = 0
events = []
for t, level in rows:
if level != candidate:
candidate = level
since = t
if candidate != stable and t - since >= 20:
stable = candidate
events.append(f"{t} {'PRESSED' if stable else 'RELEASED'}")
return events
try:
events = filter_events(read_rows(sys.stdin.read()))
print("\n".join(events) if events else "NONE")
except ValueError:
print("ERROR")
더 읽기
면접 질문
- 버튼을 한 번 눌렀는데 여러 번 감지되는 이유를 설명해 주시면 됩니다.