빈 칸과 없음 — -와 \-
상태: 구현됨
셀이 비어 있는 것과 셀이 값이 없다고 적은 것을 구별하는 표기의 설계입니다.
지금은 그 둘이 같습니다. 옵셔널 컬럼의 빈 칸이 곧 「없음」이므로, string? 컬럼에는
빈 문자열을 적을 방법이 없습니다 — 빈 칸은 언제나 없음이 되어 JSON에 null로 나갑니다.
bool?도 같아서, 「적지 않았다」와 「거짓이다」가 한 칸을 나눠 쓰고 있습니다.
1. 문제 — 빈 칸 하나가 두 가지를 뜻하는 것
현재 각 타입이 빈 칸을 읽는 방법입니다.
| 타입 | 필수 컬럼의 빈 칸 | 옵셔널 컬럼(?)의 빈 칸 |
|---|---|---|
string | "" — 값입니다 | 없음 (HasValue = false) |
bool | false — 값입니다 | 없음 |
T[] | 빈 배열 — 값입니다 | 없음 |
int bigint float double | 오류 | 없음 |
bitset datetime timespan uuid enum 라벨 | 오류 | 없음 |
| 참조 | 없음으로 읽고 검증이 거부 | 없음 |
앞 세 줄에서 같은 표기가 컬럼의 선언에 따라 다른 것을 뜻합니다. string 컬럼의 빈 칸은
값이고, string? 컬럼의 빈 칸은 없음입니다. 그래서 컬럼을 옵셔널로 바꾸는 순간 그 컬럼에
이미 적혀 있던 빈 칸 전부의 뜻이 조용히 바뀝니다.
표현할 수 없는 값도 생깁니다. string?의 빈 문자열과 bool?의 거짓이 그것입니다.
int?에서는 0을 적으면 되지만, 문자열에는 「빈 문자열을 적는 다른 방법」이 없습니다.
없음을 적는 표기가 없다는 것이 원인입니다. 없음이 「적지 않음」으로만 표현되므로, 적지 않을 수 있는 다른 뜻들과 자리를 다투게 됩니다.
2. 결정 — 셀 문법 3개
| 셀에 적힌 것 | 뜻 |
|---|---|
| 빈 칸 | 그 타입이 빈 칸을 읽는 규칙 그대로입니다. "" · false · 빈 배열 · 숫자 컬럼이면 오류. 값이 있는 셀입니다 |
- | 없음입니다. HasValue = false이고, 값 자리에는 타입의 빈 값이 들어갑니다 |
\- | 문자열 - 한 글자입니다 |
빈 칸의 뜻은 하나도 바뀌지 않습니다. 지워지는 것은 「옵셔널 컬럼의 빈 칸은 없음이다」는
규칙 한 줄이고, 없음은 -가 가져갑니다.
?의 뜻이 명확해집니다. 지금 ?는 「빈 칸을 허용한다」이고, 그래서 string?과 bool?에
붙인 ?는 의도 표기일 뿐이었습니다. 앞으로 ?는
「이 컬럼에 -를 적을 수 있다」 입니다. 타입에 관계없이 같은 뜻이고, 모든 언어의 Has{필드}
액세서가 나타내는 것과 정확히 같은 것을 나타냅니다.
-는 옵셔널 컬럼에서만 뜻을 가집니다. int 컬럼의 -는 오류이고, int? 컬럼의 -만
없음입니다. 없음을 표현할 수 있다고 컬럼이 선언한 자리에서만 없음을 적을 수 있습니다.
특별하게 읽히는 표기 2개
- 문자를 이스케이프하는 체계가 아닙니다. 셀 전체가 정확히 - 또는 \-일 때만 특별하고,
그 밖의 자리에서 -와 \는 적힌 그대로입니다.
| 셀 | 읽히는 값 |
|---|---|
- | 없음 |
\- | - |
-5 | -5 — 숫자이든 문자열이든 그대로입니다 |
A-1 | A-1 |
-- | -- |
\-a | \-a — \가 그대로 남습니다 |
\\- | \\- |
판정은 앞뒤 공백을 제거한 텍스트로 합니다. 스프레드시트에서 뒤따르는 공백은 보이지 않으므로,
- 를 없음이 아니라고 판정하면 작성자가 원인을 찾을 수 없는 차이가 생깁니다. 값으로 읽히는
문자열의 공백은 지금처럼 보존합니다 — 제거한 텍스트는 이 판정에만 씁니다.
표현할 수 없는 값 1개
문자열 \- 입니다. \\-를 그 표기로 삼으면 낱말 2개를 특별하게 두는 것에서 이스케이프
체계로 넘어가고, 그러면 값에 이미 들어 있는 \의 뜻이 전부 바뀝니다.
실측입니다. 이전 대규모 코퍼스의 문자열 값에는 \ 문자가 198개 들어 있고, 전부 \n 두 글자로
게임의 텍스트 렌더러가 읽는 줄바꿈 표기입니다. 일반 이스케이프 체계를 도입하면 그 198개가
전부 다른 값이 됩니다. 표현하지 못하는 값 1개와 뜻이 바뀌는 값 198개 중 앞을 고릅니다.
3. 타입별 읽기
| 타입 | 빈 칸 | - (옵셔널 컬럼) | - (필수 컬럼) | \- |
|---|---|---|---|---|
string | "" | 없음 | 오류 | - |
bool | false | 없음 | 오류 | 오류 — bool이 아닙니다 |
int bigint float double | 오류 — 4절 | 없음 | 오류 | 오류 |
bitset | 오류 — 4절 | 없음 | 오류 | 오류 |
datetime timespan uuid | 오류 — 4절 | 없음 | 오류 | 오류 |
| enum 라벨 | 오류 — 4절 | 없음 | 오류 | 오류 |
| 참조 | 오류 — 5절 | 없음 | 오류 | 오류 |
T[] | 빈 배열 | 없음 (배열 자체가 없습니다) | 오류 | 아래 원소 규칙 |
필수 컬럼의 -는 거부합니다. 그것이 ?를 뜻있게 만드는 자리입니다. 메시지는 셀 위치와
함께 두 가지 고침을 냅니다 — 값을 적거나, 컬럼 타입을 int?로 적거나.
`Needed.Hp` has no value, and the sheet declares the column required.
Write one, or type the column `int?` so that a row may say it has none.
at no-value-refused.xlsx : Bad : C10
판정하는 자리는 검증입니다. 읽는 쪽은 -를 없음으로 읽어 HasValue를 거짓으로 두고,
그것이 허용되는지는 컬럼의 선언을 보는 검증이 판정합니다 —
참조의 「없음」이
같은 이유로 옮겨 둔 자리이고, 그래야 워크북 전체의 위반이 한 번에 보고됩니다.
빈 값이 없는 타입은 옵셔널이 될 수 없습니다. 지금 EmptyValueOf가 거부하는 것들이고,
이 변경이 그 목록을 늘리거나 줄이지 않습니다.
배열의 원소
배열 셀은 구분자로 나눈 원소마다 같은 판정을 받습니다.
| 배열 셀 | 읽히는 값 |
|---|---|
| 빈 칸 | 빈 배열 — 값입니다 |
- | 없음. T[]?일 때만 허용합니다 |
a;\-;b | ["a", "-", "b"] |
a;-;b | 오류 — 원소는 없음을 가지지 않습니다 |
원소가 없음을 가지지 않는 것은 옵셔널 필드에서 이미
정한 것입니다. 한 셀에 든 목록의 원소는 전부 있거나 전부 없고, 그래서 ?는 배열 괄호 뒤에만
붙습니다. a;-;b의 거부는 그 결정을 셀에서도 그대로 적용하는 것입니다.
그 결정이 바뀌면 이 거부가 허용으로 바뀝니다. 원소가 없을 수 있는
배열이 T?[]를 정의하고, 거기서 a;-;b는 가운데 원소가 없는
배열입니다. 표기는 이 문서의 것 그대로이고 — 없음은 -, \-는 문자 - — 바뀌는 것은 그것을
적을 수 있는 자리뿐입니다.
4. 빈 칸 정책 — OnBlankCell
3절에서 빈 칸이 오류인 타입들이 있습니다. 숫자·날짜·기간·uuid·enum이고, 기본은 그대로
오류입니다. 그것을 타입의 빈 값으로 읽겠다고 적을 수 있는 소스 항목의 설정을 둡니다.
OnBlankCell | 빈 칸 |
|---|---|
error (기본) | 3절 그대로입니다. 빈 값이 없는 타입의 빈 칸은 어느 셀인지 함께 거부합니다 |
empty | 타입의 빈 값으로 읽습니다 — 0 · false · 0001-01-01 · Guid.Empty · enum의 0 |
{
"Sources": {
"Xlsx": [
{ "Path": "sheets", "OnBlankCell": "empty" }
]
}
}
empty로 읽은 셀도 값이 있는 셀입니다. HasValue는 참이고, 와이어의 presence 비트도
켜집니다. 없음을 적는 방법은 - 하나뿐이라는 것이 이 설정으로 흔들리지 않습니다 — 이
설정이 정하는 것은 「빈 칸이 오류인가 0인가」이지 「빈 칸이 없음인가」가 아닙니다.
기본이 엄격한 이유. 숫자 컬럼의 빈 칸은 대부분 적다 만 것입니다. 그것을 0으로 읽으면
데이터에 0이 들어가고, 그 0은 작성자가 적은 0과 구별되지 않습니다 — 이 도구가 검출해야 할
휴먼 오류가 그대로 통과하는 자리입니다. keep-first나 empty가 그렇듯,
중복 인덱스와
수식 오류의 완화와 같은 성격의 설정입니다.
그래서 다른 팀이 유지하는 워크북을 위한 것입니다. 고칠 권한이 없는 시트에서 빈 칸 하나
때문에 워크북 전체의 변환이 멈추는 것은 읽는 쪽이 답할 수 없는 질문입니다. empty로 읽은
셀은 컬럼당 한 번 경고하므로, 몇 개를 그렇게 읽었는지가 실행 기록에 남아 시트 소유 자에게
전달됩니다.
소스 항목의 정식 프로퍼티로 둡니다. 어느 레이아웃에나 해당하는 설정이므로 LayoutOptions의
자유 키가 아니라 SheetSourceRecipe의 프로퍼티입니다 — DefaultDelimiter · OnDuplicateIndex ·
OnFormulaError가 있는 자리이고, 어느 레이아웃인지 몰라도 찾을 수 있어야 하기 때문입니다.
-에는 적용되지 않습니다. 필수 컬럼의 -는 OnBlankCell이 무엇이든 오류입니다. 이
설정은 빈 칸에 대한 것이고, -는 빈 칸이 아닙니다.
참조도 따르지 않습니다. 참조 컬럼의 빈 칸은 empty로도 통과하지 않고 5절의 메시지로
거부됩니다. 빈 값으로 읽으면 키 0이 되는데, 그것은 「가리키는 것이 없다」라고 적은 셀과
같은 값입니다 — 이 설정이 완화하려는 것은 미완성 셀을 만난 변환이 멈추는 것이지, 미완성 셀이
뜻을 갖게 되는 것이 아닙니다.
5. 없음을 적을 수 있는 자리
| 자리 | - | 근거 |
|---|---|---|
| 옵셔널 컬럼 | 허용 | 이 문서가 정하는 것 |
| 필수 컬럼 | 거부 | 3절 |
| 인덱스 컬럼 | 거부 | 로우를 식별하는 값이라 없음이 뜻할 것이 없습니다. * 접두사로 붙인 두 번째 인덱스도 같습니다 |
| 참조 컬럼 | 옵셔널이면 허용 | 참조의 「없음」이 정한 규칙 그대로이고, 표기만 빈 칸에서 -로 바뀝니다 |
| 레코드 멤버 | 멤버가 옵셔널이면 허용 | 레코드 멤버별 옵셔널 |
참조 컬럼의 빈 칸은 거부로 바뀝니다. 지금은 필수든 아니든 없음으로 읽고 검증이 판정하는데,
그 이유가 「빈 칸이 이 레이아웃의 없음 표기」였습니다. 표기가 -가 되었으므로 옵셔널 참조의
빈 칸도 거부합니다. 판정하는 자리는 그대로 검증이고, 메시지가 -를 안내하도록 바뀝니다.
`Drop.RewardItem`은 `Item`을 가리키고, 셀이 비어 있습니다.
대상의 키를 적거나, 가리키는 것이 없다는 뜻이면 `-`를 적습니다.
at Drop.xlsx : Drop : F12
6. 바뀌는 동작
| 무엇 | 지금 | 앞으로 | 작성자가 받는 신호 |
|---|---|---|---|
int 컬럼의 빈 칸 | 오류 | 오류 — 변화 없음 | 기존 메시지에 ?와 OnBlankCell 안내를 더합니다 |
int? 컬럼의 빈 칸 | 없음 | 오류 | 셀 위치와 - 안내 |
string? 컬럼의 빈 칸 | 없음 | "" | 7절의 한시적 경고 |
bool? 컬럼의 빈 칸 | 없음 | false | 7절의 한시적 경고 |
T[]? 컬럼의 빈 칸 | 없음 | 빈 배열 | 7절의 한시적 경고 |
| 참조 컬럼의 빈 칸 | 없음 | 오류 | 5절의 메시지 |
필수 컬럼의 - | 문자열이면 값 -, 나머지는 오류 | 오류 | 3절의 메시지 |
| 뒤쪽 원소 자르기 | 빈 칸으로 잘립니다 | -로 잘립니다 | 7절 |
산출물이 바뀌는 범위 — 실측
| 대상 | 값이 정확히 -인 문자열 |
|---|---|
samples/sprout/out/json | 0개 |
samples/canopy/out/json | 0개 |
test/fixtures/golden 전체 | 0개 |
필수 컬럼에 -를 적어 둔 셀은 어느 프로젝트에도 없습니다. 그래서 위 표 일곱 번째 줄의
거부는 지금 통과하는 워크북을 하나도 멈추지 않습니다. 이전 대규모 코퍼스는 이미 -를 없음으로 읽고 있으므로
당연하고, 이전 소규모 코퍼스는 -를 문자열로 읽는데도 그렇게 적힌 셀이 없습니다.
골든 — 실측
바뀐 골든은 text 하나입니다. 빈 칸이 없음을 뜻하던 픽스처들 — optional,
record-trim, record-ref, record-ref-trim, reference-optional — 의 해당 셀에 -를
적었고, 그 다섯 시나리오의 골든은 한 바이트도 바뀌지 않았습니다. 없음을 적는 방법만
바뀌었지 없음의 뜻은 바뀌지 않았다는 것이 그 결과입니다.
text는 의도한 변경입니다. Quest.Hint(text?)의 두 빈 칸 중 하나를 -로 바꾸고 하나를
빈 칸으로 두었으므로, 그 하나가 null에서 ""가 됩니다. 두 뜻이 한 골든에 나란히 남습니다.
row-sets/json-named/Narrow_alt.json의 null 2개는 빈 칸에서 온 것이 아니라 그 벌에 없는
컬럼에서 온 것이므로 그대로입니다.
뒤쪽 원소 자르기
가변 길이 레코드 배열의 자르기는 Cell.HasValue를 읽으므로
규칙 자체는 바뀌지 않습니다. 바뀌는 것은 시트에 무엇을 적어야 잘리는가입니다.
Slot1 | Slot2 | Slot3 | 지금 | 앞으로 |
|---|---|---|---|---|
| 값 | 값 | 빈 칸 | 길이 2 | 길이 3 — 빈 칸이 값입니다 |
| 값 | 값 | - | (문자열이면 값 -) | 길이 2 |
문자열 멤버를 가진 레코드에서 특히 분명합니다 — 지금은 마지막 원소의 문자열 칸을 비우면 그 원소가 사라지는데, 그것은 「빈 이름을 적었다」와 구별되지 않습니다.
7. 이행
표기 자체는 옵션으로 켜지 않습니다. 이 변경의 내용이 「한 표기가 한 가지만 뜻하게 하는
것」인데, -를 읽을지 말지를 recipe가 정하면 표기의 뜻이 워크북마다 갈립니다. 4절의
OnBlankCell이 정하는 것은 그것과 다릅니다 — 빈 칸이 오류인지 0인지이지, -가 없음인지가
아닙니다.
대신 한 릴리스 동안 경고합니다. 조용히 바뀌는 것은 string? · bool? · T[]? 컬럼의 빈
칸뿐이고 — 나머지는 전부 오류로 드러납니다 — 그 셋만 경고 대상입니다.
`Cell.Text` holds blank cells, which are now the type's empty value rather than
"no value". Write `-` in the rows that have none. (34 cells)
at blank-and-null.xlsx : Cells : D11
- 컬럼당 한 번, 개수와 첫 셀 위치를 함께 보고합니다. 셀마다 내면 빈 칸이 많은 시트에서 다른 진단이 묻힙니다. 집계는 모든 시트를 읽은 뒤에 나오므로 개수가 실제 총계입니다.
- 다음 릴리스에서 이 경고를 제거합니다. 경고가 남아 있는 동안에는 빈 칸이 값이라는 사실이 이미 적용되어 있으므로, 경고의 유무가 동작을 바꾸지 않습니다.
레이아웃은 빈 칸에 대한 자기 규칙을 유지합니다. 「빈 칸은 이 레이아웃에서 실수이므로
경고하고 없음으로 읽는다」처럼 셀 텍스트를 먼저 해석하는 레이아웃이 있고, 그것은 그 레이아웃
파일 안의 결정입니다. 코어가 정하는 것은 -와 \-의 뜻이고, 레이아웃은 그 둘을 다시 정의할
수 없습니다.