{"id":31933388,"url":"https://github.com/delacrixmorgan/spindler","last_synced_at":"2026-05-17T00:03:38.159Z","repository":{"id":316643683,"uuid":"1041631790","full_name":"delacrixmorgan/spindler","owner":"delacrixmorgan","description":"Spindler - GEDCOM Kotlin Multiplatform Parser 🌳","archived":false,"fork":false,"pushed_at":"2025-09-25T20:26:39.000Z","size":13082,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-25T21:28:15.032Z","etag":null,"topics":["android","desktop","gedcom","gedcom-parser","ios","kmp","kotlin"],"latest_commit_sha":null,"homepage":"https://jitpack.io/#delacrixmorgan/spindler","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/delacrixmorgan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.md","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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"delacrixmorgan"}},"created_at":"2025-08-20T19:22:05.000Z","updated_at":"2025-09-25T20:31:37.000Z","dependencies_parsed_at":"2025-09-26T02:47:13.890Z","dependency_job_id":null,"html_url":"https://github.com/delacrixmorgan/spindler","commit_stats":null,"previous_names":["delacrixmorgan/spindler"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/delacrixmorgan/spindler","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/delacrixmorgan%2Fspindler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/delacrixmorgan%2Fspindler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/delacrixmorgan%2Fspindler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/delacrixmorgan%2Fspindler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/delacrixmorgan","download_url":"https://codeload.github.com/delacrixmorgan/spindler/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/delacrixmorgan%2Fspindler/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279017979,"owners_count":26086237,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-14T02:00:06.444Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["android","desktop","gedcom","gedcom-parser","ios","kmp","kotlin"],"created_at":"2025-10-14T05:58:00.183Z","updated_at":"2026-05-17T00:03:38.146Z","avatar_url":"https://github.com/delacrixmorgan.png","language":"Kotlin","funding_links":["https://github.com/sponsors/delacrixmorgan"],"categories":[],"sub_categories":[],"readme":"# Spindler - GEDCOM Kotlin Multiplatform Parser 🌳\n\n![Maven Central Version](https://img.shields.io/maven-central/v/com.dontsaybojio/spindler?color=%234c1)\n\n**Spindler** is a delightfully powerful Kotlin Multiplatform Compose library that transforms GEDCOM\ngenealogy files into beautiful, type-safe Kotlin models! 🎉\n\nWhether you're building a family tree app, analysing genealogical data, or just want to explore your\nfamily history programmatically, Spindler got you covered! It handles everything from **marriage\nrecords, family relationships** and most important of all tricky **date formats** from the past\ncenturies!\n\nBuilt with modern Kotlin Multiplatform magic ✨, it works seamlessly across Android, iOS, and  \nDesktop - because family trees shouldn't be platform-locked!\n\n![screenshot_overview](screenshots/0_overview.gif)\n\n\u003e Try out the `sample` app!\n\n## 🌟 Features\n\n- 📊 **Complete GEDCOM Parsing** — Transform GEDCOM 5.5.1, 5.5.5 and 7.0 files into clear, structured\n  Kotlin models\n- 👥 **Family Relationship Mapping** — Navigate complex family trees with ease (parents, children,\n  spouses)\n- 📅 **Smart Date Parsing** — Handles historical dates, partial dates, and multiple formats  \n  automatically\n- 🌍 **KMP Ready** — Works on Android, iOS, Desktop, and anywhere Kotlin runs\n- 🔗 **Flexible Data Sources** — Load from local files, remote URLs, or raw strings\n- 🏷️ **MacFamilyTree Extensions** — Full support for MacFamilyTree-specific tags and features\n- 📍 **Rich Location Data** — Birth places, death places, marriage locations with full detail\n- 🎯 **Type-Safe Models** — No more string parsing headaches - everything is properly typed\n- ⚡ **Lightweight \u0026 Fast** — Minimal dependencies, maximum performance\n- 🧠 **Smart Defaults** — Gracefully handles missing data with sensible fallbacks\n\n## 🎭 What Makes It Special?\n\n1. **Built for Real Genealogy** — Tested with actual family history data, weird edge cases included!\n2. **Date Intelligence** — Parses \"ABT 1845\", \"BEF 1900\", \"EST 1820\" and countless historical date  \n   formats\n3. **Relationship Navigation** — Find someone's parents, children, or spouse with simple property  \n   access\n4. **MacFamilyTree Ready** — Seamlessly works with popular genealogy software exports\n5. **Multiplatform Native** — Same API across all platforms, no compromises\n\n## 📦 Installation\n\nAdd the dependency in your `build.gradle.kts`:\n\n### Step 1\n\nAdd the mavenCentral repository to your `settings.gradle.kts` file:\n\n```groovy\ndependencyResolutionManagement {\n    repositories {\n        mavenCentral()\n    }\n}\n```\n\n### Step 2\n\nAdd the dependency:\n\n```groovy\ndependencies {\n    implementation(\"com.dontsaybojio:spindler:X.X.X\")\n}\n```\n\n## 🚀 Quick Start\n\n### Loading from Local Date Source\n\nIf you're reading from a local file on device.\n\n```kotlin\nobject SpindlerLocalDataSource {\n    private val path: String = \"files/sample.ged\"\n    private val gedcomIndexDtoToModelMapper: GedcomIndexDtoToModelMapper by lazy { GedcomIndexDtoToModelMapper() }\n\n    suspend fun getData(): GedcomIndex {\n        val text = Res.readBytes(path = path).decodeToString()\n        return gedcomIndexDtoToModelMapper(text)\n    }\n}\n\n// Usage  \nval familyTree = SpindlerLocalDataSource.getData()\nprintln(\"Found ${familyTree.individuals.size} individuals!\")\nprintln(\"Found ${familyTree.families.size} families!\")  \n```  \n\n### Loading from Remote Data Source\n\nIf you're reading from an API that returns `.ged`.\n\n```kotlin\nobject SpindlerRemoteDataSource {\n    private val httpClient = HttpClient()\n    private val gedcomMapper = GedcomIndexDtoToModelMapper()\n\n    suspend fun loadData(url: String, headers: Map\u003cString, String\u003e? = null): GedcomIndex {\n        try {\n            val gedcomContent = httpClient.get(url) {\n                headers {\n                    append(\n                        HttpHeaders.Accept,\n                        \"text/plain, text/gedcom, application/octet-stream, */*\"\n                    )\n                    headers?.forEach { (key, value) -\u003e\n                        append(key, value)\n                    }\n                }\n            }.body\u003cString\u003e()\n\n            return gedcomIndexDtoToModelMapper(gedcomContent)\n        } catch (e: Exception) {\n            throw RemoteDataSourceException(\n                \"Failed to download or parse GEDCOM file from $url\",\n                e\n            )\n        } finally {\n            close()\n        }\n    }\n}\n\n// Usage  \nval familyTree = SpindlerRemoteDataSource.loadData(\"https://example.com/family.ged\")\nprintln(\"Found ${familyTree.individuals.size} individuals!\")\nprintln(\"Found ${familyTree.families.size} families!\")   \n```  \n\n## 🧬 Data Models\n\nGEDCOM separates the data into groups of `Individual` and `Family`, Spindler is structured similar\nto it as well. Within those data models, it consist of `id: String` and `node: List\u003cGedcomNode\u003e`.\n\n```kotlin\ndata class Individual(\n    val id: String,\n    val nodes: List\u003cGedcomNode\u003e,\n)\n\ndata class Family(\n    val id: String,\n    val nodes: List\u003cGedcomNode\u003e,\n)\n```\n\nSpindler takes another step further by providing all the **common attributes** in convenient methods\nthat handles all the mapping. Here's a code snippet within the `Family` data model.\n\n```kotlin\ndata class Family(\n    val id: String,\n    val nodes: List\u003cGedcomNode\u003e,\n) {\n    val marriageDateRaw: String?\n        get() = nodes.firstOrNull { it.tag == Tag.MARRIAGE }?.children\n            ?.firstOrNull { it.tag == Tag.DATE }?.value\n\n    val marriageDate: LocalDate?\n        get() = DateParsing.tryParseDate(marriageDateRaw)\n\n    val marriageDateFormatted: String\n        get() = marriageDate?.toString() ?: \"~${marriageDateRaw ?: \"N/A\"}\"\n\n    val marriagePlace: String?\n        get() = nodes.firstOrNull { it.tag == Tag.MARRIAGE }?.children\n            ?.firstOrNull { it.tag == Tag.PLACE }?.value\n}\n```\n\n### `Individual`\n\n```kotlin  \nval individual = familyTree.individuals[\"I001\"]\n\n// Basic Information  \nprintln(\"Name: ${individual.formattedName}\")\nprintln(\"Given Names: ${individual.givenNames.joinToString(\", \")}\")\nprintln(\"Surnames: ${individual.surnames.joinToString(\", \")}\")\nprintln(\"Sex: ${individual.sex.name}\")\n\n// Life Events  \nprintln(\"Born: ${individual.birthDateFormatted}\")\nprintln(\"Birth Place: ${individual.birthPlace ?: \"Unknown\"}\")\nprintln(\"Died: ${individual.deathDateFormatted}\")\n\n// Additional Details  \nprintln(\"Education: ${individual.education ?: \"N/A\"}\")\nprintln(\"Religion: ${individual.religion ?: \"N/A\"}\")\n\n// Family Relationships  \nindividual.familyIDAsChild?.let { familyId -\u003e\n    val childFamily = familyTree.families[familyId]\n    println(\"Parents' Family: $familyId\")\n}\n\nindividual.familyIDAsSpouse?.let { familyId -\u003e\n    val spouseFamily = familyTree.families[familyId]\n    println(\"Spouse Family: $familyId\")\n}\n\n// MacFamilyTree Integration  \nindividual.macFamilyTreeID?.let {\n    println(\"MacFamilyTree ID: $it\")\n}\n\n// Metadata  \nprintln(\"Last Changed: ${individual.changeDate ?: \"N/A\"}\")\nprintln(\"Created: ${individual.creationDate ?: \"N/A\"}\")  \n```  \n\n### `Family`\n\n```kotlin  \nval family = familyTree.families[\"F001\"]\n\n// Marriage Information  \nprintln(\"Marriage Date: ${family.marriageDateFormatted}\")\nprintln(\"Marriage Place: ${family.marriagePlace ?: \"Unknown\"}\")\n\n// Family Members  \nfamily.husbandID?.let { husbandId -\u003e\n    val husband = familyTree.individuals[husbandId]\n    println(\"Husband: ${husband.formattedName}\")\n}\n\nfamily.wifeID?.let { wifeId -\u003e\n    val wife = familyTree.individuals[wifeId]\n    println(\"Wife: ${wife.formattedName}\")\n}\n\n// Children  \nif (family.childrenIDs.isNotEmpty()) {\n    println(\"Children:\")\n    family.childrenIDs.forEach { childId -\u003e\n        val child = familyTree.individuals[childId]\n        println(\"  - ${child.formattedName}\")\n    }\n}\n\n// MacFamilyTree Extensions  \nfamily.macFamilyTreeLabel?.let {\n    println(\"MacFamilyTree Label: $it\")\n}  \n```  \n\n## 👪 Relationships\n\nLike how GEDCOM structures their data, each `Individual` and `Family`would have their related IDs\nstore in their data model.\n\n```kotlin\nindividual.familyIDAsChild\nindividual.familyIDAsSpouse\n\nfamily.husbandID\nfamily.wifeID\nfamily.childrenIDs\n```\n\n## 📋 Complete API Reference\n\n### `Individual`\n\n| Property             | Type               | Description                              |\n|----------------------|--------------------|------------------------------------------|\n| `id`                 | `String`           | Unique individual identifier from GEDCOM |\n| `formattedName`      | `String`           | Complete name (given names + surnames)   |\n| `givenNames`         | `List\u003cString\u003e`     | All given/first names                    |\n| `surnames`           | `List\u003cString\u003e`     | All surname/family names                 |\n| `sex`                | `Sex`              | Gender (MALE, FEMALE, UNKNOWN)           |\n| `birthDate`          | `LocalDate?`       | Parsed birth date (null if unparseable)  |\n| `birthDateRaw`       | `String?`          | Original birth date string from GEDCOM   |\n| `birthDateFormatted` | `String`           | User-friendly birth date display         |\n| `birthPlace`         | `String?`          | Birth location                           |\n| `deathDate`          | `LocalDate?`       | Parsed death date (null if unparseable)  |\n| `deathDateRaw`       | `String?`          | Original death date string from GEDCOM   |\n| `deathDateFormatted` | `String`           | User-friendly death date display         |\n| `education`          | `String?`          | Educational information                  |\n| `religion`           | `String?`          | Religious affiliation                    |\n| `familyIDAsChild`    | `String?`          | Family ID where this person is a child   |\n| `familyIDAsSpouse`   | `String?`          | Family ID where this person is a spouse  |\n| `macFamilyTreeID`    | `String?`          | MacFamilyTree-specific identifier (_FID) |\n| `changeDate`         | `String?`          | Last modification date                   |\n| `creationDate`       | `String?`          | Creation date                            |\n| `nodes`              | `List\u003cGedcomNode\u003e` | Raw GEDCOM nodes for advanced access     |\n\n### `Family`\n\n| Property                | Type               | Description                                |\n|-------------------------|--------------------|--------------------------------------------|\n| `id`                    | `String`           | Unique family identifier from GEDCOM       |\n| `marriageDate`          | `LocalDate?`       | Parsed marriage date (null if unparseable) |\n| `marriageDateRaw`       | `String?`          | Original marriage date string from GEDCOM  |\n| `marriageDateFormatted` | `String`           | User-friendly marriage date display        |\n| `marriagePlace`         | `String?`          | Marriage location                          |\n| `husbandID`             | `String?`          | Individual ID of the husband               |\n| `wifeID`                | `String?`          | Individual ID of the wife                  |\n| `childrenIDs`           | `List\u003cString\u003e`     | List of individual IDs for all children    |\n| `macFamilyTreeLabel`    | `String?`          | MacFamilyTree-specific label               |\n| `changeDate`            | `String?`          | Last modification date                     |\n| `creationDate`          | `String?`          | Creation date                              |\n| `nodes`                 | `List\u003cGedcomNode\u003e` | Raw GEDCOM nodes for advanced access       |\n\n### `GedcomIndex`\n\n| Property      | Type                      | Description                   |\n|---------------|---------------------------|-------------------------------|\n| `individuals` | `Map\u003cString, Individual\u003e` | All individuals indexed by ID |\n| `families`    | `Map\u003cString, Family\u003e`     | All families indexed by ID    |\n\n## 🎯 Advanced Usage\n\n### Custom Date Parsing\n\nSpindler handles complex historical dates automatically:\n\n```kotlin  \n// These all parse correctly:  \n// \"1845\"           -\u003e 1845-01-01  \n// \"ABT 1845\"       -\u003e ~1845 (approximate)  \n// \"BEF 1900\"       -\u003e ~1900 (before)  \n// \"EST 1820\"       -\u003e ~1820 (estimated)  \n// \"25 DEC 1800\"    -\u003e 1800-12-25  \n\nval individual = familyTree.individuals[\"I001\"]\nindividual.birthDate          // LocalDate? - null if not parseable  \nindividual.birthDateRaw       // String? - Raw GEDCOM text  \nindividual.birthDateFormatted // String - always has a value  \n```  \n\n### Working with Raw Nodes\n\nFor advanced use cases or if the methods aren't covered, you can easily use the `GedcomNode` to\naccess the raw GEDCOM structure to get what you need:\n\n```kotlin  \nval individual = familyTree.individuals[\"I001\"]\n\n// Find all custom tags  \nval customTags = individual.nodes.filter {\n    it.tag.startsWith(\"_\") // Custom tags often start with _}  \n\n// Access specific node data  \n    val occupationNode = individual.nodes.firstOrNull { it.tag == \"OCCU\" }\n    val occupation = occupationNode?.value\n```  \n\n## 🏗️ Supported Platforms\n\n- ✅ **Android** - API 21+ (Android 5.0+)\n- ✅ **Desktop/JVM** - Java 8+\n- ✅ **iOS** - iOS 11.0+, all architectures (x64, arm64, simulator arm64)\n- 🔄 **Web** - Coming soon!\n\n## 🤝 Contributing\n\nWe'd love your help making Spindler even better! Here's how:\n\n1. **Found a bug?** Open an issue with a sample GEDCOM file\n2. **Have a feature idea?** Start a discussion - we're always listening!\n3. **Want to contribute code?** Fork, branch, code, test, create a PR!\n4. **Genealogy expert?** Help us handle more edge cases and formats\n\n## ❤️ Acknowledgments\n\n- [GEDCOM](https://www.gedcom.org/)\n- [MacFamilyTree](https://www.syniumsoftware.com/macfamilytree)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdelacrixmorgan%2Fspindler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdelacrixmorgan%2Fspindler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdelacrixmorgan%2Fspindler/lists"}