Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1
This commit is contained in:
@@ -365,6 +365,50 @@ function generalFormat(value: Frac): string {
|
||||
return fracToString(value);
|
||||
}
|
||||
|
||||
// ── 지수(과학) 형식 `0.00E+00` ───────────────────────────────────────────────
|
||||
|
||||
interface SciPlan {
|
||||
mantissaToks: Tok[]; // `E` 앞 가수 자리(자릿수·소수점은 수 구역과 같은 규칙)
|
||||
expDigits: number; // `E+`·`E-` 뒤 자리표시 개수(모자라면 0 으로 채움)
|
||||
expSign: "+" | "-"; // `E-00` 은 양의 지수에서 부호를 안 보임(엑셀과 같음)
|
||||
}
|
||||
|
||||
/** `E`(부호 있음) 뒤 숫자 자리표시가 끝까지 이어질 때만 지수 형식 — 아니면 null. */
|
||||
function detectScientific(toks: Tok[]): SciPlan | null {
|
||||
for (let i = 0; i < toks.length; i++) {
|
||||
const t = toks[i];
|
||||
if (t.k !== "lit" || (t.ch !== "E" && t.ch !== "e")) continue;
|
||||
const signTok = toks[i + 1];
|
||||
if (!signTok || signTok.k !== "lit" || (signTok.ch !== "+" && signTok.ch !== "-")) continue;
|
||||
let j = i + 2;
|
||||
let digits = 0;
|
||||
while (j < toks.length && toks[j].k === "digit") {
|
||||
digits++;
|
||||
j++;
|
||||
}
|
||||
if (digits === 0 || j !== toks.length) continue;
|
||||
return { mantissaToks: toks.slice(0, i), expDigits: digits, expSign: signTok.ch };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderScientificSection(plan: SciPlan, magnitude: Frac): string {
|
||||
const value = toDouble(magnitude);
|
||||
const pointIdx = plan.mantissaToks.findIndex((t) => t.k === "point");
|
||||
const fracDigits = plan.mantissaToks
|
||||
.slice(pointIdx >= 0 ? pointIdx + 1 : plan.mantissaToks.length)
|
||||
.filter((t) => t.k === "digit").length;
|
||||
let exp = value === 0 ? 0 : Math.floor(Math.log10(Math.abs(value)));
|
||||
let mantissaText = (value / Math.pow(10, exp)).toFixed(fracDigits);
|
||||
if (Math.abs(Number(mantissaText)) >= 10) {
|
||||
exp += 1;
|
||||
mantissaText = (value / Math.pow(10, exp)).toFixed(fracDigits);
|
||||
}
|
||||
const expSign = exp < 0 ? "-" : plan.expSign === "+" ? "+" : "";
|
||||
const expText = String(Math.abs(exp)).padStart(plan.expDigits, "0");
|
||||
return `${mantissaText}E${expSign}${expText}`;
|
||||
}
|
||||
|
||||
// ── 날짜 형식(`yyyy-mm-dd` · `m/d` · 요일 `aaa`/`aaaa`) ───────────────────────
|
||||
// 엑셀 일련번호(1970-01-01 = 25569) → 달력. 1900 윤년 버그는 misc.ts 와 같이 안 다룸.
|
||||
|
||||
@@ -411,10 +455,19 @@ function detectDateFormat(s: string): DatePart[] | null {
|
||||
let j = i;
|
||||
while (j < s.length && s[j].toLowerCase() === lower) j++;
|
||||
const width = j - i;
|
||||
if (lower === "a" && (width === 3 || width === 4)) {
|
||||
// 앞뒤가 영문 알파벳이면 딴 낱말 속에 섞인 우연한 글자(`Fixed` 의 d · `Currency` 의 y 등) —
|
||||
// 자리표시로 안 봄(2026-09-28 — 그 우연 때문에 이름 형식이 날짜로 깨지던 것 고침).
|
||||
const before = i > 0 ? s[i - 1] : "";
|
||||
const after = j < s.length ? s[j] : "";
|
||||
const boundaryOk = !/[A-Za-z]/.test(before) && !/[A-Za-z]/.test(after);
|
||||
if (boundaryOk && lower === "a" && (width === 3 || width === 4)) {
|
||||
parts.push({ k: "a", width });
|
||||
hasDateToken = true;
|
||||
} else if (lower !== "a" && (width === 1 || width === 2 || (lower === "y" && width === 4))) {
|
||||
} else if (
|
||||
boundaryOk &&
|
||||
lower !== "a" &&
|
||||
(width === 1 || width === 2 || (lower === "y" && width === 4))
|
||||
) {
|
||||
parts.push({ k: lower as "y" | "m" | "d", width: width as 1 | 2 | 4 });
|
||||
hasDateToken = true;
|
||||
} else {
|
||||
@@ -466,11 +519,44 @@ function chooseSection(sections: string[], value: Frac): { code: string; forceMi
|
||||
return { code: sections[0], forceMinus: false };
|
||||
}
|
||||
|
||||
// ── 엑셀 「칸 형식 이름」(코드 대신) — `mso-number-format:Fixed` 처럼 클립보드가 이름을 넘김 ─────
|
||||
// 엑셀 붙여넣기 · 일부 자리에서 형식 코드 대신 이름(Fixed·Standard·Currency·Percent·General 등)이
|
||||
// 옴 — 그대로 코드로 읽으면 `y·m·d` 낱자가 날짜 자리표시로 잘못 잡혀 글자가 깨짐(2026-09-28 실측:
|
||||
// `Fixed`→`Fixe11`). 우리 코드로 바꿔치기 · 모르는 이름은 일반(General)으로.
|
||||
|
||||
const EXCEL_NAMED_FORMATS: Record<string, string> = {
|
||||
general: "General",
|
||||
"general number": "General",
|
||||
fixed: "0.00",
|
||||
standard: "#,##0.00",
|
||||
currency: "#,##0.00", // 이 문서 실측 — 통화 기호가 안 보임(사용자 확인)
|
||||
percent: "0.00%",
|
||||
scientific: "0.00E+00",
|
||||
"short date": "yyyy-mm-dd",
|
||||
"long date": "yyyy-mm-dd aaaa",
|
||||
};
|
||||
|
||||
const BARE_WORD_RE = /^[A-Za-z][A-Za-z ]*$/;
|
||||
|
||||
/** 따옴표 한 겹 · 흔한 `\x` 이스케이프까지 방어적으로 풀어(대조용) 이름표와 맞대 봄 —
|
||||
* 맞으면 우리 코드로 · 알파벳 낱말뿐인 모르는 이름이면 일반으로 · 아니면 원래 코드 그대로. */
|
||||
function resolveExcelStyleName(code: string): string {
|
||||
let key = code.trim();
|
||||
if (key.length >= 2 && key.startsWith('"') && key.endsWith('"')) key = key.slice(1, -1);
|
||||
key = key.replace(/\\(.)/g, "$1").trim().toLowerCase().replace(/\s+/g, " ");
|
||||
const mapped = EXCEL_NAMED_FORMATS[key];
|
||||
if (mapped !== undefined) return mapped;
|
||||
// 알파벳 낱말뿐이면서 진짜 날짜 자리표시(`aaa` · `yyyy` 등)도 아니면 모르는 이름 — 일반으로.
|
||||
if (BARE_WORD_RE.test(key) && detectDateFormat(code) === null) return "General";
|
||||
return code;
|
||||
}
|
||||
|
||||
function formatNumber(value: Frac, code: string | undefined): FormattedText {
|
||||
if (code === undefined || code === "" || code.trim().toLowerCase() === "general") {
|
||||
const resolved = code === undefined ? undefined : resolveExcelStyleName(code);
|
||||
if (resolved === undefined || resolved === "" || resolved.trim().toLowerCase() === "general") {
|
||||
return { 글: generalFormat(value), 정렬: "right" };
|
||||
}
|
||||
const sections = splitSections(code);
|
||||
const sections = splitSections(resolved);
|
||||
const { code: chosen, forceMinus } = chooseSection(sections, value);
|
||||
const { rest, color } = extractColor(chosen);
|
||||
const datePlan = detectDateFormat(rest);
|
||||
@@ -484,6 +570,11 @@ function formatNumber(value: Frac, code: string | undefined): FormattedText {
|
||||
if (color) extra.색 = color;
|
||||
if (fillChar) extra.채움글 = fillChar;
|
||||
const magnitude = cmp(value, ZERO) < 0 ? frac(-value.n, value.d) : value;
|
||||
const sci = detectScientific(toks);
|
||||
if (sci) {
|
||||
const body = renderScientificSection(sci, magnitude);
|
||||
return { 글: (forceMinus ? "-" : "") + body, 정렬: "right", ...extra };
|
||||
}
|
||||
const fraction = detectFraction(toks);
|
||||
if (fraction) {
|
||||
const body = renderFractionSection(fraction, magnitude);
|
||||
@@ -500,7 +591,8 @@ function formatNumber(value: Frac, code: string | undefined): FormattedText {
|
||||
|
||||
function formatText(value: string, code: string | undefined): FormattedText {
|
||||
if (code === undefined || code === "") return { 글: value, 정렬: "left" };
|
||||
const sections = splitSections(code);
|
||||
const resolved = resolveExcelStyleName(code);
|
||||
const sections = splitSections(resolved);
|
||||
// 구역 하나뿐(`;` 없음)에 `@` 가 있을 때만 그 구역을 씀(순수 수 형식 하나뿐이면 글엔 안 씀) ·
|
||||
// 넷째 구역은 명시적 글 자리라 `@` 없어도 그대로(고정 라벨 버릇) · 2~3 구역엔 글 자리가 없어 그대로.
|
||||
let textCode: string | undefined;
|
||||
|
||||
@@ -485,6 +485,57 @@ ok(
|
||||
);
|
||||
ok("참거짓은 형식 무시", fmt(true, "0.00") === "TRUE", fmt(true, "0.00"));
|
||||
|
||||
// 엑셀 「칸 형식 이름」(코드 대신 · mso-number-format:Fixed 등) — 2026-09-28 사용자 표 실측 회귀 방지
|
||||
// (콘크리트 43.91 · 14,182 · 0.48 · 21.07 · 모르터 1.93 · 계 15,166 · 23.20)
|
||||
ok("이름형식 Fixed", fmt(toFrac("43.91"), "Fixed") === "43.91", fmt(toFrac("43.91"), "Fixed"));
|
||||
ok(
|
||||
"이름형식 Fixed(소수 끝)",
|
||||
fmt(toFrac("1.93"), "Fixed") === "1.93",
|
||||
fmt(toFrac("1.93"), "Fixed"),
|
||||
);
|
||||
ok(
|
||||
"이름형식 Standard(천 단위)",
|
||||
fmt(toFrac("14182"), "Standard") === "14,182.00",
|
||||
fmt(toFrac("14182"), "Standard"),
|
||||
);
|
||||
ok(
|
||||
"이름형식 Standard 계",
|
||||
fmt(toFrac("15166"), "Standard") === "15,166.00",
|
||||
fmt(toFrac("15166"), "Standard"),
|
||||
);
|
||||
ok(
|
||||
"이름형식 Currency(기호 없음 — 사용자 확인)",
|
||||
fmt(toFrac("14182"), "Currency") === "14,182.00" &&
|
||||
!fmt(toFrac("14182"), "Currency").includes("₩"),
|
||||
fmt(toFrac("14182"), "Currency"),
|
||||
);
|
||||
ok("이름형식 Percent", fmt(toFrac("0.48"), "Percent") === "48.00%", fmt(toFrac("0.48"), "Percent"));
|
||||
ok(
|
||||
"이름형식 General Number",
|
||||
fmt(toFrac("21.07"), "General Number") === "21.07",
|
||||
fmt(toFrac("21.07"), "General Number"),
|
||||
);
|
||||
ok(
|
||||
"이름형식 Scientific",
|
||||
fmt(toFrac("43.91"), "Scientific") === "4.39E+01",
|
||||
fmt(toFrac("43.91"), "Scientific"),
|
||||
);
|
||||
ok(
|
||||
"이름형식 Short Date",
|
||||
fmt(dateSerial(2026, 3, 5) as Scalar, "Short Date") === "2026-03-05",
|
||||
fmt(dateSerial(2026, 3, 5) as Scalar, "Short Date"),
|
||||
);
|
||||
ok(
|
||||
"이름형식 모르는 이름은 일반",
|
||||
fmt(toFrac("23.20"), "AnUnknownStyleName") === "23.2",
|
||||
fmt(toFrac("23.20"), "AnUnknownStyleName"),
|
||||
);
|
||||
ok(
|
||||
"실무 코드는 이름표를 안 건드림(회귀)",
|
||||
fmt(frac(1234567n), "#,##0") === "1,234,567",
|
||||
fmt(frac(1234567n), "#,##0"),
|
||||
);
|
||||
|
||||
// ── 채우기 ──────────────────────────────────────────────────────────────
|
||||
|
||||
function bookWith(cells: Record<string, Cell>): Workbook {
|
||||
|
||||
Reference in New Issue
Block a user