A LibreOffice Calc add-in (UNO component, Java) exposing US Census Bureau data as worksheet formulas:
| Function | Signature | Returns |
|---|---|---|
CENSUS_GET |
CENSUS_GET(year; dataset; variables; geography; [in_geography]; [api_key]) |
spillable array: header row + one row per geography, all values as text |
CENSUS_VALUE |
CENSUS_VALUE(year; dataset; variable; geography; [in_geography]; [api_key]) |
a single value (numeric when possible, else text) |
CENSUS_VARLABEL |
CENSUS_VARLABEL(year; dataset; variable) |
the variable's human-readable label |
CENSUS_DATASETS |
CENSUS_DATASETS(year) |
spillable (Dataset, Title) array of datasets available that year |
In Calc's UI, arguments are separated by semicolons:
=CENSUS_GET(2022; "acs/acs5"; "NAME,B01003_001E"; "state:*").
CENSUS_GETandCENSUS_VALUEtake two optional trailing arguments:in_geography(the API'sin=clause, e.g."state:06", needed for sub-state geographies) andapi_key(overrides theCENSUS_API_KEYenvironment variable, typed literally or as a cell reference). One or the other is required for these two functions — see §2.
- LibreOffice + SDK. Default install path
C:\Program Files\LibreOffice, with the SDK under…\LibreOffice\sdk. The SDK providessdk\bin\unoidl-write.exeandsdk\bin\javamaker.exe. - A JDK to build with — any JDK 8 or newer (
javac,jar). The build targets Java 8 bytecode (--release 8), so the add-in runs on the Oracle JRE 8 that LibreOffice accepts out of the box. Pass its path with-Jdk, or setJAVA_HOME, or putjavaconPATH. - Runtime JRE — nothing to change. The component is Java-8 bytecode and
uses only
java.net.HttpURLConnection, so LibreOffice's existing/default Oracle JRE (8+) runs it as-is. No Tools ▸ Options ▸ Advanced change needed.
Why not
java.net.http.HttpClient? That needs Java 11+, but a LibreOffice install'sjavavendors.xmlallow-list typically only accepts Oracle/Sun/IBM/ Azul/Amazon by default — modern non-Oracle JDKs (e.g. Temurin, JetBrains JBR) may be rejected until added to that list. Targeting Java 8 +HttpURLConnection(both JDK standard library) avoids installing a new JRE while still meeting the "JDK standard library only" requirement.
Confirm the tools resolve:
& 'C:\Program Files\LibreOffice\sdk\bin\unoidl-write.exe' --help
& 'C:\Program Files\LibreOffice\sdk\bin\javamaker.exe' # prints usage
javac -version # any 8+-
A JDK 8 (
javac,jar). If your distro's package manager has one (apt install openjdk-8-jdk-headless,dnf install java-1.8.0-openjdk-devel, …) use that. Otherwise, no root needed — fetch a JDK 8 build straight from Eclipse Adoptium/Temurin and unpack it under your home directory:curl -s "https://api.adoptium.net/v3/assets/latest/8/hotspot?architecture=x64&image_type=jdk&os=linux&vendor=eclipse" \ | grep -o '"link": *"[^"]*tar.gz"' | head -1 | cut -d'"' -f4 # download that URL, then: mkdir -p ~/jdks && tar xzf OpenJDK8U-jdk_x64_linux_hotspot_*.tar.gz -C ~/jdks export JAVA_HOME=~/jdks/jdk8u<version> # match the extracted directory name export PATH="$JAVA_HOME/bin:$PATH"
-
LibreOffice + SDK. If your distro packages a matching SDK (
apt install libreoffice-dev libreoffice-dev-commonon Debian/Ubuntu), use that and skip to confirming the tools below. Otherwise, download the generic Linux tarballs from https://download.documentfoundation.org/libreoffice/stable/ (the_rpm.tar.gzand matching_rpm_sdk.tar.gzfor your version/arch) and extract each.rpminside them withrpm2cpio/cpiointo a prefix directory — no root required:tar xzf LibreOffice_*_rpm.tar.gz LibreOffice_*_rpm_sdk.tar.gz mkdir -p ~/opt for rpm in LibreOffice_*_rpm/RPMS/*.rpm LibreOffice_*_rpm_sdk/RPMS/*.rpm; do rpm2cpio "$rpm" | cpio -idm --no-absolute-filenames -D ~/opt done mv ~/opt/opt/libreoffice* ~/libreoffice26.2 # adjust to the extracted version export LO_HOME=~/libreoffice26.2
This lays out the same
program/andsdk/bin/tree the Windows install has (unoidl-write,javamaker,types.rdb,program/classes/*.jar). -
Java vendor allow-list. LibreOffice only loads a JVM whose
java.vendorappears in$LO_HOME/program/javavendors.xml(Sun, Oracle, IBM, Blackdown, BEA, Azul, Amazon by default). A stock Temurin/Adoptium build reports vendorTemurin, which is not on that list by default —unopkgwill fail withCannotRegisterImplementationException: Could not create Java implementation loaderwhen installing the extension. Add an entry for it in your localjavavendors.xml(this file lives inside your own LibreOffice install, not a system-shared one, so editing it is safe):<vendor name="Temurin"> <minVersion>1.8.0</minVersion> </vendor>
Insert it next to the other
<vendor>entries, inside<vendorInfos>. (If your JDK came from your distro's package manager, its vendor is usually already on the list and this step is unnecessary.)
Confirm the tools resolve:
"$LO_HOME/sdk/bin/unoidl-write" # prints usage
"$LO_HOME/sdk/bin/javamaker" # prints usage
"$JAVA_HOME/bin/javac" -version # any 8+CENSUS_GET and CENSUS_VALUE require a key. Older Census API documentation describes
a keyless allowance for low-volume, non-commercial use, but the live API no longer honors
that for data queries — a keyless request comes back as a redirect to an HTML "missing key"
page, which the add-in detects and reports as a clear error rather than a cryptic parse
failure. CENSUS_VARLABEL and CENSUS_DATASETS are metadata/discovery endpoints and
genuinely don't need a key. Get a free one at https://api.census.gov/data/key_signup.html.
Two ways, in priority order:
- The
api_keyfunction argument — the optional trailing argument ofCENSUS_GETandCENSUS_VALUE. Type it literally, or (recommended) put it in one cell and reference that cell:=CENSUS_VALUE(2022; "acs/acs5"; "B01003_001E"; "state:06"; ""; $B$1). Wins when supplied. - The
CENSUS_API_KEYenvironment variable — used whenever the argument is omitted.
If neither is set, CENSUS_GET/CENSUS_VALUE calls return a Calc error value; see
Troubleshooting below.
The key is never hardcoded in the add-in. Set it for the LibreOffice process — i.e. set
it, then launch soffice from that same shell (or set it as a persistent user env var and
restart LibreOffice):
$env:CENSUS_API_KEY = 'your_40_char_key'
& 'C:\Program Files\LibreOffice\program\soffice.exe'Persistent (survives reboots; restart LibreOffice afterwards):
setx CENSUS_API_KEY "your_40_char_key"Linux / macOS — same idea, export instead of $env:/setx:
export CENSUS_API_KEY='your_40_char_key'
"$LO_HOME/program/soffice"Persistent: add the export line to ~/.bashrc (or ~/.zshrc), then restart your shell
and LibreOffice.
From the project root:
# JAVA_HOME set, or javac on PATH:
pwsh -File build.ps1
# or point at a specific JDK:
pwsh -File build.ps1 -Jdk 'C:\Program Files\Java\jdk-11'This runs the full pipeline and produces build\Census.oxt:
1. unoidl-write idl\** -> build\types\XCensus.rdb
2. javamaker build\types\XCensus.rdb -> build\gen\**.class
3. javac src\**.java -> build\classes\**.class
4. jar classes + bindings -> build\oxt\census.jar
5. zip staging tree -> build\Census.oxt
export JAVA_HOME=~/jdks/jdk8u<version> # or wherever your JDK 8 lives
export LO_HOME=~/libreoffice26.2 # or wherever LibreOffice + SDK live
./build.sh
# or pass paths explicitly instead of the env vars:
./build.sh --jdk ~/jdks/jdk8u<version> --libreoffice ~/libreoffice26.2This produces build/Census.oxt via the same five steps. Two JDK-8-specific quirks it
works around, in case you're compiling by hand:
javac --release 8doesn't exist on JDK 8 itself (the flag was added in JDK 9);build.shdetects a1.xjavac -versionand falls back to-source 8 -target 8, which is equivalent for a straight JDK-8 build.jaron JDK 8 can reject duplicate directory entries when packaging two class trees that share a package path;build.shmerges both trees into one staging directory first, then jars that single tree.
Close LibreOffice first, then use unopkg from the program dir:
& 'C:\Program Files\LibreOffice\program\unopkg.exe' add --force build\Census.oxt
# list / remove:
& 'C:\Program Files\LibreOffice\program\unopkg.exe' list
& 'C:\Program Files\LibreOffice\program\unopkg.exe' remove com.example.censusYou can also install by double-clicking build\Census.oxt (opens the Extension Manager).
After installing, restart LibreOffice from a shell that has CENSUS_API_KEY set (step 2)
if you want CENSUS_GET/CENSUS_VALUE to work.
Linux / macOS:
"$LO_HOME/program/unopkg" add --force build/Census.oxt
# list / remove:
"$LO_HOME/program/unopkg" list
"$LO_HOME/program/unopkg" remove com.example.censusIn any sheet:
=CENSUS_VALUE(2022; "acs/acs5"; "NAME"; "state:06") -> California
=CENSUS_VALUE(2022; "acs/acs5"; "B01003_001E"; "state:06") -> 39356104
=CENSUS_VARLABEL(2022; "acs/acs5"; "B01003_001E") -> Estimate!!Total
=CENSUS_DATASETS(2022) -> spills (Dataset, Title) rows
For CENSUS_GET, select an output range large enough for the result, type the formula,
and confirm with Ctrl+Shift+Enter (array formula) — LibreOffice has no dynamic spill:
=CENSUS_GET(2022; "acs/acs5"; "NAME,B01003_001E"; "state:*") -> 52 rows: name, population
For a county within a state (needs the in_geography argument):
=CENSUS_GET(2022; "acs/acs5"; "NAME,B01003_001E"; "county:*"; "state:06") -> California's counties
- Errors → Calc error values. Bad datasets/variables/years, no data for a geography, a
missing API key on
CENSUS_GET/CENSUS_VALUE, or a network failure all raise a UNO exception, which Calc shows as an error value (e.g.Err:502) in the cell — not an exception string. - Multi-cell / spilling. To get all rows of
CENSUS_GETorCENSUS_DATASETS, select the output range and enter it as an array formula — Ctrl+Shift+Enter, or tick Array in the Function Wizard. A single-cell entry shows only the first value. - Geography.
geographyis the API'sfor=clause (e.g."state:*","county:*","state:06"); the optionalin_geographyargument is thein=clause (e.g."state:06"), needed whenevergeographyis below the state level. CENSUS_VALUEexpects one specific geography (not a wildcard) — it returns the first data row's value. UseCENSUS_GETwith a wildcard to pull every geography at once.- Minimal parsing.
CENSUS_GETreturns every cell as text, matching the API's own JSON-array-of-arrays shape.CENSUS_VALUEcoerces numeric-looking values to numbers; a null value becomes an empty cell. - Per-session caching. Every raw response is cached by request URL for the life of the office session, so recalculation does not re-hit the API. Restart LibreOffice to clear the cache.
- No third-party jars. HTTP uses
java.net.HttpURLConnection; JSON is parsed by a small hand-rolled parser (Json.java). Nothing beyond the JDK + UNO is bundled.
tools/test_census.py drives a headless LibreOffice over a UNO socket and checks all four
functions plus the error paths against live Census data:
$env:CENSUS_API_KEY = 'your_key' # required for CENSUS_GET/CENSUS_VALUE
& 'C:\Program Files\LibreOffice\program\soffice.exe' --headless --norestore --accept="socket,host=localhost,port=2002;urp;"
& 'C:\Program Files\LibreOffice\program\python.exe' tools\test_census.py # prints RESULT: PASSLinux / macOS — same idea, LibreOffice's bundled python (it ships the uno module)
instead of python.exe, run in the background so the shell isn't blocked:
export CENSUS_API_KEY='your_key' # required for CENSUS_GET/CENSUS_VALUE
"$LO_HOME/program/soffice" --headless --norestore --accept="socket,host=localhost,port=2002;urp;" &
"$LO_HOME/program/python" tools/test_census.py # prints RESULT: PASSunoidl-write/javamaker"not found" → pass the right--libreoffice/-LibreOfficepath; the SDK must be installed (it is a separate download from LibreOffice).- Functions show
#NAME?→ the extension isn't registered; confirm withunopkg listand restart LibreOffice. - Every
CENSUS_GET/CENSUS_VALUEcell is an error value (Err:502) butCENSUS_VARLABEL/CENSUS_DATASETSwork fine → you're missing an API key (step 2); the data-query endpoint requires one even for a single request. - Every Census cell is an error value (
Err:502) → check the dataset/variable/year are valid, or that geography syntax is correct (e.g."state:*", not"states:*"). unopkg addfails withCannotRegisterImplementationException: Could not create Java implementation loader(Linux/macOS) → your JDK's vendor isn't in$LO_HOME/program/javavendors.xml's allow-list — see the Java vendor allow-list note in Prerequisites.