@odoo/o-spreadsheet 19.1.2 → 19.2.0-alpha.2

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.
@@ -143,12 +143,11 @@ type BarChartRuntime = {
143
143
  interface GenericCriterion {
144
144
  type: GenericCriterionType;
145
145
  values: string[];
146
+ isPercent?: boolean;
147
+ isBottom?: boolean;
146
148
  }
147
- type GenericCriterionType = "containsText" | "notContainsText" | "isEqualText" | "isEmail" | "isLink" | "dateIs" | "dateIsBefore" | "dateIsOnOrBefore" | "dateIsAfter" | "dateIsOnOrAfter" | "dateIsBetween" | "dateIsNotBetween" | "dateIsValid" | "isEqual" | "isNotEqual" | "isGreaterThan" | "isGreaterOrEqualTo" | "isLessThan" | "isLessOrEqualTo" | "isBetween" | "isNotBetween" | "isBoolean" | "isValueInList" | "isValueInRange" | "customFormula" | "beginsWithText" | "endsWithText" | "isNotEmpty" | "isEmpty";
149
+ type GenericCriterionType = "containsText" | "notContainsText" | "isEqualText" | "isEmail" | "isLink" | "dateIs" | "dateIsBefore" | "dateIsOnOrBefore" | "dateIsAfter" | "dateIsOnOrAfter" | "dateIsBetween" | "dateIsNotBetween" | "dateIsValid" | "isEqual" | "isNotEqual" | "isGreaterThan" | "isGreaterOrEqualTo" | "isLessThan" | "isLessOrEqualTo" | "isBetween" | "isNotBetween" | "isBoolean" | "isValueInList" | "isValueInRange" | "customFormula" | "beginsWithText" | "endsWithText" | "isNotEmpty" | "isEmpty" | "top10";
148
150
  type DateCriterionValue = "today" | "tomorrow" | "yesterday" | "lastWeek" | "lastMonth" | "lastYear" | "exactDate";
149
- type EvaluatedCriterion<T extends GenericCriterion = GenericCriterion> = Omit<T, "values"> & {
150
- values: CellValue[];
151
- };
152
151
 
153
152
  interface RangePart {
154
153
  readonly colFixed: boolean;
@@ -646,6 +645,8 @@ interface CellIsRule extends SingleColorRule {
646
645
  operator: ConditionalFormattingOperatorValues;
647
646
  values: string[];
648
647
  dateValue?: DateCriterionValue;
648
+ isPercent?: boolean;
649
+ isBottom?: boolean;
649
650
  }
650
651
  type ThresholdType = "value" | "number" | "percentage" | "percentile" | "formula";
651
652
  type ColorScaleThreshold = {
@@ -688,7 +689,7 @@ interface IconSetRule {
688
689
  upperInflectionPoint: IconThreshold;
689
690
  lowerInflectionPoint: IconThreshold;
690
691
  }
691
- declare const cfOperators: readonly ["containsText", "notContainsText", "isGreaterThan", "isGreaterOrEqualTo", "isLessThan", "isLessOrEqualTo", "isBetween", "isNotBetween", "beginsWithText", "endsWithText", "isNotEmpty", "isEmpty", "isNotEqual", "isEqual", "customFormula", "dateIs", "dateIsBefore", "dateIsAfter", "dateIsOnOrBefore", "dateIsOnOrAfter"];
692
+ declare const cfOperators: readonly ["containsText", "notContainsText", "isGreaterThan", "isGreaterOrEqualTo", "isLessThan", "isLessOrEqualTo", "isBetween", "isNotBetween", "beginsWithText", "endsWithText", "isNotEmpty", "isEmpty", "isNotEqual", "isEqual", "customFormula", "dateIs", "dateIsBefore", "dateIsAfter", "dateIsOnOrBefore", "dateIsOnOrAfter", "top10"];
692
693
  type ConditionalFormattingOperatorValues = (typeof cfOperators)[number];
693
694
 
694
695
  interface ConditionalFormatState {
@@ -2756,6 +2757,9 @@ type SheetValidationResult = {
2756
2757
  declare class EvaluationDataValidationPlugin extends CoreViewPlugin {
2757
2758
  static getters: readonly ["getDataValidationInvalidCriterionValueMessage", "getInvalidDataValidationMessage", "getValidationResultForCellValue", "getDataValidationRangeValues", "isCellValidCheckbox", "getDataValidationCellStyle", "getDataValidationChipStyle", "isDataValidationInvalid"];
2758
2759
  validationResults: Record<UID, SheetValidationResult>;
2760
+ criterionPreComputeResult: Record<UID, {
2761
+ [dvRuleId: UID]: unknown;
2762
+ }>;
2759
2763
  handle(cmd: CoreViewCommand): void;
2760
2764
  isDataValidationInvalid(cellPosition: CellPosition): boolean;
2761
2765
  getDataValidationCellStyle(position: CellPosition): Style | undefined;
@@ -2767,7 +2771,7 @@ declare class EvaluationDataValidationPlugin extends CoreViewPlugin {
2767
2771
  * The value must be canonicalized.
2768
2772
  */
2769
2773
  getDataValidationInvalidCriterionValueMessage(criterionType: DataValidationCriterionType, value: string): string | undefined;
2770
- getDataValidationRangeValues(sheetId: UID, criterion: EvaluatedCriterion): {
2774
+ getDataValidationRangeValues(sheetId: UID, criterion: GenericCriterion): {
2771
2775
  value: string;
2772
2776
  label: string;
2773
2777
  }[];
@@ -3,8 +3,8 @@
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
5
  * @version 19.1.0-alpha.3
6
- * @date 2026-01-07T16:20:59.139Z
7
- * @hash febc3e9
6
+ * @date 2026-01-07T16:20:57.293Z
7
+ * @hash ac2fa3e
8
8
  */
9
9
 
10
10
  class FunctionCodeBuilder {
@@ -14251,6 +14251,7 @@ const DVTerms = {
14251
14251
  dateValue: _t("The value must be a date"),
14252
14252
  validRange: _t("The value must be a valid range"),
14253
14253
  validFormula: _t("The formula must be valid"),
14254
+ positiveNumber: _t("The value must be a positive number"),
14254
14255
  },
14255
14256
  Errors: {
14256
14257
  ["InvalidRange" /* CommandResult.InvalidRange */]: _t("The range is invalid."),
@@ -22402,6 +22403,7 @@ const CF_OPERATOR_TYPE_CONVERSION_MAP = {
22402
22403
  greaterThanOrEqual: "isGreaterOrEqualTo",
22403
22404
  lessThan: "isLessThan",
22404
22405
  lessThanOrEqual: "isLessOrEqualTo",
22406
+ top10: "top10",
22405
22407
  };
22406
22408
  /** Conversion map CF types in XLSX <=> Cf types in o_spreadsheet */
22407
22409
  const CF_TYPE_CONVERSION_MAP = {
@@ -22942,6 +22944,7 @@ function convertConditionalFormats(xlsxCfs, dxfs, warningManager) {
22942
22944
  const rule = cf.cfRules[0];
22943
22945
  let operator;
22944
22946
  const values = [];
22947
+ const cfAdditionalProperties = {};
22945
22948
  if (rule.dxfId === undefined &&
22946
22949
  !(rule.type === "colorScale" || rule.type === "iconSet" || rule.type === "dataBar"))
22947
22950
  continue;
@@ -22950,7 +22953,6 @@ function convertConditionalFormats(xlsxCfs, dxfs, warningManager) {
22950
22953
  case "containsErrors":
22951
22954
  case "notContainsErrors":
22952
22955
  case "duplicateValues":
22953
- case "top10":
22954
22956
  case "uniqueValues":
22955
22957
  case "timePeriod":
22956
22958
  // Not supported
@@ -23001,6 +23003,18 @@ function convertConditionalFormats(xlsxCfs, dxfs, warningManager) {
23001
23003
  values.push(prefixFormulaWithEqual(rule.formula[1]));
23002
23004
  }
23003
23005
  break;
23006
+ case "top10":
23007
+ if (rule.rank === undefined)
23008
+ continue;
23009
+ operator = CF_OPERATOR_TYPE_CONVERSION_MAP[rule.type];
23010
+ values.push(rule.rank.toString());
23011
+ if (rule.percent) {
23012
+ cfAdditionalProperties.isPercent = true;
23013
+ }
23014
+ if (rule.bottom) {
23015
+ cfAdditionalProperties.isBottom = true;
23016
+ }
23017
+ break;
23004
23018
  }
23005
23019
  if (operator && rule.dxfId !== undefined) {
23006
23020
  cfs.push({
@@ -23011,6 +23025,7 @@ function convertConditionalFormats(xlsxCfs, dxfs, warningManager) {
23011
23025
  type: "CellIsRule",
23012
23026
  operator: operator,
23013
23027
  values: values,
23028
+ ...cfAdditionalProperties,
23014
23029
  style: convertStyle({ fontStyle: dxfs[rule.dxfId].font, fillStyle: dxfs[rule.dxfId].fill }, warningManager),
23015
23030
  },
23016
23031
  });
@@ -23304,6 +23319,8 @@ function convertOperator(operator) {
23304
23319
  return "greaterThanOrEqual";
23305
23320
  case "dateIsOnOrBefore":
23306
23321
  return "lessThanOrEqual";
23322
+ case "top10":
23323
+ return "top10";
23307
23324
  }
23308
23325
  }
23309
23326
  // -------------------------------------
@@ -29364,15 +29381,20 @@ criterionEvaluatorRegistry.add("isValueInList", {
29364
29381
  getPreview: (criterion) => _t("Value one of: %s", criterion.values.join(", ")),
29365
29382
  });
29366
29383
  criterionEvaluatorRegistry.add("isValueInRange", {
29367
- type: "isValueInList",
29368
- isValueValid: (value, criterion, getters, sheetId) => {
29384
+ type: "isValueInRange",
29385
+ preComputeCriterion: (criterion, criterionRanges, getters) => {
29386
+ if (criterionRanges.length === 0) {
29387
+ return new Set();
29388
+ }
29389
+ const sheetId = criterionRanges[0].sheetId;
29390
+ const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
29391
+ return new Set(criterionValues.map((value) => value.value.toString().toLowerCase()));
29392
+ },
29393
+ isValueValid: (value, criterion, valuesSet) => {
29369
29394
  if (!value) {
29370
29395
  return false;
29371
29396
  }
29372
- const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
29373
- return criterionValues
29374
- .map((value) => value.value.toLowerCase())
29375
- .includes(value.toString().toLowerCase());
29397
+ return valuesSet.has(value.toString().toLowerCase());
29376
29398
  },
29377
29399
  getErrorString: (criterion) => _t("The value must be a value in the range %s", String(criterion.values[0])),
29378
29400
  isCriterionValueValid: (value) => rangeReference.test(value),
@@ -29449,6 +29471,67 @@ criterionEvaluatorRegistry.add("isNotEmpty", {
29449
29471
  name: _t("Is not empty"),
29450
29472
  getPreview: () => _t("Is not empty"),
29451
29473
  });
29474
+ criterionEvaluatorRegistry.add("top10", {
29475
+ type: "top10",
29476
+ preComputeCriterion: (criterion, criterionRanges, getters) => {
29477
+ let value = tryToNumber(criterion.values[0], DEFAULT_LOCALE);
29478
+ if (value === undefined || value <= 0) {
29479
+ return undefined;
29480
+ }
29481
+ const numberValues = [];
29482
+ for (const range of criterionRanges) {
29483
+ for (const value of getters.getRangeValues(range)) {
29484
+ if (typeof value === "number") {
29485
+ numberValues.push(value);
29486
+ }
29487
+ }
29488
+ }
29489
+ const sortedValues = numberValues.sort((a, b) => a - b);
29490
+ if (criterion.isPercent) {
29491
+ value = clip(value, 1, 100);
29492
+ }
29493
+ let index = 0;
29494
+ if (criterion.isBottom && !criterion.isPercent) {
29495
+ index = value - 1;
29496
+ }
29497
+ else if (criterion.isBottom && criterion.isPercent) {
29498
+ index = Math.floor((sortedValues.length * value) / 100) - 1;
29499
+ }
29500
+ else if (!criterion.isBottom && criterion.isPercent) {
29501
+ index = sortedValues.length - Math.floor((sortedValues.length * value) / 100);
29502
+ }
29503
+ else {
29504
+ index = sortedValues.length - value;
29505
+ }
29506
+ index = clip(index, 0, sortedValues.length - 1);
29507
+ return sortedValues[index];
29508
+ },
29509
+ isValueValid: (value, criterion, threshold) => {
29510
+ if (typeof value !== "number" || threshold === undefined) {
29511
+ return false;
29512
+ }
29513
+ return criterion.isBottom ? value <= threshold : value >= threshold;
29514
+ },
29515
+ getErrorString: (criterion) => {
29516
+ const args = {
29517
+ value: String(criterion.values[0]),
29518
+ percentSymbol: criterion.isPercent ? "%" : "",
29519
+ };
29520
+ return criterion.isBottom
29521
+ ? _t("The value must be in bottom %(value)s%(percentSymbol)s", args)
29522
+ : _t("The value must be in top %(value)s%(percentSymbol)s", args);
29523
+ },
29524
+ isCriterionValueValid: (value) => checkValueIsPositiveNumber(value),
29525
+ criterionValueErrorString: DVTerms.CriterionError.positiveNumber,
29526
+ numberOfValues: () => 1,
29527
+ name: _t("Is in Top/Bottom ranking"),
29528
+ getPreview: (criterion) => {
29529
+ const args = { value: criterion.values[0], percentSymbol: criterion.isPercent ? "%" : "" };
29530
+ return criterion.isBottom
29531
+ ? _t("Value is in bottom %(value)s%(percentSymbol)s", args)
29532
+ : _t("Value is in top %(value)s%(percentSymbol)s", args);
29533
+ },
29534
+ });
29452
29535
  function getNumberCriterionlocalizedValues(criterion, locale) {
29453
29536
  return criterion.values.map((value) => {
29454
29537
  return value !== undefined
@@ -29470,6 +29553,10 @@ function checkValueIsNumber(value) {
29470
29553
  const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);
29471
29554
  return valueAsNumber !== undefined;
29472
29555
  }
29556
+ function checkValueIsPositiveNumber(value) {
29557
+ const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);
29558
+ return valueAsNumber !== undefined && valueAsNumber > 0;
29559
+ }
29473
29560
 
29474
29561
  // -----------------------------------------------------------------------------
29475
29562
  // Constants
@@ -39800,6 +39887,10 @@ class EvaluationConditionalFormatPlugin extends CoreViewPlugin {
39800
39887
  break;
39801
39888
  case "CellIsRule":
39802
39889
  const formulas = cf.rule.values.map((value) => value.startsWith("=") ? compile(value) : undefined);
39890
+ const evaluator = criterionEvaluatorRegistry.get(cf.rule.operator);
39891
+ const criterion = { ...cf.rule, type: cf.rule.operator };
39892
+ const ranges = cf.ranges.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));
39893
+ const preComputedCriterion = evaluator.preComputeCriterion?.(criterion, ranges, this.getters);
39803
39894
  for (const ref of cf.ranges) {
39804
39895
  const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
39805
39896
  for (let row = zone.top; row <= zone.bottom; row++) {
@@ -39812,7 +39903,7 @@ class EvaluationConditionalFormatPlugin extends CoreViewPlugin {
39812
39903
  }
39813
39904
  return value;
39814
39905
  });
39815
- if (this.getRuleResultForTarget(target, { ...cf.rule, values })) {
39906
+ if (this.getRuleResultForTarget(target, { ...cf.rule, values }, preComputedCriterion)) {
39816
39907
  if (!computedStyle[col])
39817
39908
  computedStyle[col] = [];
39818
39909
  // we must combine all the properties of all the CF rules applied to the given cell
@@ -39975,7 +40066,7 @@ class EvaluationConditionalFormatPlugin extends CoreViewPlugin {
39975
40066
  }
39976
40067
  }
39977
40068
  }
39978
- getRuleResultForTarget(target, rule) {
40069
+ getRuleResultForTarget(target, rule, preComputedCriterion) {
39979
40070
  const cell = this.getters.getEvaluatedCell(target);
39980
40071
  if (cell.type === CellValueType.error) {
39981
40072
  return false;
@@ -39992,11 +40083,12 @@ class EvaluationConditionalFormatPlugin extends CoreViewPlugin {
39992
40083
  return false;
39993
40084
  }
39994
40085
  const evaluatedCriterion = {
40086
+ ...rule,
39995
40087
  type: rule.operator,
39996
40088
  values: evaluatedCriterionValues.map(toScalar),
39997
40089
  dateValue: rule.dateValue || "exactDate",
39998
40090
  };
39999
- return evaluator.isValueValid(cell.value ?? "", evaluatedCriterion, this.getters, sheetId);
40091
+ return evaluator.isValueValid(cell.value ?? "", evaluatedCriterion, preComputedCriterion);
40000
40092
  }
40001
40093
  }
40002
40094
 
@@ -40013,17 +40105,20 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
40013
40105
  "isDataValidationInvalid",
40014
40106
  ];
40015
40107
  validationResults = {};
40108
+ criterionPreComputeResult = {};
40016
40109
  handle(cmd) {
40017
40110
  if (invalidateEvaluationCommands.has(cmd.type) ||
40018
40111
  cmd.type === "EVALUATE_CELLS" ||
40019
40112
  (cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd))) {
40020
40113
  this.validationResults = {};
40114
+ this.criterionPreComputeResult = {};
40021
40115
  return;
40022
40116
  }
40023
40117
  switch (cmd.type) {
40024
40118
  case "ADD_DATA_VALIDATION_RULE":
40025
40119
  case "REMOVE_DATA_VALIDATION_RULE":
40026
40120
  delete this.validationResults[cmd.sheetId];
40121
+ delete this.criterionPreComputeResult[cmd.sheetId];
40027
40122
  break;
40028
40123
  }
40029
40124
  }
@@ -40166,7 +40261,15 @@ class EvaluationDataValidationPlugin extends CoreViewPlugin {
40166
40261
  return undefined;
40167
40262
  }
40168
40263
  const evaluatedCriterion = { ...criterion, values: evaluatedCriterionValues.map(toScalar) };
40169
- if (evaluator.isValueValid(cellValue, evaluatedCriterion, this.getters, sheetId)) {
40264
+ if (!this.criterionPreComputeResult[sheetId]) {
40265
+ this.criterionPreComputeResult[sheetId] = {};
40266
+ }
40267
+ let preComputedCriterion = this.criterionPreComputeResult[sheetId][rule.id];
40268
+ if (preComputedCriterion === undefined) {
40269
+ preComputedCriterion = evaluator.preComputeCriterion?.(rule.criterion, rule.ranges, this.getters);
40270
+ this.criterionPreComputeResult[sheetId][rule.id] = preComputedCriterion;
40271
+ }
40272
+ if (evaluator.isValueValid(cellValue, evaluatedCriterion, preComputedCriterion)) {
40170
40273
  return undefined;
40171
40274
  }
40172
40275
  return evaluator.getErrorString(evaluatedCriterion, this.getters, sheetId);
@@ -45498,6 +45601,7 @@ class FilterEvaluationPlugin extends UIPlugin {
45498
45601
  if (filterValue.type === "none")
45499
45602
  continue;
45500
45603
  const evaluator = criterionEvaluatorRegistry.get(filterValue.type);
45604
+ const preComputedCriterion = evaluator.preComputeCriterion?.(filterValue, [filter.filteredRange], this.getters);
45501
45605
  const evaluatedCriterionValues = filterValue.values.map((value) => {
45502
45606
  if (!value.startsWith("=")) {
45503
45607
  return parseLiteral(value, DEFAULT_LOCALE);
@@ -45515,7 +45619,7 @@ class FilterEvaluationPlugin extends UIPlugin {
45515
45619
  for (let row = filteredZone.top; row <= filteredZone.bottom; row++) {
45516
45620
  const position = { sheetId, col: filter.col, row };
45517
45621
  const value = this.getters.getEvaluatedCell(position).value ?? "";
45518
- if (!evaluator.isValueValid(value, evaluatedCriterion, this.getters, sheetId)) {
45622
+ if (!evaluator.isValueValid(value, evaluatedCriterion, preComputedCriterion)) {
45519
45623
  hiddenRows.add(row);
45520
45624
  }
45521
45625
  }
@@ -49656,6 +49760,8 @@ function cellRuleFormula(ranges, rule) {
49656
49760
  case undefined:
49657
49761
  throw new Error("dateValue should be defined");
49658
49762
  }
49763
+ case "top10":
49764
+ return [];
49659
49765
  }
49660
49766
  }
49661
49767
  function cellRuleTypeAttributes(rule) {
@@ -49688,6 +49794,14 @@ function cellRuleTypeAttributes(rule) {
49688
49794
  case "dateIs":
49689
49795
  case "customFormula":
49690
49796
  return [["type", "expression"]];
49797
+ case "top10": {
49798
+ return [
49799
+ ["type", "top10"],
49800
+ ["rank", rule.values[0]],
49801
+ ["percent", rule.isPercent ? "1" : "0"],
49802
+ ["bottom", rule.isBottom ? "1" : "0"],
49803
+ ];
49804
+ }
49691
49805
  }
49692
49806
  }
49693
49807
  function addDataBarRule(cf, rule) {
@@ -51560,5 +51674,5 @@ export { BadExpressionError, BasePlugin, CellErrorType, CircularDependencyError,
51560
51674
 
51561
51675
 
51562
51676
  __info__.version = "19.1.0-alpha.3";
51563
- __info__.date = "2026-01-07T16:20:59.139Z";
51564
- __info__.hash = "febc3e9";
51677
+ __info__.date = "2026-01-07T16:20:57.293Z";
51678
+ __info__.hash = "ac2fa3e";
@@ -3,8 +3,8 @@
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
5
  * @version 19.1.0-alpha.3
6
- * @date 2026-01-07T16:20:59.139Z
7
- * @hash febc3e9
6
+ * @date 2026-01-07T16:20:57.293Z
7
+ * @hash ac2fa3e
8
8
  */
9
9
 
10
10
  (function (exports) {
@@ -14254,6 +14254,7 @@
14254
14254
  dateValue: _t("The value must be a date"),
14255
14255
  validRange: _t("The value must be a valid range"),
14256
14256
  validFormula: _t("The formula must be valid"),
14257
+ positiveNumber: _t("The value must be a positive number"),
14257
14258
  },
14258
14259
  Errors: {
14259
14260
  ["InvalidRange" /* CommandResult.InvalidRange */]: _t("The range is invalid."),
@@ -22405,6 +22406,7 @@
22405
22406
  greaterThanOrEqual: "isGreaterOrEqualTo",
22406
22407
  lessThan: "isLessThan",
22407
22408
  lessThanOrEqual: "isLessOrEqualTo",
22409
+ top10: "top10",
22408
22410
  };
22409
22411
  /** Conversion map CF types in XLSX <=> Cf types in o_spreadsheet */
22410
22412
  const CF_TYPE_CONVERSION_MAP = {
@@ -22945,6 +22947,7 @@
22945
22947
  const rule = cf.cfRules[0];
22946
22948
  let operator;
22947
22949
  const values = [];
22950
+ const cfAdditionalProperties = {};
22948
22951
  if (rule.dxfId === undefined &&
22949
22952
  !(rule.type === "colorScale" || rule.type === "iconSet" || rule.type === "dataBar"))
22950
22953
  continue;
@@ -22953,7 +22956,6 @@
22953
22956
  case "containsErrors":
22954
22957
  case "notContainsErrors":
22955
22958
  case "duplicateValues":
22956
- case "top10":
22957
22959
  case "uniqueValues":
22958
22960
  case "timePeriod":
22959
22961
  // Not supported
@@ -23004,6 +23006,18 @@
23004
23006
  values.push(prefixFormulaWithEqual(rule.formula[1]));
23005
23007
  }
23006
23008
  break;
23009
+ case "top10":
23010
+ if (rule.rank === undefined)
23011
+ continue;
23012
+ operator = CF_OPERATOR_TYPE_CONVERSION_MAP[rule.type];
23013
+ values.push(rule.rank.toString());
23014
+ if (rule.percent) {
23015
+ cfAdditionalProperties.isPercent = true;
23016
+ }
23017
+ if (rule.bottom) {
23018
+ cfAdditionalProperties.isBottom = true;
23019
+ }
23020
+ break;
23007
23021
  }
23008
23022
  if (operator && rule.dxfId !== undefined) {
23009
23023
  cfs.push({
@@ -23014,6 +23028,7 @@
23014
23028
  type: "CellIsRule",
23015
23029
  operator: operator,
23016
23030
  values: values,
23031
+ ...cfAdditionalProperties,
23017
23032
  style: convertStyle({ fontStyle: dxfs[rule.dxfId].font, fillStyle: dxfs[rule.dxfId].fill }, warningManager),
23018
23033
  },
23019
23034
  });
@@ -23307,6 +23322,8 @@
23307
23322
  return "greaterThanOrEqual";
23308
23323
  case "dateIsOnOrBefore":
23309
23324
  return "lessThanOrEqual";
23325
+ case "top10":
23326
+ return "top10";
23310
23327
  }
23311
23328
  }
23312
23329
  // -------------------------------------
@@ -29367,15 +29384,20 @@
29367
29384
  getPreview: (criterion) => _t("Value one of: %s", criterion.values.join(", ")),
29368
29385
  });
29369
29386
  criterionEvaluatorRegistry.add("isValueInRange", {
29370
- type: "isValueInList",
29371
- isValueValid: (value, criterion, getters, sheetId) => {
29387
+ type: "isValueInRange",
29388
+ preComputeCriterion: (criterion, criterionRanges, getters) => {
29389
+ if (criterionRanges.length === 0) {
29390
+ return new Set();
29391
+ }
29392
+ const sheetId = criterionRanges[0].sheetId;
29393
+ const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
29394
+ return new Set(criterionValues.map((value) => value.value.toString().toLowerCase()));
29395
+ },
29396
+ isValueValid: (value, criterion, valuesSet) => {
29372
29397
  if (!value) {
29373
29398
  return false;
29374
29399
  }
29375
- const criterionValues = getters.getDataValidationRangeValues(sheetId, criterion);
29376
- return criterionValues
29377
- .map((value) => value.value.toLowerCase())
29378
- .includes(value.toString().toLowerCase());
29400
+ return valuesSet.has(value.toString().toLowerCase());
29379
29401
  },
29380
29402
  getErrorString: (criterion) => _t("The value must be a value in the range %s", String(criterion.values[0])),
29381
29403
  isCriterionValueValid: (value) => rangeReference.test(value),
@@ -29452,6 +29474,67 @@
29452
29474
  name: _t("Is not empty"),
29453
29475
  getPreview: () => _t("Is not empty"),
29454
29476
  });
29477
+ criterionEvaluatorRegistry.add("top10", {
29478
+ type: "top10",
29479
+ preComputeCriterion: (criterion, criterionRanges, getters) => {
29480
+ let value = tryToNumber(criterion.values[0], DEFAULT_LOCALE);
29481
+ if (value === undefined || value <= 0) {
29482
+ return undefined;
29483
+ }
29484
+ const numberValues = [];
29485
+ for (const range of criterionRanges) {
29486
+ for (const value of getters.getRangeValues(range)) {
29487
+ if (typeof value === "number") {
29488
+ numberValues.push(value);
29489
+ }
29490
+ }
29491
+ }
29492
+ const sortedValues = numberValues.sort((a, b) => a - b);
29493
+ if (criterion.isPercent) {
29494
+ value = clip(value, 1, 100);
29495
+ }
29496
+ let index = 0;
29497
+ if (criterion.isBottom && !criterion.isPercent) {
29498
+ index = value - 1;
29499
+ }
29500
+ else if (criterion.isBottom && criterion.isPercent) {
29501
+ index = Math.floor((sortedValues.length * value) / 100) - 1;
29502
+ }
29503
+ else if (!criterion.isBottom && criterion.isPercent) {
29504
+ index = sortedValues.length - Math.floor((sortedValues.length * value) / 100);
29505
+ }
29506
+ else {
29507
+ index = sortedValues.length - value;
29508
+ }
29509
+ index = clip(index, 0, sortedValues.length - 1);
29510
+ return sortedValues[index];
29511
+ },
29512
+ isValueValid: (value, criterion, threshold) => {
29513
+ if (typeof value !== "number" || threshold === undefined) {
29514
+ return false;
29515
+ }
29516
+ return criterion.isBottom ? value <= threshold : value >= threshold;
29517
+ },
29518
+ getErrorString: (criterion) => {
29519
+ const args = {
29520
+ value: String(criterion.values[0]),
29521
+ percentSymbol: criterion.isPercent ? "%" : "",
29522
+ };
29523
+ return criterion.isBottom
29524
+ ? _t("The value must be in bottom %(value)s%(percentSymbol)s", args)
29525
+ : _t("The value must be in top %(value)s%(percentSymbol)s", args);
29526
+ },
29527
+ isCriterionValueValid: (value) => checkValueIsPositiveNumber(value),
29528
+ criterionValueErrorString: DVTerms.CriterionError.positiveNumber,
29529
+ numberOfValues: () => 1,
29530
+ name: _t("Is in Top/Bottom ranking"),
29531
+ getPreview: (criterion) => {
29532
+ const args = { value: criterion.values[0], percentSymbol: criterion.isPercent ? "%" : "" };
29533
+ return criterion.isBottom
29534
+ ? _t("Value is in bottom %(value)s%(percentSymbol)s", args)
29535
+ : _t("Value is in top %(value)s%(percentSymbol)s", args);
29536
+ },
29537
+ });
29455
29538
  function getNumberCriterionlocalizedValues(criterion, locale) {
29456
29539
  return criterion.values.map((value) => {
29457
29540
  return value !== undefined
@@ -29473,6 +29556,10 @@
29473
29556
  const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);
29474
29557
  return valueAsNumber !== undefined;
29475
29558
  }
29559
+ function checkValueIsPositiveNumber(value) {
29560
+ const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);
29561
+ return valueAsNumber !== undefined && valueAsNumber > 0;
29562
+ }
29476
29563
 
29477
29564
  // -----------------------------------------------------------------------------
29478
29565
  // Constants
@@ -39803,6 +39890,10 @@
39803
39890
  break;
39804
39891
  case "CellIsRule":
39805
39892
  const formulas = cf.rule.values.map((value) => value.startsWith("=") ? compile(value) : undefined);
39893
+ const evaluator = criterionEvaluatorRegistry.get(cf.rule.operator);
39894
+ const criterion = { ...cf.rule, type: cf.rule.operator };
39895
+ const ranges = cf.ranges.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));
39896
+ const preComputedCriterion = evaluator.preComputeCriterion?.(criterion, ranges, this.getters);
39806
39897
  for (const ref of cf.ranges) {
39807
39898
  const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
39808
39899
  for (let row = zone.top; row <= zone.bottom; row++) {
@@ -39815,7 +39906,7 @@
39815
39906
  }
39816
39907
  return value;
39817
39908
  });
39818
- if (this.getRuleResultForTarget(target, { ...cf.rule, values })) {
39909
+ if (this.getRuleResultForTarget(target, { ...cf.rule, values }, preComputedCriterion)) {
39819
39910
  if (!computedStyle[col])
39820
39911
  computedStyle[col] = [];
39821
39912
  // we must combine all the properties of all the CF rules applied to the given cell
@@ -39978,7 +40069,7 @@
39978
40069
  }
39979
40070
  }
39980
40071
  }
39981
- getRuleResultForTarget(target, rule) {
40072
+ getRuleResultForTarget(target, rule, preComputedCriterion) {
39982
40073
  const cell = this.getters.getEvaluatedCell(target);
39983
40074
  if (cell.type === CellValueType.error) {
39984
40075
  return false;
@@ -39995,11 +40086,12 @@
39995
40086
  return false;
39996
40087
  }
39997
40088
  const evaluatedCriterion = {
40089
+ ...rule,
39998
40090
  type: rule.operator,
39999
40091
  values: evaluatedCriterionValues.map(toScalar),
40000
40092
  dateValue: rule.dateValue || "exactDate",
40001
40093
  };
40002
- return evaluator.isValueValid(cell.value ?? "", evaluatedCriterion, this.getters, sheetId);
40094
+ return evaluator.isValueValid(cell.value ?? "", evaluatedCriterion, preComputedCriterion);
40003
40095
  }
40004
40096
  }
40005
40097
 
@@ -40016,17 +40108,20 @@
40016
40108
  "isDataValidationInvalid",
40017
40109
  ];
40018
40110
  validationResults = {};
40111
+ criterionPreComputeResult = {};
40019
40112
  handle(cmd) {
40020
40113
  if (invalidateEvaluationCommands.has(cmd.type) ||
40021
40114
  cmd.type === "EVALUATE_CELLS" ||
40022
40115
  (cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd))) {
40023
40116
  this.validationResults = {};
40117
+ this.criterionPreComputeResult = {};
40024
40118
  return;
40025
40119
  }
40026
40120
  switch (cmd.type) {
40027
40121
  case "ADD_DATA_VALIDATION_RULE":
40028
40122
  case "REMOVE_DATA_VALIDATION_RULE":
40029
40123
  delete this.validationResults[cmd.sheetId];
40124
+ delete this.criterionPreComputeResult[cmd.sheetId];
40030
40125
  break;
40031
40126
  }
40032
40127
  }
@@ -40169,7 +40264,15 @@
40169
40264
  return undefined;
40170
40265
  }
40171
40266
  const evaluatedCriterion = { ...criterion, values: evaluatedCriterionValues.map(toScalar) };
40172
- if (evaluator.isValueValid(cellValue, evaluatedCriterion, this.getters, sheetId)) {
40267
+ if (!this.criterionPreComputeResult[sheetId]) {
40268
+ this.criterionPreComputeResult[sheetId] = {};
40269
+ }
40270
+ let preComputedCriterion = this.criterionPreComputeResult[sheetId][rule.id];
40271
+ if (preComputedCriterion === undefined) {
40272
+ preComputedCriterion = evaluator.preComputeCriterion?.(rule.criterion, rule.ranges, this.getters);
40273
+ this.criterionPreComputeResult[sheetId][rule.id] = preComputedCriterion;
40274
+ }
40275
+ if (evaluator.isValueValid(cellValue, evaluatedCriterion, preComputedCriterion)) {
40173
40276
  return undefined;
40174
40277
  }
40175
40278
  return evaluator.getErrorString(evaluatedCriterion, this.getters, sheetId);
@@ -45501,6 +45604,7 @@
45501
45604
  if (filterValue.type === "none")
45502
45605
  continue;
45503
45606
  const evaluator = criterionEvaluatorRegistry.get(filterValue.type);
45607
+ const preComputedCriterion = evaluator.preComputeCriterion?.(filterValue, [filter.filteredRange], this.getters);
45504
45608
  const evaluatedCriterionValues = filterValue.values.map((value) => {
45505
45609
  if (!value.startsWith("=")) {
45506
45610
  return parseLiteral(value, DEFAULT_LOCALE);
@@ -45518,7 +45622,7 @@
45518
45622
  for (let row = filteredZone.top; row <= filteredZone.bottom; row++) {
45519
45623
  const position = { sheetId, col: filter.col, row };
45520
45624
  const value = this.getters.getEvaluatedCell(position).value ?? "";
45521
- if (!evaluator.isValueValid(value, evaluatedCriterion, this.getters, sheetId)) {
45625
+ if (!evaluator.isValueValid(value, evaluatedCriterion, preComputedCriterion)) {
45522
45626
  hiddenRows.add(row);
45523
45627
  }
45524
45628
  }
@@ -49659,6 +49763,8 @@
49659
49763
  case undefined:
49660
49764
  throw new Error("dateValue should be defined");
49661
49765
  }
49766
+ case "top10":
49767
+ return [];
49662
49768
  }
49663
49769
  }
49664
49770
  function cellRuleTypeAttributes(rule) {
@@ -49691,6 +49797,14 @@
49691
49797
  case "dateIs":
49692
49798
  case "customFormula":
49693
49799
  return [["type", "expression"]];
49800
+ case "top10": {
49801
+ return [
49802
+ ["type", "top10"],
49803
+ ["rank", rule.values[0]],
49804
+ ["percent", rule.isPercent ? "1" : "0"],
49805
+ ["bottom", rule.isBottom ? "1" : "0"],
49806
+ ];
49807
+ }
49694
49808
  }
49695
49809
  }
49696
49810
  function addDataBarRule(cf, rule) {
@@ -51672,8 +51786,8 @@
51672
51786
 
51673
51787
 
51674
51788
  __info__.version = "19.1.0-alpha.3";
51675
- __info__.date = "2026-01-07T16:20:59.139Z";
51676
- __info__.hash = "febc3e9";
51789
+ __info__.date = "2026-01-07T16:20:57.293Z";
51790
+ __info__.hash = "ac2fa3e";
51677
51791
 
51678
51792
 
51679
51793
  })(this.o_spreadsheet_engine = this.o_spreadsheet_engine || {});