{"id":16012746,"url":"https://github.com/alexpovel/decompound","last_synced_at":"2026-02-03T12:01:31.309Z","repository":{"id":188794600,"uuid":"676679284","full_name":"alexpovel/decompound","owner":"alexpovel","description":"Decompose a compound word into its constituent parts. Works in any language, as you provide the rules around what constitutes a (single) word. The algorithm is Unicode-aware.","archived":false,"fork":false,"pushed_at":"2023-10-01T19:51:07.000Z","size":42,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-12-27T01:40:43.642Z","etag":null,"topics":["compound","decomposition","rust","split","string","word"],"latest_commit_sha":null,"homepage":"https://docs.rs/decompound","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexpovel.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-08-09T18:53:19.000Z","updated_at":"2025-11-21T16:23:37.000Z","dependencies_parsed_at":"2024-10-27T16:14:03.622Z","dependency_job_id":"2c39f6ce-4cca-4508-88da-768adbde798a","html_url":"https://github.com/alexpovel/decompound","commit_stats":null,"previous_names":["alexpovel/decompound"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/alexpovel/decompound","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpovel%2Fdecompound","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpovel%2Fdecompound/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpovel%2Fdecompound/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpovel%2Fdecompound/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexpovel","download_url":"https://codeload.github.com/alexpovel/decompound/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpovel%2Fdecompound/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29045440,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-03T10:09:22.136Z","status":"ssl_error","status_checked_at":"2026-02-03T10:09:16.814Z","response_time":96,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["compound","decomposition","rust","split","string","word"],"created_at":"2024-10-08T14:20:54.868Z","updated_at":"2026-02-03T12:01:29.635Z","avatar_url":"https://github.com/alexpovel.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Decompound\n\nSplit (decompose) a compound word into its constituent parts. Works in any language, as\nyou provide the rules around what constitutes a (*single*) word. The algorithm is\nUnicode-aware.\n\nUseful for [culling down existing dictionaries at build time](#motivation).\n\nThe docs are best viewed via [docs.rs](https://docs.rs/decompound) (but note those docs\nare tied to releases, so might not be on par with the head of trunk).\n\n[![codecov](https://codecov.io/github/alexpovel/decompound/graph/badge.svg?token=VCCO4HF6MT)](https://codecov.io/github/alexpovel/decompound)[![crates](https://img.shields.io/crates/v/decompound.svg)](https://crates.io/crates/decompound)\n\n## Usage\n\nUsage is very straightforward. There is only one (free) function of interest,\n[`decompound`]. Its party piece is a closure argument, deciding whether a *single* word\nis valid. As this can be highly dynamic and language-specific, this decision is left to\nthe user.\n\n```rust\nuse decompound::{decompound, DecompositionOptions};\n\nlet is_valid_single_word = |w: \u0026str| [\"bed\", \"room\"].contains(\u0026w);\n\nassert_eq!(\n    decompound(\n        \"bedroom\",\n        \u0026is_valid_single_word,\n        DecompositionOptions::empty(),\n    ).unwrap(),\n    vec![\"bed\", \"room\"]\n);\n```\n\nCandidates for validity checks are simple dictionary lookups (for example, using\n[`std::collections::HashSet`], [`phf`](https://crates.io/crates/phf), [Finite State\nTransducers](https://docs.rs/fst/latest/fst/), [binary\nsearch](https://docs.rs/b4s/latest/b4s/), ...), or any elaborate algorithm of your\nchoice.\n\n### Configuration\n\nConfiguration is exposed as a [bit\nfield](https://docs.rs/bitflags/latest/bitflags/index.html) via\n[`DecompositionOptions`]. It affords more complex use cases, freely combinable.\nUsefulness largely depends on the natural language at hand. Some, for example German,\nmight require:\n\n```rust\nuse decompound::{decompound, DecompositionError, DecompositionOptions};\n\nlet is_valid_single_word = |w: \u0026str| [\"Rüben\", \"Knollen\", \"Küche\"].contains(\u0026w);\n\nassert_eq!(\n    decompound(\n        \"Rübenknollen-Küche\",\n        \u0026is_valid_single_word,\n        // Wouldn't find anything without titlecasing `knollen` to `Knollen`,\n        // and splitting on hyphens.\n        DecompositionOptions::SPLIT_HYPHENATED\n        | DecompositionOptions::TRY_TITLECASE_SUFFIX\n    ).unwrap(),\n    vec![\"Rüben\", \"Knollen\", \"Küche\"]\n);\n```\n\nFor more options and examples, refer to the docs of [`DecompositionOptions`].\n\n### Failure modes\n\nIf the word cannot be decomposed, a [`DecompositionError`] is returned.\n\n```rust\nuse decompound::{decompound, DecompositionError, DecompositionOptions};\n\nlet is_valid_single_word = |w: \u0026str| [\"water\", \"melon\"].contains(\u0026w);\n\nassert_eq!(\n    decompound(\n        \"snowball\",\n        \u0026is_valid_single_word,\n        DecompositionOptions::empty(),\n    ).unwrap_err(),\n    DecompositionError::NothingValid\n);\n```\n\n#### Overeager validity checks\n\nNothing prevents you from providing a closure *which itself accepts compound words*.\nCompound words (like `railroad`) being included in a lookup dictionary (instead of\n*only* its root words `rail` and `road`) is an example \"pathological\" case.\nAccommodating compound words *yourself* is precisely what this crate is [supposed to\nalleviate](#motivation). If you already have and do not want to or cannot drop that\ncapability, this crate might be obsolete for your case (hence \"overeager checks\").\n\nAlthough [`decompound`] prefers splits if possible, such as\n\n```rust\nuse decompound::{decompound, DecompositionError, DecompositionOptions};\n\n// Contains a compound word *and* its root words.\nlet is_valid_single_word = |w: \u0026str| [\"blueberry\", \"blue\", \"berry\"].contains(\u0026w);\n\nassert_eq!(\n    decompound(\n        \"blueberry\",\n        \u0026is_valid_single_word,\n        DecompositionOptions::empty(),\n    ).unwrap(),\n    vec![\"blue\", \"berry\"]\n);\n```\n\nif root words are missing but the compound itself is present, decomposition technically\n*fails*. This case is considered an error, and marked as such. That is [more\nergonomic](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/) than\nbeing returned a [`Vec`] of constituents of length 1, requiring more awkward error\nhandling at the call site.\n\n```rust\nuse decompound::{decompound, DecompositionError, DecompositionOptions};\n\n// *Only* contains a compound word, not its root words.\nlet is_valid_single_word = |w: \u0026str| [\"firefly\"].contains(\u0026w);\n\nassert_eq!(\n    decompound(\n        \"firefly\",\n        \u0026is_valid_single_word,\n        DecompositionOptions::empty(),\n    ).unwrap_err(),\n    DecompositionError::SingleWord(\"firefly\".to_string())\n);\n```\n\nMatch on this variant if this case is not an error in your domain (this crate itself\n[does so\ninternally](https://github.com/alexpovel/decompound/blob/b2f4151940c4c55bdb3684c2b74f6336faaf9d2e/src/lib.rs#L187-L190),\ntoo).\n\n## Motivation\n\nThe crate implementation is simple and nothing you wouldn't be able to write yourself.\n\nThere is a catch though. As mentioned, this crate can help you move checks for compound\nwords from static (a fixed dictionary) to runtime ([`decompound`]). For some languages,\nthis is strictly *required*, as the set of compound words might be immense, or\n(effectively, not mathematically) unbounded, meaning root words may be combined to\narbitrary lengths.\n\nGerman is such a case. No dictionary exists to cover all possible\nGerman words. However, [existing ones](https://sourceforge.net/projects/germandict/) are\nalmost guaranteed to themselves contain *some* compound words (which is generally\nhelpful). When using such dictionaries *and* this crate to cover all remaining,\narbitrary compound words, **duplication arises, and the dictionary is no longer\nminimal**. Most, perhaps all, compound words in the dictionary could be detected at\nruntime instead (providing a single source of truth along the way).\n\nCulling the dictionary might lead to significant, [perhaps\nnecessary](https://github.com/rust-lang/crates.io/issues/195) savings in size (memory\nand executable). For culling, a [build\nscript](https://doc.rust-lang.org/cargo/reference/build-scripts.html) is needed.\nHowever, now both the code being built *and* the build script\n[depend](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#build-dependencies)\non that same detection algorithm. Where does *it* live?\n\n- The build script cannot depend on what it's building. Therefore, the algorithm cannot\n  live inside the crate under construction.\n- The temptation of copy-pasting it into the build script arises. However, if what the\n  dictionary is being culled with with gets out of sync with what's done at runtime,\n  bugs arise. Maintainability suffers. Therefore, duplicating the algorithm is not an\n  option.\n- Currently (2023-08-19), **there is no place for the check to live except another\n  crate**, external to both the build script and actual code. **That's this crate.** It\n  affords:\n\n  - a **non-cyclic build graph**,\n  - a single source of truth for the algorithm,\n  - usage of *any* dictionary, no out-of-band preprocessing necessary (the original\n  dictionary can be kept).\n\n## Developing\n\nThe library crate contains an [accompanying binary](./src/main.rs). It is accessible\nsimply via `cargo run`. This allows running this crate locally, provided a word list on\nstdin. For example:\n\n```console\n$ sudo apt update \u0026\u0026 sudo apt install --yes wngerman\n$ cat /usr/share/dict/ngerman | cargo run -- --try-titlecase-suffix --split-hyphenated 'Affengruppen-Überfall' 2\u003e/dev/null\nAffen\nGruppen\nÜberfall\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexpovel%2Fdecompound","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexpovel%2Fdecompound","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexpovel%2Fdecompound/lists"}