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.9
record 셀 { 열: int, 행: int, 참조: string, 원본: string }10
record 시트 { 셀들: Array<셀>, 색인: Map<string, int> }11
record 주소 { 열: int, 행: int }13
type 연산자 = "+" | "-" | "*" | "/" | ">" | "<" | ">=" | "<=" | "=" | "<>"14
type 연산단계 = "비교" | "덧셈" | "곱셈"15
type 구두점 = "(" | ")" | "," | ":"16
type 함수종류 = "SUM" | "COUNT" | "AVERAGE" | "MIN" | "MAX" | "ABS" | "IF"17
enum 토큰 { 수(string), 셀(string), 함수(string), 연산(연산자), 기호(구두점) }18
record 읽은토큰 { 토큰: 토큰, 다음: int }20
// 셀 참조는 오류 문구에 쓰는 원문과 표에서 찾는 표준 이름을 함께 둡니다.21
record 셀참조 { 원문: string, 이름: string }22
// 연산들[i] 는 항들[i] 까지 계산한 값과 항들[i + 1] 을 잇습니다. 같은 단계의 연산은 왼쪽부터 계산합니다.23
record 연쇄식 { 항들: Array<수식>, 연산들: Array<연산자> }24
record 호출식 { 함수: 함수종류, 인자들: Array<함수인자> }25
enum 함수인자 { 범위(Array<string>), 값(수식) }26
enum 수식 { 수(float), 셀(셀참조), 부호반전(수식), 연쇄(연쇄식), 호출(호출식) }27
record 읽은식 { 식: 수식, 다음: int }29
enum 평가 { 수(float), 글(string), 빈, 오류(string) }30
enum 내용 { 고정(평가), 식(수식) }32
let 빈시트안내 = "<div class=\"meta\">셀을 입력하세요. 예: A1 10 / A2 =A1*2</div>"34
// 공용 도우미 시작 (이스케이프 숫자인가 모두숫자인가 오류알림 연속끝 대문자로 간선 위상정렬결과 인접목록 위상정렬하기 순환소속인가 결과풀기 오류바꾸기)35
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.37
// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.38
function 이스케이프(원문: string) -> string {39
원문.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)40
}42
function 숫자인가(글자: string) -> bool {43
let 코드 = 글자.codePointAt(0) ?? 044
코드 >= 48 && 코드 <= 5745
}47
// 빈 문자열은 참입니다. 부호와 소수점을 받지 않으려고 toInt 앞에서 씁니다.48
function 모두숫자인가(글: string) -> bool {49
글.scalars().reduce(true, (모두, 글자) => 모두 && 숫자인가(글자))50
}52
// 문구는 고정 문장이거나 이미 이스케이프한 값이어야 합니다.53
function 오류알림(문구: string) -> string {54
"<div class=\"err\">{문구}</div>"55
}57
// 시작부터 맞는가가 참인 글자가 이어지는 끝 위치를 돌려줍니다. 맞는 글자가 없으면 시작 그대로입니다.58
function 연속끝(글자들: Array<string>, 시작: int, 맞는가: (string) -> bool) -> int {59
let mut 끝 = 시작60
while 끝 < 글자들.length && 맞는가(글자들[끝]) { 끝 += 1 }61
끝62
}64
// ASCII 소문자만 바꿉니다. 다른 문자의 대문자 규칙을 섞지 않습니다.65
function 대문자로(글: string) -> string {66
글.scalars().map(글자 => {67
let 코드 = 글자.codePointAt(0) ?? 068
if 코드 >= 97 && 코드 <= 122 { fromCodePoint(코드 - 32) ?? 글자 } else { 글자 }69
}).join("")70
}72
type 간선 = { 출발: int, 도착: int }74
// 순서는 Kahn 대기열 순서, 층은 최장 경로 길이입니다. 남은 진입차수가 0 보다 큰 노드는 순환 때문에 놓지 못한 노드입니다.75
type 위상정렬결과 = { 순서: Array<int>, 층들: Array<int>, 남은진입차수: Array<int> }77
// 한 번 만들어 두면 위상 정렬과 순환 검사가 노드마다 간선 전체를 다시 훑지 않습니다. 행 안의 순서는 간선 순서입니다.78
function 인접목록(노드수: int, 간선들: Array<간선>) -> Array<Array<int>> {79
let mut 인접: Array<Array<int>> = [for i in 0..<노드수 => []]80
for 연결 in 간선들 { 인접[연결.출발].push(연결.도착) }81
인접82
}84
function 위상정렬하기(인접: Array<Array<int>>) -> 위상정렬결과 {85
let n = 인접.length86
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 머리 = 093
while 머리 < 대기열.length {94
let 현재 = 대기열[머리]95
머리 += 196
for 도착 in 인접[현재] {97
진입차수[도착] -= 198
층들[도착] = 층들[도착].atLeast(층들[현재] + 1)99
if 진입차수[도착] == 0 { 대기열.push(도착) }100
}101
}102
{ 순서: 대기열, 층들: 층들, 남은진입차수: 진입차수 }103
}105
// 순환을 가리키기만 하는 노드도 위상 정렬에서 남으므로 길이 1 이상의 경로로 자기 자신에게 돌아오는지 따로 봅니다.106
function 순환소속인가(시작: int, 인접: Array<Array<int>>) -> bool {107
let mut 방문: Set<int> = Set.of()108
// 인접 목록의 행에 직접 넣지 않도록 복사해서 시작합니다.109
let mut 대기열 = 인접[시작].slice(0, 인접[시작].length)110
let mut 머리 = 0111
while 머리 < 대기열.length {112
let 현재 = 대기열[머리]113
머리 += 1114
if 현재 == 시작 { return true }115
if !(현재 in 방문) {116
방문.add(현재)117
for 다음 in 인접[현재] { 대기열.push(다음) }118
}119
}120
false121
}123
// 성공 값과 안내가 모두 바로 보여 줄 HTML 조각일 때 진입점에서 한 번 풉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.124
function 결과풀기(값: Result<string, string>) -> string {125
match 값 {126
case Ok(결과) => 결과127
case Err(안내) => 안내128
}129
}131
// 성공 값은 그대로 두고 오류만 문구로 바꿉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.132
function 오류바꾸기<T, E>(결과: Result<T, E>, 바꾸기: (E) -> string) -> Result<T, string> {133
match 결과 {134
case Ok(값) => Ok(값)135
case Err(오류) => Err(바꾸기(오류))136
}137
}139
// 공용 도우미 끝141
function 영문인가(글자: string) -> bool {142
let 코드 = 글자.codePointAt(0) ?? 0143
(코드 >= 65 && 코드 <= 90) || (코드 >= 97 && 코드 <= 122)144
}146
function 수글자인가(글자: string) -> bool {147
숫자인가(글자) || 글자 == "."148
}150
// 거듭제곱 대신 10.0 을 차례로 곱해 소수부를 나눌 값을 정합니다.151
function 십의거듭제곱(지수: int) -> float {152
let mut 값 = 1.0153
for i in 0..<지수 { 값 *= 10.0 }154
값155
}157
// 정수, 소수, 앞의 부호 하나를 받습니다. toInt 가 읽지 못하는 64비트 밖의 정수부나 소수부는 0 으로 읽습니다.158
function 숫자파싱(원문: 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)))169
}171
// 정수면 정수로, 아니면 소수 넷째 자리까지 버림하고 끝의 0 을 지웁니다.172
function 표시(값: float) -> string {173
let 원문 = "{값}"174
let 조각들 = 원문.split(".")175
if 조각들.length < 2 { return 원문 }176
let 소수자리 = 조각들[1].scalars().slice(0, 4)177
let mut 길이 = 소수자리.length178
while 길이 > 0 && 소수자리[길이 - 1] == "0" { 길이 -= 1 }179
if 길이 == 0 { 조각들[0] } else { 조각들[0] + "." + 소수자리.slice(0, 길이).join("") }180
}182
// 열 번호는 0 부터라 0 은 A, 25 는 Z, 26 은 AA 입니다.183
function 열문자(열: int) -> string {184
let 글자 = fromCodePoint(65 + 열 % 26) ?? "A"185
if 열 < 26 { 글자 } else { 열문자(열 / 26 - 1) + 글자 }186
}188
function 셀이름(열: int, 행: int) -> string {189
열문자(열) + "{행}"190
}192
// 영문 뒤에 숫자가 와야 하고 행은 1 부터입니다. 대문자로 바꾼 참조만 받습니다.193
function 주소읽기(참조: 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, 행: 행 })202
}204
function 연산토큰(연산: 연산자, 다음: int) -> Result<읽은토큰, string> {205
Ok(읽은토큰 { 토큰: 토큰.연산(연산), 다음: 다음 })206
}208
function 기호토큰(글: 구두점, 다음: int) -> Result<읽은토큰, string> {209
Ok(읽은토큰 { 토큰: 토큰.기호(글), 다음: 다음 })210
}212
// 영문 뒤에 숫자가 붙으면 셀 참조, 아니면 함수 이름입니다.213
function 토큰읽기(글자들: 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
}249
}251
function 토큰화(본문: string) -> Result<Array<토큰>, string> {252
let 글자들 = 본문.scalars()253
let mut 토큰들: Array<토큰> = []254
let mut i = 0255
while i < 글자들.length {256
if 글자들[i] == " " || 글자들[i] == "\t" {257
i += 1258
} else {259
let 읽음 = 토큰읽기(글자들, i)?260
토큰들.push(읽음.토큰)261
i = 읽음.다음262
}263
}264
Ok(토큰들)265
}267
function 토큰글(대상: 토큰) -> string {268
match 대상 {269
case 수(원문) | 셀(원문) | 함수(원문) => 원문270
case 연산(글) => 글271
case 기호(글) => 글272
}273
}275
function 셀인가(토큰들: Array<토큰>, 위치: int) -> bool {276
if 위치 >= 토큰들.length { return false }277
match 토큰들[위치] {278
case 셀(_) => true279
case 수(_) | 함수(_) | 연산(_) | 기호(_) => false280
}281
}283
function 구두점인가(토큰들: Array<토큰>, 위치: int, 찾는기호: 구두점) -> bool {284
if 위치 >= 토큰들.length { return false }285
match 토큰들[위치] {286
case 기호(찾은기호) => 찾은기호 == 찾는기호287
case 수(_) | 셀(_) | 함수(_) | 연산(_) => false288
}289
}291
function 범위인가(토큰들: Array<토큰>, 위치: int) -> bool {292
셀인가(토큰들, 위치) && 구두점인가(토큰들, 위치 + 1, ":") && 셀인가(토큰들, 위치 + 2)293
}295
function 단계(연산: 연산자) -> 연산단계 {296
match 연산 {297
case "+" | "-" => "덧셈"298
case "*" | "/" => "곱셈"299
case ">" | "<" | ">=" | "<=" | "=" | "<>" => "비교"300
}301
}303
function 연산자읽기(토큰들: Array<토큰>, 위치: int, 찾는단계: 연산단계) -> Option<연산자> {304
if 위치 >= 토큰들.length { return None }305
match 토큰들[위치] {306
case 연산(찾은연산) => if 단계(찾은연산) == 찾는단계 { Some(찾은연산) } else { None }307
case 수(_) | 셀(_) | 함수(_) | 기호(_) => None308
}309
}311
function 함수종류파싱(이름: 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
}322
}324
// 범위 인자도 하나로 세므로 ABS(A1:A3) 은 여기를 지나고 계산할 때 값 개수로 다시 셉니다.325
function 인자수확인(함수: 함수종류, 인자수: 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
}331
}333
function 연쇄로(항들: Array<수식>, 연산들: Array<연산자>, 다음: int) -> 읽은식 {334
let 식 = if 연산들.length == 0 { 항들[0] } else { 수식.연쇄(연쇄식 { 항들: 항들, 연산들: 연산들 }) }335
읽은식 { 식: 식, 다음: 다음 }336
}338
// 재귀 하강 파서입니다. 우선순위는 비교, 덧셈, 곱셈, 단항, 기본 순으로 높아집니다.339
// 비교는 이어 쓸 수 없어 한 번만 받고 남은 토큰은 부르는 쪽이 오류로 봅니다.340
function 비교식읽기(토큰들: Array<토큰>, 위치: int) -> Result<읽은식, string> {341
let 왼쪽 = 덧셈식읽기(토큰들, 위치)?342
match 연산자읽기(토큰들, 왼쪽.다음, "비교") {343
case Some(연산) => {344
let 오른쪽 = 덧셈식읽기(토큰들, 왼쪽.다음 + 1)?345
Ok(연쇄로([왼쪽.식, 오른쪽.식], [연산], 오른쪽.다음))346
}347
case None => Ok(왼쪽)348
}349
}351
// 한 단계의 연산을 왼쪽부터 이어 읽고 항은 한 단계 높은 하위읽기로 읽습니다. 재귀 대신 반복으로 읽어 긴 식도 호출 깊이가 늘지 않습니다.352
function 연쇄읽기(토큰들: 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
}368
}370
function 덧셈식읽기(토큰들: Array<토큰>, 위치: int) -> Result<읽은식, string> {371
연쇄읽기(토큰들, 위치, "덧셈", 곱셈식읽기)372
}374
function 곱셈식읽기(토큰들: Array<토큰>, 위치: int) -> Result<읽은식, string> {375
연쇄읽기(토큰들, 위치, "곱셈", 단항식읽기)376
}378
function 단항식읽기(토큰들: Array<토큰>, 위치: int) -> Result<읽은식, string> {379
match 연산자읽기(토큰들, 위치, "덧셈") {380
case Some(부호) => {381
let 안쪽 = 단항식읽기(토큰들, 위치 + 1)?382
if 부호 == "-" { Ok(읽은식 { 식: 수식.부호반전(안쪽.식), 다음: 안쪽.다음 }) } else { Ok(안쪽) }383
}384
case None => 기본식읽기(토큰들, 위치)385
}386
}388
function 기본식읽기(토큰들: 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
}411
}413
// 끝점이 뒤집힌 B2:A1 도 A1:B2 와 같은 사각형이고 열마다 위에서 아래로 펼칩니다.414
function 범위펼치기(시작: 주소, 끝: 주소) -> Array<string> {415
let 열시작 = 시작.열.atMost(끝.열)416
let 열끝 = 시작.열.atLeast(끝.열)417
let 행시작 = 시작.행.atMost(끝.행)418
let 행끝 = 시작.행.atLeast(끝.행)419
[for 열 in 열시작..<(열끝 + 1) for 행 in 행시작..<(행끝 + 1) => 셀이름(열, 행)]420
}422
// 범위는 읽을 때 칸 이름으로 펼쳐 두고 의존 관계와 계산에 그대로 씁니다.423
// 함수 이름은 인자를 모두 읽은 뒤에 확인하므로 인자의 오류가 먼저 보고됩니다.424
function 호출읽기(토큰들: Array<토큰>, 위치: int, 이름: string) -> Result<읽은식, string> {425
if !구두점인가(토큰들, 위치, "(") { return Err("{이름} 뒤에 (가 필요합니다") }426
let mut 인자들: Array<함수인자> = []427
let mut i = 위치 + 1428
while !구두점인가(토큰들, i, ")") {429
if i >= 토큰들.length { return Err("괄호가 닫히지 않았습니다") }430
let 시작 = if 인자들.length == 0 {431
i432
} else if 구두점인가(토큰들, i, ",") {433
i + 1434
} else {435
return Err("인자 사이에 콤마가 필요합니다")436
}437
if 범위인가(토큰들, 시작) {438
let 첫칸 = 주소읽기(토큰글(토큰들[시작]))?439
let 끝칸 = 주소읽기(토큰글(토큰들[시작 + 2]))?440
인자들.push(함수인자.범위(범위펼치기(첫칸, 끝칸)))441
i = 시작 + 3442
} else {443
let 읽음 = 비교식읽기(토큰들, 시작)?444
인자들.push(함수인자.값(읽음.식))445
i = 읽음.다음446
}447
}448
let 함수 = 인자수확인(함수종류파싱(이름)?, 인자들.length)?449
Ok(읽은식 { 식: 수식.호출(호출식 { 함수: 함수, 인자들: 인자들 }), 다음: i + 1 })450
}452
function 수식읽기(본문: string) -> Result<수식, string> {453
let 토큰들 = 토큰화(본문)?454
if 토큰들.length == 0 { return Err("빈 수식") }455
let 읽음 = 비교식읽기(토큰들, 0)?456
if 읽음.다음 != 토큰들.length { Err("수식이 올바르지 않습니다") } else { Ok(읽음.식) }457
}459
// 수식의 구문과 참조 형식은 값과 상관없이 여기서 먼저 확인하므로 0 나누기 같은 값 오류가 구문 오류를 가리지 않습니다.460
function 내용해석(원본: 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
}474
}476
function 셀값(참조: 셀참조, 색인: 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
}485
}487
// 범위 안의 빈 셀과 텍스트 셀은 건너뛰고 오류 셀은 오류로 전합니다.488
function 범위값들(이름들: 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(값들)501
}503
function 참거짓값(참인가: bool) -> float {504
if 참인가 { 1.0 } else { 0.0 }505
}507
function 연산계산(연산: 연산자, 왼쪽: 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
}520
}522
function 합계(값들: Array<float>) -> float {523
값들.reduce(0.0, (합, 값) => 합 + 값)524
}526
// IF 도 세 인자를 모두 계산하므로 고르지 않은 쪽의 오류도 전해집니다.527
function 집계(함수: 함수종류, 값들: 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
}550
}552
function 함수계산(함수호출: 호출식, 색인: 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
집계(함수호출.함수, 값들)563
}565
// 오른쪽 항은 왼쪽까지 계산한 뒤에 계산하므로 앞의 값 오류가 먼저 보고됩니다.566
function 연쇄계산(묶음: 연쇄식, 색인: Map<string, int>, 결과들: Array<평가>) -> Result<float, string> {567
let mut 누적 = 계산(묶음.항들[0], 색인, 결과들)?568
for i in 0..<묶음.연산들.length {569
누적 = 연산계산(묶음.연산들[i], 누적, 계산(묶음.항들[i + 1], 색인, 결과들)?)?570
}571
Ok(누적)572
}574
// 부호 반전은 0.0 에서 빼므로 0 을 뒤집어도 -0 이 되지 않습니다.575
function 계산(대상: 수식, 색인: Map<string, int>, 결과들: Array<평가>) -> Result<float, string> {576
match 대상 {577
case 수(값) => Ok(값)578
case 셀(참조) => 셀값(참조, 색인, 결과들)579
case 부호반전(안쪽) => Ok(0.0 - 계산(안쪽, 색인, 결과들)?)580
case 연쇄(묶음) => 연쇄계산(묶음, 색인, 결과들)581
case 호출(함수호출) => 함수계산(함수호출, 색인, 결과들)582
}583
}585
function 셀계산(해석: 내용, 색인: Map<string, int>, 결과들: Array<평가>) -> 평가 {586
match 해석 {587
case 고정(값) => 값588
case 식(본문) => {589
match 계산(본문, 색인, 결과들) {590
case Ok(값) => 평가.수(값)591
case Err(문구) => 평가.오류(문구)592
}593
}594
}595
}597
function 인자참조들(인자: 함수인자) -> Array<string> {598
match 인자 {599
case 범위(이름들) => 이름들600
case 값(인자식) => 참조모으기(인자식)601
}602
}604
function 참조모으기(대상: 수식) -> Array<string> {605
match 대상 {606
case 수(_) => []607
case 셀(참조) => [참조.이름]608
case 부호반전(안쪽) => 참조모으기(안쪽)609
case 연쇄(묶음) => [for 항 in 묶음.항들 for 이름 in 참조모으기(항) => 이름]610
case 호출(함수호출) => [for 인자 in 함수호출.인자들 for 이름 in 인자참조들(인자) => 이름]611
}612
}614
// 값 앞에 공백이 있어 `=` 로 시작하지 않는 수식은 간선 없이 자기 차례에 그때까지의 결과로 계산합니다.615
function 선행셀들(칸: 셀, 해석: 내용, 색인: 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 => 자리]623
}625
function 칸표시(이름: 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
}634
}636
function 행그리기(행: int, 열이름들: Array<string>, 색인: Map<string, int>, 결과들: Array<평가>) -> string {637
let 칸들 = 열이름들.map(열이름 => "<td>" + 칸표시(열이름 + "{행}", 색인, 결과들) + "</td>")638
"<tr><th>{행}</th>" + 칸들.join("") + "</tr>"639
}641
function 표그리기(셀들: 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>"648
}650
function Json문자열(원문: string) -> string {651
원문.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\t", "\\t")652
}654
// 저장본은 셀을 처음 적은 순서대로 원본 값과 수식을 그대로 담습니다.655
function 저장본(셀들: Array<셀>) -> string {656
let 항목들 = 셀들.map(칸 => "\"" + Json문자열(칸.참조) + "\":\"" + Json문자열(칸.원본) + "\"")657
"<div class=\"meta\">JSON 저장</div><pre class=\"out\">" + 이스케이프("\{" + 항목들.join(",") + "\}") + "</pre>"658
}660
function 줄읽기(줄: 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(셀 { 열: 칸.열, 행: 칸.행, 참조: 셀이름(칸.열, 칸.행), 원본: 원본 })667
}669
// 같은 셀을 다시 적으면 처음 자리를 지키고 값만 바꿉니다.670
function 시트읽기(입력: 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(시트 { 셀들: 셀들, 색인: 색인 })689
}691
// 위상 순서대로 계산하므로 각 셀은 앞서 계산한 결과만 읽습니다. 순환 표시는 계산이 끝난 뒤에 붙입니다.692
function 시트계산(읽은시트: 시트) -> string {693
let 셀들 = 읽은시트.셀들694
let 색인 = 읽은시트.색인695
let n = 셀들.length696
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
순환알림 + 표그리기(셀들, 색인, 최종결과들) + 저장본(셀들)711
}713
function 시트보기(입력: string) -> string {714
결과풀기(시트읽기(입력).map(시트계산))715
}717
input() |> 시트보기 |> printSHA256 ba19873ef8e80e04521efa34ca26d55d6a062aa1ee3191320c7418fd23dd033a