Get started with XL in 5 minutes.
XL is published as JVM libraries to Maven Central. Use the aggregate xl artifact for easy setup.
import mill._, scalalib._
object myproject extends ScalaModule {
def scalaVersion = "3.9.0"
def ivyDeps = Agg(ivy"com.tjclp::xl:0.22.0")
}scalaVersion := "3.9.0"
libraryDependencies += "com.tjclp" %% "xl" % "0.22.0"//> using dep com.tjclp::xl:0.22.0For minimal dependencies, use individual modules:
| Module | Description |
|---|---|
xl-core |
Pure domain model, macros, DSL |
xl-ooxml |
OOXML read/write |
xl-cats-effect |
IO streaming with Cats Effect |
xl-evaluator |
Formula parser and evaluator |
For scripts, skip the build setup entirely: a two-line scala-cli header and ONE import give you the whole library — core API, DSL, compile-time literals, formula evaluation, sync Excel I/O, and streaming.
//> using scala 3.9.0
//> using dep com.tjclp::xl:0.22.0
import com.tjclp.xl.scripting.{*, given}
val sheet = Sheet("Model")
.put(ref"A1", 100)
.put(ref"A2", 200)
.put(ref"A3", fx"=SUM(A1:A2)")
val recalc = Workbook(sheet).recalculate() // total: per-cell errors, never throws
if !recalc.isClean then recalc.errors.foreach(e => println(e.render))
Excel.write(recalc.workbook, "model.xlsx")Save as model.sc, run with scala-cli run model.sc. Never combine this import with com.tjclp.xl.{*, given} in the same file (ambiguous forwarders).
More: reference/scripting.md (full guide) · ../examples/scripting_tour.sc (runnable tour) · the xl-scripting Claude Code skill (../plugin/skills/xl-scripting/)
import com.tjclp.xl.* // Domain model + macros + DSL
import com.tjclp.xl.unsafe.* // Explicit opt-in for .unsafe
import com.tjclp.xl.io.ExcelIO
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import java.nio.file.Path
// Build a simple sheet (Sales) with two rows
val sheet =
Sheet("Sales").unsafe
.put(
ref"A1" -> "Product",
ref"B1" -> "Revenue",
ref"A2" -> "Widget",
ref"B2" -> 1000
)
.unsafe
// Create a workbook and add the sheet
val workbook =
Workbook.empty
.flatMap(_.put(sheet))
.unsafe
// Write to file
ExcelIO.instance[IO].write(workbook, Path.of("sales.xlsx")).unsafeRunSync()Result: sales.xlsx with a 2×2 grid.
import com.tjclp.xl.io.ExcelIO
import cats.effect.unsafe.implicits.global
import cats.effect.IO
import com.tjclp.xl.*
val excel = ExcelIO.instance[IO]
val path = Path.of("sales.xlsx")
val program: IO[Unit] =
excel.read(path).flatMap { workbook =>
// Access first sheet
val sheet = workbook.sheets.head
// Read cell value at B2 (returns a Cell; empty cells have CellValue.Empty)
val cell = sheet(ref"B2")
IO.println(s"Revenue cell value: ${cell.value}")
}
program.unsafeRunSync()import com.tjclp.xl.*
import com.tjclp.xl.codec.syntax.*
val sheet = workbook.sheets.head
// Type-safe reading with Either
sheet.readTyped[Int](ref"B2") match
case Right(Some(revenue)) => println(s"Revenue: $$${revenue}")
case Right(None) => println("Empty cell")
case Left(error) => println(s"Type error: ${error}")import com.tjclp.xl.*
// Define a header style (bold, centered, gray background)
val headerStyle = CellStyle.default
.bold.size(14.0).fontFamily("Arial")
.bgGray.bordered
.center.middle
// Apply style to A1 and B1 using the sheet extension
val styledSheet =
sheet
.withCellStyle(ref"A1", headerStyle)
.withCellStyle(ref"B1", headerStyle)Shortcut with Patch DSL:
import com.tjclp.xl.*
import com.tjclp.xl.dsl.*
import com.tjclp.xl.unsafe.*
val patch =
(ref"A1" := "Product") ++
(ref"B1" := "Revenue") ++
ref"A1".styled(headerStyle) ++
ref"B1".styled(headerStyle)
val styledSheet2 = sheet.put(patch).unsafe// Use in-memory API (simple, full features)
import com.tjclp.xl.*
import com.tjclp.xl.unsafe.*
import com.tjclp.xl.io.ExcelIO
import cats.effect.IO
import cats.effect.unsafe.implicits.global
val sheet =
Sheet("Data").unsafe
.put(
ref"A1" -> "Hello",
ref"B1" -> 42
)
.unsafe
val workbook =
Workbook.empty
.flatMap(_.put(sheet))
.unsafe
ExcelIO.instance[IO].write(workbook, path).unsafeRunSync()Memory: ~10MB
// Use streaming write (constant memory)
import com.tjclp.xl.*
import com.tjclp.xl.io.{Excel, RowData}
import fs2.Stream
Stream.range(1, 1_000_001)
.map(i => RowData(i, Map(
0 -> CellValue.Text(s"Row $i"),
1 -> CellValue.Number(BigDecimal(i))
)))
.through(Excel.forIO.writeStream(path, "Data"))
.compile.drain
.unsafeRunSync()Memory: ~10MB constant (even for 10M rows!)
Limitations: Pure row-stream writers use inline strings and minimal styles, and do not accept workbook metadata such as merges. Use in-memory writes or writeWorkbookStream when you need full workbook metadata.
import com.tjclp.xl.*
import com.tjclp.xl.unsafe.*
val sheet1 = Sheet("Sales").unsafe.put(ref"A1" -> "Sales Data").unsafe
val sheet2 = Sheet("Inventory").unsafe.put(ref"A1" -> "Inventory").unsafe
val workbook = Workbook(Vector(sheet1, sheet2))import com.tjclp.xl.*
sheet.fillBy(ref"A1:Z10") { (col, row) =>
CellValue.Text(s"${col.toLetter}${row.index1}")
}import com.tjclp.xl.richtext.RichText.*
val text = "Error: ".red.bold + "File not found"
sheet.put(ref"A1" -> text)val html = sheet.toHtml(ref"A1:B10")
println(html) // <table>...</table> with inline CSSimport com.tjclp.xl.*
import com.tjclp.xl.formula.{FormulaParser, Evaluator}
import com.tjclp.xl.formula.SheetEvaluator.*
import com.tjclp.xl.unsafe.*
// Build a sheet with data and formulas
val sheet = Sheet("Finance").unsafe
.put(
ref"A1" -> "Revenue",
ref"A2" -> BigDecimal("1000000"),
ref"A3" -> BigDecimal("1500000"),
ref"B1" -> "Total",
ref"B2" -> CellValue.Formula("=SUM(A2:A3)"), // Formula cell
ref"C1" -> "Average",
ref"C2" -> CellValue.Formula("=AVERAGE(A2:A3)")
)
.unsafe
// Evaluate individual formula
val totalResult = sheet.evaluateFormula("=SUM(A2:A3)")
// Right(CellValue.Number(2500000))
// Evaluate cell with formula
val cellResult = sheet.evaluateCell(ref"B2")
// Right(CellValue.Number(2500000))
// Evaluate all formulas in sheet (with dependency checking)
val allResults = sheet.evaluateWithDependencyCheck()
// Right(Map(
// ARef(B2) -> CellValue.Number(2500000),
// ARef(C2) -> CellValue.Number(1250000)
// ))
// Handle circular references safely
val cyclicSheet = Sheet("Cyclic").unsafe
.put(
ref"A1" -> CellValue.Formula("=B1"),
ref"B1" -> CellValue.Formula("=A1") // Circular reference!
)
.unsafe
cyclicSheet.evaluateWithDependencyCheck() match
case Left(error) =>
println(s"Cycle detected: ${error.message}")
// "Circular reference detected: A1 → B1 → A1"
case Right(_) => // Won't happenAvailable Functions (108 total):
- Aggregate: SUM, COUNT, COUNTA, COUNTBLANK, AVERAGE, MEDIAN, MIN, MAX, STDEV, STDEVP, VAR, VARP
- Statistical: LARGE, SMALL, RANK, PERCENTILE, QUARTILE
- Conditional: SUMIF, COUNTIF, SUMIFS, COUNTIFS, AVERAGEIF, AVERAGEIFS, MAXIFS, MINIFS, SUMPRODUCT
- Logical / Selection: IF, IFS, IFERROR, SWITCH, CHOOSE, AND, OR, NOT, ISNUMBER, ISTEXT, ISBLANK, ISERR, ISERROR
- Text: CONCATENATE, LEFT, RIGHT, MID, LEN, UPPER, LOWER, TRIM, FIND, SUBSTITUTE, TEXT, VALUE
- Date: TODAY, NOW, DATE, YEAR, MONTH, DAY, EOMONTH, EDATE, DATEDIF, NETWORKDAYS, WORKDAY, YEARFRAC
- Math: ABS, ROUND, ROUNDUP, ROUNDDOWN, INT, MOD, MROUND, POWER, SQRT, LOG, LN, EXP, FLOOR, CEILING, TRUNC, SIGN, PI
- Financial: NPV, IRR, XNPV, XIRR, PMT, FV, PV, RATE, NPER
- Lookup / Reference: VLOOKUP, HLOOKUP, XLOOKUP, INDEX, MATCH, OFFSET, INDIRECT, ROW, COLUMN, ROWS, COLUMNS, ADDRESS
- Dynamic Arrays: TRANSPOSE, SEQUENCE, SORT, UNIQUE, FILTER
- Random: RAND, RANDBETWEEN
See CLAUDE.md for the complete list.
- Read the README: README.md for comprehensive documentation
- Check examples: docs/reference/examples.md for common patterns
- Performance guide: docs/reference/performance-guide.md for optimization
- Migrating from POI: docs/reference/migration-from-poi.md if coming from Java
import com.tjclp.xl.* // Core types, macros, DSL
import com.tjclp.xl.codec.syntax.* // Type-safe codecs (readTyped, etc.)
import com.tjclp.xl.dsl.* // := operator, ++ combinator
import com.tjclp.xl.io.{ExcelIO, Excel} // File and streaming I/O
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import java.nio.file.PathSheet- Single worksheetWorkbook- Multi-sheet workbookCell- Single cell (ref + value + style)ARef- Cell reference (A1, B2, etc.)CellRange- Range of cells (A1:B10)CellValue- Cell content (Text, Number, Bool, DateTime, Formula, etc.)CellStyle- Formatting (font, fill, border, numFmt, align)
sheet.put(ref, value)- Set cell valuesheet.put(cells)- Batch set cellssheet.put(updates*)- Type-safe batch with auto-formattingsheet(ref)- Read cell (returns aCell, empty cells haveCellValue.Empty)sheet.readTyped[A](ref)- Type-safe read (Either[CodecError, Option[A]])sheet.put(patch)- Apply deferred updates
// Single cell
sheet.put(ref"A1", "Hello")
// Batch cells
sheet.put(
ref"A1" -> "Name",
ref"B1" -> 42,
ref"C1" -> java.time.LocalDate.now()
)
// Apply style
sheet.withCellStyle(ref"A1", headerStyle)
// Range operations
sheet.fillBy(ref"A1:A10") { (_col, row) =>
CellValue.Number(BigDecimal(row.index1))
}
// Read with type safety
sheet.readTyped[BigDecimal](ref"C1")Solution: Call .unsafeRunSync() to execute IO:
ExcelIO.instance[IO].write(wb, path).unsafeRunSync()Solution: Import codec givens:
import com.tjclp.xl.codec.syntax.*Solution: Check cell reference syntax (must be valid A1 notation):
ref"A1" // ✅ Valid
ref"AA100" // ✅ Valid
ref"1A" // ❌ Invalid (number first)
ref"XFE1" // ❌ Invalid (column out of range)Solution: Use SheetName.apply for validation:
import com.tjclp.xl.addressing.SheetName
SheetName("My Sheet!") match
case Right(name) => Workbook(Sheet(name))
case Left(err) => println(s"Invalid: $err")
// Or use unsafe (throws on invalid):
Workbook(Sheet(SheetName.unsafe("ValidName")))Small files (<10k rows): Use the in‑memory API and batch with sheet.put(ref -> value, ...).
Large files (100k+ rows): Use streaming write:
import com.tjclp.xl.*
import com.tjclp.xl.io.{Excel, RowData}
import fs2.Stream
import cats.effect.unsafe.implicits.global
Stream.range(1, 1_000_001)
.map(i => RowData(i, Map(0 -> CellValue.Text(s"Row $i"))))
.through(Excel.forIO.writeStream(path, "Data"))
.compile.drain
.unsafeRunSync()See: docs/reference/performance-guide.md for mode selection guide
- Documentation: README.md
- Examples: docs/reference/examples.md
- Issues: GitHub Issues
- Contributing: CONTRIBUTING.md
Happy spreadsheeting!