@aiquants/spreadsheet 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1103 -0
- package/LICENSE.md +155 -0
- package/README.md +817 -0
- package/dist/index.cjs +21 -0
- package/dist/index.d.cts +1960 -0
- package/dist/index.d.ts +1960 -0
- package/dist/index.js +7711 -0
- package/dist/styles/spreadsheet.css +2 -0
- package/dist/styles/spreadsheet.standalone.css +3 -0
- package/package.json +94 -0
- package/src/Spreadsheet.tsx +2313 -0
- package/src/clipboard/clipPayload.ts +1242 -0
- package/src/clipboard/fixtures/excel.fixture.ts +301 -0
- package/src/clipboard/fixtures/html.fixture.ts +90 -0
- package/src/clipboard/fixtures/sheets.fixture.ts +83 -0
- package/src/clipboard/htmlInterop.ts +1694 -0
- package/src/clipboard/numberFormatCodec.ts +870 -0
- package/src/clipboard/pasteSpecial.ts +608 -0
- package/src/clipboard/tiling.ts +370 -0
- package/src/clipboard/tsv.ts +186 -0
- package/src/clipboard/useClipboard.ts +1154 -0
- package/src/commands/selfCalc.ts +279 -0
- package/src/commands/useCommands.ts +946 -0
- package/src/editor/useCellEditor.ts +708 -0
- package/src/grid/CellView.tsx +51 -0
- package/src/grid/GridBody.tsx +489 -0
- package/src/header/ColumnHeader.tsx +167 -0
- package/src/header/RowHeader.tsx +144 -0
- package/src/header/columnName.ts +31 -0
- package/src/header/resizeGates.ts +146 -0
- package/src/header/resizeStrips.ts +79 -0
- package/src/header/useHeaderResize.ts +598 -0
- package/src/header/useHeaderSelection.ts +454 -0
- package/src/index.ts +122 -0
- package/src/keyboard/compositionGuard.ts +53 -0
- package/src/keyboard/ensureCellVisible.ts +77 -0
- package/src/keyboard/shortcuts.ts +718 -0
- package/src/keyboard/useGridKeyboard.ts +940 -0
- package/src/model/axisModel.ts +186 -0
- package/src/model/cellAnnotations.ts +92 -0
- package/src/model/cellCoord.ts +114 -0
- package/src/model/cellFormat.ts +562 -0
- package/src/model/cellPaint.ts +537 -0
- package/src/model/conditionalFormat.ts +805 -0
- package/src/model/dataAdapter.ts +478 -0
- package/src/model/dataValidation.ts +542 -0
- package/src/model/formulaRefs.ts +984 -0
- package/src/model/frozenPartition.ts +100 -0
- package/src/model/labels.ts +228 -0
- package/src/model/viewStore.ts +124 -0
- package/src/overlay/SelectionOverlay.tsx +1425 -0
- package/src/overlay/useFillHandle.ts +432 -0
- package/src/popover/CommentPopover.tsx +109 -0
- package/src/popover/LinkPopover.tsx +108 -0
- package/src/popover/ListPopover.tsx +161 -0
- package/src/popover/MessagePopover.tsx +36 -0
- package/src/popover/NotePopover.tsx +106 -0
- package/src/popover/NoteTip.tsx +26 -0
- package/src/popover/PopoverLayer.tsx +210 -0
- package/src/popover/placement.ts +138 -0
- package/src/popover/usePopovers.ts +642 -0
- package/src/selection/selectionState.ts +239 -0
- package/src/selection/useAutoScroller.ts +226 -0
- package/src/selection/usePointDrag.ts +273 -0
- package/src/selection/useSelectionEngine.ts +687 -0
- package/src/styles/components.entry.css +10 -0
- package/src/styles/spreadsheet.css +1029 -0
- package/src/styles/standalone.entry.css +9 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Self-calculation over the selected cells' texts (v0.8 §4.1.12 — the
|
|
3
|
+
* `multiplyBy100` / `multiplyBy1000` / `divideBy100` / `divideBy1000` / `signReversal` commands,
|
|
4
|
+
* ported from the sheets-enhancer extension's `calculateClipboardData`). Pure, React / DOM-free.
|
|
5
|
+
*
|
|
6
|
+
* Contract (each clause is pinned by selfCalc.spec.ts):
|
|
7
|
+
* - Text side (`scaleDecimalText` / `reverseSignText`): "is it a number" is decided by
|
|
8
|
+
* `parseNumber` from cellFormat.ts (the single source of the decimal grammar); the arithmetic is
|
|
9
|
+
* EXACT string digit shifting / sign flipping — no double is ever constructed, so integers beyond
|
|
10
|
+
* 2^53 keep every digit and "0.07" × 100 is "7" (not 7.000000000000001). The output is the
|
|
11
|
+
* canonical decimal (no "+", no "-0", no redundant leading / trailing zeros); an exponent-form
|
|
12
|
+
* input keeps its exponent form with the exponent shifted ("1e3" × 100 = "1e5"). Non-numeric
|
|
13
|
+
* text (for the sign: anything but TRUE / FALSE, case-insensitive) ⇒ null = "leave the cell";
|
|
14
|
+
* so is a RESULT that leaves the parseNumber grammar / finite envelope ("9e307" × 100,
|
|
15
|
+
* "1e-9999" ÷ 100, a 308-digit plain integer × 100) — the operation must never write text the
|
|
16
|
+
* package itself no longer recognises as a number (one-way domain escape; Excel refuses too).
|
|
17
|
+
* - Formula side (`scaleFormula` / `reverseSignFormula`): reversible wrap / peel. Wrap =
|
|
18
|
+
* `=(body)*100` / `=(body)/100` / `=-(body)`; peel removes exactly our own INVERSE wrapper
|
|
19
|
+
* (`=(inner)/100` under ×100, `=(inner)*100` under ÷100, `=-(inner)` under sign) and ONLY when
|
|
20
|
+
* the leading "(" pairs with the ")" before the suffix — "…" string literals and '…' quoted
|
|
21
|
+
* sheet names are opaque (a doubled quote toggles the literal state twice, so no escape rule).
|
|
22
|
+
* The extension's greedy `^\((.+)\)/100$` mis-peels `=(A1)/100+(B1)/100` into the broken
|
|
23
|
+
* `=A1)/100+(B1`; here that formula wraps.
|
|
24
|
+
* - A formula is "=" + a non-empty body; anything else is a caller bug ⇒ RangeError (fail-fast,
|
|
25
|
+
* the same rule getCellFormula's "=" contract enforces). An exponent outside ±2 / ±3 is a
|
|
26
|
+
* RangeError too (JS callers).
|
|
27
|
+
* - Whether a cell IS a formula is the CALLER's decision (the `getCellFormula` capability branch
|
|
28
|
+
* in useCommands): no function here sniffs a leading "=" on the text side, so a v0.7 adapter
|
|
29
|
+
* without `getCellFormula` sees "=A1" as plain text (untouched) — never as a formula.
|
|
30
|
+
*
|
|
31
|
+
* 選択セルの text に対する自己計算 (v0.8 §4.1.12 — multiplyBy100 / multiplyBy1000 / divideBy100 /
|
|
32
|
+
* divideBy1000 / signReversal、sheets-enhancer 拡張 `calculateClipboardData` の移植)。純関数・
|
|
33
|
+
* React / DOM 非依存。契約: text 側は「数値か」を cellFormat.ts の parseNumber で決め (十進文法の
|
|
34
|
+
* 単一真実源)、演算は文字列の桁シフト / 符号反転で**正確** (double を作らない — 2^53 超は全桁保持、
|
|
35
|
+
* "0.07"×100 = "7")。出力は正準十進 ("+" / "-0" / 余分な前後 0 なし)、指数形入力は指数形のまま指数を
|
|
36
|
+
* シフト。非数値 (符号は大文字小文字を問わない TRUE / FALSE 以外) は null = セルを触らない。**結果**が
|
|
37
|
+
* parseNumber の文法 / 有限域を出る演算 ("9e307"×100 / "1e-9999"÷100 / 308 桁整数の ×100) も同じ
|
|
38
|
+
* null (数値領域からの片道脱出を書かない — Excel も拒否)。数式側は
|
|
39
|
+
* 可逆ラップ / 剥離 — 剥離は自前の**逆**ラッパだけ、かつ先頭 "(" が接尾辞直前の ")" と対になるとき
|
|
40
|
+
* だけ (文字列リテラル / クオート済みシート名は不透過。拡張の貪欲 regex は `=(A1)/100+(B1)/100` を
|
|
41
|
+
* `=A1)/100+(B1` に壊すが、ここではラップする)。数式は "=" + 非空本体、それ以外と ±2 / ±3 以外の
|
|
42
|
+
* 指数は呼び手バグとして RangeError。「数式か」は呼び手 (useCommands の getCellFormula 能力分岐) が
|
|
43
|
+
* 決め、text 側で "=" を嗅がない (getCellFormula 未宣言の v0.7 アダプタでは "=A1" は素の text)。
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { parseNumber } from "../model/cellFormat.ts"
|
|
47
|
+
|
|
48
|
+
/** Power-of-ten exponent of a self-calc scale: +2 / +3 multiply by 100 / 1000, -2 / -3 divide. / 自己計算の 10 の冪指数: +2 / +3 は ×100 / ×1000、-2 / -3 は ÷100 / ÷1000。 */
|
|
49
|
+
export type ScaleExponent = 2 | 3 | -2 | -3
|
|
50
|
+
|
|
51
|
+
/** Decomposed decimal literal: sign, all digits without the point, point position (digits before the point) and the literal exponent (null when the input had none). / 分解した十進リテラル: 符号、小数点抜きの全桁、小数点位置 (点より前の桁数)、リテラル指数 (無ければ null)。 */
|
|
52
|
+
type DecimalParts = { negative: boolean; digits: string; point: number; exponent: number | null }
|
|
53
|
+
|
|
54
|
+
/** Rejects an exponent outside the ±2 / ±3 vocabulary (JS callers) with RangeError. / ±2 / ±3 以外の指数 (JS 呼び手) を RangeError で拒否。 */
|
|
55
|
+
const assertScaleExponent = (exponent: number): void => {
|
|
56
|
+
if (exponent !== 2 && exponent !== 3 && exponent !== -2 && exponent !== -3) {
|
|
57
|
+
throw new RangeError(`[spreadsheet] self-calc exponent must be 2, 3, -2 or -3; got ${String(exponent)}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Splits a parseNumber-validated literal into its parts. The grammar guarantees a sign of at most
|
|
63
|
+
* one character, at least one digit, at most one point and an exponent of 1-4 digits, so every
|
|
64
|
+
* slice below is total.
|
|
65
|
+
* parseNumber 検証済みリテラルを部品へ分解。文法が符号 1 文字以下・桁 1 以上・点 1 以下・指数 1-4 桁を
|
|
66
|
+
* 保証するため、以下のスライスは全域で安全。
|
|
67
|
+
*/
|
|
68
|
+
const splitDecimal = (literal: Intl.StringNumericLiteral): DecimalParts => {
|
|
69
|
+
let rest: string = literal
|
|
70
|
+
let negative = false
|
|
71
|
+
// 符号 (+ は落とす、- は保持)
|
|
72
|
+
if (rest.startsWith("-")) {
|
|
73
|
+
negative = true
|
|
74
|
+
rest = rest.slice(1)
|
|
75
|
+
} else if (rest.startsWith("+")) {
|
|
76
|
+
rest = rest.slice(1)
|
|
77
|
+
}
|
|
78
|
+
// 指数部 (1-4 桁の整数 — Number は正確)
|
|
79
|
+
const at = rest.search(/[eE]/)
|
|
80
|
+
let exponent: number | null = null
|
|
81
|
+
if (at >= 0) {
|
|
82
|
+
exponent = Number(rest.slice(at + 1))
|
|
83
|
+
rest = rest.slice(0, at)
|
|
84
|
+
}
|
|
85
|
+
// 小数点で整数部 / 小数部へ
|
|
86
|
+
const dot = rest.indexOf(".")
|
|
87
|
+
const intPart = dot < 0 ? rest : rest.slice(0, dot)
|
|
88
|
+
const fracPart = dot < 0 ? "" : rest.slice(dot + 1)
|
|
89
|
+
return { negative, digits: intPart + fracPart, point: intPart.length, exponent }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Canonical decimal text for a digit run with the point after `point` digits (`point` may fall
|
|
94
|
+
* left of the first digit or right of the last): leading / trailing zeros dropped, zero unsigned.
|
|
95
|
+
* 桁列と小数点位置 (先頭より左 / 末尾より右も可) から正準十進文字列を作る: 前後の余分な 0 を落とし、
|
|
96
|
+
* ゼロは符号を持たない。
|
|
97
|
+
*/
|
|
98
|
+
const renderDecimal = (negative: boolean, digits: string, point: number): string => {
|
|
99
|
+
let int: string
|
|
100
|
+
let frac: string
|
|
101
|
+
// 小数点位置を桁列の範囲へ収める (左へ出れば 0 詰め、右へ出れば 0 付加)
|
|
102
|
+
if (point <= 0) {
|
|
103
|
+
int = "0"
|
|
104
|
+
frac = "0".repeat(-point) + digits
|
|
105
|
+
} else if (point >= digits.length) {
|
|
106
|
+
int = digits + "0".repeat(point - digits.length)
|
|
107
|
+
frac = ""
|
|
108
|
+
} else {
|
|
109
|
+
int = digits.slice(0, point)
|
|
110
|
+
frac = digits.slice(point)
|
|
111
|
+
}
|
|
112
|
+
// 正準化: 整数部の先頭 0 (1 桁は残す) と小数部の末尾 0 を落とす
|
|
113
|
+
int = int.replace(/^0+(?=\d)/, "")
|
|
114
|
+
frac = frac.replace(/0+$/, "")
|
|
115
|
+
// ゼロは符号を持たない
|
|
116
|
+
if (int === "0" && frac === "") {
|
|
117
|
+
return "0"
|
|
118
|
+
}
|
|
119
|
+
return `${negative ? "-" : ""}${int}${frac === "" ? "" : `.${frac}`}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Canonical text of decimal parts after shifting the power of ten by `shift`: a plain literal
|
|
124
|
+
* moves its point, an exponent-form literal keeps its (normalised) mantissa and shifts the
|
|
125
|
+
* exponent; a zero mantissa is "0" whatever the exponent. A result that leaves the parseNumber
|
|
126
|
+
* grammar / finite envelope (an exponent grown past 4 digits, or a value overflowing double to
|
|
127
|
+
* Infinity — plain OR exponent form) is null = the cell is left alone, the same rule as
|
|
128
|
+
* non-numeric input.
|
|
129
|
+
* 10 の冪を `shift` だけずらした正準文字列: 通常形は小数点を移動、指数形は (正準化した) 仮数を保って
|
|
130
|
+
* 指数をずらす。仮数ゼロは指数に依らず "0"。結果が parseNumber の文法 / 有限域を出る場合 (指数の
|
|
131
|
+
* 5 桁化、通常形 / 指数形を問わない double の Infinity 溢れ) は非数値入力と同じ null = セルを
|
|
132
|
+
* 触らない。
|
|
133
|
+
*/
|
|
134
|
+
const renderShifted = (parts: DecimalParts, shift: number): string | null => {
|
|
135
|
+
let candidate: string
|
|
136
|
+
if (parts.exponent === null) {
|
|
137
|
+
candidate = renderDecimal(parts.negative, parts.digits, parts.point + shift)
|
|
138
|
+
} else {
|
|
139
|
+
const mantissa = renderDecimal(parts.negative, parts.digits, parts.point)
|
|
140
|
+
candidate = mantissa === "0" ? "0" : `${mantissa}e${parts.exponent + shift}`
|
|
141
|
+
}
|
|
142
|
+
// 結果が自前の数値文法 / 有限域を出ると (例: "9e307"×100 = Infinity 溢れ、"1e-9999"÷100 = 指数
|
|
143
|
+
// 5 桁化) セルが数値領域から片道脱出する — 逆演算も null 化して自己計算では戻せなくなるため、
|
|
144
|
+
// Excel が拒否するのと同様に「触らない」へ倒す
|
|
145
|
+
return parseNumber(candidate) === null ? null : candidate
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Decimal string × 10^exponent EXACTLY (string digit shift — no double). Non-numeric text (by
|
|
150
|
+
* parseNumber) ⇒ null (the cell is left alone); a result outside the parseNumber grammar / finite
|
|
151
|
+
* envelope ⇒ null too (no one-way escape from the numeric domain). Output is canonical (see
|
|
152
|
+
* fileoverview).
|
|
153
|
+
* 十進文字列の正確な 10^exponent 倍 (文字列の桁シフト — double 不使用)。非数値 (parseNumber 判定) は
|
|
154
|
+
* null (セルを触らない)。結果が parseNumber の文法 / 有限域を出る場合も null (数値領域からの片道
|
|
155
|
+
* 脱出を書かない)。出力は正準形 (fileoverview 参照)。
|
|
156
|
+
*/
|
|
157
|
+
export const scaleDecimalText = (text: string, exponent: ScaleExponent): string | null => {
|
|
158
|
+
assertScaleExponent(exponent)
|
|
159
|
+
const literal = parseNumber(text)
|
|
160
|
+
// 数値でなければ触らない
|
|
161
|
+
if (literal === null) {
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
return renderShifted(splitDecimal(literal), exponent)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Sign reversal of a cell text: a number (by parseNumber) negates exactly (zero stays "0"),
|
|
169
|
+
* TRUE / FALSE (case-insensitive, trimmed) flip to the canonical upper-case opposite, anything
|
|
170
|
+
* else ⇒ null (the cell is left alone).
|
|
171
|
+
* セル text の符号反転: 数値 (parseNumber 判定) は正確に反転 (ゼロは "0" のまま)、TRUE / FALSE
|
|
172
|
+
* (大文字小文字を問わず、trim 後) は正準大文字の逆へ、それ以外は null (セルを触らない)。
|
|
173
|
+
*/
|
|
174
|
+
export const reverseSignText = (text: string): string | null => {
|
|
175
|
+
const literal = parseNumber(text)
|
|
176
|
+
if (literal !== null) {
|
|
177
|
+
const parts = splitDecimal(literal)
|
|
178
|
+
return renderShifted({ ...parts, negative: !parts.negative }, 0)
|
|
179
|
+
}
|
|
180
|
+
// 真偽値 (Excel / Sheets の大文字小文字を問わない TRUE / FALSE) は反転して正準大文字へ
|
|
181
|
+
const upper = text.trim().toUpperCase()
|
|
182
|
+
if (upper === "TRUE") {
|
|
183
|
+
return "FALSE"
|
|
184
|
+
}
|
|
185
|
+
if (upper === "FALSE") {
|
|
186
|
+
return "TRUE"
|
|
187
|
+
}
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Body of a formula ("=" + non-empty body); anything else is a caller bug ⇒ RangeError. / 数式の本体 ("=" + 非空本体)。それ以外は呼び手バグとして RangeError。 */
|
|
192
|
+
const formulaBody = (formula: string): string => {
|
|
193
|
+
if (!formula.startsWith("=") || formula.length === 1) {
|
|
194
|
+
throw new RangeError(`[spreadsheet] self-calc expects a formula ("=" followed by a body); got ${JSON.stringify(formula)}`)
|
|
195
|
+
}
|
|
196
|
+
return formula.slice(1)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* True when `text` has balanced parentheses — depth never negative and zero at the end — with
|
|
201
|
+
* "…" string literals and '…' quoted sheet names opaque (a doubled quote closes and reopens, which
|
|
202
|
+
* is the same state as an escape — no escape rule needed); an unterminated quote is unbalanced.
|
|
203
|
+
* `text` の括弧が釣り合う (深さが負にならず末尾で 0) とき true。"…" 文字列リテラルと '…' クオート済み
|
|
204
|
+
* シート名は不透過 (二重化クオートは閉じ + 再開で脱出と同じ状態 — 脱出規則は不要)、未終端クオートは
|
|
205
|
+
* 不釣合い扱い。
|
|
206
|
+
*/
|
|
207
|
+
const isParenBalanced = (text: string): boolean => {
|
|
208
|
+
let depth = 0
|
|
209
|
+
let quote: '"' | "'" | null = null
|
|
210
|
+
for (let i = 0; i < text.length; i++) {
|
|
211
|
+
const ch = text[i]
|
|
212
|
+
if (quote !== null) {
|
|
213
|
+
// クオート内: 同種のクオートで閉じる (二重化 "" / '' は閉じ + 再開と同じ状態遷移なので脱出規則は不要)
|
|
214
|
+
if (ch === quote) {
|
|
215
|
+
quote = null
|
|
216
|
+
}
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
if (ch === '"' || ch === "'") {
|
|
220
|
+
quote = ch
|
|
221
|
+
} else if (ch === "(") {
|
|
222
|
+
depth++
|
|
223
|
+
} else if (ch === ")") {
|
|
224
|
+
depth--
|
|
225
|
+
// 閉じ過多は先頭 "(" が接尾辞直前の ")" と対でない証拠
|
|
226
|
+
if (depth < 0) {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return quote === null && depth === 0
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Inner text when `body` is exactly `open + inner + close` with a non-empty, balanced inner (so
|
|
236
|
+
* the "(" ending `open` pairs with the ")" starting `close`); null otherwise.
|
|
237
|
+
* `body` が `open + inner + close` そのもので inner が非空かつ釣り合う (open 末尾の "(" が close 先頭の
|
|
238
|
+
* ")" と対になる) とき inner、それ以外は null。
|
|
239
|
+
*/
|
|
240
|
+
const peelWrapper = (body: string, open: string, close: string): string | null => {
|
|
241
|
+
// 形が合わなければ自前ラッパではない
|
|
242
|
+
if (body.length <= open.length + close.length || !body.startsWith(open) || !body.endsWith(close)) {
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
const inner = body.slice(open.length, body.length - close.length)
|
|
246
|
+
return isParenBalanced(inner) ? inner : null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Wraps a formula as `=(body)*100` / `*1000` / `/100` / `/1000`, or peels our own INVERSE wrapper
|
|
251
|
+
* (`=(inner)/100` under ×100 etc.) back to `=inner` — reversible: ×100 then ÷100 returns the
|
|
252
|
+
* original. Same-direction wrappers nest (`=((A1)*100)*100`). RangeError for a non-formula or an
|
|
253
|
+
* exponent outside ±2 / ±3.
|
|
254
|
+
* 数式を `=(body)*100` / `*1000` / `/100` / `/1000` へラップ、または自前の**逆**ラッパ (×100 なら
|
|
255
|
+
* `=(inner)/100` 等) を `=inner` へ剥離 — 可逆 (×100 の後の ÷100 は元へ)。同方向は入れ子
|
|
256
|
+
* (`=((A1)*100)*100`)。非数式と ±2 / ±3 以外の指数は RangeError。
|
|
257
|
+
*/
|
|
258
|
+
export const scaleFormula = (formula: string, exponent: ScaleExponent): string => {
|
|
259
|
+
assertScaleExponent(exponent)
|
|
260
|
+
const body = formulaBody(formula)
|
|
261
|
+
const power = Math.abs(exponent) === 2 ? "100" : "1000"
|
|
262
|
+
// ラップ演算子と、剥離対象 (逆演算) の演算子
|
|
263
|
+
const wrapOp = exponent > 0 ? "*" : "/"
|
|
264
|
+
const peelOp = exponent > 0 ? "/" : "*"
|
|
265
|
+
const inner = peelWrapper(body, "(", `)${peelOp}${power}`)
|
|
266
|
+
return inner === null ? `=(${body})${wrapOp}${power}` : `=${inner}`
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Wraps a formula as `=-(body)`, or peels our own wrapper `=-(inner)` back to `=inner` —
|
|
271
|
+
* reversible (two reversals return the original). RangeError for a non-formula.
|
|
272
|
+
* 数式を `=-(body)` へラップ、または自前ラッパ `=-(inner)` を `=inner` へ剥離 — 可逆 (2 回で元へ)。
|
|
273
|
+
* 非数式は RangeError。
|
|
274
|
+
*/
|
|
275
|
+
export const reverseSignFormula = (formula: string): string => {
|
|
276
|
+
const body = formulaBody(formula)
|
|
277
|
+
const inner = peelWrapper(body, "-(", ")")
|
|
278
|
+
return inner === null ? `=-(${body})` : `=${inner}`
|
|
279
|
+
}
|