slow.monster

STUDIO HAZE · jeniapp

Topaz source

Open by house policy. Topaz is our own language.

Topaz 5.20.2 · 63 files

apps/sheets/sheets.tpz

Topaz · 32508 B · Original file

1// 미니 스프레드시트. 셀 값과 수식을 읽어 의존 순서대로 계산하고 순환 참조를 찾아 표와 JSON 저장본으로 보여 줍니다.
2// 입력은 줄마다 `A1 10`, `A3 =A1+A2`, `B1 =SUM(A1:A3)` 처럼 셀 이름과 값을 적고, `=` 로 시작하는 값은 수식입니다.
3// 함수는 SUM, AVERAGE, MIN, MAX, COUNT, ABS, IF, 비교는 > < >= <= = <> 입니다. 빈 셀은 0 으로 읽고 같은 셀을 다시 적으면 마지막 값을 씁니다.
4// 출력은 표와 JSON 저장본을 담은 HTML 조각이거나 줄 번호가 붙은 오류 한 줄입니다.
5// 같은 입력이면 인터프리터와 웹 빌드에서 바이트까지 같은 출력을 냅니다.
6// 엑셀 호환, 문자열 함수, 반올림 함수, 복사할 때 참조 고쳐 쓰기, 아주 큰 표는 다루지 않습니다.
7// Mini spreadsheet: evaluates cell formulas in dependency order, reports circular references and renders the grid with a JSON copy.
9record 셀 { 열: int, 행: int, 참조: string, 원본: string }
10record 시트 { 셀들: Array<셀>, 색인: Map<string, int> }
11record 주소 { 열: int, 행: int }
13type 연산자 = "+" | "-" | "*" | "/" | ">" | "<" | ">=" | "<=" | "=" | "<>"
14type 연산단계 = "비교" | "덧셈" | "곱셈"
15type 구두점 = "(" | ")" | "," | ":"
16type 함수종류 = "SUM" | "COUNT" | "AVERAGE" | "MIN" | "MAX" | "ABS" | "IF"
17enum 토큰 { 수(string), 셀(string), 함수(string), 연산(연산자), 기호(구두점) }
18record 읽은토큰 { 토큰: 토큰, 다음: int }
20// 셀 참조는 오류 문구에 쓰는 원문과 표에서 찾는 표준 이름을 함께 둡니다.
21record 셀참조 { 원문: string, 이름: string }
22// 연산들[i] 는 항들[i] 까지 계산한 값과 항들[i + 1] 을 잇습니다. 같은 단계의 연산은 왼쪽부터 계산합니다.
23record 연쇄식 { 항들: Array<수식>, 연산들: Array<연산자> }
24record 호출식 { 함수: 함수종류, 인자들: Array<함수인자> }
25enum 함수인자 { 범위(Array<string>), 값(수식) }
26enum 수식 { 수(float), 셀(셀참조), 부호반전(수식), 연쇄(연쇄식), 호출(호출식) }
27record 읽은식 { 식: 수식, 다음: int }
29enum 평가 { 수(float), 글(string), 빈, 오류(string) }
30enum 내용 { 고정(평가), 식(수식) }
32let 빈시트안내 = "<div class=\"meta\">셀을 입력하세요. 예: A1 10 / A2 =A1*2</div>"
34// 공용 도우미 시작 (이스케이프 숫자인가 모두숫자인가 오류알림 연속끝 대문자로 간선 위상정렬결과 인접목록 위상정렬하기 순환소속인가 결과풀기 오류바꾸기)
35// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.
37// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.
38function 이스케이프(원문: string) -> string {
39 원문.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
42function 숫자인가(글자: string) -> bool {
43 let 코드 = 글자.codePointAt(0) ?? 0
44 코드 >= 48 && 코드 <= 57
47// 빈 문자열은 참입니다. 부호와 소수점을 받지 않으려고 toInt 앞에서 씁니다.
48function 모두숫자인가(글: string) -> bool {
49 글.scalars().reduce(true, (모두, 글자) => 모두 && 숫자인가(글자))
52// 문구는 고정 문장이거나 이미 이스케이프한 값이어야 합니다.
53function 오류알림(문구: string) -> string {
54 "<div class=\"err\">{문구}</div>"
57// 시작부터 맞는가가 참인 글자가 이어지는 끝 위치를 돌려줍니다. 맞는 글자가 없으면 시작 그대로입니다.
58function 연속끝(글자들: Array<string>, 시작: int, 맞는가: (string) -> bool) -> int {
59 let mut 끝 = 시작
60 while 끝 < 글자들.length && 맞는가(글자들[끝]) { 끝 += 1 }
61 끝
64// ASCII 소문자만 바꿉니다. 다른 문자의 대문자 규칙을 섞지 않습니다.
65function 대문자로(글: string) -> string {
66 글.scalars().map(글자 => {
67 let 코드 = 글자.codePointAt(0) ?? 0
68 if 코드 >= 97 && 코드 <= 122 { fromCodePoint(코드 - 32) ?? 글자 } else { 글자 }
69 }).join("")
72type 간선 = { 출발: int, 도착: int }
74// 순서는 Kahn 대기열 순서, 층은 최장 경로 길이입니다. 남은 진입차수가 0 보다 큰 노드는 순환 때문에 놓지 못한 노드입니다.
75type 위상정렬결과 = { 순서: Array<int>, 층들: Array<int>, 남은진입차수: Array<int> }
77// 한 번 만들어 두면 위상 정렬과 순환 검사가 노드마다 간선 전체를 다시 훑지 않습니다. 행 안의 순서는 간선 순서입니다.
78function 인접목록(노드수: int, 간선들: Array<간선>) -> Array<Array<int>> {
79 let mut 인접: Array<Array<int>> = [for i in 0..<노드수 => []]
80 for 연결 in 간선들 { 인접[연결.출발].push(연결.도착) }
81 인접
84function 위상정렬하기(인접: Array<Array<int>>) -> 위상정렬결과 {
85 let n = 인접.length
86 let mut 진입차수 = [for i in 0..<n => 0]
87 for 도착들 in 인접 {
88 for 도착 in 도착들 { 진입차수[도착] += 1 }
89 }
90 let mut 층들 = [for i in 0..<n => 0]
91 let mut 대기열 = [for i in 0..<n if 진입차수[i] == 0 => i]
92 let mut 머리 = 0
93 while 머리 < 대기열.length {
94 let 현재 = 대기열[머리]
95 머리 += 1
96 for 도착 in 인접[현재] {
97 진입차수[도착] -= 1
98 층들[도착] = 층들[도착].atLeast(층들[현재] + 1)
99 if 진입차수[도착] == 0 { 대기열.push(도착) }
100 }
101 }
102 { 순서: 대기열, 층들: 층들, 남은진입차수: 진입차수 }
105// 순환을 가리키기만 하는 노드도 위상 정렬에서 남으므로 길이 1 이상의 경로로 자기 자신에게 돌아오는지 따로 봅니다.
106function 순환소속인가(시작: int, 인접: Array<Array<int>>) -> bool {
107 let mut 방문: Set<int> = Set.of()
108 // 인접 목록의 행에 직접 넣지 않도록 복사해서 시작합니다.
109 let mut 대기열 = 인접[시작].slice(0, 인접[시작].length)
110 let mut 머리 = 0
111 while 머리 < 대기열.length {
112 let 현재 = 대기열[머리]
113 머리 += 1
114 if 현재 == 시작 { return true }
115 if !(현재 in 방문) {
116 방문.add(현재)
117 for 다음 in 인접[현재] { 대기열.push(다음) }
118 }
119 }
120 false
123// 성공 값과 안내가 모두 바로 보여 줄 HTML 조각일 때 진입점에서 한 번 풉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.
124function 결과풀기(값: Result<string, string>) -> string {
125 match 값 {
126 case Ok(결과) => 결과
127 case Err(안내) => 안내
128 }
131// 성공 값은 그대로 두고 오류만 문구로 바꿉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.
132function 오류바꾸기<T, E>(결과: Result<T, E>, 바꾸기: (E) -> string) -> Result<T, string> {
133 match 결과 {
134 case Ok(값) => Ok(값)
135 case Err(오류) => Err(바꾸기(오류))
136 }
139// 공용 도우미 끝
141function 영문인가(글자: string) -> bool {
142 let 코드 = 글자.codePointAt(0) ?? 0
143 (코드 >= 65 && 코드 <= 90) || (코드 >= 97 && 코드 <= 122)
146function 수글자인가(글자: string) -> bool {
147 숫자인가(글자) || 글자 == "."
150// 거듭제곱 대신 10.0 을 차례로 곱해 소수부를 나눌 값을 정합니다.
151function 십의거듭제곱(지수: int) -> float {
152 let mut 값 = 1.0
153 for i in 0..<지수 { 값 *= 10.0 }
154 값
157// 정수, 소수, 앞의 부호 하나를 받습니다. toInt 가 읽지 못하는 64비트 밖의 정수부나 소수부는 0 으로 읽습니다.
158function 숫자파싱(원문: string) -> Option<float> {
159 let 글자들 = 원문.scalars()
160 let 부호있음 = 원문.startsWith("-") || 원문.startsWith("+")
161 let 부호 = if 원문.startsWith("-") { -1.0 } else { 1.0 }
162 let 본문 = if 부호있음 { 글자들.slice(1, 글자들.length).join("") } else { 원문 }
163 let 조각들 = 본문.split(".")
164 let 숫자들 = 조각들.join("")
165 if 조각들.length > 2 || 숫자들 == "" || !모두숫자인가(숫자들) { return None }
166 let 소수부 = if 조각들.length == 2 { 조각들[1] } else { "" }
167 let 정수값 = toFloat(toInt(조각들[0]) ?? 0)
168 Some(부호 * (정수값 + toFloat(toInt(소수부) ?? 0) / 십의거듭제곱(소수부.scalars().length)))
171// 정수면 정수로, 아니면 소수 넷째 자리까지 버림하고 끝의 0 을 지웁니다.
172function 표시(값: float) -> string {
173 let 원문 = "{값}"
174 let 조각들 = 원문.split(".")
175 if 조각들.length < 2 { return 원문 }
176 let 소수자리 = 조각들[1].scalars().slice(0, 4)
177 let mut 길이 = 소수자리.length
178 while 길이 > 0 && 소수자리[길이 - 1] == "0" { 길이 -= 1 }
179 if 길이 == 0 { 조각들[0] } else { 조각들[0] + "." + 소수자리.slice(0, 길이).join("") }
182// 열 번호는 0 부터라 0 은 A, 25 는 Z, 26 은 AA 입니다.
183function 열문자(열: int) -> string {
184 let 글자 = fromCodePoint(65 + 열 % 26) ?? "A"
185 if 열 < 26 { 글자 } else { 열문자(열 / 26 - 1) + 글자 }
188function 셀이름(열: int, 행: int) -> string {
189 열문자(열) + "{행}"
192// 영문 뒤에 숫자가 와야 하고 행은 1 부터입니다. 대문자로 바꾼 참조만 받습니다.
193function 주소읽기(참조: string) -> Result<주소, string> {
194 let 글자들 = 참조.scalars()
195 let 문자끝 = 연속끝(글자들, 0, 영문인가)
196 if 문자끝 == 0 { return Err("잘못된 참조: {참조}") }
197 let 열 = 글자들.slice(0, 문자끝).reduce(0, (누적, 글자) => 누적 * 26 + ((글자.codePointAt(0) ?? 0) - 64))
198 let 숫자끝 = 연속끝(글자들, 문자끝, 숫자인가)
199 let 행 = 글자들.slice(문자끝, 숫자끝).reduce(0, (누적, 글자) => 누적 * 10 + (글자.codePointAt(0) ?? 48) - 48)
200 if 숫자끝 == 문자끝 || 숫자끝 != 글자들.length || 행 == 0 { return Err("잘못된 참조: {참조}") }
201 Ok(주소 { 열: 열 - 1, 행: 행 })
204function 연산토큰(연산: 연산자, 다음: int) -> Result<읽은토큰, string> {
205 Ok(읽은토큰 { 토큰: 토큰.연산(연산), 다음: 다음 })
208function 기호토큰(글: 구두점, 다음: int) -> Result<읽은토큰, string> {
209 Ok(읽은토큰 { 토큰: 토큰.기호(글), 다음: 다음 })
212// 영문 뒤에 숫자가 붙으면 셀 참조, 아니면 함수 이름입니다.
213function 토큰읽기(글자들: Array<string>, 위치: int) -> Result<읽은토큰, string> {
214 let 글자 = 글자들[위치]
215 if 수글자인가(글자) {
216 let 끝 = 연속끝(글자들, 위치, 수글자인가)
217 return Ok(읽은토큰 { 토큰: 토큰.수(글자들.slice(위치, 끝).join("")), 다음: 끝 })
218 }
219 if 영문인가(글자) {
220 let 낱말끝 = 연속끝(글자들, 위치, 영문인가)
221 let 끝 = 연속끝(글자들, 낱말끝, 숫자인가)
222 let 원문 = 대문자로(글자들.slice(위치, 끝).join(""))
223 let 낱말 = if 끝 > 낱말끝 { 토큰.셀(원문) } else { 토큰.함수(원문) }
224 return Ok(읽은토큰 { 토큰: 낱말, 다음: 끝 })
225 }
226 let 뒤글자 = if 위치 + 1 < 글자들.length { 글자들[위치 + 1] } else { "" }
227 match 글자 {
228 case ">" => if 뒤글자 == "=" { 연산토큰(">=", 위치 + 2) } else { 연산토큰(">", 위치 + 1) }
229 case "<" => {
230 if 뒤글자 == "=" {
231 연산토큰("<=", 위치 + 2)
232 } else if 뒤글자 == ">" {
233 연산토큰("<>", 위치 + 2)
234 } else {
235 연산토큰("<", 위치 + 1)
236 }
237 }
238 case "=" => 연산토큰("=", 위치 + 1)
239 case "+" => 연산토큰("+", 위치 + 1)
240 case "-" => 연산토큰("-", 위치 + 1)
241 case "*" => 연산토큰("*", 위치 + 1)
242 case "/" => 연산토큰("/", 위치 + 1)
243 case "(" => 기호토큰("(", 위치 + 1)
244 case ")" => 기호토큰(")", 위치 + 1)
245 case "," => 기호토큰(",", 위치 + 1)
246 case ":" => 기호토큰(":", 위치 + 1)
247 case _ => Err("알 수 없는 문자: {글자}")
248 }
251function 토큰화(본문: string) -> Result<Array<토큰>, string> {
252 let 글자들 = 본문.scalars()
253 let mut 토큰들: Array<토큰> = []
254 let mut i = 0
255 while i < 글자들.length {
256 if 글자들[i] == " " || 글자들[i] == "\t" {
257 i += 1
258 } else {
259 let 읽음 = 토큰읽기(글자들, i)?
260 토큰들.push(읽음.토큰)
261 i = 읽음.다음
262 }
263 }
264 Ok(토큰들)
267function 토큰글(대상: 토큰) -> string {
268 match 대상 {
269 case 수(원문) | 셀(원문) | 함수(원문) => 원문
270 case 연산(글) => 글
271 case 기호(글) => 글
272 }
275function 셀인가(토큰들: Array<토큰>, 위치: int) -> bool {
276 if 위치 >= 토큰들.length { return false }
277 match 토큰들[위치] {
278 case 셀(_) => true
279 case 수(_) | 함수(_) | 연산(_) | 기호(_) => false
280 }
283function 구두점인가(토큰들: Array<토큰>, 위치: int, 찾는기호: 구두점) -> bool {
284 if 위치 >= 토큰들.length { return false }
285 match 토큰들[위치] {
286 case 기호(찾은기호) => 찾은기호 == 찾는기호
287 case 수(_) | 셀(_) | 함수(_) | 연산(_) => false
288 }
291function 범위인가(토큰들: Array<토큰>, 위치: int) -> bool {
292 셀인가(토큰들, 위치) && 구두점인가(토큰들, 위치 + 1, ":") && 셀인가(토큰들, 위치 + 2)
295function 단계(연산: 연산자) -> 연산단계 {
296 match 연산 {
297 case "+" | "-" => "덧셈"
298 case "*" | "/" => "곱셈"
299 case ">" | "<" | ">=" | "<=" | "=" | "<>" => "비교"
300 }
303function 연산자읽기(토큰들: Array<토큰>, 위치: int, 찾는단계: 연산단계) -> Option<연산자> {
304 if 위치 >= 토큰들.length { return None }
305 match 토큰들[위치] {
306 case 연산(찾은연산) => if 단계(찾은연산) == 찾는단계 { Some(찾은연산) } else { None }
307 case 수(_) | 셀(_) | 함수(_) | 기호(_) => None
308 }
311function 함수종류파싱(이름: string) -> Result<함수종류, string> {
312 match 이름 {
313 case "SUM" => Ok("SUM")
314 case "COUNT" => Ok("COUNT")
315 case "AVERAGE" => Ok("AVERAGE")
316 case "MIN" => Ok("MIN")
317 case "MAX" => Ok("MAX")
318 case "ABS" => Ok("ABS")
319 case "IF" => Ok("IF")
320 case _ => Err("알 수 없는 함수: " + 이름)
321 }
324// 범위 인자도 하나로 세므로 ABS(A1:A3) 은 여기를 지나고 계산할 때 값 개수로 다시 셉니다.
325function 인자수확인(함수: 함수종류, 인자수: int) -> Result<함수종류, string> {
326 match 함수 {
327 case "SUM" | "COUNT" | "AVERAGE" | "MIN" | "MAX" => if 인자수 > 0 { Ok(함수) } else { Err("{함수}은 인자가 필요합니다") }
328 case "ABS" => if 인자수 == 1 { Ok(함수) } else { Err("ABS는 인자 1개입니다") }
329 case "IF" => if 인자수 == 3 { Ok(함수) } else { Err("IF는 인자 3개입니다") }
330 }
333function 연쇄로(항들: Array<수식>, 연산들: Array<연산자>, 다음: int) -> 읽은식 {
334 let 식 = if 연산들.length == 0 { 항들[0] } else { 수식.연쇄(연쇄식 { 항들: 항들, 연산들: 연산들 }) }
335 읽은식 { 식: 식, 다음: 다음 }
338// 재귀 하강 파서입니다. 우선순위는 비교, 덧셈, 곱셈, 단항, 기본 순으로 높아집니다.
339// 비교는 이어 쓸 수 없어 한 번만 받고 남은 토큰은 부르는 쪽이 오류로 봅니다.
340function 비교식읽기(토큰들: Array<토큰>, 위치: int) -> Result<읽은식, string> {
341 let 왼쪽 = 덧셈식읽기(토큰들, 위치)?
342 match 연산자읽기(토큰들, 왼쪽.다음, "비교") {
343 case Some(연산) => {
344 let 오른쪽 = 덧셈식읽기(토큰들, 왼쪽.다음 + 1)?
345 Ok(연쇄로([왼쪽.식, 오른쪽.식], [연산], 오른쪽.다음))
346 }
347 case None => Ok(왼쪽)
348 }
351// 한 단계의 연산을 왼쪽부터 이어 읽고 항은 한 단계 높은 하위읽기로 읽습니다. 재귀 대신 반복으로 읽어 긴 식도 호출 깊이가 늘지 않습니다.
352function 연쇄읽기(토큰들: Array<토큰>, 위치: int, 찾는단계: 연산단계, 하위읽기: (Array<토큰>, int) -> Result<읽은식, string>) -> Result<읽은식, string> {
353 let 첫항 = 하위읽기(토큰들, 위치)?
354 let mut 항들: Array<수식> = [첫항.식]
355 let mut 연산들: Array<연산자> = []
356 let mut 다음 = 첫항.다음
357 while true {
358 match 연산자읽기(토큰들, 다음, 찾는단계) {
359 case Some(연산) => {
360 let 항 = 하위읽기(토큰들, 다음 + 1)?
361 항들.push(항.식)
362 연산들.push(연산)
363 다음 = 항.다음
364 }
365 case None => return Ok(연쇄로(항들, 연산들, 다음))
366 }
367 }
370function 덧셈식읽기(토큰들: Array<토큰>, 위치: int) -> Result<읽은식, string> {
371 연쇄읽기(토큰들, 위치, "덧셈", 곱셈식읽기)
374function 곱셈식읽기(토큰들: Array<토큰>, 위치: int) -> Result<읽은식, string> {
375 연쇄읽기(토큰들, 위치, "곱셈", 단항식읽기)
378function 단항식읽기(토큰들: Array<토큰>, 위치: int) -> Result<읽은식, string> {
379 match 연산자읽기(토큰들, 위치, "덧셈") {
380 case Some(부호) => {
381 let 안쪽 = 단항식읽기(토큰들, 위치 + 1)?
382 if 부호 == "-" { Ok(읽은식 { 식: 수식.부호반전(안쪽.식), 다음: 안쪽.다음 }) } else { Ok(안쪽) }
383 }
384 case None => 기본식읽기(토큰들, 위치)
385 }
388function 기본식읽기(토큰들: Array<토큰>, 위치: int) -> Result<읽은식, string> {
389 if 위치 >= 토큰들.length { return Err("수식이 끝났습니다") }
390 match 토큰들[위치] {
391 case 수(원문) => {
392 match 숫자파싱(원문) {
393 case Some(값) => Ok(읽은식 { 식: 수식.수(값), 다음: 위치 + 1 })
394 case None => Err("잘못된 수: {원문}")
395 }
396 }
397 case 셀(원문) => {
398 if 구두점인가(토큰들, 위치 + 1, ":") { return Err("범위는 함수 안에서만 쓸 수 있습니다") }
399 let 칸 = 주소읽기(원문)?
400 Ok(읽은식 { 식: 수식.셀(셀참조 { 원문: 원문, 이름: 셀이름(칸.열, 칸.행) }), 다음: 위치 + 1 })
401 }
402 case 함수(이름) => 호출읽기(토큰들, 위치 + 1, 이름)
403 case 기호(글) => {
404 if 글 != "(" { return Err("예상치 못한 토큰: {글}") }
405 let 안쪽 = 비교식읽기(토큰들, 위치 + 1)?
406 if !구두점인가(토큰들, 안쪽.다음, ")") { return Err("괄호가 닫히지 않았습니다") }
407 Ok(읽은식 { 식: 안쪽.식, 다음: 안쪽.다음 + 1 })
408 }
409 case 연산(글) => Err("예상치 못한 토큰: {글}")
410 }
413// 끝점이 뒤집힌 B2:A1 도 A1:B2 와 같은 사각형이고 열마다 위에서 아래로 펼칩니다.
414function 범위펼치기(시작: 주소, 끝: 주소) -> Array<string> {
415 let 열시작 = 시작.열.atMost(끝.열)
416 let 열끝 = 시작.열.atLeast(끝.열)
417 let 행시작 = 시작.행.atMost(끝.행)
418 let 행끝 = 시작.행.atLeast(끝.행)
419 [for 열 in 열시작..<(열끝 + 1) for 행 in 행시작..<(행끝 + 1) => 셀이름(열, 행)]
422// 범위는 읽을 때 칸 이름으로 펼쳐 두고 의존 관계와 계산에 그대로 씁니다.
423// 함수 이름은 인자를 모두 읽은 뒤에 확인하므로 인자의 오류가 먼저 보고됩니다.
424function 호출읽기(토큰들: Array<토큰>, 위치: int, 이름: string) -> Result<읽은식, string> {
425 if !구두점인가(토큰들, 위치, "(") { return Err("{이름} 뒤에 (가 필요합니다") }
426 let mut 인자들: Array<함수인자> = []
427 let mut i = 위치 + 1
428 while !구두점인가(토큰들, i, ")") {
429 if i >= 토큰들.length { return Err("괄호가 닫히지 않았습니다") }
430 let 시작 = if 인자들.length == 0 {
431 i
432 } else if 구두점인가(토큰들, i, ",") {
433 i + 1
434 } else {
435 return Err("인자 사이에 콤마가 필요합니다")
436 }
437 if 범위인가(토큰들, 시작) {
438 let 첫칸 = 주소읽기(토큰글(토큰들[시작]))?
439 let 끝칸 = 주소읽기(토큰글(토큰들[시작 + 2]))?
440 인자들.push(함수인자.범위(범위펼치기(첫칸, 끝칸)))
441 i = 시작 + 3
442 } else {
443 let 읽음 = 비교식읽기(토큰들, 시작)?
444 인자들.push(함수인자.값(읽음.식))
445 i = 읽음.다음
446 }
447 }
448 let 함수 = 인자수확인(함수종류파싱(이름)?, 인자들.length)?
449 Ok(읽은식 { 식: 수식.호출(호출식 { 함수: 함수, 인자들: 인자들 }), 다음: i + 1 })
452function 수식읽기(본문: string) -> Result<수식, string> {
453 let 토큰들 = 토큰화(본문)?
454 if 토큰들.length == 0 { return Err("빈 수식") }
455 let 읽음 = 비교식읽기(토큰들, 0)?
456 if 읽음.다음 != 토큰들.length { Err("수식이 올바르지 않습니다") } else { Ok(읽음.식) }
459// 수식의 구문과 참조 형식은 값과 상관없이 여기서 먼저 확인하므로 0 나누기 같은 값 오류가 구문 오류를 가리지 않습니다.
460function 내용해석(원본: string) -> 내용 {
461 let 값글 = 원본.trim()
462 if 값글 == "" { return 내용.고정(평가.빈) }
463 if 값글.startsWith("=") {
464 let 글자들 = 값글.scalars()
465 return match 수식읽기(글자들.slice(1, 글자들.length).join("")) {
466 case Ok(본문) => 내용.식(본문)
467 case Err(문구) => 내용.고정(평가.오류(문구))
468 }
469 }
470 match 숫자파싱(값글) {
471 case Some(값) => 내용.고정(평가.수(값))
472 case None => 내용.고정(평가.글(값글))
473 }
476function 셀값(참조: 셀참조, 색인: Map<string, int>, 결과들: Array<평가>) -> Result<float, string> {
477 let 자리 = 색인.getOr(참조.이름, -1)
478 if 자리 < 0 { return Ok(0.0) }
479 match 결과들[자리] {
480 case 오류(_) => Err("참조 셀 오류 {참조.원문}")
481 case 글(_) => Err("텍스트 셀 {참조.원문}")
482 case 빈 => Ok(0.0)
483 case 수(값) => Ok(값)
484 }
487// 범위 안의 빈 셀과 텍스트 셀은 건너뛰고 오류 셀은 오류로 전합니다.
488function 범위값들(이름들: Array<string>, 색인: Map<string, int>, 결과들: Array<평가>) -> Result<Array<float>, string> {
489 let mut 값들: Array<float> = []
490 for 이름 in 이름들 {
491 let 자리 = 색인.getOr(이름, -1)
492 if 자리 >= 0 {
493 match 결과들[자리] {
494 case 오류(_) => return Err("참조 셀 오류 {이름}")
495 case 수(값) => 값들.push(값)
496 case 글(_) | 빈 => {}
497 }
498 }
499 }
500 Ok(값들)
503function 참거짓값(참인가: bool) -> float {
504 if 참인가 { 1.0 } else { 0.0 }
507function 연산계산(연산: 연산자, 왼쪽: float, 오른쪽: float) -> Result<float, string> {
508 match 연산 {
509 case "+" => Ok(왼쪽 + 오른쪽)
510 case "-" => Ok(왼쪽 - 오른쪽)
511 case "*" => Ok(왼쪽 * 오른쪽)
512 case "/" => if 오른쪽 == 0.0 { Err("0으로 나눌 수 없습니다") } else { Ok(왼쪽 / 오른쪽) }
513 case ">" => Ok(참거짓값(왼쪽 > 오른쪽))
514 case "<" => Ok(참거짓값(왼쪽 < 오른쪽))
515 case ">=" => Ok(참거짓값(왼쪽 >= 오른쪽))
516 case "<=" => Ok(참거짓값(왼쪽 <= 오른쪽))
517 case "=" => Ok(참거짓값(왼쪽 == 오른쪽))
518 case "<>" => Ok(참거짓값(왼쪽 != 오른쪽))
519 }
522function 합계(값들: Array<float>) -> float {
523 값들.reduce(0.0, (합, 값) => 합 + 값)
526// IF 도 세 인자를 모두 계산하므로 고르지 않은 쪽의 오류도 전해집니다.
527function 집계(함수: 함수종류, 값들: Array<float>) -> Result<float, string> {
528 match 함수 {
529 case "SUM" => Ok(합계(값들))
530 case "COUNT" => Ok(toFloat(값들.length))
531 case "AVERAGE" => if 값들.length == 0 { Err("AVERAGE 대상이 없습니다") } else { Ok(합계(값들) / toFloat(값들.length)) }
532 case "MIN" => {
533 if 값들.length == 0 { return Err("MIN 대상이 없습니다") }
534 Ok(값들.reduce(값들[0], (작은값, 값) => Math.min(값, 작은값)))
535 }
536 case "MAX" => {
537 if 값들.length == 0 { return Err("MAX 대상이 없습니다") }
538 Ok(값들.reduce(값들[0], (큰값, 값) => Math.max(값, 큰값)))
539 }
540 case "ABS" => {
541 if 값들.length != 1 { return Err("ABS는 인자 1개입니다") }
542 // Math.abs 는 -0 을 0 으로 바꿔 ABS(-0) 의 출력이 달라지므로 쓰지 않습니다.
543 Ok(if 값들[0] < 0.0 { 0.0 - 값들[0] } else { 값들[0] })
544 }
545 case "IF" => {
546 if 값들.length != 3 { return Err("IF는 인자 3개입니다") }
547 Ok(if 값들[0] != 0.0 { 값들[1] } else { 값들[2] })
548 }
549 }
552function 함수계산(함수호출: 호출식, 색인: Map<string, int>, 결과들: Array<평가>) -> Result<float, string> {
553 let mut 값들: Array<float> = []
554 for 인자 in 함수호출.인자들 {
555 match 인자 {
556 case 범위(이름들) => {
557 for 값 in 범위값들(이름들, 색인, 결과들)? { 값들.push(값) }
558 }
559 case 값(인자식) => 값들.push(계산(인자식, 색인, 결과들)?)
560 }
561 }
562 집계(함수호출.함수, 값들)
565// 오른쪽 항은 왼쪽까지 계산한 뒤에 계산하므로 앞의 값 오류가 먼저 보고됩니다.
566function 연쇄계산(묶음: 연쇄식, 색인: Map<string, int>, 결과들: Array<평가>) -> Result<float, string> {
567 let mut 누적 = 계산(묶음.항들[0], 색인, 결과들)?
568 for i in 0..<묶음.연산들.length {
569 누적 = 연산계산(묶음.연산들[i], 누적, 계산(묶음.항들[i + 1], 색인, 결과들)?)?
570 }
571 Ok(누적)
574// 부호 반전은 0.0 에서 빼므로 0 을 뒤집어도 -0 이 되지 않습니다.
575function 계산(대상: 수식, 색인: Map<string, int>, 결과들: Array<평가>) -> Result<float, string> {
576 match 대상 {
577 case 수(값) => Ok(값)
578 case 셀(참조) => 셀값(참조, 색인, 결과들)
579 case 부호반전(안쪽) => Ok(0.0 - 계산(안쪽, 색인, 결과들)?)
580 case 연쇄(묶음) => 연쇄계산(묶음, 색인, 결과들)
581 case 호출(함수호출) => 함수계산(함수호출, 색인, 결과들)
582 }
585function 셀계산(해석: 내용, 색인: Map<string, int>, 결과들: Array<평가>) -> 평가 {
586 match 해석 {
587 case 고정(값) => 값
588 case 식(본문) => {
589 match 계산(본문, 색인, 결과들) {
590 case Ok(값) => 평가.수(값)
591 case Err(문구) => 평가.오류(문구)
592 }
593 }
594 }
597function 인자참조들(인자: 함수인자) -> Array<string> {
598 match 인자 {
599 case 범위(이름들) => 이름들
600 case 값(인자식) => 참조모으기(인자식)
601 }
604function 참조모으기(대상: 수식) -> Array<string> {
605 match 대상 {
606 case 수(_) => []
607 case 셀(참조) => [참조.이름]
608 case 부호반전(안쪽) => 참조모으기(안쪽)
609 case 연쇄(묶음) => [for 항 in 묶음.항들 for 이름 in 참조모으기(항) => 이름]
610 case 호출(함수호출) => [for 인자 in 함수호출.인자들 for 이름 in 인자참조들(인자) => 이름]
611 }
614// 값 앞에 공백이 있어 `=` 로 시작하지 않는 수식은 간선 없이 자기 차례에 그때까지의 결과로 계산합니다.
615function 선행셀들(칸: 셀, 해석: 내용, 색인: Map<string, int>) -> Array<int> {
616 let 이름들 = match 해석 {
617 case 식(본문) => if 칸.원본.startsWith("=") { 참조모으기(본문) } else { [] }
618 case 고정(_) => []
619 }
620 let mut 자리들: Set<int> = Set.of()
621 for 이름 in 이름들 { 자리들.add(색인.getOr(이름, -1)) }
622 [for 자리 in 자리들 if 자리 >= 0 => 자리]
625function 칸표시(이름: string, 색인: Map<string, int>, 결과들: Array<평가>) -> string {
626 let 자리 = 색인.getOr(이름, -1)
627 if 자리 < 0 { return "" }
628 match 결과들[자리] {
629 case 수(값) => 표시(값)
630 case 글(원문) => 이스케이프(원문)
631 case 오류(문구) => "<span class=\"bad\">오류: " + 이스케이프(문구) + "</span>"
632 case 빈 => ""
633 }
636function 행그리기(행: int, 열이름들: Array<string>, 색인: Map<string, int>, 결과들: Array<평가>) -> string {
637 let 칸들 = 열이름들.map(열이름 => "<td>" + 칸표시(열이름 + "{행}", 색인, 결과들) + "</td>")
638 "<tr><th>{행}</th>" + 칸들.join("") + "</tr>"
641function 표그리기(셀들: Array<셀>, 색인: Map<string, int>, 결과들: Array<평가>) -> string {
642 let 최대열 = 셀들.reduce(0, (큰값, 칸) => 큰값.atLeast(칸.열))
643 let 최대행 = 셀들.reduce(0, (큰값, 칸) => 큰값.atLeast(칸.행))
644 let 열이름들 = [for 열 in 0..<(최대열 + 1) => 열문자(열)]
645 let 머리칸들 = 열이름들.map(열이름 => "<th>" + 열이름 + "</th>").join("")
646 let 행들 = [for 행 in 1..<(최대행 + 1) => 행그리기(행, 열이름들, 색인, 결과들)].join("")
647 "<table class=\"sheet\"><thead><tr><th></th>" + 머리칸들 + "</tr></thead><tbody>" + 행들 + "</tbody></table>"
650function Json문자열(원문: string) -> string {
651 원문.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\t", "\\t")
654// 저장본은 셀을 처음 적은 순서대로 원본 값과 수식을 그대로 담습니다.
655function 저장본(셀들: Array<셀>) -> string {
656 let 항목들 = 셀들.map(칸 => "\"" + Json문자열(칸.참조) + "\":\"" + Json문자열(칸.원본) + "\"")
657 "<div class=\"meta\">JSON 저장</div><pre class=\"out\">" + 이스케이프("\{" + 항목들.join(",") + "\}") + "</pre>"
660function 줄읽기(줄: string, 줄번호: int) -> Result<셀, string> {
661 let 부분들 = 줄.split(" ")
662 if 부분들.length < 2 { return Err(오류알림("{줄번호}번째 줄: 형식은 `셀 값` 입니다. 예: A1 10")) }
663 // 문구에 입력한 셀 이름이 그대로 들어 있어 이스케이프한 뒤에 오류알림에 넣습니다.
664 let 칸 = 오류바꾸기(주소읽기(대문자로(부분들[0])), 문구 => 오류알림("{줄번호}번째 줄: " + 이스케이프(문구)))?
665 let 원본 = 부분들.slice(1, 부분들.length).join(" ")
666 Ok(셀 { 열: 칸.열, 행: 칸.행, 참조: 셀이름(칸.열, 칸.행), 원본: 원본 })
669// 같은 셀을 다시 적으면 처음 자리를 지키고 값만 바꿉니다.
670function 시트읽기(입력: string) -> Result<시트, string> {
671 let 줄들 = 입력.split("\n")
672 let mut 셀들: Array<셀> = []
673 let mut 색인: Map<string, int> = Map.new()
674 for i in 0..<줄들.length {
675 let 다듬은줄 = 줄들[i].trim()
676 if 다듬은줄 != "" {
677 let 새셀 = 줄읽기(다듬은줄, i + 1)?
678 let 기존 = 색인.getOr(새셀.참조, -1)
679 if 기존 >= 0 {
680 셀들[기존] = 새셀
681 } else {
682 색인.insert(새셀.참조, 셀들.length)
683 셀들.push(새셀)
684 }
685 }
686 }
687 if 셀들.length == 0 { return Err(빈시트안내) }
688 Ok(시트 { 셀들: 셀들, 색인: 색인 })
691// 위상 순서대로 계산하므로 각 셀은 앞서 계산한 결과만 읽습니다. 순환 표시는 계산이 끝난 뒤에 붙입니다.
692function 시트계산(읽은시트: 시트) -> string {
693 let 셀들 = 읽은시트.셀들
694 let 색인 = 읽은시트.색인
695 let n = 셀들.length
696 let 내용들 = [for 칸 in 셀들 => 내용해석(칸.원본)]
697 // 간선은 선행 셀에서 그 값을 읽는 셀로 갑니다. 자기 참조도 간선이라 한 칸짜리 순환이 됩니다.
698 let 간선들: Array<간선> = [for 자리 in 0..<n for 선행 in 선행셀들(셀들[자리], 내용들[자리], 색인) => { 출발: 선행, 도착: 자리 }]
699 let 인접 = 인접목록(n, 간선들)
700 let 위상 = 위상정렬하기(인접)
701 let mut 결과들: Array<평가> = [for i in 0..<n => 평가.빈]
702 for 자리 in 위상.순서 { 결과들[자리] = 셀계산(내용들[자리], 색인, 결과들) }
703 let 남음들 = [for i in 0..<n => 위상.남은진입차수[i] > 0]
704 let 순환들 = [for i in 0..<n => 남음들[i] && 순환소속인가(i, 인접)]
705 let 최종결과들 = [
706 for i in 0..<n => if 순환들[i] { 평가.오류("순환") } else if 남음들[i] { 평가.오류("순환에 의존") } else { 결과들[i] }
707 ]
708 let 순환이름들 = [for i in 0..<n if 순환들[i] => 이스케이프(셀들[i].참조)]
709 let 순환알림 = if 순환이름들.length > 0 { 오류알림("순환 참조가 있습니다: " + 순환이름들.join(", ")) } else { "" }
710 순환알림 + 표그리기(셀들, 색인, 최종결과들) + 저장본(셀들)
713function 시트보기(입력: string) -> string {
714 결과풀기(시트읽기(입력).map(시트계산))
717input() |> 시트보기 |> print

SHA256 ba19873ef8e80e04521efa34ca26d55d6a062aa1ee3191320c7418fd23dd033a