Jump to content

LibreOffice Calc NumToWords Extension Complete Guide

From Game in the Brain Wiki

LibreOffice Calc NumToWords Extension - Complete Solution & Troubleshooting Guide

Overview

A LibreOffice Calc add-in that converts numbers to their English word representation using the custom `=NUMTOWORDS()` spreadsheet function. This page documents the working solution and the complete debugging journey including all 15+ issues encountered and resolved.

Quick Start - Working Solution

The extension is available in two versions:

  • Working Python Version: `libreoffice-calc-numbers-to-words/python/NumToWordsPy.oxt`
  • Experimental Java Version: `libreoffice-calc-numbers-to-words/java/dist/NumToWords.oxt` (for reference)

Installation

Option 1 - GUI (Recommended)

1. Download `NumToWordsPy.oxt` from the repository 2. Open LibreOffice Calc 3. Go to Tools → Extension Manager 4. Click Add and select `NumToWordsPy.oxt` 5. Restart LibreOffice Calc completely 6. Test with: `=NUMTOWORDS(123)`

Option 2 - Command Line

# Download the extension
curl -LO https://github.com/xunema/libreoffice-calc-numbers-to-words/releases/download/v2.0.0/NumToWordsPy.oxt

# Close LibreOffice first!
pkill soffice

# Clear cache (important!)
rm -rf ~/.config/libreoffice/4/user/uno_packages/cache/
rm -rf ~/.config/libreoffice/4/user/extensions/

# Install
unopkg add NumToWordsPy.oxt

# Verify
unopkg list | grep numtowords

Usage

=NUMTOWORDS(number)
=NUMTOWORDS(number, formatStyle)
Format Styles
formatStyle Mode Example Output
0 (default) Cardinal one hundred and twenty-three
1 Ordinal forty-second
2 Currency ninety-nine dollars and ninety-nine cents

Examples

=NUMTOWORDS(123)            → one hundred and twenty-three
=NUMTOWORDS(42, 1)          → forty-second
=NUMTOWORDS(99.99, 2)       → ninety-nine dollars and ninety-nine cents
=NUMTOWORDS(-5)             → minus five
=NUMTOWORDS(1000000)        → one million
=NUMTOWORDS(12.34)          → twelve point three four

---

The Core Technique - Why This Works

The "Invisible XPropertySet" Pattern

This is the critical secret that makes LibreOffice Calc Add-Ins work:

LibreOffice auto-injects the calling cell's `XPropertySet` as the first invisible argument to your Python method. This is NOT listed in the XCU file and is handled transparently by the UNO bridge.

Python Method Signature:

def numToWords(self, number, formatStyle=None):
    # Receives: (self, XPropertySet, number, formatStyle)
    # But XPropertySet is INVISIBLE - LibreOffice injects it automatically!
    pass

XCU Parameters (ONLY list the visible ones):

<node oor:name="Parameters">
  <node oor:name="number" oor:op="replace">...</node>      <!-- Index 0 in XCU -->
  <node oor:name="formatStyle" oor:op="replace">          <!-- Index 1 in XCU -->
    <prop oor:name="IsOptional"><value>true</value></prop>
  </node>
</node>

Key Points:

  • The Python method receives 3 arguments: `(self, XPropertySet, number, formatStyle)`
  • The XCU only declares 2 parameters: `number` and `formatStyle`
  • The `XPropertySet` is injected by LibreOffice invisibly
  • The XCU parameter count must match the visible parameters only!

Architecture Pattern (Following libnumbertext)

The working extension follows the hybrid Python+Java pattern from the proven libnumbertext project:

1. Python UNO Bridge (`numtowords.uno.py`)

  * Handles UNO registration via `unohelper.ImplementationHelper`
  * Provides `g_ImplementationHelper.addImplementation()`
  * No complex factory methods needed

2. Plain Python Logic

  * Core conversion in pure Python (no Java required!)
  * Simple algorithm, no UNO dependencies in logic

3. XCU Registration (`CalcAddIns.xcu`)

  * Declares function to Calc's formula engine
  * Uses `IsOptional` flag for optional parameters
  * CompatibilityName matches implementation exactly

4. RDB Type Library (`NumToWords.rdb`)

  * Compiled IDL interface
  * Tells LibreOffice the exact parameter types
  Critical for avoiding Err:504!

5. Legacy Manifest (`META-INF/manifest.xml`)

  * Uses `type=Python` (not modern `.components`)
  * Includes RDB entry for type library

---

Complete Debugging Chronicle - All 15+ Issues Encountered

Issue 1: Inner Class $ Notation Problem

Error: `Cannot determine registration class!`

Cause: Using Java inner class with `$` notation in `.components` file:

<!-- WRONG -->
<implementation name="com.numbertext.converter.NumToWords$_NumToWords">

Solution: Use top-level class only, no inner classes:

<!-- CORRECT -->
<implementation name="com.numbertext.converter.NumToWordsImpl">

---

Issue 2: Disconnected Factory Methods

Error: `Cannot determine registration class!`

Cause: `__getComponentFactory` and `__writeRegistryServiceInfo` were in `NumToWords.java`, but `.components` pointed to `NumToWordsImpl.java`. LibreOffice looks for these methods via reflection on the class named in the `<implementation>` tag.

Solution: Move factory methods into the implementation class:

public class NumToWordsImpl extends WeakBase implements ... {
    public static XSingleComponentFactory __getComponentFactory(String sImplName) {
        // Factory logic here
    }
    
    public static boolean __writeRegistryServiceInfo(XRegistryKey regKey) {
        // Registration logic here
    }
}

---

Issue 3: Java Version Mismatch

Error: `Cannot determine registration class!` (silent failure)

Cause: Compiling with Java 21 (default in Ubuntu 24.04) but LibreOffice configured to use Java 11 JRE. The `UnsupportedClassVersionError` manifests as generic registration failure.

Detection:

javap -verbose NumToWordsImpl.class | grep "major version"
# major version 65 = Java 21 ❌
# major version 55 = Java 11 ✅

Solution: Compile with `--release 11` flag:

javac --release 11 -d build ...

---

Issue 4: Corrupted Registry Cache

Error: `Insert duplicate implementation name` or `Cannot determine registration class!`

Cause: Previous broken installations left zombie entries in `~/.config/libreoffice/4/user/uno_packages/cache/`

Solution: Aggressive cache clearing:

pkill soffice
sleep 2
rm -rf ~/.config/libreoffice/4/user/uno_packages/cache/*
rm -rf ~/.config/libreoffice/4/user/extensions/tmp/*

---

Issue 5: Conflicting Manifest Entries (The "Final Boss")

Error: `Cannot determine registration class!`

Cause: `META-INF/manifest.xml` contained BOTH:

  • Legacy: `application/vnd.sun.star.uno-component;type=Java`
  • Modern: `application/vnd.sun.star.uno-components`

LibreOffice processes legacy first, finds no `RegistrationClassName`, aborts before reaching modern entry.

Solution: Use ONLY the legacy Python approach:

<manifest:file-entry
    manifest:media-type="application/vnd.sun.star.uno-component;type=Python"
    manifest:full-path="numtowords.uno.py"/>

---

Issue 6: Missing RDB Type Library

Error: `Err:504` (Error in parameter list)

Cause: No `.rdb` file included in extension. LibreOffice has no type map, cannot hand cell values to Python.

Solution: Create IDL and compile to RDB:

# Create IDL
# Compile
unoidl-write types.rdb offapi.rdb XOcNumToWords.idl oc_numtowords.rdb

# Add to manifest
<manifest:file-entry
    manifest:media-type="application/vnd.sun.star.uno-typelibrary;type=RDB"
    manifest:full-path="oc_numtowords.rdb"/>

---

Issue 7: Incorrect Python *args Pattern

Error: `Err:504` or `#NAME?`

Cause: Attempted to use `*args` to handle variable arguments:

# WRONG - LibreOffice doesn't support *args
def ocnumtowords(self, *args):
    ...

Solution: Use explicit parameters matching IDL exactly:

# CORRECT - Explicit signature
def numToWords(self, number, formatStyle=None):
    # XPropertySet is injected invisibly before 'number'
    ...

---

Issue 8: XCU Parameter Count Mismatch

Error: `Err:504`

Cause: Listed all 3 parameters in XCU (including invisible XPropertySet):

<!-- WRONG -->
<node oor:name="Parameters">
  <node oor:name="prop"/>    <!-- Don't list this! -->
  <node oor:name="number"/>
  <node oor:name="formatStyle"/>
</node>

Solution: Only list visible parameters:

<!-- CORRECT -->
<node oor:name="Parameters">
  <node oor:name="number"/>        <!-- Visible param 1 -->
  <node oor:name="formatStyle"/>  <!-- Visible param 2 -->
</node>

---

Issue 9: Missing IsOptional Flag

Error: `Err:504` when calling with single argument

Cause: Optional parameter not marked as optional in XCU:

<!-- WRONG -->
<node oor:name="formatStyle" oor:op="replace">
  <!-- No IsOptional property -->
</node>

Solution: Add `IsOptional` property:

<!-- CORRECT -->
<node oor:name="formatStyle" oor:op="replace">
  <prop oor:name="IsOptional" oor:type="xs:boolean">
    <value>true</value>
  </prop>
</node>

---

Issue 10: Method Name Case Sensitivity

Error: `#NAME?` or function not found

Cause: XCU node name doesn't match Python method name exactly:

<!-- WRONG -->
<node oor:name="ocnumtowords">  <!-- lowercase -->
# Python method
def ocNumToWords(self, ...):  # camelCase - mismatch!

Solution: Names must match exactly (case-sensitive):

<!-- CORRECT -->
<node oor:name="numToWords">
# Python method
def numToWords(self, ...):  # Exact match!

---

Issue 11: CompatibilityName Format

Error: Function not appearing in Calc

Cause: Incorrect `CompatibilityName` format:

<!-- WRONG -->
<prop oor:name="CompatibilityName">
  <value>AutoAddIn.NumToWords.numToWords</value>
</prop>

Solution: Use simple dotted format:

<!-- CORRECT -->
<prop oor:name="CompatibilityName">
  <value>com.numbertext.converter.NumToWordsPy.numToWords</value>
</prop>

---

Issue 12: IDL Parameter Naming (Underscores vs CamelCase)

Error: RDB compilation fails

Cause: Using underscores in IDL parameter names:

// WRONG
string numToWords([in] any format_style);  // Underscore not allowed!

Solution: Use camelCase in IDL:

// CORRECT
string numToWords([in] any formatStyle);

---

Issue 13: RDB Not Included in OXT

Error: `Err:504` even with correct IDL

Cause: Build script didn't include `.rdb` in the OXT package:

# WRONG - Missing rdb/
zip -r extension.oxt oc_reg.uno.py oc_CalcAddIn.xcu META-INF/

Solution: Include RDB directory:

# CORRECT
zip -r extension.oxt oc_reg.uno.py oc_CalcAddIn.xcu META-INF/ rdb/

---

Issue 14: Headless Test Environment Limitations

Error: Extension registers but functions return `Err:504`

Cause: Testing via `unopkg` and headless Python UNO connection doesn't fully initialize the AddIn service. Calc's formula engine needs complete GUI context.

Solution: Must test in real LibreOffice Calc GUI: 1. Install via Extension Manager GUI 2. Full restart of LibreOffice 3. Test in actual spreadsheet

The CLI test environment cannot fully validate AddIn functionality!

---

Issue 15: Service Name Mismatch

Error: Extension installs but not recognized as AddIn

Cause: Service name doesn't include `com.sun.star.sheet.AddIn`:

# WRONG
SERVICE_NAME = "com.numbertext.converter"  # Missing AddIn service!

Solution: Must include AddIn service:

# CORRECT
SERVICE_NAME = "com.sun.star.sheet.AddIn"
# Or tuple of services:
("com.numbertext.converter", "com.sun.star.sheet.AddIn")

---

Project Structure

Working Python Extension

libreoffice-calc-numbers-to-words/python/
├── NumToWordsPy.oxt              # Ready-to-install OXT
├── numtowords.uno.py            # Python UNO component
├── CalcAddIns.xcu               # Calc function registration
├── NumToWords.rdb               # Compiled UNO type library
├── description.xml              # Extension metadata
└── META-INF/manifest.xml        # OXT manifest (legacy Python type)

Key Files Explained

numtowords.uno.py:

  • Implements `XAddIn`, `XServiceInfo`, `XLocalizable`
  • Uses `unohelper.ImplementationHelper` for registration
  • Core conversion logic in pure Python
  • Method receives invisible XPropertySet!

CalcAddIns.xcu:

  • Registers function with Calc's formula engine
  • Only declares visible parameters
  • Uses `IsOptional` for optional arguments
  • CompatibilityName matches implementation

NumToWords.rdb:

  • Compiled from `NumToWords.idl`
  • Provides type information for UNO bridge
  • Critical for parameter passing!

META-INF/manifest.xml:

  • Legacy Python component type
  • Includes RDB reference
  • No modern `.components` approach

---

Complete Build Commands

For Python Extension (Working)

# 1. Create IDL
# 2. Compile IDL to RDB
unoidl-write /usr/lib/libreoffice/program/types.rdb \
    /usr/lib/libreoffice/program/types/offapi.rdb \
    idl/XNumToWords.idl \
    NumToWords.rdb

# 3. Package OXT
zip -r NumToWordsPy.oxt \
    numtowords.uno.py \
    CalcAddIns.xcu \
    NumToWords.rdb \
    description.xml \
    META-INF/

# 4. Install
unopkg add NumToWordsPy.oxt

---

Testing

Verification Steps

1. Check extension is registered:

unopkg list | grep numtowords
# Should show: com.numbertext.converter.NumToWordsPy

2. Check RDB is loaded:

# Look for RDB in bundled packages
unopkg list | grep -A 10 numtowords | grep "uno-typelibrary"

3. Test in LibreOffice Calc GUI:

=NUMTOWORDS(123)      # Should return: one hundred and twenty-three
=NUMTOWORDS(42, 1)    # Should return: forty-second
=NUMTOWORDS(99, 2)    # Should return: ninety-nine dollars

---

Lessons Learned

Key Takeaways

1. Python is easier than Java for LibreOffice extensions

  * `unohelper.ImplementationHelper` handles registration
  * No complex factory methods needed
  * No Java version compatibility issues

2. The invisible XPropertySet is critical

  * LibreOffice injects it automatically
  * Don't list it in XCU
  * Don't try to handle it with `*args`

3. RDB type library is mandatory

  * Without it, you get `Err:504`
  * Compile IDL to RDB using `unoidl-write`
  * Include RDB in OXT manifest

4. CLI testing has limitations

  * `unopkg` can install but not fully test
  * AddIn functions need GUI context
  * Always test in real LibreOffice Calc

5. Match names exactly

  * Python method name ↔ XCU node name
  * Case-sensitive!
  * CompatibilityName format matters

---

References

---

Version History

  • v2.0.0 - Working Python implementation (current)
  • v1.0.0 - Original Python attempt (failed registration)
  • Java attempts - Multiple iterations (all failed, documented above)

---

Documentation created by OpenCode AI Agent (kimi-k2.5) Last updated: February 27, 2026