diff --git a/plugin/skills/xl-cli/SKILL.md b/plugin/skills/xl-cli/SKILL.md index db26d4f4..710716fb 100644 --- a/plugin/skills/xl-cli/SKILL.md +++ b/plugin/skills/xl-cli/SKILL.md @@ -74,6 +74,8 @@ xl -f -s cell # Cell details + dependencies xl -f -s search # Find cells xl -f -s stats # Calculate statistics xl -f -s eval # Evaluate formula +xl -f -s evala # Array formula result grid +xl -f -s evala --at # Spill to target cell ``` ### Output Formats @@ -374,6 +376,7 @@ xl -f data.xlsx -s "Sheet1" stats B2:B100 # Quick statistics xl -f data.xlsx -s Sheet1 view --formulas A1:D10 # Show formulas xl -f data.xlsx -s Sheet1 cell C5 # Dependencies xl -f data.xlsx -s Sheet1 eval "=SUM(A1:A10)" --with "A1=500" # What-if +xl -f data.xlsx -s Sheet1 eval "=SUM(A1:A5)" --with "A1=0,A5=0" # Multiple overrides (comma-separated) ``` See [reference/FORMULAS.md](reference/FORMULAS.md) for 82 supported functions. @@ -468,6 +471,8 @@ xl -f huge.xlsx --max-size 0 sheets # Disable 100MB limit xl -f huge.xlsx --max-size 500 cell A1 # Custom 500MB limit ``` +**Note**: Streaming CSV shows formula expressions without the `=` prefix (streaming mode reads raw cell content). + **Streaming supports**: search, stats, bounds, view (markdown/csv/json), put, putf, style **Requires in-memory**: cell (dependencies), eval (formulas), HTML/SVG/PDF (styles), formula dragging @@ -483,7 +488,7 @@ xl -f huge.xlsx --max-size 500 cell A1 # Custom 500MB limit | `--file ` | `-f` | Input file (required) | | `--sheet ` | `-s` | Sheet name | | `--output ` | `-o` | Output file (for writes) | -| `--backend ` | | scalaxml (default) or saxstax (36-39% faster) | +| `--backend ` | | Write backend: scalaxml (default) or saxstax (36-39% faster). Reads always use StAX. | | `--max-size ` | | Override 100MB security limit (0 = unlimited) | | `--stream` | | O(1) memory mode for reads + writes (search/stats/bounds/view/put/putf/style) | @@ -514,7 +519,8 @@ xl -f huge.xlsx --max-size 500 cell A1 # Custom 500MB limit | `cell ` | `--no-style` | | `search ` | `--limit`, `--sheets` | | `stats ` | Calculate count, sum, min, max, mean | -| `eval ` | `--with` for overrides | +| `eval ` | `--with` for overrides (comma-separated: `--with "A1=0,A5=0"`) | +| `evala ` | `--at` to spill result starting at ref | Run `xl view --help` for complete options. diff --git a/xl-cli/src/com/tjclp/xl/cli/Main.scala b/xl-cli/src/com/tjclp/xl/cli/Main.scala index 10f80f35..a90331e1 100644 --- a/xl-cli/src/com/tjclp/xl/cli/Main.scala +++ b/xl-cli/src/com/tjclp/xl/cli/Main.scala @@ -546,14 +546,19 @@ EXAMPLES: // --- Analyze --- private val formulaArg = Opts.argument[String]("formula") - private val withOpt = - Opts.option[String]("with", "Comma-separated overrides (e.g., A1=100,B2=200)", "w").orNone + private val withOpts = + Opts + .options[String]("with", "Cell overrides (e.g., A1=100,B2=200). Repeatable.", "w") + .map(_.toList) + .withDefault(Nil) + + private def parseOverrides(withStrs: List[String]): List[String] = + withStrs.flatMap(_.split(",").map(_.trim).filter(_.nonEmpty)) val evalCmd: Opts[CliCommand] = Opts.subcommand("eval", "Evaluate formula without modifying sheet") { - (formulaArg, withOpt).mapN { (formula, withStr) => - val overrides = withStr.toList.flatMap(_.split(",").map(_.trim).filter(_.nonEmpty)) - CliCommand.Eval(formula, overrides) + (formulaArg, withOpts).mapN { (formula, withStrs) => + CliCommand.Eval(formula, parseOverrides(withStrs)) } } @@ -562,9 +567,8 @@ EXAMPLES: val evalArrayCmd: Opts[CliCommand] = Opts.subcommand("evala", "Evaluate array formula and display result grid") { - (formulaArg, atOpt, withOpt).mapN { (formula, target, withStr) => - val overrides = withStr.toList.flatMap(_.split(",").map(_.trim).filter(_.nonEmpty)) - CliCommand.EvalArray(formula, target, overrides) + (formulaArg, atOpt, withOpts).mapN { (formula, target, withStrs) => + CliCommand.EvalArray(formula, target, parseOverrides(withStrs)) } } diff --git a/xl-core/src/com/tjclp/xl/display/NumFmtFormatter.scala b/xl-core/src/com/tjclp/xl/display/NumFmtFormatter.scala index 26ac2a35..19dbdb24 100644 --- a/xl-core/src/com/tjclp/xl/display/NumFmtFormatter.scala +++ b/xl-core/src/com/tjclp/xl/display/NumFmtFormatter.scala @@ -122,15 +122,30 @@ object NumFmtFormatter: * Rules: * - Integers: No decimal point * - Decimals: Up to 11 significant digits - * - Scientific: For very large/small numbers + * - Scientific: For very large/small numbers (>= 1e12 or < 1e-4) */ private def formatGeneral(n: BigDecimal): String = if n.isWhole then n.toBigInt.toString else - val str = n.underlying.stripTrailingZeros.toPlainString - // Excel's General format shows up to 11 significant digits - if str.length > 11 then f"${n.toDouble}%.6E" - else str + val plain = n.underlying.stripTrailingZeros.toPlainString + val sigDigits = countSignificantDigits(plain) + if sigDigits > 11 then + val mc = new java.math.MathContext(11) + val rounded = n.underlying.round(mc) + val roundedPlain = rounded.stripTrailingZeros.toPlainString + val abs = n.abs + if abs >= BigDecimal("1E12") || abs < BigDecimal("1E-4") then f"${rounded.doubleValue}%.6E" + else roundedPlain + else plain + + private def countSignificantDigits(plain: String): Int = + val s = if plain.startsWith("-") then plain.substring(1) else plain + if s.contains('.') then + val stripped = s.stripPrefix("0.").dropWhile(_ == '0') + stripped.replace(".", "").length + else + val trimmed = s.reverse.dropWhile(_ == '0') + if trimmed.isEmpty then 1 else trimmed.length /** * Format a date/time value. diff --git a/xl-core/test/src/com/tjclp/xl/display/DisplaySpec.scala b/xl-core/test/src/com/tjclp/xl/display/DisplaySpec.scala index f0f776a5..fdccec2b 100644 --- a/xl-core/test/src/com/tjclp/xl/display/DisplaySpec.scala +++ b/xl-core/test/src/com/tjclp/xl/display/DisplaySpec.scala @@ -72,6 +72,60 @@ class DisplaySpec extends FunSuite: assertEquals(result, "123.45") } + test("formatValue - General format (zero)") { + val value = CellValue.Number(BigDecimal("0")) + val result = NumFmtFormatter.formatValue(value, NumFmt.General) + assertEquals(result, "0") + } + + test("formatValue - General format (9 sig digits stays plain)") { + val value = CellValue.Number(BigDecimal("0.000123456789")) + val result = NumFmtFormatter.formatValue(value, NumFmt.General) + assertEquals(result, "0.000123456789") + } + + test("formatValue - General format (10 sig digits negative stays plain)") { + val value = CellValue.Number(BigDecimal("-99999999.99")) + val result = NumFmtFormatter.formatValue(value, NumFmt.General) + assertEquals(result, "-99999999.99") + } + + test("formatValue - General format (exactly 11 sig digits stays plain)") { + val value = CellValue.Number(BigDecimal("12345678.901")) + val result = NumFmtFormatter.formatValue(value, NumFmt.General) + assertEquals(result, "12345678.901") + } + + test("formatValue - General format (12 sig digits medium rounds to plain)") { + val value = CellValue.Number(BigDecimal("123456789.012")) + val result = NumFmtFormatter.formatValue(value, NumFmt.General) + assertEquals(result, "123456789.01") + } + + test("formatValue - General format (13 sig digits very small triggers scientific)") { + val value = CellValue.Number(BigDecimal("0.0000123456789012")) + val result = NumFmtFormatter.formatValue(value, NumFmt.General) + assert(result.contains("E"), s"Expected scientific notation, got: $result") + } + + test("formatValue - General format (15 sig digits very large triggers scientific)") { + val value = CellValue.Number(BigDecimal("9999999999999.12")) + val result = NumFmtFormatter.formatValue(value, NumFmt.General) + assert(result.contains("E"), s"Expected scientific notation, got: $result") + } + + test("formatValue - General format (0.0001 at threshold stays plain)") { + val value = CellValue.Number(BigDecimal("0.0001")) + val result = NumFmtFormatter.formatValue(value, NumFmt.General) + assertEquals(result, "0.0001") + } + + test("formatValue - General format (below 1e-4 with >11 sig digits triggers scientific)") { + val value = CellValue.Number(BigDecimal("0.0000999999999999")) + val result = NumFmtFormatter.formatValue(value, NumFmt.General) + assert(result.contains("E"), s"Expected scientific notation, got: $result") + } + test("formatValue - Text value") { val value = CellValue.Text("Hello World") val result = NumFmtFormatter.formatValue(value, NumFmt.General)