Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions plugin/skills/xl-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ xl -f <file> -s <sheet> cell <ref> # Cell details + dependencies
xl -f <file> -s <sheet> search <pattern> # Find cells
xl -f <file> -s <sheet> stats <range> # Calculate statistics
xl -f <file> -s <sheet> eval <formula> # Evaluate formula
xl -f <file> -s <sheet> evala <formula> # Array formula result grid
xl -f <file> -s <sheet> evala <formula> --at <ref> # Spill to target cell
```

### Output Formats
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -483,7 +488,7 @@ xl -f huge.xlsx --max-size 500 cell A1 # Custom 500MB limit
| `--file <path>` | `-f` | Input file (required) |
| `--sheet <name>` | `-s` | Sheet name |
| `--output <path>` | `-o` | Output file (for writes) |
| `--backend <type>` | | scalaxml (default) or saxstax (36-39% faster) |
| `--backend <type>` | | Write backend: scalaxml (default) or saxstax (36-39% faster). Reads always use StAX. |
| `--max-size <MB>` | | Override 100MB security limit (0 = unlimited) |
| `--stream` | | O(1) memory mode for reads + writes (search/stats/bounds/view/put/putf/style) |

Expand Down Expand Up @@ -514,7 +519,8 @@ xl -f huge.xlsx --max-size 500 cell A1 # Custom 500MB limit
| `cell <ref>` | `--no-style` |
| `search <pattern>` | `--limit`, `--sheets` |
| `stats <range>` | Calculate count, sum, min, max, mean |
| `eval <formula>` | `--with` for overrides |
| `eval <formula>` | `--with` for overrides (comma-separated: `--with "A1=0,A5=0"`) |
| `evala <formula>` | `--at` to spill result starting at ref |

Run `xl view --help` for complete options.

Expand Down
20 changes: 12 additions & 8 deletions xl-cli/src/com/tjclp/xl/cli/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand All @@ -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))
}
}

Expand Down
25 changes: 20 additions & 5 deletions xl-core/src/com/tjclp/xl/display/NumFmtFormatter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 54 additions & 0 deletions xl-core/test/src/com/tjclp/xl/display/DisplaySpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down