{"id":13995521,"url":"https://github.com/Flight-School/RegularExpressionDecoder","last_synced_at":"2025-07-22T22:31:06.466Z","repository":{"id":56408006,"uuid":"170880272","full_name":"Flight-School/RegularExpressionDecoder","owner":"Flight-School","description":"A decoder that constructs objects from regular expression matches.","archived":true,"fork":false,"pushed_at":"2020-11-09T19:29:08.000Z","size":45,"stargazers_count":175,"open_issues_count":0,"forks_count":7,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-07-09T08:17:30.860Z","etag":null,"topics":["codable","regular-expression","swift"],"latest_commit_sha":null,"homepage":"https://flight.school","language":"Swift","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/Flight-School.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},"funding":{"github":["mattt"],"custom":"https://flight.school/books/strings"}},"created_at":"2019-02-15T14:47:51.000Z","updated_at":"2024-10-19T11:49:17.000Z","dependencies_parsed_at":"2022-08-15T18:10:28.447Z","dependency_job_id":null,"html_url":"https://github.com/Flight-School/RegularExpressionDecoder","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Flight-School/RegularExpressionDecoder","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flight-School%2FRegularExpressionDecoder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flight-School%2FRegularExpressionDecoder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flight-School%2FRegularExpressionDecoder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flight-School%2FRegularExpressionDecoder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Flight-School","download_url":"https://codeload.github.com/Flight-School/RegularExpressionDecoder/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Flight-School%2FRegularExpressionDecoder/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266585670,"owners_count":23952163,"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-07-22T02:00:09.085Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"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":["codable","regular-expression","swift"],"created_at":"2024-08-09T14:03:27.613Z","updated_at":"2025-07-22T22:31:06.206Z","avatar_url":"https://github.com/Flight-School.png","language":"Swift","readme":"# Regular Expression Decoder\n\n[![Build Status][build status badge]][build status]\n[![License][license badge]][license]\n[![Swift Version][swift version badge]][swift version]\n\nA decoder that constructs objects from regular expression matches.\n\n---\n\nFor more information about creating your own custom decoders,\nconsult Chapter 7 of the\n[Flight School Guide to Swift Codable](https://flight.school/books/codable).\nFor more information about using regular expressions in Swift,\ncheck out Chapter 6 of the\n[Flight School Guide to Swift Strings](https://flight.school/books/strings).\n\n## Requirements\n\n- Swift 5+\n- iOS 11+ or macOS 10.13+\n\n## Usage\n\n```swift\nimport RegularExpressionDecoder\n\nlet ticker = \"\"\"\nAAPL 170.69▲0.51\nGOOG 1122.57▲2.41\nAMZN 1621.48▼18.52\nMSFT 106.57=0.00\nSWIFT 5.0.0▲1.0.0\n\"\"\"\n\nlet pattern: RegularExpressionPattern\u003cStock, Stock.CodingKeys\u003e = #\"\"\"\n\\b\n(?\u003c\\#(.symbol)\u003e[A-Z]{1,4}) \\s+\n(?\u003c\\#(.price)\u003e\\d{1,}\\.\\d{2}) \\s*\n(?\u003c\\#(.sign)\u003e([▲▼=])\n(?\u003c\\#(.change)\u003e\\d{1,}\\.\\d{2})\n\\b\n\"\"\"#\n\nlet decoder = try RegularExpressionDecoder\u003cStock\u003e(\n                    pattern: pattern,\n                    options: .allowCommentsAndWhitespace\n                  )\n\ntry decoder.decode([Stock].self, from: ticker)\n// Decodes [AAPL, GOOG, AMZN, MSFT] (but not SWIFT, which is invalid)\n```\n\n## Explanation\n\nLet's say that you're building an app that parses stock quotes\nfrom a text-based stream of price changes.\n\n```swift\nlet ticker = \"\"\"\nAAPL 170.69▲0.51\nGOOG 1122.57▲2.41\nAMZN 1621.48▼18.52\nMSFT 106.57=0.00\n\"\"\"\n```\n\nEach stock is represented by the following structure:\n\n- The **symbol**, consisting of 1 to 4 uppercase letters, followed by a space\n- The **price**, formatted as a number with 2 decimal places\n- A **sign**, indicating a price gain (`▲`), loss (`▼`), or no change (`=`)\n- The **magnitude** of the gain or loss, formatted the same as the price\n\nThese format constraints lend themselves naturally\nto representation by a \u003cdfn\u003eregular expression\u003c/dfn\u003e,\nsuch as:\n\n```perl\n/\\b[A-Z]{1,4} \\d{1,}\\.\\d{2}[▲▼=]\\d{1,}\\.\\d{2}\\b/\n```\n\n\u003e Note:\n\u003e The `\\b` metacharacter anchors matches to word boundaries.\n\nThis regular expression can distinguish between\nvalid and invalid stock quotes.\n\n```swift\n\"AAPL 170.69▲0.51\" // valid\n\"SWIFT 5.0.0▲1.0.0\" // invalid\n```\n\nHowever, to extract individual components from a quote\n(symbol, price, etc.)\nthe regular expression must contain \u003cdfn\u003ecapture groups\u003c/dfn\u003e,\nof which there are two varieties:\n\u003cdfn\u003epositional capture groups\u003c/dfn\u003e and\n\u003cdfn\u003enamed capture groups\u003c/dfn\u003e.\n\nPositional capture groups are demarcated in the pattern\nby enclosing parentheses (`(___)`).\nWith some slight modifications,\nwe can make original regular expression capture each part of the stock quote:\n\n```perl\n/\\b([A-Z]{1,4}) (\\d{1,}\\.\\d{2})([▲▼=])(\\d{1,}\\.\\d{2})\\b/\n```\n\nWhen matched,\nthe symbol can be accessed by the first capture group,\nthe price by the second,\nand so on.\n\nFor large numbers of capture groups ---\nespecially in patterns with nested groups ---\none can easily lose track of which parts correspond to which positions.\nSo another approach is to assign names to capture groups,\nwhich are denoted by the syntax `(?\u003cNAME\u003e___)`.\n\n```perl\n/\\b\n(?\u003csymbol\u003e[A-Z]{1,4}) \\s+\n(?\u003cprice\u003e\\d{1,}\\.\\d{2}) \\s*\n(?\u003csign\u003e([▲▼=])\n(?\u003cchange\u003e\\d{1,}\\.\\d{2})\n\\b/\n```\n\n\u003e Note:\n\u003e Most regular expression engines ---\n\u003e including the one used by `NSRegularExpression` ---\n\u003e provide a mode to ignore whitespace;\n\u003e this lets you segment long patterns over multiple lines,\n\u003e making them easier to read and understand.\n\nTheoretically, this approach allows you to access each group by name\nfor each match of the regular expression.\nIn practice, doing this in Swift can be inconvenient,\nas it requires you to interact with cumbersome `NSRegularExpression` APIs\nand somehow incorporate it into your model layer.\n\n`RegularExpressionDecoder` provides a convenient solution\nto constructing `Decodable` objects from regular expression matches\nby automatically matching coding keys to capture group names.\nAnd it can do so safely,\nthanks to the new `ExpressibleByStringInterpolation` protocol in Swift 5.\n\nTo understand how,\nlet's start by considering the following `Stock` model,\nwhich adopts the `Decodable` protocol:\n\n```swift\nstruct Stock: Decodable {\n    let symbol: String\n    var price: Double\n\n    enum Sign: String, Decodable {\n        case gain = \"▲\"\n        case unchanged = \"=\"\n        case loss = \"▼\"\n    }\n\n    private var sign: Sign\n    private var change: Double = 0.0\n    var movement: Double {\n        switch sign {\n        case .gain: return +change\n        case .unchanged: return 0.0\n        case .loss: return -change\n        }\n    }\n}\n```\n\nSo far, so good.\n\nNow, normally, the Swift compiler\nautomatically synthesizes conformance to `Decodable`,\nincluding a nested `CodingKeys` type.\nBut in order to make this next part work correctly,\nwe'll have to do this ourselves:\n\n```swift\nextension Stock {\n    enum CodingKeys: String, CodingKey {\n        case symbol\n        case price\n        case sign\n        case change\n    }\n}\n```\n\nHere's where things get really interesting:\nremember our regular expression with named capture patterns from before?\n_We can replace the hard-coded names\nwith interpolations of the `Stock` type's coding keys._\n\n```swift\nimport RegularExpressionDecoder\n\nlet pattern: RegularExpressionPattern\u003cStock, Stock.CodingKeys\u003e = #\"\"\"\n\\b\n(?\u003c\\#(.symbol)\u003e[A-Z]{1,4}) \\s+\n(?\u003c\\#(.price)\u003e\\d{1,}\\.\\d{2}) \\s*\n(?\u003c\\#(.sign)\u003e[▲▼=])\n(?\u003c\\#(.change)\u003e\\d{1,}\\.\\d{2})\n\\b\n\"\"\"#\n```\n\n\u003e Note:\n\u003e This example benefits greatly from another new feature in Swift 5:\n\u003e \u003cdfn\u003eraw string literals\u003c/dfn\u003e.\n\u003e Those octothorps (`#`) at the start and end\n\u003e tell the compiler to ignore escape characters (`\\`)\n\u003e unless they also include an octothorp (`\\#( )`).\n\u003e Using raw string literals,\n\u003e we can write regular expression metacharacters like `\\b`, `\\d`, and `\\s`\n\u003e without double escaping them (i.e. `\\\\b`).\n\nThanks to `ExpressibleByStringInterpolation`,\nwe can restrict interpolation segments to only accept those coding keys,\nthereby ensuring a direct 1:1 match between capture groups\nand their decoded properties.\nAnd not only that ---\nthis approach lets us to verify that keys have valid regex-friendly names\nand aren't captured more than once.\nIt's enormously powerful,\nallowing code to be incredibly expressive\nwithout compromising safety or performance.\n\nWhen all is said and done,\n`RegularExpressionDecoder` lets you decode types\nfrom a string according to a regular expression pattern\nmuch the same as you might from JSON or a property list\nusing a decoder:\n\n```swift\nlet decoder = try RegularExpressionDecoder\u003cStock\u003e(\n                        pattern: pattern,\n                        options: .allowCommentsAndWhitespace\n                  )\n\ntry decoder.decode([Stock].self, from: ticker)\n// Decodes [AAPL, GOOG, AMZN, MSFT]\n```\n\n## License\n\nMIT\n\n## Contact\n\nMattt ([@mattt](https://twitter.com/mattt))\n\n[build status]: https://github.com/Flight-School/RegularExpressionDecoder/actions?query=workflow%3ACI\n[build status badge]: https://github.com/Flight-School/RegularExpressionDecoder/workflows/CI/badge.svg\n[license]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat\n[license badge]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat\n[swift version]: https://swift.org/download/\n[swift version badge]: http://img.shields.io/badge/swift%20version-5.0-orange.svg?style=flat\n","funding_links":["https://github.com/sponsors/mattt","https://flight.school/books/strings"],"categories":["Swift"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FFlight-School%2FRegularExpressionDecoder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FFlight-School%2FRegularExpressionDecoder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FFlight-School%2FRegularExpressionDecoder/lists"}