@rowsncolumns/fast-formula-parser 6.2.44 → 6.2.46

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/dist/esm/index.js CHANGED
@@ -701,7 +701,9 @@ const excelNumberPrePass = (pattern, value, l10n, render) => {
701
701
  if (!info)
702
702
  return { value };
703
703
  if (info.kind === "date") {
704
- if (info.isElapsed && sections.length === 1)
704
+ // A negative date or time — an elapsed `[h]:mm` duration included —
705
+ // shows `#######` in Excel's 1900 date system.
706
+ if (info.isElapsed && sections.length === 1 && value >= 0)
705
707
  return { value };
706
708
  if (value < 0 || value >= MAX_DATE_SERIAL_EXCLUSIVE) {
707
709
  return { text: DATE_OVERFLOW_DISPLAY, value };
@@ -716,6 +718,16 @@ const excelNumberPrePass = (pattern, value, l10n, render) => {
716
718
  if (index === 1 && value < 0 && pre.text === undefined && pre.value === 0) {
717
719
  return { text: render(sections[index], 0), value };
718
720
  }
721
+ // A one-section format keeps the minus of a negative that rounds to zero:
722
+ // Excel shows -0.4 in `0` as `-0` (and -0.004 in `0.00` as `-0.00`).
723
+ if (index === 0 &&
724
+ implicitSign &&
725
+ value < 0 &&
726
+ info.kind === "number" &&
727
+ pre.text === undefined &&
728
+ pre.value === 0) {
729
+ return { text: `-${render(sections[0], 0)}`, value };
730
+ }
719
731
  return pre;
720
732
  };
721
733
 
@@ -3801,6 +3813,500 @@ function isDateFormat (pattern) {
3801
3813
  return isDate(data.partitions);
3802
3814
  }
3803
3815
 
3816
+ /**
3817
+ * East Asian number-format tokens `numfmt` does not render: the `[DBNum1]`–`[DBNum4]` numeral
3818
+ * systems, the Japanese emperor-era tokens (`g`/`gg`/`ggg`, and `e`/`ee` under a Japanese locale)
3819
+ * and the `aaa`/`aaaa` weekday tokens. TypeScript twin of the engine's Rust
3820
+ * `rnc-cellmodel/src/cjk_format.rs` — keep the two in step.
3821
+ *
3822
+ * {@link prepareCjkNumberFormat} rewrites a format code so the base formatter can render it (era
3823
+ * names, era years and weekday names become quoted literals computed from the date serial,
3824
+ * `[DBNumN]` is removed, and under `[DBNumN]` a date's year becomes a quoted literal in that numeral
3825
+ * system); `finish` then turns the remaining ASCII digits into the numeral system:
3826
+ *
3827
+ * - `General` and date fields read as a number with units (`123` → `百二十三`, `15日` → `十五日`);
3828
+ * - digit placeholders (`0`, `#`) and years read digit by digit (`[DBNum1]0` → `一二三`).
3829
+ *
3830
+ * The language comes from the code's `[$-LCID]` / `[$-tag]` token, then the workbook locale, and
3831
+ * defaults to Japanese.
3832
+ */
3833
+ const DBNUM_TOKEN = /^\[dbnum([1-4])\]$/i;
3834
+ /** Cheap pre-filter: a `DBNum` token, a `g` (era name or `General`), `aaa`, or an `e` beside a locale token. */
3835
+ const mightContainTokens = (code) => /[gG]|dbnum|aaa/i.test(code) || (/[eE]/.test(code) && code.includes("[$"));
3836
+ const prepareCjkNumberFormat = (code, value, workbookLocale) => {
3837
+ if (!mightContainTokens(code))
3838
+ return undefined;
3839
+ const tag = localeTag(code);
3840
+ const tagLang = tag ? cjkLangOfTag(tag) : undefined;
3841
+ const lang = tagLang ??
3842
+ (workbookLocale ? cjkLangOfTag(workbookLocale) : undefined) ??
3843
+ "ja";
3844
+ const gannen = !!tag && tag.toLowerCase().includes("-x-gannen");
3845
+ const date = serialToDate(value);
3846
+ const sections = splitFormatSections(code);
3847
+ const rewritten = [];
3848
+ const levels = [];
3849
+ const dated = [];
3850
+ let changed = false;
3851
+ for (const section of sections) {
3852
+ const level = dbnumLevel(section);
3853
+ const result = rewriteSection(section, {
3854
+ dbnum: level === undefined ? undefined : { level, lang },
3855
+ weekdayLang: lang,
3856
+ eraYears: tagLang === "ja" || hasEraNameToken(section),
3857
+ gannen,
3858
+ date,
3859
+ });
3860
+ changed ||= result.changed || level !== undefined;
3861
+ rewritten.push(result.text);
3862
+ levels.push(level);
3863
+ dated.push(result.dateToken);
3864
+ }
3865
+ if (!changed)
3866
+ return undefined;
3867
+ const anyCondition = sections.some((s) => analyzeFormatSection(s).hasCondition);
3868
+ const index = anyCondition
3869
+ ? 0
3870
+ : Math.min(pickNumberSection(sections.length, value).index, sections.length - 1);
3871
+ const level = levels[index];
3872
+ const dbnum = level !== undefined && (level <= 3 || (level === 4 && lang === "ko"))
3873
+ ? { level, lang }
3874
+ : undefined;
3875
+ let mode;
3876
+ if (dated[index]) {
3877
+ mode = "date";
3878
+ }
3879
+ else {
3880
+ switch (analyzeFormatSection(stripDbNum(sections[index])).kind) {
3881
+ case "general":
3882
+ mode = "general";
3883
+ break;
3884
+ case "date":
3885
+ mode = "date";
3886
+ break;
3887
+ case "number":
3888
+ case "other":
3889
+ mode = "digits";
3890
+ break;
3891
+ default:
3892
+ mode = "unchanged";
3893
+ }
3894
+ }
3895
+ return {
3896
+ code: rewritten.join(";"),
3897
+ finish: (out) => dbnum && mode !== "unchanged" ? convertDigits(out, dbnum, mode) : out,
3898
+ };
3899
+ };
3900
+ const runLength = (chars, from, letter) => {
3901
+ let n = 0;
3902
+ while (from + n < chars.length && chars[from + n].toLowerCase() === letter)
3903
+ n += 1;
3904
+ return n;
3905
+ };
3906
+ const startsWithCi = (chars, from, word) => chars
3907
+ .slice(from, from + word.length)
3908
+ .join("")
3909
+ .toLowerCase() === word;
3910
+ const quote = (text) => `"${text}"`;
3911
+ const rewriteSection = (section, ctx) => {
3912
+ const chars = Array.from(section);
3913
+ let out = "";
3914
+ let changed = false;
3915
+ let dateToken = false;
3916
+ let i = 0;
3917
+ while (i < chars.length) {
3918
+ const c = chars[i];
3919
+ const lower = c.toLowerCase();
3920
+ if (c === '"') {
3921
+ const close = chars.indexOf('"', i + 1);
3922
+ const end = close === -1 ? chars.length : close + 1;
3923
+ out += chars.slice(i, end).join("");
3924
+ i = end;
3925
+ }
3926
+ else if (c === "\\" || c === "_" || c === "*") {
3927
+ out += chars.slice(i, i + 2).join("");
3928
+ i += 2;
3929
+ }
3930
+ else if (c === "[") {
3931
+ const close = chars.indexOf("]", i);
3932
+ const end = close === -1 ? chars.length : close + 1;
3933
+ const token = chars.slice(i, end).join("");
3934
+ if (DBNUM_TOKEN.test(token))
3935
+ changed = true;
3936
+ else
3937
+ out += token;
3938
+ i = end;
3939
+ }
3940
+ else if (lower === "g" && startsWithCi(chars, i, "general")) {
3941
+ out += chars.slice(i, i + 7).join("");
3942
+ i += 7;
3943
+ }
3944
+ else if (lower === "g") {
3945
+ const run = runLength(chars, i, "g");
3946
+ if (ctx.date) {
3947
+ const era = eraOf(ctx.date);
3948
+ out += quote(run === 1 ? era.letter : run === 2 ? era.short : era.name);
3949
+ }
3950
+ changed = true;
3951
+ dateToken = true;
3952
+ i += run;
3953
+ }
3954
+ else if (lower === "e" && chars[i + 1] !== "+" && chars[i + 1] !== "-") {
3955
+ const run = runLength(chars, i, "e");
3956
+ if (ctx.date && ctx.eraYears) {
3957
+ const eraYear = ctx.date.year - eraOf(ctx.date).start[0] + 1;
3958
+ const text = eraYear === 1 && ctx.gannen
3959
+ ? "元"
3960
+ : run >= 2
3961
+ ? String(eraYear).padStart(2, "0")
3962
+ : String(eraYear);
3963
+ out += quote(text);
3964
+ changed = true;
3965
+ dateToken = true;
3966
+ }
3967
+ else if (ctx.date && ctx.dbnum) {
3968
+ out += quote(perDigit(String(ctx.date.year), ctx.dbnum));
3969
+ changed = true;
3970
+ dateToken = true;
3971
+ }
3972
+ else {
3973
+ out += chars.slice(i, i + run).join("");
3974
+ }
3975
+ i += run;
3976
+ }
3977
+ else if (lower === "a" && startsWithCi(chars, i, "am/pm")) {
3978
+ out += chars.slice(i, i + 5).join("");
3979
+ i += 5;
3980
+ }
3981
+ else if (lower === "a" && startsWithCi(chars, i, "a/p")) {
3982
+ out += chars.slice(i, i + 3).join("");
3983
+ i += 3;
3984
+ }
3985
+ else if (lower === "a" && runLength(chars, i, "a") >= 3) {
3986
+ const run = runLength(chars, i, "a");
3987
+ if (ctx.date)
3988
+ out += quote(weekdayName(ctx.weekdayLang, ctx.date.weekday, run >= 4));
3989
+ changed = true;
3990
+ dateToken = true;
3991
+ i += run;
3992
+ }
3993
+ else if (lower === "y" && ctx.dbnum && ctx.date) {
3994
+ const run = runLength(chars, i, "y");
3995
+ const digits = run <= 2
3996
+ ? String(ctx.date.year % 100).padStart(2, "0")
3997
+ : String(ctx.date.year);
3998
+ out += quote(perDigit(digits, ctx.dbnum));
3999
+ changed = true;
4000
+ dateToken = true;
4001
+ i += run;
4002
+ }
4003
+ else {
4004
+ out += c;
4005
+ i += 1;
4006
+ }
4007
+ }
4008
+ return { text: out, changed, dateToken };
4009
+ };
4010
+ /** `[...]` tokens outside quoted literals and escapes. */
4011
+ const bracketTokens = (section) => {
4012
+ const tokens = [];
4013
+ let inQuotes = false;
4014
+ for (let i = 0; i < section.length; i += 1) {
4015
+ const c = section[i];
4016
+ if (c === '"')
4017
+ inQuotes = !inQuotes;
4018
+ else if (!inQuotes && (c === "\\" || c === "_" || c === "*"))
4019
+ i += 1;
4020
+ else if (!inQuotes && c === "[") {
4021
+ const end = section.indexOf("]", i);
4022
+ if (end !== -1) {
4023
+ tokens.push(section.slice(i, end + 1));
4024
+ i = end;
4025
+ }
4026
+ }
4027
+ }
4028
+ return tokens;
4029
+ };
4030
+ const dbnumLevel = (section) => {
4031
+ for (const token of bracketTokens(section)) {
4032
+ const m = DBNUM_TOKEN.exec(token);
4033
+ if (m)
4034
+ return Number(m[1]);
4035
+ }
4036
+ return undefined;
4037
+ };
4038
+ const stripDbNum = (section) => bracketTokens(section)
4039
+ .filter((t) => DBNUM_TOKEN.test(t))
4040
+ .reduce((s, t) => s.replace(t, ""), section);
4041
+ /** Whether a section names an era (`g`) outside quotes, brackets and `General`. */
4042
+ const hasEraNameToken = (section) => {
4043
+ let inQuotes = false;
4044
+ let inBrackets = false;
4045
+ for (let i = 0; i < section.length; i += 1) {
4046
+ const c = section[i];
4047
+ if (inQuotes)
4048
+ inQuotes = c !== '"';
4049
+ else if (inBrackets)
4050
+ inBrackets = c !== "]";
4051
+ else if (c === '"')
4052
+ inQuotes = true;
4053
+ else if (c === "[")
4054
+ inBrackets = true;
4055
+ else if (c === "\\" || c === "_" || c === "*")
4056
+ i += 1;
4057
+ else if (c.toLowerCase() === "g") {
4058
+ if (section.slice(i, i + 7).toLowerCase() === "general")
4059
+ i += 6;
4060
+ else
4061
+ return true;
4062
+ }
4063
+ }
4064
+ return false;
4065
+ };
4066
+ /** The locale spec of the code's first `[$…-spec]` token (`411`, `ja-JP`, `ja-JP-x-gannen`). */
4067
+ const localeTag = (code) => {
4068
+ const start = code.indexOf("[$");
4069
+ if (start === -1)
4070
+ return undefined;
4071
+ const end = code.indexOf("]", start);
4072
+ if (end === -1)
4073
+ return undefined;
4074
+ const inner = code.slice(start + 2, end);
4075
+ const dash = inner.indexOf("-");
4076
+ if (dash === -1)
4077
+ return undefined;
4078
+ const spec = inner.slice(dash + 1);
4079
+ return spec || undefined;
4080
+ };
4081
+ /** Language of an LCID (hex, low 16 bits) or a BCP-47 tag, for the CJK locales. */
4082
+ const cjkLangOfTag = (input) => {
4083
+ const spec = input.split(",")[0].trim();
4084
+ if (/^[0-9a-f]+$/i.test(spec)) {
4085
+ switch (parseInt(spec, 16) & 0xffff) {
4086
+ case 0x0411:
4087
+ return "ja";
4088
+ case 0x0804:
4089
+ case 0x1004:
4090
+ return "zh-Hans";
4091
+ case 0x0404:
4092
+ case 0x0c04:
4093
+ case 0x1404:
4094
+ return "zh-Hant";
4095
+ case 0x0412:
4096
+ return "ko";
4097
+ default:
4098
+ return undefined;
4099
+ }
4100
+ }
4101
+ const [primary, ...rest] = spec.toLowerCase().split(/[-_]/);
4102
+ if (primary === "ja")
4103
+ return "ja";
4104
+ if (primary === "ko")
4105
+ return "ko";
4106
+ if (primary === "zh") {
4107
+ return rest.some((p) => p === "hant" || p === "tw" || p === "hk" || p === "mo")
4108
+ ? "zh-Hant"
4109
+ : "zh-Hans";
4110
+ }
4111
+ return undefined;
4112
+ };
4113
+ // ---- dates ------------------------------------------------------------------------------------
4114
+ /**
4115
+ * Excel 1900-system serial → calendar date, keeping Excel's fictitious 1900-02-29 (serial 60)
4116
+ * and its weekday numbering (serial 1 is a Sunday). The time of day rounds to the nearest second.
4117
+ */
4118
+ const serialToDate = (value) => {
4119
+ if (!Number.isFinite(value) || value < 0 || value >= 2958466)
4120
+ return undefined;
4121
+ const days = Math.floor(Math.round(value * 86400) / 86400);
4122
+ const weekday = (days + 6) % 7;
4123
+ if (days === 60)
4124
+ return { year: 1900, month: 2, day: 29, weekday };
4125
+ const offset = days < 60 ? days : days - 1;
4126
+ const d = new Date(Date.UTC(1899, 11, 31) + offset * 86400000);
4127
+ return {
4128
+ year: d.getUTCFullYear(),
4129
+ month: d.getUTCMonth() + 1,
4130
+ day: d.getUTCDate(),
4131
+ weekday,
4132
+ };
4133
+ };
4134
+ /** Japanese eras with the start dates Windows' Japanese calendar (and so Excel) uses. */
4135
+ const ERAS = [
4136
+ { start: [1868, 1, 1], letter: "M", short: "明", name: "明治" },
4137
+ { start: [1912, 7, 30], letter: "T", short: "大", name: "大正" },
4138
+ { start: [1926, 12, 25], letter: "S", short: "昭", name: "昭和" },
4139
+ { start: [1989, 1, 8], letter: "H", short: "平", name: "平成" },
4140
+ { start: [2019, 5, 1], letter: "R", short: "令", name: "令和" },
4141
+ ];
4142
+ const eraOf = (date) => {
4143
+ const key = date.year * 10000 + date.month * 100 + date.day;
4144
+ for (let i = ERAS.length - 1; i >= 0; i -= 1) {
4145
+ const [y, m, d] = ERAS[i].start;
4146
+ if (key >= y * 10000 + m * 100 + d)
4147
+ return ERAS[i];
4148
+ }
4149
+ return ERAS[0];
4150
+ };
4151
+ const weekdayName = (lang, weekday, long) => {
4152
+ const w = weekday % 7;
4153
+ switch (lang) {
4154
+ case "ja": {
4155
+ const short = "日月火水木金土"[w];
4156
+ return long ? `${short}曜日` : short;
4157
+ }
4158
+ case "ko": {
4159
+ const short = "일월화수목금토"[w];
4160
+ return long ? `${short}요일` : short;
4161
+ }
4162
+ default: {
4163
+ const short = "日一二三四五六"[w];
4164
+ return long ? `星期${short}` : short;
4165
+ }
4166
+ }
4167
+ };
4168
+ const KANJI = Array.from("〇一二三四五六七八九");
4169
+ const FULL_WIDTH = Array.from("0123456789");
4170
+ const BIG_HANS = ["万", "亿", "兆"];
4171
+ const BIG_TRAD = ["萬", "億", "兆"];
4172
+ const BIG_JA = ["万", "億", "兆"];
4173
+ const numerals = ({ level, lang }) => {
4174
+ const zh = lang === "zh-Hans" || lang === "zh-Hant";
4175
+ const level1Big = lang === "ja" ? BIG_JA : lang === "zh-Hans" ? BIG_HANS : BIG_TRAD;
4176
+ if (level === 2) {
4177
+ switch (lang) {
4178
+ case "ja":
4179
+ return {
4180
+ digits: Array.from("〇壱弐参四伍六七八九"),
4181
+ small: ["拾", "百", "阡"],
4182
+ big: BIG_TRAD,
4183
+ omitOne: false,
4184
+ fillZero: false,
4185
+ };
4186
+ case "zh-Hans":
4187
+ return {
4188
+ digits: Array.from("零壹贰叁肆伍陆柒捌玖"),
4189
+ small: ["拾", "佰", "仟"],
4190
+ big: BIG_HANS,
4191
+ omitOne: false,
4192
+ fillZero: true,
4193
+ };
4194
+ case "zh-Hant":
4195
+ return {
4196
+ digits: Array.from("零壹貳參肆伍陸柒捌玖"),
4197
+ small: ["拾", "佰", "仟"],
4198
+ big: BIG_TRAD,
4199
+ omitOne: false,
4200
+ fillZero: true,
4201
+ };
4202
+ case "ko":
4203
+ return {
4204
+ digits: Array.from("零壹貳參四伍六七八九"),
4205
+ small: ["拾", "百", "阡"],
4206
+ big: BIG_TRAD,
4207
+ omitOne: false,
4208
+ fillZero: false,
4209
+ };
4210
+ }
4211
+ }
4212
+ if (level === 3) {
4213
+ return {
4214
+ digits: FULL_WIDTH,
4215
+ small: ["十", "百", "千"],
4216
+ big: level1Big,
4217
+ omitOne: false,
4218
+ fillZero: zh,
4219
+ };
4220
+ }
4221
+ if (level === 4 && lang === "ko") {
4222
+ return {
4223
+ digits: Array.from("영일이삼사오육칠팔구"),
4224
+ small: ["십", "백", "천"],
4225
+ big: ["만", "억", "조"],
4226
+ omitOne: true,
4227
+ fillZero: false,
4228
+ };
4229
+ }
4230
+ return {
4231
+ digits: KANJI,
4232
+ small: ["十", "百", "千"],
4233
+ big: level1Big,
4234
+ omitOne: !zh,
4235
+ fillZero: zh,
4236
+ };
4237
+ };
4238
+ const perDigit = (digits, dbnum) => {
4239
+ const table = numerals(dbnum).digits;
4240
+ return Array.from(digits)
4241
+ .map((c) => (c >= "0" && c <= "9" ? table[Number(c)] : c))
4242
+ .join("");
4243
+ };
4244
+ /** `digits` read as a number with units; past 兆 (≥ 10^16) digit by digit. */
4245
+ const withUnits = (digits, dbnum) => {
4246
+ const n = numerals(dbnum);
4247
+ const trimmed = digits.replace(/^0+/, "");
4248
+ if (!trimmed)
4249
+ return n.digits[0];
4250
+ if (trimmed.length > 16)
4251
+ return perDigit(digits, dbnum);
4252
+ const values = Array.from(trimmed, Number);
4253
+ const groups = Math.ceil(values.length / 4);
4254
+ let out = "";
4255
+ let pendingZero = false;
4256
+ for (let g = 0; g < groups; g += 1) {
4257
+ const groupIndex = groups - 1 - g;
4258
+ const end = values.length - groupIndex * 4;
4259
+ const group = values.slice(Math.max(0, end - 4), end);
4260
+ let wrote = false;
4261
+ group.forEach((d, k) => {
4262
+ const pos = group.length - 1 - k;
4263
+ if (d === 0) {
4264
+ pendingZero ||= out.length > 0 || wrote;
4265
+ return;
4266
+ }
4267
+ if (pendingZero && n.fillZero)
4268
+ out += "零";
4269
+ pendingZero = false;
4270
+ if (!(d === 1 && pos > 0 && n.omitOne))
4271
+ out += n.digits[d];
4272
+ if (pos > 0)
4273
+ out += n.small[pos - 1];
4274
+ wrote = true;
4275
+ });
4276
+ if (wrote && groupIndex > 0)
4277
+ out += n.big[groupIndex - 1];
4278
+ }
4279
+ return out;
4280
+ };
4281
+ const isAsciiDigit = (c) => !!c && c >= "0" && c <= "9";
4282
+ const convertDigits = (text, dbnum, mode) => {
4283
+ const chars = Array.from(text);
4284
+ let out = "";
4285
+ let i = 0;
4286
+ while (i < chars.length) {
4287
+ if (!isAsciiDigit(chars[i])) {
4288
+ out += chars[i];
4289
+ i += 1;
4290
+ continue;
4291
+ }
4292
+ let end = i;
4293
+ while (end < chars.length && isAsciiDigit(chars[end]))
4294
+ end += 1;
4295
+ const run = chars.slice(i, end).join("");
4296
+ const fraction = i >= 2 &&
4297
+ (chars[i - 1] === "." || chars[i - 1] === ",") &&
4298
+ isAsciiDigit(chars[i - 2]);
4299
+ const units = mode === "date"
4300
+ ? dbnum.level !== 3
4301
+ : mode === "general"
4302
+ ? !fraction
4303
+ : false;
4304
+ out += units ? withUnits(run, dbnum) : perDigit(run, dbnum);
4305
+ i = end;
4306
+ }
4307
+ return out;
4308
+ };
4309
+
3804
4310
  // Excel indexed colors mapping used for legacy color references
3805
4311
  const IndexedColors = Object.freeze([
3806
4312
  // 0-7
@@ -3877,35 +4383,44 @@ const IndexedColors = Object.freeze([
3877
4383
  "#333333",
3878
4384
  ]);
3879
4385
 
3880
- const isSingleSectionElapsedTimePattern = (pattern) => !pattern.includes(";") &&
3881
- /\[(?:h+|m+|s+)\]/i.test(pattern.replace(/\[\$[^\]]+\]/g, ""));
3882
4386
  const intlMonthNames = (locale, options) => {
3883
4387
  const formatter = new Intl.DateTimeFormat(locale, {
3884
4388
  ...options,
3885
4389
  timeZone: "UTC",
3886
4390
  });
3887
- return Array.from({ length: 12 }, (_, month) => formatter
3888
- .formatToParts(new Date(Date.UTC(2020, month, 1)))
3889
- .find((part) => part.type === "month")?.value ?? "");
4391
+ const names = Array.from({ length: 12 }, (_, month) => {
4392
+ const parts = formatter.formatToParts(new Date(Date.UTC(2020, month, 1)));
4393
+ const index = parts.findIndex((part) => part.type === "month");
4394
+ const name = parts[index]?.value ?? "";
4395
+ // A numeric month comes split from its counter ("1" + "月"); the CLDR
4396
+ // name on its own (what the Rust engine renders) is "1月".
4397
+ const next = parts[index + 1];
4398
+ return /^\d+$/.test(name) && next?.type === "literal" && !/\d/.test(next.value)
4399
+ ? name + next.value.trim()
4400
+ : name;
4401
+ });
4402
+ // Beside a day, zh/ja write the month as a number ("3月15日") even where
4403
+ // CLDR's `MMMM` on its own is a word (zh "三月"): take the standalone name.
4404
+ if (options.day && names.every((name) => /^\d/.test(name))) {
4405
+ return intlMonthNames(locale, { month: options.month });
4406
+ }
4407
+ return names;
3890
4408
  };
3891
4409
  const intlRegisteredLocales = new Set();
3892
4410
  /**
3893
- * numfmt ships name tables for a fixed set of languages; a tag it does not know
3894
- * (et, ms, nn, …) silently renders English month/weekday names and en-US
3895
- * separators. Excel and `Intl.DateTimeFormat` localize every locale, so an
3896
- * unknown tag gets a numfmt locale built from Intl on first use: separators
3897
- * from `Intl.NumberFormat`, the in-date month names (numfmt's single table is
3898
- * the form used beside a day number — see `getStandaloneMonthLocale`), the
3899
- * in-date weekday names (the form `dateStyle: "full"` shows) and the AM/PM
3900
- * designators. Tags Intl itself does not know keep numfmt's fallback.
4411
+ * Every locale renders from a numfmt locale built from Intl (CLDR) the way the
4412
+ * Rust engine (the Excel reference, `rnc_cellmodel::locale_data`) builds its
4413
+ * own: separators from `Intl.NumberFormat`, the in-date month and weekday
4414
+ * names, and English `AM`/`PM` and `TRUE`/`FALSE` (numfmt's defaults), not
4415
+ * numfmt's shipped tables (de `WAHR`, ja `午前`, en-GB `Sep`). A month shown
4416
+ * without a day takes the standalone names (`getStandaloneMonthLocale`). Tags
4417
+ * Intl does not know keep numfmt's fallback.
3901
4418
  */
3902
4419
  const ensureNumfmtLocale = (locale) => {
3903
4420
  if (intlRegisteredLocales.has(locale))
3904
4421
  return;
3905
4422
  intlRegisteredLocales.add(locale);
3906
4423
  try {
3907
- if (getLocale(locale))
3908
- return;
3909
4424
  const canonical = Intl.DateTimeFormat.supportedLocalesOf([locale])[0];
3910
4425
  if (!canonical)
3911
4426
  return;
@@ -3924,20 +4439,11 @@ const ensureNumfmtLocale = (locale) => {
3924
4439
  .formatToParts(new Date(Date.UTC(2024, 0, 7 + i)))
3925
4440
  .find((p) => p.type === "weekday")?.value ?? "");
3926
4441
  };
3927
- const dayPeriod = (hour) => new Intl.DateTimeFormat(locale, {
3928
- hour: "numeric",
3929
- hour12: true,
3930
- timeZone: "UTC",
3931
- })
3932
- .formatToParts(new Date(Date.UTC(2024, 0, 7, hour)))
3933
- .find((p) => p.type === "dayPeriod")?.value;
3934
4442
  const dateParts = new Intl.DateTimeFormat(locale, {
3935
4443
  timeZone: "UTC",
3936
4444
  }).formatToParts(new Date(Date.UTC(2024, 0, 7)));
3937
4445
  const monthIndex = dateParts.findIndex((p) => p.type === "month");
3938
4446
  const dayIndex = dateParts.findIndex((p) => p.type === "day");
3939
- const am = dayPeriod(9);
3940
- const pm = dayPeriod(21);
3941
4447
  addLocale({
3942
4448
  group: part("group") ?? "\u00a0",
3943
4449
  decimal: part("decimal") ?? ".",
@@ -3945,7 +4451,6 @@ const ensureNumfmtLocale = (locale) => {
3945
4451
  mmm: intlMonthNames(locale, { day: "numeric", month: "short" }),
3946
4452
  dddd: weekdayNames("long"),
3947
4453
  ddd: weekdayNames("short"),
3948
- ...(am && pm && am !== pm ? { ampm: [am, pm] } : {}),
3949
4454
  preferMDY: monthIndex >= 0 && dayIndex >= 0 && monthIndex < dayIndex,
3950
4455
  }, locale);
3951
4456
  }
@@ -3953,6 +4458,31 @@ const ensureNumfmtLocale = (locale) => {
3953
4458
  // Malformed tag: numfmt keeps its own fallback.
3954
4459
  }
3955
4460
  };
4461
+ const scientificLocales = new Map();
4462
+ /**
4463
+ * The Rust engine (ssfmt) renders a scientific mantissa with `.` in every
4464
+ * locale (`1.00E+03` in de-DE), so an exponent section renders under a
4465
+ * companion locale whose decimal separator is `.`.
4466
+ */
4467
+ const getScientificLocale = (locale) => {
4468
+ const cached = scientificLocales.get(locale);
4469
+ if (cached !== undefined)
4470
+ return cached;
4471
+ let overlay = locale;
4472
+ try {
4473
+ const base = getLocale(locale);
4474
+ if (base && base.decimal !== ".") {
4475
+ overlay = `sci_${locale.replace(/[^a-z\d]/gi, "")}`;
4476
+ addLocale({ ...base, decimal: "." }, overlay);
4477
+ }
4478
+ }
4479
+ catch {
4480
+ overlay = locale;
4481
+ }
4482
+ scientificLocales.set(locale, overlay);
4483
+ return overlay;
4484
+ };
4485
+ const SCIENTIFIC_SECTION = /^(?:"[^"]*"|\[[^\]]*\]|\\.|[^"\[\\eE])*[eE][+-]/;
3956
4486
  const localeForPattern = (pattern, locale) => {
3957
4487
  return locale;
3958
4488
  };
@@ -4203,6 +4733,17 @@ const withWorkbookSeparatorsCached = (code, locale) => {
4203
4733
  return rewritten;
4204
4734
  };
4205
4735
  const ssfFormat = (pattern, value, locale) => {
4736
+ // East Asian tokens numfmt does not render (`[DBNumN]`, Japanese era `g`/`e`,
4737
+ // `aaa` weekdays): rewrite the code to plain literals, then convert the
4738
+ // rendered digits to the `[DBNumN]` numeral system (see cjk-number-format).
4739
+ if (typeof value === "number") {
4740
+ const cjk = prepareCjkNumberFormat(pattern, value, locale);
4741
+ if (cjk)
4742
+ return cjk.finish(ssfFormatBase(cjk.code, value, locale));
4743
+ }
4744
+ return ssfFormatBase(pattern, value, locale);
4745
+ };
4746
+ function ssfFormatBase(pattern, value, locale, layout) {
4206
4747
  try {
4207
4748
  if (locale)
4208
4749
  ;
@@ -4211,12 +4752,25 @@ const ssfFormat = (pattern, value, locale) => {
4211
4752
  shortAmPm = withShortAmPmMarker(pattern);
4212
4753
  shortAmPmPatternCache.set(pattern, shortAmPm);
4213
4754
  }
4214
- const code = withWorkbookSeparatorsCached(shortAmPm ?? pattern, locale);
4755
+ const baseCode = withWorkbookSeparatorsCached(shortAmPm ?? pattern, locale);
4756
+ const code = layout ? layout.transform(baseCode) : baseCode;
4215
4757
  let patternLocale = localeForPattern(pattern, locale);
4758
+ if (patternLocale && SCIENTIFIC_SECTION.test(pattern)) {
4759
+ patternLocale = getScientificLocale(patternLocale);
4760
+ }
4216
4761
  if (shortAmPm !== undefined && patternLocale) {
4217
4762
  patternLocale = getShortAmPmLocale(patternLocale);
4218
4763
  }
4219
- const options = { locale: patternLocale };
4764
+ // numfmt types `fillChar` / `skipChar` as booleans; at runtime they are
4765
+ // the marker strings it emits ahead of the fill / skip character.
4766
+ const options = layout
4767
+ ? {
4768
+ locale: patternLocale,
4769
+ nbsp: layout.nbsp,
4770
+ fillChar: layout.fillChar,
4771
+ skipChar: layout.skipChar,
4772
+ }
4773
+ : { locale: patternLocale };
4220
4774
  if (typeof value === "number") {
4221
4775
  // Excel decides rounding, `General`, huge magnitudes and conditional
4222
4776
  // sections from the 15-significant-digit decimal, not the binary
@@ -4227,16 +4781,12 @@ const ssfFormat = (pattern, value, locale) => {
4227
4781
  }
4228
4782
  value = pre.value;
4229
4783
  }
4230
- const isNegativeElapsedTime = typeof value === "number" &&
4231
- value < 0 &&
4232
- isSingleSectionElapsedTimePattern(pattern);
4233
- const formatted = format(code, isNegativeElapsedTime ? Math.abs(value) : value, options);
4234
- return isNegativeElapsedTime ? `-${formatted}` : formatted;
4784
+ return format(code, value, options);
4235
4785
  }
4236
4786
  catch (err) {
4237
- return String(value);
4787
+ return layout ? undefined : String(value);
4238
4788
  }
4239
- };
4789
+ }
4240
4790
  for (let n = 1; n <= 56; n++) {
4241
4791
  IndexedColors[n + 7];
4242
4792
  }
@@ -4429,6 +4979,7 @@ const FORMULA_LOCALE_DATA = {
4429
4979
  "FACTDOUBLE": "ZWEIFAKULTÄT",
4430
4980
  "FALSE": "FALSCH",
4431
4981
  "FDIST": "FVERT",
4982
+ "FIELDVALUE": "FELDWERT",
4432
4983
  "FILTER": "FILTER",
4433
4984
  "FILTERXML": "XMLFILTERN",
4434
4985
  "FIND": "FINDEN",
@@ -4492,6 +5043,8 @@ const FORMULA_LOCALE_DATA = {
4492
5043
  "IMLN": "IMLN",
4493
5044
  "IMLOG10": "IMLOG10",
4494
5045
  "IMLOG2": "IMLOG2",
5046
+ "IMPORTCSV": "IMPORTCSV",
5047
+ "IMPORTTEXT": "IMPORTTEXT",
4495
5048
  "IMPOWER": "IMAPOTENZ",
4496
5049
  "IMPRODUCT": "IMPRODUKT",
4497
5050
  "IMREAL": "IMREALTEIL",
@@ -4629,6 +5182,7 @@ const FORMULA_LOCALE_DATA = {
4629
5182
  "PRODUCT": "PRODUKT",
4630
5183
  "PROPER": "GROSS2",
4631
5184
  "PV": "BW",
5185
+ "PY": "PY",
4632
5186
  "QUARTILE": "QUARTILE",
4633
5187
  "QUARTILE.EXC": "QUARTILE.EXKL",
4634
5188
  "QUARTILE.INC": "QUARTILE.INKL",
@@ -4998,6 +5552,7 @@ const FORMULA_LOCALE_DATA = {
4998
5552
  "FACTDOUBLE": "FACT.DOBLE",
4999
5553
  "FALSE": "FALSO",
5000
5554
  "FDIST": "DISTR.F",
5555
+ "FIELDVALUE": "VALORCAMPO",
5001
5556
  "FILTER": "FILTRAR",
5002
5557
  "FILTERXML": "XMLFILTRO",
5003
5558
  "FIND": "ENCONTRAR",
@@ -5062,6 +5617,8 @@ const FORMULA_LOCALE_DATA = {
5062
5617
  "IMLN": "IM.LN",
5063
5618
  "IMLOG10": "IM.LOG10",
5064
5619
  "IMLOG2": "IM.LOG2",
5620
+ "IMPORTCSV": "IMPORTCSV",
5621
+ "IMPORTTEXT": "IMPORTTEXT",
5065
5622
  "IMPOWER": "IM.POT",
5066
5623
  "IMPRODUCT": "IM.PRODUCT",
5067
5624
  "IMREAL": "IM.REAL",
@@ -5199,6 +5756,7 @@ const FORMULA_LOCALE_DATA = {
5199
5756
  "PRODUCT": "PRODUCTO",
5200
5757
  "PROPER": "NOMPROPIO",
5201
5758
  "PV": "VA",
5759
+ "PY": "PY",
5202
5760
  "QUARTILE": "CUARTIL",
5203
5761
  "QUARTILE.EXC": "CUARTIL.EXC",
5204
5762
  "QUARTILE.INC": "CUARTIL.INC",
@@ -5551,6 +6109,7 @@ const FORMULA_LOCALE_DATA = {
5551
6109
  "FACTDOUBLE": "FACTDOUBLE",
5552
6110
  "FALSE": "FAUX",
5553
6111
  "FDIST": "LOI.F",
6112
+ "FIELDVALUE": "VALEURCHAMP",
5554
6113
  "FILTER": "FILTRE",
5555
6114
  "FILTERXML": "FILTRE.XML",
5556
6115
  "FIND": "TROUVE",
@@ -5615,6 +6174,8 @@ const FORMULA_LOCALE_DATA = {
5615
6174
  "IMLN": "COMPLEXE.LN",
5616
6175
  "IMLOG10": "COMPLEXE.LOG10",
5617
6176
  "IMLOG2": "COMPLEXE.LOG2",
6177
+ "IMPORTCSV": "IMPORTCSV",
6178
+ "IMPORTTEXT": "IMPORTTEXT",
5618
6179
  "IMPOWER": "COMPLEXE.PUISSANCE",
5619
6180
  "IMPRODUCT": "COMPLEXE.PRODUIT",
5620
6181
  "IMREAL": "COMPLEXE.REEL",
@@ -5750,6 +6311,7 @@ const FORMULA_LOCALE_DATA = {
5750
6311
  "PRODUCT": "PRODUIT",
5751
6312
  "PROPER": "NOMPROPRE",
5752
6313
  "PV": "VA",
6314
+ "PY": "PY",
5753
6315
  "QUARTILE": "QUARTILE",
5754
6316
  "QUARTILE.EXC": "QUARTILE.EXCLURE",
5755
6317
  "QUARTILE.INC": "QUARTILE.INCLURE",
@@ -6101,6 +6663,7 @@ const FORMULA_LOCALE_DATA = {
6101
6663
  "FACTDOUBLE": "FATT.DOPPIO",
6102
6664
  "FALSE": "FALSO",
6103
6665
  "FDIST": "DISTRIB.F",
6666
+ "FIELDVALUE": "VALORECAMPO",
6104
6667
  "FILTER": "FILTER",
6105
6668
  "FILTERXML": "FILTRO.XML",
6106
6669
  "FIND": "TROVA",
@@ -6165,6 +6728,7 @@ const FORMULA_LOCALE_DATA = {
6165
6728
  "IMLN": "COMP.LN",
6166
6729
  "IMLOG10": "COMP.LOG10",
6167
6730
  "IMLOG2": "COMP.LOG2",
6731
+ "IMPORTTEXT": "IMPORTTEXT",
6168
6732
  "IMPOWER": "COMP.POTENZA",
6169
6733
  "IMPRODUCT": "COMP.PRODOTTO",
6170
6734
  "IMREAL": "COMP.PARTE.REALE",
@@ -6302,6 +6866,7 @@ const FORMULA_LOCALE_DATA = {
6302
6866
  "PRODUCT": "PRODOTTO",
6303
6867
  "PROPER": "MAIUSC.INIZ",
6304
6868
  "PV": "VA",
6869
+ "PY": "PY",
6305
6870
  "QUARTILE": "QUARTILE",
6306
6871
  "QUARTILE.EXC": "ESC.QUARTILE",
6307
6872
  "QUARTILE.INC": "INC.QUARTILE",
@@ -6460,6 +7025,7 @@ const FORMULA_LOCALE_DATA = {
6460
7025
  },
6461
7026
  "uncertain": [
6462
7027
  "FINDB",
7028
+ "IMPORTCSV",
6463
7029
  "JIS",
6464
7030
  "LEFTB",
6465
7031
  "LENB",
@@ -6662,6 +7228,7 @@ const FORMULA_LOCALE_DATA = {
6662
7228
  "FACTDOUBLE": "DUBBELE.FACULTEIT",
6663
7229
  "FALSE": "ONWAAR",
6664
7230
  "FDIST": "F.VERDELING",
7231
+ "FIELDVALUE": "VELDWAARDE",
6665
7232
  "FILTER": "FILTER",
6666
7233
  "FILTERXML": "XML.FILTEREN",
6667
7234
  "FIND": "VIND.ALLES",
@@ -6726,6 +7293,7 @@ const FORMULA_LOCALE_DATA = {
6726
7293
  "IMLN": "C.LN",
6727
7294
  "IMLOG10": "C.LOG10",
6728
7295
  "IMLOG2": "C.LOG2",
7296
+ "IMPORTCSV": "IMPORTCSV",
6729
7297
  "IMPOWER": "C.MACHT",
6730
7298
  "IMPRODUCT": "C.PRODUCT",
6731
7299
  "IMREAL": "C.REEEL.DEEL",
@@ -6863,6 +7431,7 @@ const FORMULA_LOCALE_DATA = {
6863
7431
  "PRODUCT": "PRODUCT",
6864
7432
  "PROPER": "BEGINLETTERS",
6865
7433
  "PV": "HW",
7434
+ "PY": "PY",
6866
7435
  "QUARTILE": "KWARTIEL",
6867
7436
  "QUARTILE.EXC": "KWARTIEL.EXC",
6868
7437
  "QUARTILE.INC": "KWARTIEL.INC",
@@ -7024,6 +7593,7 @@ const FORMULA_LOCALE_DATA = {
7024
7593
  },
7025
7594
  "uncertain": [
7026
7595
  "FINDB",
7596
+ "IMPORTTEXT",
7027
7597
  "JIS",
7028
7598
  "LEFTB",
7029
7599
  "LENB",
@@ -7216,6 +7786,7 @@ const FORMULA_LOCALE_DATA = {
7216
7786
  "FACTDOUBLE": "SILNIA.DWUKR",
7217
7787
  "FALSE": "FAŁSZ",
7218
7788
  "FDIST": "ROZKŁAD.F",
7789
+ "FIELDVALUE": "POLE.WARTOŚĆ",
7219
7790
  "FILTER": "FILTER",
7220
7791
  "FILTERXML": "FILTERXML",
7221
7792
  "FIND": "ZNAJDŹ",
@@ -7280,6 +7851,7 @@ const FORMULA_LOCALE_DATA = {
7280
7851
  "IMLN": "LN.LICZBY.ZESP",
7281
7852
  "IMLOG10": "LOG10.LICZBY.ZESP",
7282
7853
  "IMLOG2": "LOG2.LICZBY.ZESP",
7854
+ "IMPORTCSV": "IMPORTCSV",
7283
7855
  "IMPOWER": "POTĘGA.LICZBY.ZESP",
7284
7856
  "IMPRODUCT": "ILOCZYN.LICZB.ZESP",
7285
7857
  "IMREAL": "CZ.RZECZ.LICZBY.ZESP",
@@ -7418,6 +7990,7 @@ const FORMULA_LOCALE_DATA = {
7418
7990
  "PRODUCT": "ILOCZYN",
7419
7991
  "PROPER": "Z.WIELKIEJ.LITERY",
7420
7992
  "PV": "PV",
7993
+ "PY": "PY",
7421
7994
  "QUARTILE": "KWARTYL",
7422
7995
  "QUARTILE.EXC": "KWARTYL.PRZEDZ.OTW",
7423
7996
  "QUARTILE.INC": "KWARTYL.PRZEDZ.ZAMK",
@@ -7579,6 +8152,7 @@ const FORMULA_LOCALE_DATA = {
7579
8152
  },
7580
8153
  "uncertain": [
7581
8154
  "FINDB",
8155
+ "IMPORTTEXT",
7582
8156
  "LEFTB",
7583
8157
  "LENB",
7584
8158
  "MIDB",
@@ -7770,6 +8344,7 @@ const FORMULA_LOCALE_DATA = {
7770
8344
  "FACTDOUBLE": "FATDUPLO",
7771
8345
  "FALSE": "FALSO",
7772
8346
  "FDIST": "DISTF",
8347
+ "FIELDVALUE": "VALORDOCAMPO",
7773
8348
  "FILTER": "FILTRAR",
7774
8349
  "FILTERXML": "FILTRARXML",
7775
8350
  "FIND": "LOCALIZAR",
@@ -7834,6 +8409,7 @@ const FORMULA_LOCALE_DATA = {
7834
8409
  "IMLN": "IMLN",
7835
8410
  "IMLOG10": "IMLOG10",
7836
8411
  "IMLOG2": "IMLOG2",
8412
+ "IMPORTCSV": "IMPORTCSV",
7837
8413
  "IMPOWER": "IMPOT",
7838
8414
  "IMPRODUCT": "IMPROD",
7839
8415
  "IMREAL": "IMREAL",
@@ -7971,6 +8547,7 @@ const FORMULA_LOCALE_DATA = {
7971
8547
  "PRODUCT": "PRODUTO",
7972
8548
  "PROPER": "INICIAL.MAIÚSCULA",
7973
8549
  "PV": "VA",
8550
+ "PY": "PY",
7974
8551
  "QUARTILE": "QUARTIL",
7975
8552
  "QUARTILE.EXC": "QUARTIL.EXC",
7976
8553
  "QUARTILE.INC": "QUARTIL.INC",
@@ -8130,6 +8707,7 @@ const FORMULA_LOCALE_DATA = {
8130
8707
  },
8131
8708
  "uncertain": [
8132
8709
  "FINDB",
8710
+ "IMPORTTEXT",
8133
8711
  "JIS",
8134
8712
  "LEFTB",
8135
8713
  "LENB",
@@ -8322,6 +8900,7 @@ const FORMULA_LOCALE_DATA = {
8322
8900
  "FACTDOUBLE": "FATDUPLO",
8323
8901
  "FALSE": "FALSO",
8324
8902
  "FDIST": "DISTF",
8903
+ "FIELDVALUE": "VALORCAMPO",
8325
8904
  "FILTER": "FILTRO",
8326
8905
  "FILTERXML": "FILTROXML",
8327
8906
  "FIND": "PROCURAR",
@@ -8386,6 +8965,8 @@ const FORMULA_LOCALE_DATA = {
8386
8965
  "IMLN": "IMLN",
8387
8966
  "IMLOG10": "IMLOG10",
8388
8967
  "IMLOG2": "IMLOG2",
8968
+ "IMPORTCSV": "IMPORTCSV",
8969
+ "IMPORTTEXT": "IMPORTTEXT",
8389
8970
  "IMPOWER": "IMPOT",
8390
8971
  "IMPRODUCT": "IMPROD",
8391
8972
  "IMREAL": "IMREAL",
@@ -8522,6 +9103,7 @@ const FORMULA_LOCALE_DATA = {
8522
9103
  "PRODUCT": "MULT",
8523
9104
  "PROPER": "PRI.MAIÚSCULA",
8524
9105
  "PV": "VP",
9106
+ "PY": "PY",
8525
9107
  "QUARTILE": "QUARTIL",
8526
9108
  "QUARTILE.EXC": "QUARTIL.EXC",
8527
9109
  "QUARTILE.INC": "QUARTIL.INC",
@@ -8864,6 +9446,7 @@ const FORMULA_LOCALE_DATA = {
8864
9446
  "FACTDOUBLE": "ДВФАКТР",
8865
9447
  "FALSE": "ЛОЖЬ",
8866
9448
  "FDIST": "FРАСП",
9449
+ "FIELDVALUE": "ЗНАЧПОЛЯ",
8867
9450
  "FILTER": "ФИЛЬТР",
8868
9451
  "FILTERXML": "ФИЛЬТР.XML",
8869
9452
  "FIND": "НАЙТИ",
@@ -8929,6 +9512,7 @@ const FORMULA_LOCALE_DATA = {
8929
9512
  "IMLN": "МНИМ.LN",
8930
9513
  "IMLOG10": "МНИМ.LOG10",
8931
9514
  "IMLOG2": "МНИМ.LOG2",
9515
+ "IMPORTCSV": "IMPORTCSV",
8932
9516
  "IMPOWER": "МНИМ.СТЕПЕНЬ",
8933
9517
  "IMPRODUCT": "МНИМ.ПРОИЗВЕД",
8934
9518
  "IMREAL": "МНИМ.ВЕЩ",
@@ -9070,6 +9654,7 @@ const FORMULA_LOCALE_DATA = {
9070
9654
  "PRODUCT": "ПРОИЗВЕД",
9071
9655
  "PROPER": "ПРОПНАЧ",
9072
9656
  "PV": "ПС",
9657
+ "PY": "PY",
9073
9658
  "QUARTILE": "КВАРТИЛЬ",
9074
9659
  "QUARTILE.EXC": "КВАРТИЛЬ.ИСКЛ",
9075
9660
  "QUARTILE.INC": "КВАРТИЛЬ.ВКЛ",
@@ -9234,6 +9819,7 @@ const FORMULA_LOCALE_DATA = {
9234
9819
  "#VALUE!": "#ЗНАЧ!"
9235
9820
  },
9236
9821
  "uncertain": [
9822
+ "IMPORTTEXT",
9237
9823
  "SINGLE"
9238
9824
  ]
9239
9825
  },
@@ -9419,6 +10005,7 @@ const FORMULA_LOCALE_DATA = {
9419
10005
  "FACTDOUBLE": "ÇİFTFAKTÖR",
9420
10006
  "FALSE": "YANLIŞ",
9421
10007
  "FDIST": "FDAĞ",
10008
+ "FIELDVALUE": "FIELDVALUE",
9422
10009
  "FILTER": "FİLTRE",
9423
10010
  "FILTERXML": "XMLFİLTRELE",
9424
10011
  "FIND": "BUL",
@@ -9483,6 +10070,7 @@ const FORMULA_LOCALE_DATA = {
9483
10070
  "IMLN": "SANLN",
9484
10071
  "IMLOG10": "SANLOG10",
9485
10072
  "IMLOG2": "SANLOG2",
10073
+ "IMPORTCSV": "IMPORTCSV",
9486
10074
  "IMPOWER": "SANKUVVET",
9487
10075
  "IMPRODUCT": "SANÇARP",
9488
10076
  "IMREAL": "SANGERÇEK",
@@ -9621,6 +10209,7 @@ const FORMULA_LOCALE_DATA = {
9621
10209
  "PRODUCT": "ÇARPIM",
9622
10210
  "PROPER": "YAZIM.DÜZENİ",
9623
10211
  "PV": "BD",
10212
+ "PY": "PY",
9624
10213
  "QUARTILE": "DÖRTTEBİRLİK",
9625
10214
  "QUARTILE.EXC": "DÖRTTEBİRLİK.HRC",
9626
10215
  "QUARTILE.INC": "DÖRTTEBİRLİK.DHL",
@@ -9781,6 +10370,7 @@ const FORMULA_LOCALE_DATA = {
9781
10370
  },
9782
10371
  "uncertain": [
9783
10372
  "FINDB",
10373
+ "IMPORTTEXT",
9784
10374
  "LEFTB",
9785
10375
  "LENB",
9786
10376
  "MIDB",
@@ -31546,6 +32136,19 @@ const unavailable = () => {
31546
32136
  throw FormulaError.NA;
31547
32137
  };
31548
32138
 
32139
+ // The file imports (IMPORTTEXT, IMPORTCSV) and Python in Excel (PY) need a capability this engine
32140
+ // blocks (local files, a Python runtime): Excel reports that as #BLOCKED!. Linked data type fields
32141
+ // (FIELDVALUE, and `_FV` — `A1.Price` as a workbook file stores it) have no linked data types to
32142
+ // read here: #FIELD!, as Excel answers for a field the value does not have. Same answers as the
32143
+ // rnc engine (formualizer `builtins/external.rs`), so these are never #NAME? on either engine
32144
+ // (demo-app-monorepo#49969 FX-15).
32145
+ const blocked = () => {
32146
+ throw FormulaError.BLOCKED;
32147
+ };
32148
+ const noField = () => {
32149
+ throw FormulaError.FIELD;
32150
+ };
32151
+
31549
32152
  const ExternalFunctions = {
31550
32153
  CUBEKPIMEMBER: unavailable,
31551
32154
  CUBEMEMBER: unavailable,
@@ -31562,6 +32165,11 @@ const ExternalFunctions = {
31562
32165
  TRANSLATE: unavailable,
31563
32166
  DETECTLANGUAGE: unavailable,
31564
32167
  QUERY: unavailable,
32168
+ IMPORTTEXT: blocked,
32169
+ IMPORTCSV: blocked,
32170
+ PY: blocked,
32171
+ FIELDVALUE: noField,
32172
+ _FV: noField,
31565
32173
  };
31566
32174
 
31567
32175
  var api$3 = {};
@@ -53227,6 +53835,11 @@ class FormulaParser {
53227
53835
  );
53228
53836
  this.onRange = config.onRange;
53229
53837
  this.onCell = config.onCell;
53838
+ // Observer of every cell / range the evaluation actually reads, with the
53839
+ // cell being evaluated. `INDIRECT` / `OFFSET` targets only exist at run
53840
+ // time, so this is how a host learns the dynamic precedents of a formula
53841
+ // (the dependency parser cannot see them).
53842
+ this.onReferenceResolved = config.onReferenceResolved;
53230
53843
  // Optional host callback reporting row visibility for SUBTOTAL:
53231
53844
  // ({sheet, row}) => {hiddenByFilter?: boolean, hiddenByUser?: boolean}
53232
53845
  this.onRowHidden = config.onRowHidden;
@@ -53376,18 +53989,36 @@ class FormulaParser {
53376
53989
  const bounds = this.onSpilledRange(ref);
53377
53990
  if (bounds instanceof FormulaError) return bounds;
53378
53991
  if (bounds && bounds.from && bounds.to) {
53379
- return this.onRange({
53992
+ const spilled = {
53380
53993
  from: bounds.from,
53381
53994
  to: bounds.to,
53382
53995
  sheet: bounds.sheet ?? ref.sheet,
53383
- });
53996
+ };
53997
+ this.noteReferenceResolved("range", spilled, finalPosition);
53998
+ return this.onRange(spilled);
53384
53999
  }
53385
54000
  }
53386
54001
  return FormulaError.REF;
53387
54002
  }
54003
+ this.noteReferenceResolved("cell", ref, finalPosition);
53388
54004
  return this.onCell(ref);
53389
54005
  }
53390
54006
 
54007
+ /**
54008
+ * Tell the host which reference an evaluation is about to read.
54009
+ * @param {"cell"|"range"} kind
54010
+ * @param ref - the resolved reference (sheet already defaulted)
54011
+ * @param position - the cell being evaluated, when known
54012
+ */
54013
+ noteReferenceResolved(kind, ref, position) {
54014
+ if (typeof this.onReferenceResolved !== "function") return;
54015
+ try {
54016
+ this.onReferenceResolved({ kind, ref, position });
54017
+ } catch {
54018
+ // An observer must never break the evaluation.
54019
+ }
54020
+ }
54021
+
53391
54022
  /**
53392
54023
  * Get values from the range reference.
53393
54024
  * @param ref
@@ -53402,6 +54033,7 @@ class FormulaParser {
53402
54033
  if (ref.sheetEnd != null) {
53403
54034
  return this.get3DRange(ref, true);
53404
54035
  }
54036
+ this.noteReferenceResolved("range", ref, finalPosition);
53405
54037
  return this.onRange(ref);
53406
54038
  }
53407
54039
 
@@ -53438,6 +54070,11 @@ class FormulaParser {
53438
54070
  for (const sheet of sheets) {
53439
54071
  const { sheetEnd: _ignored, ...perSheet } = ref;
53440
54072
  perSheet.sheet = sheet;
54073
+ this.noteReferenceResolved(
54074
+ isRange ? "range" : "cell",
54075
+ perSheet,
54076
+ this.position,
54077
+ );
53441
54078
  const value = isRange ? this.onRange(perSheet) : this.onCell(perSheet);
53442
54079
  if (value instanceof FormulaError) return value;
53443
54080
  if (Array.isArray(value)) {