sheets.tpz
// Mini spreadsheet. Parses `<cell> <value>` lines, builds a cell dependency graph, evaluates
// formulas in topological order, detects circular references, renders the grid, and serializes
// the sheet to JSON. Written in Topaz. Deterministic and offline.
// Key identifiers: 셀=cell, 평가=evaluated result, 토큰=token, 줄결과=parse-with-position,
// 열=column, 행=row, 참조=ref like A1, 원본=raw text, 종류=kind, 수=number, 글=text,
// 오류=error, 토큰들=tokens, 위치=position, 결과들=results, 셀들=cells, 의존=dependency,
// 진입차수=in-degree, 인접=adjacency, 대기열=queue, 순서=topo order, 표시=display,
// 글자정리=escapeChar, 줄정리=escapeText, 숫자파싱=parseNumber, 참조파싱=parseRef.
//
// 미니 스프레드시트. `<셀> <값>` 줄을 읽어 셀 의존성 그래프를 만들고, 수식을 위상 순서로
// 계산하며, 순환 참조를 찾아 알리고, 표를 그리고, 시트를 JSON으로 직렬화합니다.
// A mini spreadsheet that evaluates formulas through a dependency graph in topological order.
// 줄마다 `A1 10`, `A3 =A1+A2`, `B1 =SUM(A1:A3)` 처럼 적습니다. `=`로 시작하면 수식입니다.
// One cell per line like `A1 10`, `A3 =A1+A2`, `B1 =SUM(A1:A3)`. A leading `=` marks a formula.
// 함수: SUM, AVERAGE, MIN, MAX, COUNT, ABS, IF. 비교: > < >= <= = <>.
// Functions: SUM, AVERAGE, MIN, MAX, COUNT, ABS, IF. Comparisons: > < >= <= = <>.
// 안 하는 것: 엑셀 호환, 셀 편집 UI, 차트, 문자열 함수, 매우 큰 표, 복사 시 참조 재작성, 반올림 함수.
// Won't do: Excel compatibility, cell-editing UI, charts, string functions, very large grids,
// reference rewriting on copy, a rounding function.
// 빈 셀 참조는 0으로 봅니다. 같은 셀을 두 번 적으면 마지막 정의가 이깁니다.
// An empty-cell reference reads as 0. A duplicate cell definition keeps the last one.
type 셀 = { 열: int, 행: int, 참조: string, 원본: string }
// 닫힌 태그들. 셀 이름과 함수 원문은 입력 경계라 string으로 두고, 내부 분기 태그만 좁힌다.
// Closed tags. Cell names and raw function text stay string at the input boundary; internal dispatch tags are narrowed.
type 평가종류 = "수" | "글" | "빈" | "오류"
type 토큰종류 = "수" | "셀" | "함수" | "비교" | "연산" | "괄호열" | "괄호닫" | "콤마" | "콜론"
type 비교연산 = ">" | "<" | ">=" | "<=" | "=" | "<>"
type 덧셈연산 = "+" | "-"
type 곱셈연산 = "*" | "/"
type 함수종류 = "SUM" | "COUNT" | "AVERAGE" | "MIN" | "MAX" | "ABS" | "IF"
type 평가 = { 종류: 평가종류, 수: float, 글: string, 오류: string }
type 토큰 = { 종류: 토큰종류, 글: string }
type 줄결과 = { 값: float, 위치: int, 오류: string } // evaluator carries value, next position, error
// 레코드 리터럴은 문자열 리터럴을 넓히므로 태그 있는 레코드는 헬퍼에서 만든다.
// Record literals widen string literals, so tagged records are built through helpers.
function 토큰만들기(종류: 토큰종류, 글: string) -> 토큰 {
{ 종류: 종류, 글: 글 }
}
function 평가만들기(종류: 평가종류, 수: float, 글: string, 오류: string) -> 평가 {
{ 종류: 종류, 수: 수, 글: 글, 오류: 오류 }
}
function 빈평가() -> 평가 { 평가만들기("빈", 0.0, "", "") }
function 수평가(수: float) -> 평가 { 평가만들기("수", 수, "", "") }
function 글평가(글: string) -> 평가 { 평가만들기("글", 0.0, 글, "") }
function 오류평가(오류: string) -> 평가 { 평가만들기("오류", 0.0, "", 오류) }
function 글자정리(글: string) -> string {
if 글 == "&" { "&" }
else if 글 == "<" { "<" }
else if 글 == ">" { ">" }
else if 글 == "\"" { """ }
else { 글 }
}
function 줄정리(원문: string) -> string {
let mut 출력 = ""
for 글 in 원문.scalars() { 출력 = 출력 + 글자정리(글) }
출력
}
function 숫자인가(글: string) -> bool {
let 코 = 글.codePointAt(0) ?? 0
코 >= 48 && 코 <= 57
}
function 영문인가(글: string) -> bool {
let 코 = 글.codePointAt(0) ?? 0
(코 >= 65 && 코 <= 90) || (코 >= 97 && 코 <= 122)
}
function 대문자글(글: string) -> string {
let 코 = 글.codePointAt(0) ?? 0
if 코 >= 97 && 코 <= 122 { fromCodePoint(코 - 32) ?? 글 } else { 글 }
}
function 비교연산파싱(글: string) -> Result<비교연산, string> {
match 글 {
case ">" => Ok(">")
case "<" => Ok("<")
case ">=" => Ok(">=")
case "<=" => Ok("<=")
case "=" => Ok("=")
case "<>" => Ok("<>")
case _ => Err("알 수 없는 비교 연산자: " + 글)
}
}
function 비교참(연: 비교연산, 왼: float, 오: float) -> bool {
match 연 {
case ">" => 왼 > 오
case "<" => 왼 < 오
case ">=" => 왼 >= 오
case "<=" => 왼 <= 오
case "=" => 왼 == 오
case "<>" => 왼 != 오
}
}
function 덧셈연산파싱(글: string) -> Result<덧셈연산, string> {
match 글 {
case "+" => Ok("+")
case "-" => Ok("-")
case _ => Err("덧셈 연산자가 필요합니다")
}
}
function 덧셈계산(연: 덧셈연산, 왼: float, 오: float) -> float {
match 연 {
case "+" => 왼 + 오
case "-" => 왼 - 오
}
}
function 곱셈연산파싱(글: string) -> Result<곱셈연산, string> {
match 글 {
case "*" => Ok("*")
case "/" => Ok("/")
case _ => Err("곱셈 연산자가 필요합니다")
}
}
function 곱셈계산(연: 곱셈연산, 왼: float, 오: float) -> float {
match 연 {
case "*" => 왼 * 오
case "/" => 왼 / 오
}
}
function 함수종류파싱(이름: string) -> Result<함수종류, string> {
match 이름 {
case "SUM" => Ok("SUM")
case "COUNT" => Ok("COUNT")
case "AVERAGE" => Ok("AVERAGE")
case "MIN" => Ok("MIN")
case "MAX" => Ok("MAX")
case "ABS" => Ok("ABS")
case "IF" => Ok("IF")
case _ => Err("알 수 없는 함수: " + 이름)
}
}
function 집계함수인가(함수: 함수종류) -> bool {
match 함수 {
case "SUM" => true
case "COUNT" => true
case "AVERAGE" => true
case "MIN" => true
case "MAX" => true
case "ABS" => false
case "IF" => false
}
}
// 정수 거듭제곱(10^n)을 float로. round/floor 없이 자릿수 나눗셈에 씁니다.
// 10^n as a float, used for decimal place division without a round/floor builtin.
function 십승(엔: int) -> float {
let mut 값 = 1.0
let mut 회 = 0
while 회 < 엔 { 값 = 값 * 10.0; 회 = 회 + 1 }
값
}
// 문자열을 float로 파싱합니다. 정수, 소수, 앞의 음수 부호를 허용합니다. 실패하면 오류.
// Parse a string to a float. Allows an integer, a decimal, and a leading minus. Errors on failure.
function 숫자파싱(원: string) -> 줄결과 {
let 글자들 = 원.scalars()
if 글자들.length == 0 { return { 값: 0.0, 위치: 0, 오류: "빈 수" } }
let mut 부호 = 1.0
let mut 시 = 0
if 글자들[0] == "-" { 부호 = -1.0; 시 = 1 }
else if 글자들[0] == "+" { 시 = 1 }
let mut 정수부 = ""
let mut 소수부 = ""
let mut 점봤나 = false
let mut 자리 = 시
let mut 글자있나 = false
while 자리 < 글자들.length {
let 글 = 글자들[자리]
if 글 == "." {
if 점봤나 { return { 값: 0.0, 위치: 0, 오류: "잘못된 수: {원}" } }
점봤나 = true
} else if 숫자인가(글) {
글자있나 = true
if 점봤나 { 소수부 = 소수부 + 글 } else { 정수부 = 정수부 + 글 }
} else {
return { 값: 0.0, 위치: 0, 오류: "잘못된 수: {원}" }
}
자리 = 자리 + 1
}
if !글자있나 { return { 값: 0.0, 위치: 0, 오류: "잘못된 수: {원}" } }
let 정수값 = if 정수부 == "" { 0 } else { toInt(정수부) ?? 0 }
let mut 값 = toFloat(정수값)
if 소수부 != "" {
let 소수값 = toInt(소수부) ?? 0
값 = 값 + toFloat(소수값) / 십승(소수부.scalars().length)
}
{ 값: 부호 * 값, 위치: 0, 오류: "" }
}
// float를 표시용 문자열로. 정수면 정수로, 아니면 소수 최대 4자리까지(버림) 보입니다.
// Format a float for display: integers as integers, otherwise up to 4 decimal places (truncated).
function 표시(엑스: float) -> string {
let 원 = "{엑스}"
let 조각 = 원.split(".")
if 조각.length < 2 { return 원 }
let 정수 = 조각[0]
let 소수 = 조각[1]
// 모두 0이면 정수만.
// If the fraction is all zeros, show the integer only.
let mut 영뿐 = true
for 글 in 소수.scalars() { if 글 != "0" { 영뿐 = false } }
if 영뿐 { return 정수 }
let mut 자른소수 = ""
let mut 개수 = 0
for 글 in 소수.scalars() {
if 개수 < 4 { 자른소수 = 자른소수 + 글; 개수 = 개수 + 1 }
}
// 끝자리 0 제거.
// Strip trailing zeros.
let 자글 = 자른소수.scalars()
let mut 끝 = 자글.length
while 끝 > 0 && 자글[끝 - 1] == "0" { 끝 = 끝 - 1 }
if 끝 == 0 { return 정수 }
let mut 마무리 = ""
let mut 부 = 0
while 부 < 끝 { 마무리 = 마무리 + 자글[부]; 부 = 부 + 1 }
정수 + "." + 마무리
}
// 열 색인을 문자로(0->A, 25->Z, 26->AA). 표 머리글에 씁니다.
// Column index to letters (0->A, 25->Z, 26->AA). Used for table headers.
function 열문자(열: int) -> string {
let mut 값 = 열
let mut 결과 = ""
let mut 계속 = true
while 계속 {
let 나머지 = 값 - (값 / 26) * 26
결과 = (fromCodePoint(65 + 나머지) ?? "A") + 결과
값 = 값 / 26 - 1
if 값 < 0 { 계속 = false }
}
결과
}
// "A1" 같은 참조를 (열, 행)으로. 문자 뒤 숫자라야 합니다. 행은 1부터.
// Parse a ref like "A1" into (column, row). Letters then digits. Row is 1-based.
function 참조파싱(참조: string) -> { 열: int, 행: int, 오류: string } {
let 글자들 = 참조.scalars()
if 글자들.length == 0 { return { 열: 0, 행: 0, 오류: "빈 참조" } }
let mut 자리 = 0
let mut 열 = 0
let mut 문자수 = 0
while 자리 < 글자들.length && 영문인가(글자들[자리]) {
let 대 = 대문자글(글자들[자리])
let 코 = 대.codePointAt(0) ?? 0
열 = 열 * 26 + (코 - 65 + 1)
문자수 = 문자수 + 1
자리 = 자리 + 1
}
if 문자수 == 0 { return { 열: 0, 행: 0, 오류: "잘못된 참조: {참조}" } }
let mut 숫자수 = 0
let mut 행 = 0
while 자리 < 글자들.length && 숫자인가(글자들[자리]) {
행 = 행 * 10 + (글자들[자리].codePointAt(0) ?? 48) - 48
숫자수 = 숫자수 + 1
자리 = 자리 + 1
}
if 숫자수 == 0 || 자리 != 글자들.length || 행 == 0 {
return { 열: 0, 행: 0, 오류: "잘못된 참조: {참조}" }
}
{ 열: 열 - 1, 행: 행, 오류: "" }
}
// 참조를 표준형으로. "a01" "A01" "A1"은 모두 같은 칸 "A1"이 됩니다(열 대문자 + 정수 행).
// 파싱 실패면 대문자만 적용해 돌려줍니다(검증은 호출부에서).
// Canonicalize a ref: "a01", "A01", "A1" all become the same cell "A1" (uppercase column + integer row).
// If it does not parse, just uppercase it (the caller validates).
function 참조정규화(참조: string) -> string {
let 검 = 참조파싱(참조)
if 검.오류 != "" {
let mut 출 = ""
for 글 in 참조.scalars() { 출 = 출 + 대문자글(글) }
return 출
}
열문자(검.열) + "{검.행}"
}
// 앞 엔 글자를 뺀 나머지 문자열. s.slice는 스칼라 인덱스를 받는다.
// The substring after dropping the first 엔 characters. s.slice is scalar-indexed.
function 꼬리(원: string, 엔: int) -> string {
let 글자들 = 원.scalars()
글자들.slice(엔, 글자들.length).join("")
}
function 셀찾기(참조: string, 셀들: Array<셀>) -> int {
let mut 자리 = 0
while 자리 < 셀들.length {
if 셀들[자리].참조 == 참조 { return 자리 }
자리 = 자리 + 1
}
-1
}
// 수식을 토큰으로 나눕니다. 수, 셀참조, 함수이름, 연산자, 비교, 괄호, 콤마, 콜론.
// Tokenize a formula: numbers, cell refs, function names, operators, comparisons, parens, comma, colon.
function 토큰화(식: string) -> { 토큰들: Array<토큰>, 오류: string } {
let 글자들 = 식.scalars()
let mut 토큰들: Array<토큰> = []
let mut 자리 = 0
while 자리 < 글자들.length {
let 글 = 글자들[자리]
if 글 == " " || 글 == "\t" {
자리 = 자리 + 1
} else if 숫자인가(글) || 글 == "." {
let mut 수글 = ""
while 자리 < 글자들.length && (숫자인가(글자들[자리]) || 글자들[자리] == ".") {
수글 = 수글 + 글자들[자리]
자리 = 자리 + 1
}
토큰들.push(토큰만들기("수", 수글))
} else if 영문인가(글) {
let mut 단어 = ""
while 자리 < 글자들.length && 영문인가(글자들[자리]) {
단어 = 단어 + 대문자글(글자들[자리])
자리 = 자리 + 1
}
let mut 숫자 = ""
while 자리 < 글자들.length && 숫자인가(글자들[자리]) {
숫자 = 숫자 + 글자들[자리]
자리 = 자리 + 1
}
if 숫자 != "" {
토큰들.push(토큰만들기("셀", 단어 + 숫자))
} else {
토큰들.push(토큰만들기("함수", 단어))
}
} else if 글 == ">" || 글 == "<" {
자리 = 자리 + 1
if 자리 < 글자들.length && 글자들[자리] == "=" {
토큰들.push(토큰만들기("비교", 글 + "="))
자리 = 자리 + 1
} else if 글 == "<" && 자리 < 글자들.length && 글자들[자리] == ">" {
토큰들.push(토큰만들기("비교", "<>"))
자리 = 자리 + 1
} else {
토큰들.push(토큰만들기("비교", 글))
}
} else if 글 == "=" {
토큰들.push(토큰만들기("비교", "="))
자리 = 자리 + 1
} else if 글 == "+" || 글 == "-" || 글 == "*" || 글 == "/" {
토큰들.push(토큰만들기("연산", 글))
자리 = 자리 + 1
} else if 글 == "(" {
토큰들.push(토큰만들기("괄호열", "("))
자리 = 자리 + 1
} else if 글 == ")" {
토큰들.push(토큰만들기("괄호닫", ")"))
자리 = 자리 + 1
} else if 글 == "," {
토큰들.push(토큰만들기("콤마", ","))
자리 = 자리 + 1
} else if 글 == ":" {
토큰들.push(토큰만들기("콜론", ":"))
자리 = 자리 + 1
} else {
return { 토큰들: 토큰들, 오류: "알 수 없는 문자: {글}" }
}
}
{ 토큰들: 토큰들, 오류: "" }
}
// 범위 A1:A3을 셀 참조 목록으로 펼칩니다(사각형).
// Expand a range A1:A3 into a list of cell refs (rectangular).
function 범위펼치기(시작참조: string, 끝참조: string) -> { 참조들: Array<string>, 오류: string } {
let 가 = 참조파싱(시작참조)
let 나 = 참조파싱(끝참조)
let mut 참조들: Array<string> = []
if 가.오류 != "" { return { 참조들: 참조들, 오류: 가.오류 } }
if 나.오류 != "" { return { 참조들: 참조들, 오류: 나.오류 } }
// 끝점이 뒤집힌 범위(B2:A1)는 표준 시트처럼 정규화한다(A1:B2). 의도된 동작이라 거부하지 않는다.
// A reversed range (B2:A1) is normalized like a standard spreadsheet (A1:B2). Intentional, not rejected.
let mut 열로 = if 가.열 < 나.열 { 가.열 } else { 나.열 }
let 열까지 = if 가.열 < 나.열 { 나.열 } else { 가.열 }
let mut 행시작 = if 가.행 < 나.행 { 가.행 } else { 나.행 }
let 행끝 = if 가.행 < 나.행 { 나.행 } else { 가.행 }
while 열로 <= 열까지 {
let mut 행 = 행시작
while 행 <= 행끝 {
참조들.push(열문자(열로) + "{행}")
행 = 행 + 1
}
열로 = 열로 + 1
}
{ 참조들: 참조들, 오류: "" }
}
// 수식 토큰에서 참조하는 셀들을 모읍니다(범위는 펼침). 의존성 그래프용.
// Collect referenced cells from formula tokens (ranges expanded). For the dependency graph.
function 참조수집(토큰들: Array<토큰>) -> Array<string> {
let mut 모음: Array<string> = []
let mut 자리 = 0
while 자리 < 토큰들.length {
if 토큰들[자리].종류 == "셀" {
let 이참조 = 토큰들[자리].글
if 자리 + 2 < 토큰들.length && 토큰들[자리 + 1].종류 == "콜론" && 토큰들[자리 + 2].종류 == "셀" {
let 펼친 = 범위펼치기(이참조, 토큰들[자리 + 2].글)
for 참 in 펼친.참조들 {
if 모음.indexOf(참) == None { 모음.push(참) }
}
자리 = 자리 + 3
} else {
let 정규 = 참조정규화(이참조)
if 모음.indexOf(정규) == None { 모음.push(정규) }
자리 = 자리 + 1
}
} else {
자리 = 자리 + 1
}
}
모음
}
// 셀 한 칸의 수치값을 읽습니다. 빈 셀은 0, 텍스트나 오류 셀은 오류로 전파합니다.
// Read one cell's numeric value. Empty reads as 0; a text or errored cell propagates an error.
function 셀값(참조: string, 셀들: Array<셀>, 결과들: Array<평가>, 구조: bool) -> 줄결과 {
// 직접 참조도 범위처럼 검증합니다. 잘못된 참조(예: 행이 0인 A0)는 0이 아니라 오류.
// Validate a direct ref like ranges do; a bad ref (e.g. A0 with row 0) errors instead of 0.
let 검 = 참조파싱(참조)
if 검.오류 != "" { return { 값: 0.0, 위치: 0, 오류: 검.오류 } }
// 구조 검증 모드: 유효한 참조는 값과 무관하게 더미 1로 봅니다.
// Structural mode: a valid ref resolves to a dummy 1, independent of cell values.
if 구조 { return { 값: 1.0, 위치: 0, 오류: "" } }
let 색인 = 셀찾기(참조정규화(참조), 셀들)
if 색인 < 0 { return { 값: 0.0, 위치: 0, 오류: "" } }
let 결 = 결과들[색인]
match 결.종류 {
case "오류" => { 값: 0.0, 위치: 0, 오류: "참조 셀 오류 {참조}" }
case "글" => { 값: 0.0, 위치: 0, 오류: "텍스트 셀 {참조}" }
case "빈" => { 값: 0.0, 위치: 0, 오류: "" }
case "수" => { 값: 결.수, 위치: 0, 오류: "" }
}
}
// 함수 인자로 들어온 범위/셀들의 수치 목록을 모읍니다. 빈 칸은 건너뜁니다.
// Gather the numeric values of a function argument that is a range or single cell. Empty cells skipped.
function 범위값들(시작: string, 끝: string, 셀들: Array<셀>, 결과들: Array<평가>, 구조: bool) -> { 값들: Array<float>, 오류: string } {
let 펼친 = 범위펼치기(시작, 끝)
let mut 값들: Array<float> = []
if 펼친.오류 != "" { return { 값들: 값들, 오류: 펼친.오류 } }
// 구조 검증 모드: 범위가 유효하면 더미 1 한 개로 빈 집계 오류를 피합니다.
// Structural mode: a valid range yields one dummy so an empty-aggregate value error cannot fire.
if 구조 { 값들.push(1.0); return { 값들: 값들, 오류: "" } }
for 참 in 펼친.참조들 {
let 색인 = 셀찾기(참, 셀들)
if 색인 >= 0 {
let 결 = 결과들[색인]
match 결.종류 {
case "오류" => return { 값들: 값들, 오류: "참조 셀 오류 {참}" }
case "수" => { 값들.push(결.수) }
case "글" => {}
case "빈" => {}
}
}
}
{ 값들: 값들, 오류: "" }
}
// 재귀하강 평가기. 비교 < 덧셈 < 곱셈 < 단항 < 기본 순으로 우선순위가 올라갑니다.
// 각 함수는 {값, 다음 위치, 오류}를 돌려줍니다(오류가 있으면 위로 전파).
// Recursive-descent evaluator. Precedence rises comparison < add < mul < unary < primary.
// Each function returns {value, next position, error}; an error propagates upward.
function 비교식(토큰들: Array<토큰>, 위치: int, 셀들: Array<셀>, 결과들: Array<평가>, 구조: bool) -> 줄결과 {
let 왼 = 덧셈식(토큰들, 위치, 셀들, 결과들, 구조)
if 왼.오류 != "" { return 왼 }
let mut 위 = 왼.위치
if 위 < 토큰들.length && 토큰들[위].종류 == "비교" {
let 연 = match 비교연산파싱(토큰들[위].글) {
case Ok(ㅇ) => ㅇ
case Err(메) => return { 값: 0.0, 위치: 위, 오류: 메 }
}
let 오 = 덧셈식(토큰들, 위 + 1, 셀들, 결과들, 구조)
if 오.오류 != "" { return 오 }
let 참 = 비교참(연, 왼.값, 오.값)
let 값 = if 참 { 1.0 } else { 0.0 }
return { 값: 값, 위치: 오.위치, 오류: "" }
}
왼
}
function 덧셈식(토큰들: Array<토큰>, 위치: int, 셀들: Array<셀>, 결과들: Array<평가>, 구조: bool) -> 줄결과 {
let mut 누적 = 곱셈식(토큰들, 위치, 셀들, 결과들, 구조)
if 누적.오류 != "" { return 누적 }
let mut 위 = 누적.위치
while 위 < 토큰들.length && 토큰들[위].종류 == "연산" && (토큰들[위].글 == "+" || 토큰들[위].글 == "-") {
let 연 = match 덧셈연산파싱(토큰들[위].글) {
case Ok(ㅇ) => ㅇ
case Err(메) => return { 값: 0.0, 위치: 위, 오류: 메 }
}
let 오 = 곱셈식(토큰들, 위 + 1, 셀들, 결과들, 구조)
if 오.오류 != "" { return 오 }
let 값 = 덧셈계산(연, 누적.값, 오.값)
누적 = { 값: 값, 위치: 오.위치, 오류: "" }
위 = 오.위치
}
누적
}
function 곱셈식(토큰들: Array<토큰>, 위치: int, 셀들: Array<셀>, 결과들: Array<평가>, 구조: bool) -> 줄결과 {
let mut 누적 = 단항식(토큰들, 위치, 셀들, 결과들, 구조)
if 누적.오류 != "" { return 누적 }
let mut 위 = 누적.위치
while 위 < 토큰들.length && 토큰들[위].종류 == "연산" && (토큰들[위].글 == "*" || 토큰들[위].글 == "/") {
let 연 = match 곱셈연산파싱(토큰들[위].글) {
case Ok(ㅇ) => ㅇ
case Err(메) => return { 값: 0.0, 위치: 위, 오류: 메 }
}
let 오 = 단항식(토큰들, 위 + 1, 셀들, 결과들, 구조)
if 오.오류 != "" { return 오 }
if !구조 && 연 == "/" && 오.값 == 0.0 { return { 값: 0.0, 위치: 오.위치, 오류: "0으로 나눌 수 없습니다" } }
let 값 = if 구조 { 1.0 } else { 곱셈계산(연, 누적.값, 오.값) }
누적 = { 값: 값, 위치: 오.위치, 오류: "" }
위 = 오.위치
}
누적
}
function 단항식(토큰들: Array<토큰>, 위치: int, 셀들: Array<셀>, 결과들: Array<평가>, 구조: bool) -> 줄결과 {
if 위치 < 토큰들.length && 토큰들[위치].종류 == "연산" && 토큰들[위치].글 == "-" {
let 안 = 단항식(토큰들, 위치 + 1, 셀들, 결과들, 구조)
if 안.오류 != "" { return 안 }
return { 값: 0.0 - 안.값, 위치: 안.위치, 오류: "" }
}
if 위치 < 토큰들.length && 토큰들[위치].종류 == "연산" && 토큰들[위치].글 == "+" {
return 단항식(토큰들, 위치 + 1, 셀들, 결과들, 구조)
}
기본식(토큰들, 위치, 셀들, 결과들, 구조)
}
function 기본식(토큰들: Array<토큰>, 위치: int, 셀들: Array<셀>, 결과들: Array<평가>, 구조: bool) -> 줄결과 {
if 위치 >= 토큰들.length { return { 값: 0.0, 위치: 위치, 오류: "수식이 끝났습니다" } }
let 토 = 토큰들[위치]
match 토.종류 {
case "수" => {
let 파 = 숫자파싱(토.글)
if 파.오류 != "" { return { 값: 0.0, 위치: 위치, 오류: 파.오류 } }
return { 값: 파.값, 위치: 위치 + 1, 오류: "" }
}
case "괄호열" => {
let 안 = 비교식(토큰들, 위치 + 1, 셀들, 결과들, 구조)
if 안.오류 != "" { return 안 }
if 안.위치 >= 토큰들.length || 토큰들[안.위치].종류 != "괄호닫" {
return { 값: 0.0, 위치: 안.위치, 오류: "괄호가 닫히지 않았습니다" }
}
return { 값: 안.값, 위치: 안.위치 + 1, 오류: "" }
}
case "셀" => {
if 위치 + 1 < 토큰들.length && 토큰들[위치 + 1].종류 == "콜론" {
return { 값: 0.0, 위치: 위치, 오류: "범위는 함수 안에서만 쓸 수 있습니다" }
}
let 값 = 셀값(토.글, 셀들, 결과들, 구조)
if 값.오류 != "" { return { 값: 0.0, 위치: 위치, 오류: 값.오류 } }
return { 값: 값.값, 위치: 위치 + 1, 오류: "" }
}
case "함수" => return 함수평가(토.글, 토큰들, 위치 + 1, 셀들, 결과들, 구조)
case "비교" => {}
case "연산" => {}
case "괄호닫" => {}
case "콤마" => {}
case "콜론" => {}
}
{ 값: 0.0, 위치: 위치, 오류: "예상치 못한 토큰: {토.글}" }
}
function 함수평가(이름: string, 토큰들: Array<토큰>, 시작: int, 셀들: Array<셀>, 결과들: Array<평가>, 구조: bool) -> 줄결과 {
if 시작 >= 토큰들.length || 토큰들[시작].종류 != "괄호열" {
return { 값: 0.0, 위치: 시작, 오류: "{이름} 뒤에 (가 필요합니다" }
}
let mut 위 = 시작 + 1
let mut 값들: Array<float> = []
let mut 인자수 = 0
let mut 첫 = true
let mut 계속 = true
while 계속 {
if 위 >= 토큰들.length { return { 값: 0.0, 위치: 위, 오류: "괄호가 닫히지 않았습니다" } }
if 토큰들[위].종류 == "괄호닫" { 위 = 위 + 1; 계속 = false }
else {
if !첫 {
if 토큰들[위].종류 == "콤마" { 위 = 위 + 1 }
else { return { 값: 0.0, 위치: 위, 오류: "인자 사이에 콤마가 필요합니다" } }
}
첫 = false
인자수 = 인자수 + 1
if 위 + 2 < 토큰들.length && 토큰들[위].종류 == "셀" && 토큰들[위 + 1].종류 == "콜론" && 토큰들[위 + 2].종류 == "셀" {
let 벌 = 범위값들(토큰들[위].글, 토큰들[위 + 2].글, 셀들, 결과들, 구조)
if 벌.오류 != "" { return { 값: 0.0, 위치: 위, 오류: 벌.오류 } }
for ㄱ in 벌.값들 { 값들.push(ㄱ) }
위 = 위 + 3
} else {
let 부분 = 비교식(토큰들, 위, 셀들, 결과들, 구조)
if 부분.오류 != "" { return 부분 }
값들.push(부분.값)
위 = 부분.위치
}
}
}
let 함수 = match 함수종류파싱(이름) {
case Ok(ㅎ) => ㅎ
case Err(메) => return { 값: 0.0, 위치: 위, 오류: 메 }
}
// 집계 함수는 인자가 최소 하나 필요하다(=SUM()는 오류). 빈 인자 목록을 구조 오류로 본다.
// Aggregate functions need at least one argument (=SUM() errors); an empty arg list is a structural error.
if 집계함수인가(함수) && 인자수 == 0 {
return { 값: 0.0, 위치: 위, 오류: "{이름}은 인자가 필요합니다" }
}
match 함수 {
case "SUM" => {
let mut 합 = 0.0
for ㄱ in 값들 { 합 = 합 + ㄱ }
return { 값: 합, 위치: 위, 오류: "" }
}
case "COUNT" => return { 값: toFloat(값들.length), 위치: 위, 오류: "" }
case "AVERAGE" => {
if 값들.length == 0 { return { 값: 0.0, 위치: 위, 오류: "AVERAGE 대상이 없습니다" } }
let mut 합 = 0.0
for ㄱ in 값들 { 합 = 합 + ㄱ }
return { 값: 합 / toFloat(값들.length), 위치: 위, 오류: "" }
}
case "MIN" => {
if 값들.length == 0 { return { 값: 0.0, 위치: 위, 오류: "MIN 대상이 없습니다" } }
let mut 작 = 값들[0]
for ㄱ in 값들 { if ㄱ < 작 { 작 = ㄱ } }
return { 값: 작, 위치: 위, 오류: "" }
}
case "MAX" => {
if 값들.length == 0 { return { 값: 0.0, 위치: 위, 오류: "MAX 대상이 없습니다" } }
let mut 큰 = 값들[0]
for ㄱ in 값들 { if ㄱ > 큰 { 큰 = ㄱ } }
return { 값: 큰, 위치: 위, 오류: "" }
}
case "ABS" => {
if 값들.length != 1 { return { 값: 0.0, 위치: 위, 오류: "ABS는 인자 1개입니다" } }
let ㄱ = 값들[0]
return { 값: if ㄱ < 0.0 { 0.0 - ㄱ } else { ㄱ }, 위치: 위, 오류: "" }
}
case "IF" => {
if 값들.length != 3 { return { 값: 0.0, 위치: 위, 오류: "IF는 인자 3개입니다" } }
return { 값: if 값들[0] != 0.0 { 값들[1] } else { 값들[2] }, 위치: 위, 오류: "" }
}
}
}
function 순환소속(시작: int, 인접: Array<Array<int>>, 개수: int) -> bool {
let mut 방문: Array<bool> = []
let mut 초기 = 0
while 초기 < 개수 { 방문.push(false); 초기 = 초기 + 1 }
let mut 큐: Array<int> = []
for 다음 in 인접[시작] { 큐.push(다음) }
let mut 머 = 0
while 머 < 큐.length {
let 현 = 큐[머]
머 = 머 + 1
if 현 == 시작 { return true }
if !방문[현] { 방문[현] = true; for 다음 in 인접[현] { 큐.push(다음) } }
}
false
}
function 제이슨문자(글: string) -> string {
if 글 == "\"" { "\\\"" }
else if 글 == "\\" { "\\\\" }
else if 글 == "\n" { "\\n" }
else if 글 == "\t" { "\\t" }
else { 글 }
}
function 제이슨값(원: string) -> string {
let mut 출 = ""
for 글 in 원.scalars() { 출 = 출 + 제이슨문자(글) }
출
}
function 보기(입력: string) -> string {
let 줄들 = 입력.split("\n")
let mut 셀들: Array<셀> = []
let mut 줄번호 = 0
for 줄 in 줄들 {
줄번호 = 줄번호 + 1
let 다듬 = 줄.trim()
if 다듬 != "" {
let 부분들 = 다듬.split(" ")
if 부분들.length < 2 {
return "<div class=\"err\">{줄번호}번째 줄: 형식은 `셀 값` 입니다. 예: A1 10</div>"
}
let 참조 = 참조정규화(부분들[0])
let 값부 = 부분들.slice(1, 부분들.length).join(" ")
let 파 = 참조파싱(참조)
if 파.오류 != "" {
return "<div class=\"err\">{줄번호}번째 줄: {파.오류}</div>"
}
let 기존 = 셀찾기(참조, 셀들)
if 기존 >= 0 {
셀들[기존] = 셀들[기존]{ 원본: 값부 }
} else {
셀들.push({ 열: 파.열, 행: 파.행, 참조: 참조, 원본: 값부 })
}
}
}
let 개수 = 셀들.length
if 개수 == 0 {
return "<div class=\"meta\">셀을 입력하세요. 예: A1 10 / A2 =A1*2</div>"
}
// 의존성 그래프. 간선 = 선행 셀 -> 의존 셀. 진입차수 = 그 셀이 참조하는 기존 셀 수.
// Dependency graph. Edge = precedent -> dependent. In-degree = number of existing cells it references.
let mut 진입차수: Array<int> = []
let mut 인접: Array<Array<int>> = []
let mut 준비 = 0
while 준비 < 개수 {
진입차수.push(0)
let mut 빈: Array<int> = []
인접.push(빈)
준비 = 준비 + 1
}
// 구문 검사용 빈 스텁. 의존성 그래프를 만들기 전에 구문/참조 오류를 먼저 잡아,
// 잘못된 수식(예: 괄호 안 닫힘)이 자기참조처럼 보여 가짜 순환으로 보고되지 않게 합니다.
// An empty stub to catch syntax/ref errors before the dependency graph, so a malformed
// formula (e.g. an unclosed paren) is not misreported as a self-cycle.
let mut 스텁: Array<평가> = []
let mut 스초기 = 0
while 스초기 < 개수 { 스텁.push(빈평가()); 스초기 = 스초기 + 1 }
let mut 자리 = 0
while 자리 < 개수 {
let 원본 = 셀들[자리].원본
if 원본.scalars().length > 0 && 원본.scalars()[0] == "=" {
let 식 = 꼬리(원본, 1)
let 토 = 토큰화(식)
// 그래프 포함 여부는 구문/참조 형태로만 판단한다(값 평가로 하지 않는다). 빈 스텁에서 값
// 의존 오류가 나도 잘 형성된 수식이면 포함해 위상 평가가 실제 선행 값으로 계산하게 한다.
// Decide graph inclusion by syntax/reference shape, not by value: a well-formed formula that
// errors on the empty stub (value-dependent) must still be graphed and evaluated for real.
let 구문올바름 = if 토.오류 != "" { false } else {
let 구검 = 비교식(토.토큰들, 0, 셀들, 스텁, true)
구검.오류 == "" && 구검.위치 == 토.토큰들.length
}
if 구문올바름 {
let 참조들 = 참조수집(토.토큰들)
for 참 in 참조들 {
let 선행 = 셀찾기(참, 셀들)
if 선행 >= 0 && 선행 != 자리 {
let mut 목록 = 인접[선행]
목록.push(자리)
인접[선행] = 목록
진입차수[자리] = 진입차수[자리] + 1
} else if 선행 == 자리 {
// 자기참조도 1개짜리 순환입니다. 자기 간선을 넣어야 순환소속이 잡습니다.
// A self-reference is a one-cell cycle. Add the self-edge so 순환소속 detects it.
let mut 목록 = 인접[자리]
목록.push(자리)
인접[자리] = 목록
진입차수[자리] = 진입차수[자리] + 1
}
}
}
}
자리 = 자리 + 1
}
// 위상 순서로 평가. 선행이 먼저 계산됩니다.
// Evaluate in topological order so precedents are computed first.
let mut 결과들: Array<평가> = []
let mut 결초기 = 0
while 결초기 < 개수 { 결과들.push(빈평가()); 결초기 = 결초기 + 1 }
let mut 대기열: Array<int> = []
let mut 첫 = 0
while 첫 < 개수 { if 진입차수[첫] == 0 { 대기열.push(첫) }; 첫 = 첫 + 1 }
let mut 머리 = 0
let mut 처리수 = 0
while 머리 < 대기열.length {
let 현재 = 대기열[머리]
머리 = 머리 + 1
처리수 = 처리수 + 1
결과들[현재] = 셀평가(셀들[현재].원본, 셀들, 결과들)
for 의 in 인접[현재] {
진입차수[의] = 진입차수[의] - 1
if 진입차수[의] == 0 { 대기열.push(의) }
}
}
let mut 순환목록 = ""
if 처리수 < 개수 {
let mut ㅅ = 0
while ㅅ < 개수 {
if 진입차수[ㅅ] > 0 {
if 순환소속(ㅅ, 인접, 개수) {
결과들[ㅅ] = 오류평가("순환")
if 순환목록 != "" { 순환목록 = 순환목록 + ", " }
순환목록 = 순환목록 + 줄정리(셀들[ㅅ].참조)
} else {
결과들[ㅅ] = 오류평가("순환에 의존")
}
}
ㅅ = ㅅ + 1
}
}
// 표 범위.
// Grid bounds.
let mut 최대열 = 0
let mut 최대행 = 0
let mut ㅂ = 0
while ㅂ < 개수 {
if 셀들[ㅂ].열 > 최대열 { 최대열 = 셀들[ㅂ].열 }
if 셀들[ㅂ].행 > 최대행 { 최대행 = 셀들[ㅂ].행 }
ㅂ = ㅂ + 1
}
let mut 출력 = ""
if 순환목록 != "" {
출력 = 출력 + "<div class=\"err\">순환 참조가 있습니다: {순환목록}</div>"
}
출력 = 출력 + "<table class=\"sheet\"><thead><tr><th></th>"
let mut 열i = 0
while 열i <= 최대열 { 출력 = 출력 + "<th>" + 열문자(열i) + "</th>"; 열i = 열i + 1 }
출력 = 출력 + "</tr></thead><tbody>"
let mut 행i = 1
while 행i <= 최대행 {
출력 = 출력 + "<tr><th>{행i}</th>"
let mut ㄷ = 0
while ㄷ <= 최대열 {
let 참 = 열문자(ㄷ) + "{행i}"
let 색 = 셀찾기(참, 셀들)
let mut 칸 = ""
if 색 >= 0 {
let 결 = 결과들[색]
match 결.종류 {
case "수" => { 칸 = 표시(결.수) }
case "글" => { 칸 = 줄정리(결.글) }
case "오류" => { 칸 = "<span class=\"bad\">오류: " + 줄정리(결.오류) + "</span>" }
case "빈" => {}
}
}
출력 = 출력 + "<td>" + 칸 + "</td>"
ㄷ = ㄷ + 1
}
출력 = 출력 + "</tr>"
행i = 행i + 1
}
출력 = 출력 + "</tbody></table>"
// JSON 직렬화(저장용). 원본 값/수식을 그대로 담습니다.
// JSON serialization (for saving). Holds the raw values and formulas.
let mut 제이 = "\{"
let mut ㅈ = 0
while ㅈ < 개수 {
if ㅈ > 0 { 제이 = 제이 + "," }
제이 = 제이 + "\"" + 제이슨값(셀들[ㅈ].참조) + "\":\"" + 제이슨값(셀들[ㅈ].원본) + "\""
ㅈ = ㅈ + 1
}
제이 = 제이 + "\}"
출력 = 출력 + "<div class=\"meta\">JSON 저장</div><pre class=\"out\">" + 줄정리(제이) + "</pre>"
출력
}
function 셀평가(원본: string, 셀들: Array<셀>, 결과들: Array<평가>) -> 평가 {
let 다듬 = 원본.trim()
if 다듬 == "" { return 빈평가() }
let 글자들 = 다듬.scalars()
if 글자들[0] == "=" {
let 식 = 꼬리(다듬, 1)
let 토 = 토큰화(식)
if 토.오류 != "" { return 오류평가(토.오류) }
if 토.토큰들.length == 0 { return 오류평가("빈 수식") }
// 값과 무관하게 구조를 먼저 검증합니다(구조=true). 그래야 0 나눗셈 같은 값 오류가
// 뒤의 구문 오류(예: 끝의 연산자)를 가리지 않습니다.
// Validate structure first, value-independent (구조=true), so a value error like a zero
// division never masks a following syntax error such as a trailing operator.
let 구검 = 비교식(토.토큰들, 0, 셀들, 결과들, true)
if 구검.오류 != "" { return 오류평가(구검.오류) }
if 구검.위치 != 토.토큰들.length { return 오류평가("수식이 올바르지 않습니다") }
// 구조가 올바르면 실제 값으로 계산합니다(구조=false).
// Structure is sound, so evaluate with real values (구조=false).
let 평 = 비교식(토.토큰들, 0, 셀들, 결과들, false)
if 평.오류 != "" { return 오류평가(평.오류) }
return 수평가(평.값)
}
let 파 = 숫자파싱(다듬)
if 파.오류 == "" { return 수평가(파.값) }
글평가(다듬)
}
print(보기(input()))