├── .circleci └── config.yml ├── .github └── dependabot.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Changelog.md ├── LICENSE ├── README.md ├── bsconfig.json ├── deploying.md ├── documentation ├── contributing.md ├── conventions.md ├── installation.md └── usage.md ├── integration-test ├── .gitignore ├── bsconfig.json ├── main.res ├── package-lock.json └── package.json ├── package-lock.json ├── package.json ├── src ├── Internal.res ├── Tablecloth.res ├── TableclothArray.res ├── TableclothArray.resi ├── TableclothBool.res ├── TableclothBool.resi ├── TableclothChar.res ├── TableclothChar.resi ├── TableclothComparator.res ├── TableclothComparator.resi ├── TableclothContainer.res ├── TableclothFloat.res ├── TableclothFloat.resi ├── TableclothFun.res ├── TableclothFun.resi ├── TableclothInt.res ├── TableclothInt.resi ├── TableclothList.res ├── TableclothList.resi ├── TableclothMap.res ├── TableclothMap.resi ├── TableclothOption.res ├── TableclothOption.resi ├── TableclothResult.res ├── TableclothResult.resi ├── TableclothSet.res ├── TableclothSet.resi ├── TableclothString.res ├── TableclothString.resi ├── TableclothTuple2.res ├── TableclothTuple2.resi ├── TableclothTuple3.res └── TableclothTuple3.resi └── test ├── ArrayTest.res ├── BoolTest.res ├── CharTest.res ├── ComparatorTest.res ├── FloatTest.res ├── FunTest.res ├── IntTest.res ├── JestStubs.res ├── ListTest.res ├── MapTest.res ├── OptionTest.res ├── ResultTest.res ├── SetTest.res ├── StringTest.res ├── Tuple2Test.res └── Tuple3Test.res /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build-and-test: 4 | resource_class: medium 5 | parameters: 6 | rescript-version: { type: string } 7 | docker: 8 | - image: cimg/node:18.10.0 9 | environment: 10 | CI: true 11 | NODE_ENV: test 12 | TC_RESCRIPT_VERSION: << parameters.rescript-version >> 13 | working_directory: ~/repo 14 | steps: 15 | - checkout: 16 | path: ~/repo 17 | - restore_cache: 18 | keys: 19 | - v2-rescript-dependencies-{{ .Branch }}-{{ checksum "package-lock.json" }} 20 | - v2-rescript-dependencies-{{ .Branch }}- 21 | - v2-rescript-dependencies- 22 | # Set the rescript version 23 | - run: cat package.json | jq '.devDependencies.rescript = "<< parameters.rescript-version >>"' > package-new.json && mv package-new.json package.json 24 | - run: cat package.json 25 | - run: npm install 26 | - run: npm run build 27 | - run: npm run test 28 | - run: npm run integration-test 29 | - save_cache: 30 | key: v2-rescript-dependencies-{{ .Branch }}-{{ checksum "package-lock.json" }} 31 | paths: 32 | - ~/repo/node_modules 33 | 34 | workflows: 35 | version: 2 36 | build: 37 | jobs: 38 | # Contributions welcome for commented out versions, see README 39 | - build-and-test: 40 | matrix: 41 | parameters: 42 | rescript-version: 43 | # CI testing with version 9 not supported as the tests depend on glennsl/rescript-jest. 44 | # The lib itself works with the version 9 45 | - "10.1.4" 46 | - "11.0.0-rc.5" 47 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bsb.lock 2 | .bsbuild 3 | .DS_Store 4 | .idea 5 | .merlin 6 | .vscode 7 | **/*.bs.js 8 | /lib 9 | /node_modules 10 | /integration-test/bs/node_modules 11 | /integration-test/bs/lib/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at paul@darklang.com or ellen@darklang.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Dark Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tablecloth-rescript 2 | 3 | [![CircleCI](https://circleci.com/gh/darklang/tablecloth.svg?style=shield)](https://circleci.com/gh/darklang/tablecloth) 4 | [![Npm](https://badge.fury.io/js/tablecloth-rescript.svg)](https://www.npmjs.com/package/tablecloth-rescript) 5 | 6 | Tablecloth is a library that shims over various standard libraries so they have the same function and module names, which using idiomatic types and patterns in each language. 7 | 8 | This is the Rescript implementation, which uses Belt, pipe-first, and camelCase. 9 | 10 | **Tablecloth is alpha-quality software, and is pre-1.0. It is currently undergoing 11 | some significant shifts and some libraries listed below are not available yet. 12 | Caveat emptor.** 13 | 14 | Check out the [website](https://www.tablecloth.dev) for our interactive API 15 | documentation, or join the community in the [Tablecloth 16 | Discord](https://www.tablecloth.dev/discord-invite). 17 | 18 | ## Installation 19 | 20 | **Note: these instructions are for the upcoming new version of tablecloth** 21 | 22 | Install via npm by: 23 | 24 | `npm install tablecloth-rescript` 25 | 26 | Then add to your `bsconfig.json` file: 27 | 28 | `"bs-dependencies" : ["tablecloth-rescript"]` 29 | 30 | ## Usage 31 | 32 | The recommended way to use Tablecloth is with a top-level open at the beginning of a file. 33 | 34 | This will ensure that all the built-in modules are replaced. 35 | 36 | ``` 37 | open Tablecloth 38 | 39 | let () = 40 | String.toList("somestring") 41 | ->List.map(Char.toCode) 42 | ->List.map((x) => x+1)) 43 | ->List.filterMap(Char.fromCode) 44 | ->String.fromList 45 | ``` 46 | 47 | ## Supported versions 48 | 49 | Tablecloth supports Rescript 9 and 10. [Older versions of Tablecloth](https://www.npmjs.com/package/tablecloth-bucklescript) supported older versions of bs-platform. 50 | 51 | ### Development 52 | 53 | When developing Tablecloth, you can test it against different versions of 54 | rescript, using the following commands: 55 | 56 | - `TC_RESCRIPT_VERSION=10.0.0 make deps` 57 | 58 | ## Contributions 59 | 60 | The maintainers are warm and friendly, and the project abides by a [Code of Conduct](./CODE_OF_CONDUCT.md). 61 | 62 | There are many small tasks to be done - a small change to a single function can be extremely 63 | helpful. We also welcome new versions of tablecloth for other languages, or even for the same 64 | language but based on other libraries. 65 | 66 | Check out the [dedicated guide](./documentation/contributing.md) on contributing for more. 67 | 68 | ## Developing 69 | 70 | Please refer to the `package.json` for a complete list of supported actions. Here is 71 | a handful of useful, supported commands: 72 | 73 | ``` 74 | npm install 75 | npm run build 76 | npm run test 77 | npm run format 78 | ``` 79 | 80 | ## License 81 | 82 | Tablecloth-rescript uses the [MIT](./LICENSE) license. 83 | 84 | ## Authors 85 | 86 | Initially written by [Darklang](https://darklang.com). 87 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tablecloth-rescript", 3 | "version": "0.0.9", 4 | "sources": [ 5 | { 6 | "dir": "src", 7 | "public": [ 8 | "Tablecloth", 9 | "TableclothBool", 10 | "TableclothChar", 11 | "TableclothString", 12 | "TableclothInt", 13 | "TableclothFloat", 14 | "TableclothContainer", 15 | "TableclothArray", 16 | "TableclothList", 17 | "TableclothOption", 18 | "TableclothResult", 19 | "TableclothTuple2", 20 | "TableclothTuple3", 21 | "TableclothComparator", 22 | "TableclothSet", 23 | "TableclothMap", 24 | "TableclothFun" 25 | ], 26 | "subdirs": false 27 | }, 28 | { 29 | "dir": "test", 30 | "type": "dev", 31 | "subdirs": false 32 | } 33 | ], 34 | "package-specs": { 35 | "module": "commonjs", 36 | "in-source": true 37 | }, 38 | "suffix": ".bs.js", 39 | "bs-dependencies": [], 40 | "bs-dev-dependencies": [ 41 | "@glennsl/rescript-jest" 42 | ], 43 | "bsc-flags": [ 44 | "-bs-g", 45 | "-bs-super-errors", 46 | "-warn-error", 47 | "@A", 48 | "-w", 49 | "-4-9-30-40-41-42-44-102" 50 | ], 51 | "warnings": { 52 | "error": "+101" 53 | }, 54 | "refmt": 3 55 | } 56 | -------------------------------------------------------------------------------- /deploying.md: -------------------------------------------------------------------------------- 1 | To deploy to npm 2 | 3 | - npm publish 4 | -------------------------------------------------------------------------------- /documentation/contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Contributing" 3 | order: "4" 4 | --- 5 | 6 | Tablecloth is an ideal library to contribute to, even if you're new to Rescript. 7 | 8 | The maintainers are warm and friendly, and the project abides by a [Code of Conduct](../CODE_OF_CONDUCT.md). 9 | 10 | There are many small tasks to be done and even a small change to a single function's documentation is extremely helpful. 11 | 12 | Here are some ways to contribute: 13 | 14 | - Check out the issues marked [help wanted](https://github.com/darklang/tablecloth/labels/help%20wanted), especially those marked [good first issue](https://github.com/darklang/tablecloth/labels/good%20first%20issue) 15 | - Fix a typo or grammatical error 16 | - Add test cases 17 | - Add examples 18 | - Add documentation 19 | - Point out a way in which the library or any of its parts are confusing 20 | - Point out inconsistencies between different functions in the library 21 | - Report an edge-case or performance problem in one of the functions 22 | - Improve a function's documentation by discussing an edge-case 23 | - Check a function cannot throw exceptions (and add a note to the function documentation to that effect) 24 | - Suggest a new function or module by [creating an issue](https://github.com/darklang/tablecloth/issues/new). 25 | - Optimize a function 26 | 27 | If you'd like to contribute but don't know where to start [open an 28 | issue](https://github.com/darklang/tablecloth/issues/new) with your thoughts 29 | or stop by the *#tablecloth* channel in the [Darklang discord](https://darklang.com/discord-invite). 30 | 31 | ## Guiding principles 32 | 33 | ### Over-communicate 34 | 35 | Create an issue first! 36 | 37 | If you are planning on removing, changing or adding new features to `Tablecloth`, please open an issue first. 38 | 39 | ### Small pull requests 40 | 41 | Many small PR's are better than one big one. 42 | 43 | Don't save up small changes for one big PR if you can avoid it. 44 | 45 | Small PR's are less likely to cause conflicts, easier to review and take less effort. 46 | -------------------------------------------------------------------------------- /documentation/conventions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Conventions" 3 | order: "2" 4 | --- 5 | 6 | ## t is the main type of a module 7 | 8 | In Rescript it is a convention for the primary type of a module to be named `t`. 9 | 10 | This means that when you want to refer to a type without `open`ing a module you don't end up repeating yourself: 11 | 12 | ```rescript 13 | let food: String.string = /* ... */ 14 | 15 | /* compared to */ 16 | 17 | let email: String.t = /* ... */ 18 | ``` 19 | 20 | ## Function suffixes 21 | 22 | Some functions come in multiple flavors. 23 | 24 | Some of these flavors are so common they are distinguished by a really short suffix. 25 | 26 | ### ___2 is an alternative behaviour 27 | 28 | When a function could behave in slightly different ways, but we want to provide the functionality of both, one of the implementations gets a two stuck on the end. 29 | 30 | The best example of this is [`Float.atan2`](/api#Float.atan2) 31 | 32 | ### ___Unsafe means "could raise an exception" 33 | 34 | Some functions have 'unsafe' versions which instead of returning an [`Option`](/api#Option) or a [`Result`](/api#Result) could raise an exception. 35 | 36 | Sometimes this can be for performance, and sometimes you just need an escape hatch. 37 | 38 | See [`Option.get`](/api#Option.get) and [`Option.getUnsafe`](/api#Option.getUnsafe) 39 | 40 | -------------------------------------------------------------------------------- /documentation/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Installation" 3 | metaTitle: "Installation" 4 | metaDescription: "Installing Tablecloth" 5 | order: "0" 6 | --- 7 | 8 | ## Rescript 9 | 10 | Install via npm by 11 | 12 | ```sh 11 13 | npm install tablecloth-rescript --save 14 | ``` 15 | 16 | Then add to your `bsconfig.json` file: 17 | 18 | ```json 19 | "bs-dependencies" : ["tablecloth-rescript"] 20 | ``` 21 | -------------------------------------------------------------------------------- /documentation/usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Usage" 3 | order: "1" 4 | --- 5 | 6 | The recommended way to use Tablecloth is with a top-level open at the beginning of a file. 7 | 8 | This will ensure all the built-in modules are replaced. 9 | 10 | ```rescript 11 | open Tablecloth; 12 | 13 | String.toList("Tablecloth") 14 | ->List.filterMap(~f=character => 15 | Char.toCode(character)->Int.add(1)->Char.fromCode 16 | ) 17 | ->String.fromList 18 | ``` 19 | 20 | ## Automatic opening 21 | 22 | To avoid having to write `open Tablecloth` at the top of every file, you can pass a compiler flag to do this automatically for you. 23 | How this is configured depends on your build system. 24 | 25 | ### With Rescript 26 | 27 | In `bsconfig.json` edit the `bsc-flags` array to look like the following: 28 | 29 | ```json 30 | "bsc-flags": [ 31 | "-open", 32 | "Tablecloth", 33 | // ... 34 | ] 35 | ``` 36 | 37 | -------------------------------------------------------------------------------- /integration-test/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules -------------------------------------------------------------------------------- /integration-test/bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bucklescript-integration-test", 3 | "version": "0.1.0", 4 | "sources": [ 5 | "." 6 | ], 7 | "package-specs": { 8 | "module": "commonjs", 9 | "in-source": true 10 | }, 11 | "suffix": ".bs.js", 12 | "bs-dependencies": [ 13 | "tablecloth-rescript" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /integration-test/main.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | 3 | let () = { 4 | assert Array.equal(Array.initialize(5, ~f=_ => 0), [0, 0, 0, 0, 0], Int.equal) 5 | 6 | assert String.equal(Bool.toString(true), "true") 7 | 8 | assert Int.equal(Char.toCode('A'), 65) 9 | 10 | assert Float.equal(Float.add(1., 1.), 2.) 11 | 12 | assert Int.equal(Int.add(2, 2), 4) 13 | 14 | assert String.equal(String.fromList(list{'A', 'B', 'C'}), "ABC") 15 | 16 | assert List.equal(List.initialize(5, ~f=_ => 0), list{0, 0, 0, 0, 0}, Int.equal) 17 | 18 | assert Option.isSome(Some(1)) 19 | 20 | assert Result.isOk(Ok(1)) 21 | 22 | assert Tuple2.equal(Tuple2.make(1, 1), (1, 1), Int.equal, Int.equal) 23 | 24 | assert Tuple3.equal(Tuple3.make(1, 1, 1), (1, 1, 1), Int.equal, Int.equal, Int.equal) 25 | 26 | assert (Set.length(Set.String.empty) == 0) 27 | 28 | assert (Map.length(Map.String.empty) == 0) 29 | 30 | assert (Fun.constant(1, 2) == 1) 31 | 32 | print_endline("Success") 33 | } 34 | 35 | -------------------------------------------------------------------------------- /integration-test/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bucklescript-integration-test", 3 | "version": "0.1.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "bucklescript-integration-test", 9 | "version": "0.1.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "tablecloth-rescript": "../" 13 | }, 14 | "devDependencies": { 15 | "rescript": "9.1.4" 16 | } 17 | }, 18 | "..": { 19 | "version": "0.1.0", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@glennsl/bs-jest": "0.7.0", 23 | "rescript": "9.1.4" 24 | } 25 | }, 26 | "../rescript": { 27 | "extraneous": true 28 | }, 29 | "node_modules/rescript": { 30 | "version": "9.1.4", 31 | "resolved": "https://registry.npmjs.org/rescript/-/rescript-9.1.4.tgz", 32 | "integrity": "sha512-aXANK4IqecJzdnDpJUsU6pxMViCR5ogAxzuqS0mOr8TloMnzAjJFu63fjD6LCkWrKAhlMkFFzQvVQYaAaVkFXw==", 33 | "dev": true, 34 | "hasInstallScript": true, 35 | "bin": { 36 | "bsc": "bsc", 37 | "bsrefmt": "bsrefmt", 38 | "bstracing": "lib/bstracing", 39 | "rescript": "rescript" 40 | } 41 | }, 42 | "node_modules/tablecloth-rescript": { 43 | "resolved": "..", 44 | "link": true 45 | } 46 | }, 47 | "dependencies": { 48 | "rescript": { 49 | "version": "9.1.4", 50 | "resolved": "https://registry.npmjs.org/rescript/-/rescript-9.1.4.tgz", 51 | "integrity": "sha512-aXANK4IqecJzdnDpJUsU6pxMViCR5ogAxzuqS0mOr8TloMnzAjJFu63fjD6LCkWrKAhlMkFFzQvVQYaAaVkFXw==", 52 | "dev": true 53 | }, 54 | "tablecloth-rescript": { 55 | "version": "file:..", 56 | "requires": { 57 | "@glennsl/bs-jest": "0.7.0", 58 | "rescript": "9.1.4" 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /integration-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bucklescript-integration-test", 3 | "description": "Checks that Tablecloth's sub modules can be accessed", 4 | "repository": "https://github.com/darklang/tablecloth", 5 | "version": "0.1.0", 6 | "scripts": { 7 | "clean": "rescript clean", 8 | "build": "rescript build -with-deps", 9 | "watch": "rescript build -with-deps -w" 10 | }, 11 | "keywords": [ 12 | "Rescript" 13 | ], 14 | "author": "", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "rescript": "9.1.4" 18 | }, 19 | "dependencies": { 20 | "tablecloth-rescript": "../" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tablecloth-rescript", 3 | "description": "A portable standard library enhancement for Rescript.", 4 | "version": "0.1.0", 5 | "repository": "https://github.com/darklang/tablecloth", 6 | "private": false, 7 | "contributors": [ 8 | "Paul Biggar " 9 | ], 10 | "keywords": [ 11 | "Rescript", 12 | "BuckleScript", 13 | "ReasonML", 14 | "Utility" 15 | ], 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@glennsl/rescript-jest": "^0.10.0", 19 | "rescript": "11.0.0-rc.5" 20 | }, 21 | "scripts": { 22 | "clean": "rescript clean", 23 | "build": "rescript build -with-deps", 24 | "test": "jest", 25 | "integration-test": "cd integration-test && npm install && npm run build", 26 | "format": "echo todo", 27 | "build:watch": "rescript build -with-deps -w" 28 | }, 29 | "jest": { 30 | "testMatch": [ 31 | "/test/*Test.bs.js", 32 | "/test/*Test.js" 33 | ], 34 | "testPathIgnorePatterns": [ 35 | "/node_modules/" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Internal.res: -------------------------------------------------------------------------------- 1 | let toBeltComparator = ( 2 | type a id, 3 | module(M: TableclothComparator.S with type identity = id and type t = a), 4 | ): Belt.Id.comparable => 5 | module( 6 | { 7 | type t = M.t 8 | 9 | type identity = M.identity 10 | 11 | let cmp = Obj.magic(M.comparator) 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /src/Tablecloth.res: -------------------------------------------------------------------------------- 1 | @ocaml.doc(" Functions for working with boolean ([true] or [false]) values. ") 2 | module Bool = TableclothBool 3 | 4 | @ocaml.doc(" Functions for working with single characters. ") 5 | module Char = TableclothChar 6 | 7 | @ocaml.doc(" Functions for working with [\"strings\"] ") 8 | module String = TableclothString 9 | 10 | @ocaml.doc(" Fixed precision integers ") 11 | module Int = TableclothInt 12 | 13 | @ocaml.doc(" Functions for working with floating point numbers. ") 14 | module Float = TableclothFloat 15 | 16 | @ocaml.doc(" Interfaces for use with container types like {!Array} or {!List} ") 17 | module Container = TableclothContainer 18 | 19 | @ocaml.doc(" A fixed lenfth collection of values ") 20 | module Array = TableclothArray 21 | 22 | @ocaml.doc(" Arbitrary length, singly linked lists ") 23 | module List = TableclothList 24 | 25 | @ocaml.doc(" Functions for working with optional values. ") 26 | module Option = TableclothOption 27 | 28 | @ocaml.doc(" Functions for working with computations which may fail. ") 29 | module Result = TableclothResult 30 | 31 | @ocaml.doc(" Functions for manipulating tuples of length two ") 32 | module Tuple2 = TableclothTuple2 33 | 34 | @ocaml.doc(" Functions for manipulating tuples of length three ") 35 | module Tuple3 = TableclothTuple3 36 | 37 | module Comparator = TableclothComparator 38 | 39 | @ocaml.doc(" A collection of unique values ") 40 | module Set = TableclothSet 41 | 42 | @ocaml.doc(" A collection of key-value pairs ") 43 | module Map = TableclothMap 44 | 45 | @ocaml.doc(" Functions for working with functions. ") 46 | module Fun = TableclothFun 47 | -------------------------------------------------------------------------------- /src/TableclothArray.res: -------------------------------------------------------------------------------- 1 | type t<'a> = array<'a> 2 | 3 | let singleton = a => [a] 4 | 5 | let clone = t => Array.map(TableclothFun.identity, t) 6 | 7 | let length = t => Belt.Array.length(t) 8 | 9 | let isEmpty = a => length(a) == 0 10 | 11 | let initialize = (length, ~f) => Belt.Array.makeBy(length, a => f(a)) 12 | 13 | let range = (~from=0, to_) => Belt.Array.makeBy(to_ - from, i => i + from) 14 | 15 | let fromList = t => Belt.List.toArray(t) 16 | 17 | let toList: array<'a> => list<'a> = t => Belt.List.fromArray(t) 18 | 19 | let toIndexedList = (array: array<'a>): list<(int, 'a)> => 20 | snd( 21 | Belt.Array.reduceReverse(array, (length(array) - 1, list{}), ((i, acc), x) => ( 22 | i - 1, 23 | list{(i, x), ...acc}, 24 | )), 25 | ) 26 | 27 | let get = (t, k) => Belt.Array.getExn(t, k) 28 | 29 | let getAt = (t, ~index) => Belt.Array.get(t, index) 30 | 31 | let first = t => getAt(t, ~index=0) 32 | 33 | let last = t => getAt(t, ~index=Array.length(t) - 1) 34 | 35 | let set = (t, index, value) => t[index] = value 36 | 37 | let setAt = (t, ~index, ~value) => t[index] = value 38 | 39 | let filter = (t, ~f) => Belt.Array.keep(t, a => f(a)) 40 | 41 | let swap = (t, i, j) => { 42 | let temp = t[i] 43 | t[i] = t[j] 44 | t[j] = temp 45 | () 46 | } 47 | 48 | let fold = (t, ~initial, ~f) => Belt.Array.reduce(t, initial, (a, b) => f(a, b)) 49 | 50 | let foldRight = (t, ~initial, ~f) => Belt.Array.reduceReverse(t, initial, (a, b) => f(a, b)) 51 | 52 | let maximum = (t, ~compare) => 53 | fold(t, ~initial=None, ~f=(max, element) => 54 | switch max { 55 | | None => Some(element) 56 | | Some(current) => compare(element, current) > 0 ? Some(element) : max 57 | } 58 | ) 59 | 60 | let minimum = (t, ~compare) => 61 | fold(t, ~initial=None, ~f=(min, element) => 62 | switch min { 63 | | None => Some(element) 64 | | Some(current) => compare(element, current) < 0 ? Some(element) : min 65 | } 66 | ) 67 | 68 | let extent = (t, ~compare) => 69 | fold(t, ~initial=None, ~f=(range, element) => 70 | switch range { 71 | | None => Some(element, element) 72 | | Some(min, max) => 73 | Some(compare(element, min) < 0 ? element : min, compare(element, max) > 0 ? element : max) 74 | } 75 | ) 76 | 77 | let sum = (type a, t, module(M: TableclothContainer.Sum with type t = a)): a => 78 | Array.fold_left(M.add, M.zero, t) 79 | 80 | let map = (t, ~f) => Belt.Array.map(t, a => f(a)) 81 | 82 | let mapWithIndex = (t, ~f) => Belt.Array.mapWithIndex(t, (a, i) => f(a, i)) 83 | 84 | let map2 = (a, b, ~f: ('a, 'b) => 'c): array<'c> => Belt.Array.zipBy(a, b, f) 85 | 86 | let map3 = (as_, bs, cs: t<'c>, ~f) => { 87 | let minLength = Belt.Array.reduce([length(bs), length(cs)], length(as_), min) 88 | 89 | Belt.Array.makeBy(minLength, i => f(as_[i], bs[i], cs[i])) 90 | } 91 | 92 | let zip = (a, b) => map2(a, b, ~f=(a, b) => (a, b)) 93 | 94 | let flatMap = (t, ~f) => Belt.Array.concatMany(Belt.Array.map(t, a => f(a))) 95 | 96 | let sliding = (~step=1, a, ~size) => { 97 | let n = Array.length(a) 98 | if size > n { 99 | [] 100 | } else { 101 | initialize(1 + (n - size) / step, ~f=i => initialize(size, ~f=j => a[i * step + j])) 102 | } 103 | } 104 | 105 | let find = (t, ~f) => { 106 | let rec find_loop = (t, ~f, ~length, i) => 107 | if i >= length { 108 | None 109 | } else if f(t[i]) { 110 | Some(t[i]) 111 | } else { 112 | find_loop(t, ~f, ~length, i + 1) 113 | } 114 | 115 | find_loop(t, ~f, ~length=length(t), 0) 116 | } 117 | 118 | let findIndex = (array, ~f) => { 119 | let rec loop = index => 120 | if index >= length(array) { 121 | None 122 | } else if f(index, array[index]) { 123 | Some(index, array[index]) 124 | } else { 125 | loop(index + 1) 126 | } 127 | 128 | loop(0) 129 | } 130 | 131 | let any = (t, ~f) => Belt.Array.some(t, a => f(a)) 132 | 133 | let all = (t, ~f) => Belt.Array.every(t, a => f(a)) 134 | 135 | let includes = (t, v, ~equal) => any(t, ~f=a => equal(v, a)) 136 | 137 | let append = (a, a') => Belt.Array.concat(a, a') 138 | 139 | let flatten = (ars: array>) => Belt.Array.concatMany(ars) 140 | 141 | let intersperse = (t, ~sep) => 142 | Belt.Array.makeBy(max(0, length(t) * 2 - 1), i => 143 | if mod(i, 2) != 0 { 144 | sep 145 | } else { 146 | t[i / 2] 147 | } 148 | ) 149 | 150 | let slice = (~to_=?, array, ~from) => { 151 | let defaultTo = switch to_ { 152 | | None => length(array) 153 | | Some(i) => i 154 | } 155 | let sliceFrom = if from >= 0 { 156 | min(length(array), from) 157 | } else { 158 | max(0, min(length(array), length(array) + from)) 159 | } 160 | 161 | let sliceTo = if defaultTo >= 0 { 162 | min(length(array), defaultTo) 163 | } else { 164 | max(0, min(length(array), length(array) + defaultTo)) 165 | } 166 | 167 | if sliceFrom >= sliceTo { 168 | [] 169 | } else { 170 | Belt.Array.makeBy(sliceTo - sliceFrom, i => array[i + sliceFrom]) 171 | } 172 | } 173 | 174 | let count = (t, ~f) => fold(t, ~initial=0, ~f=(total, element) => total + (f(element) ? 1 : 0)) 175 | 176 | let chunksOf = (t, ~size) => sliding(t, ~step=size, ~size) 177 | 178 | let reverse = t => Belt.Array.reverseInPlace(t) 179 | 180 | let forEach = (t, ~f): unit => Belt.Array.forEach(t, a => f(a)) 181 | 182 | let forEachWithIndex = (t, ~f): unit => 183 | for i in 0 to length(t) - 1 { 184 | f(i, t[i]) 185 | } 186 | 187 | let partition = (t, ~f) => { 188 | let (left, right) = foldRight(t, ~initial=(list{}, list{}), ~f=((lefts, rights), element) => 189 | if f(element) { 190 | (list{element, ...lefts}, rights) 191 | } else { 192 | (lefts, list{element, ...rights}) 193 | } 194 | ) 195 | 196 | (fromList(left), fromList(right)) 197 | } 198 | 199 | let splitAt = (t, ~index) => (slice(t, ~from=0, ~to_=index), slice(t, ~from=index, ~to_=length(t))) 200 | 201 | let splitWhen = (t, ~f) => 202 | switch findIndex(t, ~f=(_, e) => f(e)) { 203 | | None => (t, []) 204 | | Some(index, _) => splitAt(t, ~index) 205 | } 206 | 207 | let unzip = t => (Array.init(length(t), i => fst(t[i])), Array.init(length(t), i => snd(t[i]))) 208 | 209 | let repeat = (element, ~length) => Array.init(max(length, 0), _ => element) 210 | 211 | let filterMap = (t, ~f) => { 212 | let result = fold(t, ~initial=list{}, ~f=(results, element) => 213 | switch f(element) { 214 | | None => results 215 | | Some(value) => list{value, ...results} 216 | } 217 | )->fromList 218 | 219 | reverse(result) 220 | result 221 | } 222 | 223 | let sort = (a, ~compare) => Array.sort((a, b) => compare(a, b), a) 224 | 225 | let values = t => { 226 | let result = fold(t, ~initial=list{}, ~f=(results, element) => 227 | switch element { 228 | | None => results 229 | | Some(value) => list{value, ...results} 230 | } 231 | )->fromList 232 | 233 | reverse(result) 234 | result 235 | } 236 | 237 | let join = (t, ~sep) => Js.Array.joinWith(sep, t) 238 | 239 | let groupBy = (t, comparator, ~f) => 240 | fold(t, ~initial=TableclothMap.empty(comparator), ~f=(map, element) => { 241 | let key = f(element) 242 | TableclothMap.update(map, ~key, ~f=x => 243 | switch x { 244 | | None => Some(list{element}) 245 | | Some(elements) => Some(list{element, ...elements}) 246 | } 247 | ) 248 | }) 249 | 250 | let equal = (a, b, equal) => 251 | if length(a) != length(b) { 252 | false 253 | } else if length(a) == 0 { 254 | true 255 | } else { 256 | let rec loop = index => 257 | if index == length(a) { 258 | true 259 | } else { 260 | equal(a[index], b[index]) && loop(index + 1) 261 | } 262 | 263 | loop(0) 264 | } 265 | 266 | let compare = (a, b, compare) => 267 | switch TableclothInt.compare(length(a), length(b)) { 268 | | 0 => 269 | if length(a) === 0 { 270 | 0 271 | } else { 272 | let rec loop = index => 273 | if index == length(a) { 274 | 0 275 | } else { 276 | switch compare(a[index], b[index]) { 277 | | 0 => loop(index + 1) 278 | | result => result 279 | } 280 | } 281 | 282 | loop(0) 283 | } 284 | | result => result 285 | } 286 | -------------------------------------------------------------------------------- /src/TableclothBool.res: -------------------------------------------------------------------------------- 1 | type t = bool 2 | 3 | let fromInt = i => 4 | switch i { 5 | | 0 => Some(false) 6 | | 1 => Some(true) 7 | | _ => None 8 | } 9 | 10 | let fromString = string => 11 | switch string { 12 | | "false" => Some(false) 13 | | "true" => Some(true) 14 | | _ => None 15 | } 16 | 17 | let xor = (a, b) => (a && !b) || (!a && b) 18 | 19 | let not = not 20 | 21 | let and_ = (a, b) => a && b 22 | 23 | @send external toString: bool => string = "toString" 24 | 25 | let toInt = t => t ? 1 : 0 26 | 27 | let compare = compare 28 | 29 | let equal = \"=" 30 | -------------------------------------------------------------------------------- /src/TableclothBool.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" Functions for working with boolean values. 4 | 5 | Booleans in Rescript are represented by the [true] and [false] literals. 6 | 7 | Whilst a bool isnt a variant, you will get warnings if you haven't 8 | exhaustively pattern match on them: 9 | 10 | {[ 11 | let bool = false 12 | let string = switch bool { 13 | | false => \"false\" 14 | } 15 | 16 | (* 17 | Warning number 8 18 | You forgot to handle a possible case here, for example: 19 | true 20 | *) 21 | ]} 22 | ") 23 | 24 | type t = bool 25 | 26 | @@ocaml.text(" {1 Create} ") 27 | 28 | @ocaml.doc(" Convert an {!Int} into a {!Bool}. 29 | 30 | {2 Examples} 31 | 32 | {[ 33 | Bool.fromInt(0) == Some(false) 34 | Bool.fromInt(1) == Some(true) 35 | Bool.fromInt(8) == None 36 | Bool.fromInt(-3) == None 37 | ]} 38 | ") 39 | let fromInt: int => option 40 | 41 | @ocaml.doc(" Convert a {!String} into a {!Bool}. 42 | 43 | {2 Examples} 44 | 45 | {[ 46 | Bool.fromString(\"true\") == Some(true) 47 | Bool.fromString(\"false\") == Some(false) 48 | Bool.fromString(\"True\") == None 49 | Bool.fromString(\"False\") == None 50 | Bool.fromString(\"0\") == None 51 | Bool.fromString(\"1\") == None 52 | Bool.fromString(\"Not even close\") == None 53 | ]} 54 | ") 55 | let fromString: string => option 56 | 57 | @@ocaml.text(" {1 Basic operations} ") 58 | 59 | @ocaml.doc(" The exclusive or operator. 60 | 61 | Returns [true] if {b exactly one} of its operands is [true]. 62 | 63 | {2 Examples} 64 | 65 | {[ 66 | Bool.xor(true, true) == false 67 | Bool.xor(true, false) == true 68 | Bool.xor(false, true) == true 69 | Bool.xor(false, false) == false 70 | ]} 71 | ") 72 | let xor: (bool, bool) => bool 73 | 74 | @ocaml.doc(" Negate a [bool]. 75 | 76 | {2 Examples} 77 | 78 | {[ 79 | Bool.not(false) == true 80 | Bool.not(true) == false 81 | ]} 82 | ") 83 | let not: t => bool 84 | 85 | @ocaml.doc(" The logical conjunction [AND] operator. 86 | 87 | Returns [true] if {b both} of its operands are [true]. 88 | If the 'left' operand evaluates to [false], the 'right' operand is not evaluated. 89 | 90 | {2 Examples} 91 | 92 | {[ 93 | Bool.and_(true, true) == true 94 | Bool.and_(true, false) == false 95 | Bool.and_(false, true) == false 96 | Bool.and_(false, false) == false 97 | ]} 98 | ") 99 | let and_: (bool, bool) => bool 100 | 101 | @@ocaml.text(" {1 Convert} ") 102 | 103 | @ocaml.doc(" Convert a [bool] to a {!String} 104 | 105 | {2 Examples} 106 | 107 | {[ 108 | Bool.toString(true) == \"true\" 109 | Bool.toString(false) == \"false\" 110 | ]} 111 | ") 112 | let toString: bool => string 113 | 114 | @ocaml.doc(" Convert a [bool] to an {!Int}. 115 | 116 | {2 Examples} 117 | 118 | {[ 119 | Bool.toInt(true) == 1 120 | Bool.toInt(false) == 0 121 | ]} 122 | ") 123 | let toInt: bool => int 124 | 125 | @@ocaml.text(" {1 Compare} ") 126 | 127 | @ocaml.doc(" Test for the equality of two [bool] values. 128 | 129 | {2 Examples} 130 | 131 | {[ 132 | Bool.equal(true, true) == true 133 | Bool.equal(false, false) == true 134 | Bool.equal(false, true) == false 135 | ]} 136 | ") 137 | let equal: (bool, bool) => bool 138 | 139 | @ocaml.doc(" Compare two [bool] values. 140 | 141 | {2 Examples} 142 | 143 | {[ 144 | Bool.compare(true, false) == 1 145 | Bool.compare(false, true) == -1 146 | Bool.compare(true, true) == 0 147 | Bool.compare(false, false) == 0 148 | ]} 149 | ") 150 | let compare: (bool, bool) => int 151 | 152 | -------------------------------------------------------------------------------- /src/TableclothChar.res: -------------------------------------------------------------------------------- 1 | /* 2 | This file has a `Tablecloth` prefix since it uses Stdlib.Char in its implementation. 3 | Without the prefix we would encounter circular reference compiler errors. 4 | */ 5 | 6 | type t = char 7 | 8 | include TableclothComparator.Make({ 9 | type t = t 10 | 11 | let compare = (a, b) => compare(a, b) 12 | }) 13 | 14 | let toCode = (c: char) => Char.code(c) 15 | 16 | let fromCode = (i): option => 17 | if 0 <= i && i <= 255 { 18 | Some(Char.chr(i)) 19 | } else { 20 | None 21 | } 22 | 23 | let toString = c => String.make(1, c) 24 | 25 | let fromString = (str): option => 26 | switch String.length(str) { 27 | | 1 => Some(String.get(str, 0)) 28 | | _ => None 29 | } 30 | 31 | let toDigit = char => 32 | switch char { 33 | | '0' .. '9' => Some(toCode(char) - toCode('0')) 34 | | _ => None 35 | } 36 | 37 | let toLowercase = char => 38 | switch char { 39 | | 'A' .. 'Z' => Char.chr(toCode('a') + (toCode(char) - toCode('A'))) 40 | | _ => char 41 | } 42 | 43 | let toUppercase = char => 44 | switch char { 45 | | 'a' .. 'z' => Char.chr(toCode('A') + (toCode(char) - toCode('a'))) 46 | | _ => char 47 | } 48 | 49 | let isLowercase = x => 50 | switch x { 51 | | 'a' .. 'z' => true 52 | | _ => false 53 | } 54 | 55 | let isUppercase = x => 56 | switch x { 57 | | 'A' .. 'Z' => true 58 | | _ => false 59 | } 60 | 61 | let isLetter = x => 62 | switch x { 63 | | 'a' .. 'z' | 'A' .. 'Z' => true 64 | | _ => false 65 | } 66 | 67 | let isDigit = x => 68 | switch x { 69 | | '0' .. '9' => true 70 | | _ => false 71 | } 72 | 73 | let isAlphanumeric = x => 74 | switch x { 75 | | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' => true 76 | | _ => false 77 | } 78 | 79 | let isPrintable = x => 80 | switch x { 81 | | ' ' .. '~' => true 82 | | _ => false 83 | } 84 | 85 | let isWhitespace = x => 86 | switch x { 87 | | '\t' | '\n' | '\011' | '\012' | '\r' | ' ' => true 88 | | _ => false 89 | } 90 | 91 | let equal = \"=" 92 | 93 | let compare = compare 94 | -------------------------------------------------------------------------------- /src/TableclothChar.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" Functions for working with single characters. 4 | 5 | Character literals are enclosed in ['a'] pair of single quotes. 6 | 7 | {[ 8 | let digit = '7' 9 | ]} 10 | 11 | The functions in this module work on ASCII characters (range 0-255) only, 12 | {b not Unicode}. 13 | 14 | Note that in Rescript source code you can include only the characters from 0-127 range. 15 | Full list of available characters is available {{: https://www.w3schools.com/charsets/ref_html_ascii.asp } here}. 16 | 17 | Characters from 128-255 range can still be handled, but only as codes. 18 | ") 19 | 20 | type t = char 21 | 22 | @@ocaml.text(" {1 Create} 23 | 24 | You can also create a {!Char} using single quotes: 25 | 26 | {[ 27 | let char = 'c' 28 | ]} 29 | ") 30 | 31 | @ocaml.doc(" Convert an ASCII {{: https://en.wikipedia.org/wiki/Code_point } code point } to a character. 32 | 33 | The full range of extended ASCII is from [0] to [255]. 34 | For numbers outside that range, you get [None]. 35 | 36 | {2 Examples} 37 | 38 | {[ 39 | Char.fromCode(65) == Some('A') 40 | Char.fromCode(66) == Some('B') 41 | Char.fromCode(3000) == None 42 | Char.fromCode(-1) == None 43 | ]} 44 | ") 45 | let fromCode: int => option 46 | 47 | @ocaml.doc(" Converts a string to character. 48 | 49 | Returns [None] when the [string] isn't of length one. 50 | 51 | {2 Examples} 52 | 53 | {[ 54 | Char.fromString(\"A\") == Some('A') 55 | Char.fromString(\" \") == Some(' ') 56 | Char.fromString(\"\") == None 57 | Char.fromString(\"abc\") == None 58 | Char.fromString(\" a\") == None 59 | ]} 60 | ") 61 | let fromString: string => option 62 | 63 | @ocaml.doc(" Detect lower case ASCII characters. 64 | 65 | {2 Examples} 66 | 67 | {[ 68 | Char.isLowercase('a') == true 69 | Char.isLowercase('b') == true 70 | Char.isLowercase('z') == true 71 | Char.isLowercase('0') == false 72 | Char.isLowercase('A') == false 73 | Char.isLowercase('-') == false 74 | ]} 75 | ") 76 | let isLowercase: char => bool 77 | 78 | @ocaml.doc(" Detect upper case ASCII characters. 79 | 80 | {2 Examples} 81 | 82 | {[ 83 | Char.isUppercase('A') == true 84 | Char.isUppercase('B') == true 85 | Char.isUppercase('Z') == true 86 | Char.isUppercase('h') == false 87 | Char.isUppercase('0') == false 88 | Char.isUppercase('-') == false 89 | ]} 90 | ") 91 | let isUppercase: char => bool 92 | 93 | @ocaml.doc(" Detect upper and lower case ASCII alphabetic characters. 94 | 95 | {2 Examples} 96 | 97 | {[ 98 | Char.isLetter('a') == true 99 | Char.isLetter('b') == true 100 | Char.isLetter('E') == true 101 | Char.isLetter('Y') == true 102 | Char.isLetter('0') == false 103 | Char.isLetter('-') == false 104 | ]} 105 | ") 106 | let isLetter: char => bool 107 | 108 | @ocaml.doc(" Detect when a character is a number. 109 | 110 | {2 Examples} 111 | 112 | {[ 113 | Char.isDigit('0') == true 114 | Char.isDigit('1') == true 115 | Char.isDigit('9') == true 116 | Char.isDigit('a') == false 117 | Char.isDigit('b') == false 118 | ]} 119 | ") 120 | let isDigit: char => bool 121 | 122 | @ocaml.doc(" Detect upper case, lower case and digit ASCII characters. 123 | 124 | {2 Examples} 125 | 126 | {[ 127 | Char.isAlphanumeric('a') == true 128 | Char.isAlphanumeric('b') == true 129 | Char.isAlphanumeric('E') == true 130 | Char.isAlphanumeric('Y') == true 131 | Char.isAlphanumeric('0') == true 132 | Char.isAlphanumeric('7') == true 133 | Char.isAlphanumeric('-') == false 134 | ]} 135 | ") 136 | let isAlphanumeric: char => bool 137 | 138 | @ocaml.doc(" Detect if a character is a {{: https://en.wikipedia.org/wiki/ASCII#Printable_characters } printable } character 139 | 140 | A Printable character has a {!Char.toCode} in the range 32 to 127, inclusive ([' '] to ['~']). 141 | 142 | {2 Examples} 143 | 144 | {[ 145 | Char.isPrintable('G') == true 146 | Char.isPrintable('%') == true 147 | Char.isPrintable(' ') == true 148 | Char.isPrintable('\t') == false 149 | Char.isPrintable('\007') == false 150 | ]} 151 | ") 152 | let isPrintable: char => bool 153 | 154 | @ocaml.doc(" Detect one of the following characters: 155 | - ['\t'] (tab) 156 | - ['\n'] (newline) 157 | - ['\011'] (vertical tab) 158 | - ['\012'] (form feed) 159 | - ['\r'] (carriage return) 160 | - [' '] (space) 161 | 162 | {2 Examples} 163 | 164 | {[ 165 | Char.isWhitespace('\t') == true 166 | Char.isWhitespace(' ') == true 167 | Char.isWhitespace('?') == false 168 | Char.isWhitespace('G') == false 169 | ]} 170 | ") 171 | let isWhitespace: char => bool 172 | 173 | @ocaml.doc(" Converts an ASCII character to lower case, preserving non alphabetic ASCII characters. 174 | 175 | {2 Examples} 176 | 177 | {[ 178 | Char.toLowercase('A') == 'a' 179 | Char.toLowercase('B') == 'b' 180 | Char.toLowercase('7') == '7' 181 | ]} 182 | ") 183 | let toLowercase: char => char 184 | 185 | @ocaml.doc(" Convert an ASCII character to upper case, preserving non alphabetic ASCII characters. 186 | 187 | {2 Examples} 188 | 189 | {[ 190 | Char.toUppercase('a') == 'A' 191 | Char.toUppercase('b') == 'B' 192 | Char.toUppercase('7') == '7' 193 | ]} 194 | ") 195 | let toUppercase: char => char 196 | 197 | @ocaml.doc(" Convert [char] to the corresponding ASCII {{: https://en.wikipedia.org/wiki/Code_point } code point}. 198 | 199 | {2 Examples} 200 | 201 | {[ 202 | Char.toCode('A') == 65 203 | Char.toCode('B') == 66 204 | ]} 205 | ") 206 | let toCode: char => int 207 | 208 | @ocaml.doc(" Convert a character into a [string]. 209 | 210 | {2 Examples} 211 | 212 | {[ 213 | Char.toString('A') == \"A\" 214 | Char.toString('{') == \"{\" 215 | Char.toString('7') == \"7\" 216 | ]} 217 | ") 218 | let toString: char => string 219 | 220 | @ocaml.doc(" Converts a digit character to its corresponding {!Int}. 221 | 222 | Returns [None] when the character isn't a digit. 223 | 224 | {2 Examples} 225 | 226 | {[ 227 | Char.toDigit(\"7\") == Some(7) 228 | Char.toDigit(\"0\") == Some(0) 229 | Char.toDigit(\"A\") == None 230 | Char.toDigit(\"\") == None 231 | ]} 232 | ") 233 | let toDigit: char => option 234 | 235 | @ocaml.doc(" Test two {!Char}s for equality ") 236 | let equal: (t, t) => bool 237 | 238 | @ocaml.doc(" Compare two {!Char}s ") 239 | let compare: (t, t) => int 240 | 241 | @ocaml.doc(" The unique identity for {!Comparator} ") 242 | type identity 243 | 244 | let comparator: TableclothComparator.t 245 | 246 | -------------------------------------------------------------------------------- /src/TableclothComparator.res: -------------------------------------------------------------------------------- 1 | type t<'a, 'identity> = Belt.Id.cmp<'a, 'identity> 2 | 3 | type comparator<'a, 'identity> = t<'a, 'identity> 4 | 5 | module type T = { 6 | type t 7 | 8 | let compare: (t, t) => int 9 | } 10 | 11 | module type S = { 12 | type t 13 | 14 | type identity 15 | 16 | let comparator: comparator 17 | } 18 | 19 | type s<'a, 'identity> = module(S with type identity = 'identity and type t = 'a) 20 | 21 | module Make = (M: T): (S with type t = M.t) => { 22 | module BeltComparator = Belt.Id.MakeComparableU({ 23 | type t = M.t 24 | 25 | let cmp = (. a, b) => M.compare(a, b) 26 | }) 27 | 28 | type t = M.t 29 | 30 | type identity = BeltComparator.identity 31 | 32 | let comparator = BeltComparator.cmp 33 | } 34 | -------------------------------------------------------------------------------- /src/TableclothComparator.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" Comparator provide a way for custom data structures to be used with {!Map}s and {!Set}s. 4 | 5 | Say we have a module [Book] which we want to be able to create a {!Set} of 6 | 7 | {[ 8 | module Book = { 9 | type t = { 10 | isbn: string, 11 | title: string, 12 | } 13 | 14 | let compare = (bookA, bookB) => String.compare(bookA.isbn, bookB.isbn) 15 | } 16 | ]} 17 | 18 | First we need to make our module conform to the {!S} signature. 19 | 20 | This can be done by using the {!Make} functor. 21 | 22 | {[ 23 | module Book = { 24 | type t = { 25 | isbn: string, 26 | title: string, 27 | } 28 | 29 | let compare = (bookA, bookB) => String.compare(bookA.isbn, bookB.isbn) 30 | 31 | include Comparator.Make({ 32 | type t = t 33 | 34 | let compare = compare 35 | }) 36 | } 37 | ]} 38 | 39 | Now we can create a Set of books: 40 | 41 | {[ 42 | Set.fromArray(module(Book), 43 | [ 44 | {isbn: \"9788460767923\", title: \"Moby Dick or The Whale\"} 45 | ]) 46 | ]} 47 | ") 48 | 49 | module type T = { 50 | @@ocaml.text(" T represents the input for the {!Make} functor. ") 51 | 52 | type t 53 | 54 | let compare: (t, t) => int 55 | } 56 | 57 | type t<'a, 'identity> 58 | 59 | @ocaml.doc(" This just is an alias for {!t}. ") 60 | type comparator<'a, 'identity> = t<'a, 'identity> 61 | 62 | module type S = { 63 | @@ocaml.text(" The output type of {!Make}. ") 64 | 65 | type t 66 | 67 | type identity 68 | 69 | let comparator: comparator 70 | } 71 | 72 | @ocaml.doc( 73 | " A type alias that is useful typing functions which accept first class modules like {!Map.empty} or {!Set.fromArray}. " 74 | ) 75 | type s<'a, 'identity> = module(S with type identity = 'identity and type t = 'a) 76 | 77 | @ocaml.doc(" Create a new comparator by providing a module which satisifies {!T}. 78 | 79 | {2 Examples} 80 | 81 | {[ 82 | module Book = { 83 | module T = { 84 | type t = { 85 | isbn: string, 86 | title: string, 87 | } 88 | let compare = (bookA, bookB) => String.compare(bookA.isbn, bookB.isbn) 89 | } 90 | 91 | include T 92 | include Comparator.Make(T) 93 | } 94 | 95 | let books = Set.empty(module(Book)) 96 | ]} 97 | ") 98 | module Make: (M: T) => (S with type t := M.t) 99 | -------------------------------------------------------------------------------- /src/TableclothContainer.res: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" This module contains module signatures which are used in functions which 2 | accept first class modules. 3 | ") 4 | 5 | module type Sum = { 6 | @@ocaml.text(" Modules which conform to this signature can be used with functions like 7 | {!Array.sum} or {!List.sum}. 8 | ") 9 | 10 | type t 11 | 12 | let zero: t 13 | 14 | let add: (t, t) => t 15 | } 16 | -------------------------------------------------------------------------------- /src/TableclothFloat.res: -------------------------------------------------------------------------------- 1 | type t = float 2 | 3 | let fromInt = t => Js.Int.toFloat(t) 4 | 5 | let fromString = string => 6 | switch String.lowercase_ascii(string) { 7 | | "nan" => Some(Js.Float._NaN) 8 | | _ => 9 | switch Js.Float.fromString(string) { 10 | | result if Js.Float.isNaN(result) => None 11 | | result => Some(result) 12 | } 13 | } 14 | 15 | let add = \"+." 16 | 17 | let subtract = \"-." 18 | 19 | let multiply = \"*." 20 | 21 | let divide = (n, ~by) => n /. by 22 | 23 | let power = (~base, ~exponent) => Js.Math.pow_float(~base, ~exp=exponent) 24 | 25 | let negate = \"~-." 26 | 27 | let absolute = t => Js.Math.abs_float(t) 28 | 29 | let clamp = (n, ~lower, ~upper) => 30 | if upper < lower { 31 | raise( 32 | Invalid_argument( 33 | "~lower:" ++ 34 | (Js.Float.toString(lower) ++ 35 | (" must be less than or equal to ~upper:" ++ Js.Float.toString(upper))), 36 | ), 37 | ) 38 | } else if Js.Float.isNaN(lower) || (Js.Float.isNaN(upper) || Js.Float.isNaN(n)) { 39 | nan 40 | } else { 41 | max(lower, min(upper, n)) 42 | } 43 | 44 | let inRange = (n, ~lower, ~upper) => 45 | if upper < lower { 46 | raise( 47 | Invalid_argument( 48 | "~lower:" ++ 49 | (Js.Float.toString(lower) ++ 50 | (" must be less than or equal to ~upper:" ++ Js.Float.toString(upper))), 51 | ), 52 | ) 53 | } else { 54 | n >= lower && n < upper 55 | } 56 | 57 | let squareRoot = sqrt 58 | 59 | let log = (n, ~base) => Js.Math.log(n) /. Js.Math.log(base) 60 | 61 | let zero = 0.0 62 | 63 | let one = 1.0 64 | 65 | let nan = Js.Float._NaN 66 | 67 | let infinity = infinity 68 | 69 | let negativeInfinity = neg_infinity 70 | 71 | let e = Js.Math._E 72 | 73 | let pi = Js.Math._PI 74 | 75 | let epsilon = epsilon_float 76 | 77 | @scope("Number") @val external largestValue: t = "MAX_VALUE" 78 | 79 | @scope("Number") @val external smallestValue: t = "MIN_VALUE" 80 | 81 | @scope("Number") @val external maximumSafeInteger: t = "MAX_SAFE_INTEGER" 82 | 83 | @scope("Number") @val external minimumSafeInteger: t = "MIN_SAFE_INTEGER" 84 | 85 | let isNaN = t => Js.Float.isNaN(t) 86 | 87 | let isFinite = t => Js.Float.isFinite(t) 88 | 89 | let isInfinite = n => !Js.Float.isFinite(n) && !isNaN(n) 90 | 91 | @scope("Number") @val external isInteger: t => bool = "isInteger" 92 | 93 | @scope("Number") @val external isSafeInteger: t => bool = "isSafeInteger" 94 | 95 | let maximum = (x, y) => 96 | if isNaN(x) || isNaN(y) { 97 | nan 98 | } else if y > x { 99 | y 100 | } else { 101 | x 102 | } 103 | 104 | let minimum = (x, y) => 105 | if isNaN(x) || isNaN(y) { 106 | nan 107 | } else if y < x { 108 | y 109 | } else { 110 | x 111 | } 112 | 113 | let hypotenuse = (a, b) => Js.Math.hypot(a, b) 114 | 115 | type radians = float 116 | 117 | let degrees = n => n *. (pi /. 180.0) 118 | 119 | external radians: float => float = "%identity" 120 | 121 | let turns = n => n *. 2. *. pi 122 | 123 | let cos = t => Js.Math.cos(t) 124 | 125 | let acos = t => Js.Math.acos(t) 126 | 127 | let sin = t => Js.Math.sin(t) 128 | 129 | let asin = t => Js.Math.asin(t) 130 | 131 | let tan = t => Js.Math.tan(t) 132 | 133 | let atan = t => Js.Math.atan(t) 134 | 135 | let atan2 = (~y, ~x) => Js.Math.atan2(~y, ~x, ()) 136 | 137 | type direction = [ 138 | | #Zero 139 | | #AwayFromZero 140 | | #Up 141 | | #Down 142 | | #Closest([#Zero | #AwayFromZero | #Up | #Down | #ToEven]) 143 | ] 144 | 145 | let round = (~direction=#Closest(#Up), n) => 146 | switch direction { 147 | | #Up => Js.Math.ceil_float(n) 148 | | #Down => Js.Math.floor_float(n) 149 | | #Zero => Js.Math.trunc(n) 150 | | #AwayFromZero => 151 | if n > 0. { 152 | Js.Math.ceil_float(n) 153 | } else { 154 | Js.Math.floor_float(n) 155 | } 156 | | #Closest(#Zero) => 157 | if n > 0. { 158 | Js.Math.ceil_float(n -. 0.5) 159 | } else { 160 | Js.Math.floor_float(n +. 0.5) 161 | } 162 | | #Closest(#AwayFromZero) => 163 | if n > 0. { 164 | Js.Math.floor_float(n +. 0.5) 165 | } else { 166 | Js.Math.ceil_float(n -. 0.5) 167 | } 168 | | #Closest(#Down) => Js.Math.ceil_float(n -. 0.5) 169 | | #Closest(#Up) => Js.Math.round(n) 170 | | #Closest(#ToEven) => 171 | let roundNearestLowerBound = -.(2. ** 52.) 172 | let roundNearestUpperBound = 2. ** 52. 173 | if n <= roundNearestLowerBound || n >= roundNearestUpperBound { 174 | n +. 0. 175 | } else { 176 | let floor = floor(n) 177 | let ceil_or_succ = floor +. 1. 178 | let diff_floor = n -. floor 179 | let diff_ceil = ceil_or_succ -. n 180 | if diff_floor < diff_ceil { 181 | floor 182 | } else if diff_floor > diff_ceil { 183 | ceil_or_succ 184 | } else if mod_float(floor, 2.) == 0. { 185 | floor 186 | } else { 187 | ceil_or_succ 188 | } 189 | } 190 | } 191 | 192 | let floor = t => Js.Math.floor_float(t) 193 | 194 | let ceiling = t => Js.Math.ceil_float(t) 195 | 196 | let truncate = t => Js.Math.trunc(t) 197 | 198 | let fromPolar = ((r, theta)) => (r *. cos(theta), r *. sin(theta)) 199 | 200 | let toPolar = ((x, y)) => (hypotenuse(x, y), atan2(~x, ~y)) 201 | 202 | let toInt = f => 203 | if Js.Float.isFinite(f) { 204 | Some(Js.Math.unsafe_trunc(f)) 205 | } else { 206 | None 207 | } 208 | 209 | let toString = t => Js.Float.toString(t) 210 | 211 | let equal = \"=" 212 | 213 | let compare = (a, b) => compare(a, b) 214 | -------------------------------------------------------------------------------- /src/TableclothFun.res: -------------------------------------------------------------------------------- 1 | external identity: 'a => 'a = "%identity" 2 | 3 | external ignore: _ => unit = "%ignore" 4 | 5 | let constant = (a, _) => a 6 | 7 | let sequence = (_, b) => b 8 | 9 | let flip = (f, x, y) => f(y, x) 10 | 11 | let negate = (f, t) => !f(t) 12 | 13 | let apply = (f, a) => f(a) 14 | 15 | let compose = (a, g, f) => f(g(a)) 16 | 17 | let composeRight = (a, g, f) => g(f(a)) 18 | 19 | let tap = (a, ~f) => { 20 | f(a) 21 | a 22 | } 23 | 24 | let rec times = (n, ~f) => 25 | if n <= 0 { 26 | () 27 | } else { 28 | f() 29 | times(n - 1, ~f) 30 | } 31 | 32 | let forever = f => 33 | try { 34 | while true { 35 | f() 36 | } 37 | failwith("[while true] managed to return, you are in trouble now.") 38 | } catch { 39 | | exn => exn 40 | } 41 | 42 | let curry = (f: (('a, 'b)) => 'c, a, b): 'c => f((a, b)) 43 | 44 | let uncurry = (f: ('a, 'b) => 'c, (a, b): ('a, 'b)): 'c => f(a, b) 45 | 46 | let curry3 = (f, a, b, c) => f((a, b, c)) 47 | 48 | let uncurry3 = (f, (a, b, c)) => f(a, b, c) 49 | -------------------------------------------------------------------------------- /src/TableclothFun.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" Functions for working with functions. 4 | 5 | While the functions in this module can often make code more concise, this 6 | often imposes a readability burden on future readers. 7 | ") 8 | 9 | @ocaml.doc(" Given a value, returns exactly the same value. This may seem pointless at first glance but it can often be useful when an api offers you more control than you actually need. 10 | 11 | Perhaps you want to create an array of integers 12 | 13 | {[ 14 | Array.initialize(6, ~f=Fun.identity) == [0, 1, 2, 3, 4, 5] 15 | ]} 16 | 17 | (In this particular case you probably want to use {!Array.range}.) 18 | 19 | Or maybe you need to register a callback, but dont want to do anything: 20 | 21 | {[ 22 | let httpMiddleware = HttpLibrary.createMiddleWare( 23 | ~onEventYouDoCareAbout=transformAndReturn, 24 | ~onEventYouDontCareAbout=Fun.identity, 25 | ) 26 | ]} 27 | ") 28 | external identity: 'a => 'a = "%identity" 29 | 30 | @ocaml.doc(" Discards the value it is given and returns [()] 31 | 32 | This is primarily useful when working with imperative side-effecting code 33 | or to avoid [unused value] compiler warnings when you really meant it, 34 | and haven't just made a mistake. 35 | 36 | {2 Examples} 37 | 38 | {[ 39 | (* Pretend we have a module with the following signature: 40 | module type PretendMutableQueue = { 41 | type t<'a> 42 | 43 | (** Adds an element to the queue, returning the new length of the queue *) 44 | let pushReturningLength: (t<'a>, 'a) => int 45 | } 46 | *) 47 | 48 | let addArrayToQueue = (queue, array) => 49 | Array.forEach(array, ~f=element => 50 | PretendMutableQueue.pushReturningLength(queue, element) 51 | )->Fun.ignore 52 | ]} 53 | ") 54 | external ignore: _ => unit = "%ignore" 55 | 56 | @ocaml.doc(" Create a function that {b always} returns the same value. 57 | 58 | Useful with functions like {!List.map} or {!Array.initialize}. 59 | 60 | {2 Examples} 61 | 62 | {[ 63 | Array.map([1, 2, 3, 4, 5], ~f=Fun.constant(0)) == [0, 0, 0, 0, 0] 64 | Array.initialize(6, ~f=Fun.constant(0)) == [0, 0, 0, 0, 0, 0] 65 | ]} 66 | ") 67 | let constant: ('a, 'b) => 'a 68 | 69 | @ocaml.doc(" A function which always returns its second argument. ") 70 | let sequence: ('a, 'b) => 'b 71 | 72 | @ocaml.doc(" Reverses the argument order of a function. 73 | 74 | For any arguments [x] and [y], [flip(f)(x, y)] is the same as [f(y, x)]. 75 | 76 | Perhaps you want to [fold] something, but the arguments of a function you 77 | already have access to are in the wrong order. 78 | ") 79 | let flip: (('a, 'b) => 'c, 'b, 'a) => 'c 80 | 81 | @ocaml.doc(" Negate a function. 82 | 83 | This can be useful in combination with {!List.filter} / {!Array.filter} or {!List.find} / {!Array.find}. 84 | 85 | {2 Examples} 86 | 87 | {[ 88 | let isLessThanTwelve = Fun.negate(n => n >= 12) 89 | isLessThanTwelve(12) == false 90 | ]} 91 | ") 92 | let negate: ('a => bool, 'a) => bool 93 | 94 | @ocaml.doc(" Calls function [f] with an argument [x]. 95 | 96 | [apply(f, x)] is exactly the same as [f(x)]. 97 | 98 | Maybe you want to apply a function to a [switch] expression? That sort of thing. 99 | ") 100 | let apply: ('a => 'b, 'a) => 'b 101 | 102 | @ocaml.doc(" Function composition, passing result from left to right. 103 | 104 | This is usefull in cases when you want to make multiple transformations 105 | during a [map] operation. 106 | 107 | {2 Examples} 108 | 109 | {[ 110 | let numbers = [1, 2, 3, 4, 5, 6, 7] 111 | 112 | let multiplied = Array.map(numbers, ~f=Fun.compose(Int.multiply(5), Int.toString)) 113 | 114 | multiplied == [\"5\", \"10\", \"15\", \"20\", \"25\", \"30\", \"35\"] 115 | ]} 116 | ") 117 | let compose: ('a, 'a => 'b, 'b => 'c) => 'c 118 | 119 | @ocaml.doc(" Function composition, passing result from right to left. 120 | 121 | Same as [!compose], but function application order is reversed. 122 | 123 | This is usefull in cases when you want to make multiple transformations 124 | during a [map] operation. 125 | 126 | {2 Examples} 127 | 128 | {[ 129 | let numbers = [1, 2, 3, 4, 5, 6, 7] 130 | 131 | let a = (b) => b -> Fun.compose(Int.toString, Int.multiply(5)) 132 | 133 | multiplied == [\"5\", \"10\", \"15\", \"20\", \"25\", \"30\", \"35\"] 134 | ]} 135 | ") 136 | let composeRight: ('a, 'b => 'c, 'a => 'b) => 'c 137 | 138 | @ocaml.doc(" Useful for performing some side affect in {!Fun.pipe}-lined code. 139 | 140 | Most commonly used to log a value in the middle of a pipeline of function calls. 141 | 142 | {2 Examples} 143 | 144 | {[ 145 | let sanitize = (input: string): option => 146 | input 147 | ->String.trim 148 | ->Fun.tap(~f=Js.log) 149 | ->Int.fromString 150 | 151 | Array.filter([1, 3, 2, 5, 4], ~f=Int.isEven) 152 | ->Fun.tap(~f=numbers => numbers[0] = 0) 153 | ->Fun.tap(~f=Array.reverse) == [4, 0] 154 | ]} 155 | ") 156 | let tap: ('a, ~f: 'a => unit) => 'a 157 | 158 | @ocaml.doc(" Runs the provided function, forever. 159 | 160 | If an exception is thrown, returns the exception. 161 | ") 162 | let forever: (unit => unit) => exn 163 | 164 | @ocaml.doc(" Runs a function repeatedly. 165 | 166 | {2 Examples} 167 | 168 | {[ 169 | let count = ref(0) 170 | Fun.times(10, ~f=() => count.contents = count.contents + 1) 171 | count.contents == 10 172 | ]} 173 | ") 174 | let times: (int, ~f: unit => unit) => unit 175 | 176 | @ocaml.doc(" Takes a function [f] which takes a single argument of a tuple [('a, 'b)] and returns a function which takes two arguments that can be partially applied. 177 | 178 | {2 Examples} 179 | 180 | {[ 181 | let squareArea = ((width, height)) => width * height 182 | let curriedArea: (int, int) => int = Fun.curry(squareArea) 183 | let sizes = [3, 4, 5] 184 | Array.map(sizes, ~f=curriedArea(4)) == [12, 16, 20] 185 | ]} 186 | ") 187 | let curry: ((('a, 'b)) => 'c, 'a, 'b) => 'c 188 | 189 | @ocaml.doc(" Takes a function which takes two arguments and returns a function which takes a single argument of a tuple. 190 | 191 | {2 Examples} 192 | 193 | {[ 194 | let sum = (a: int, b: int): int => a + b 195 | let uncurriedSum: ((int, int)) => int = Fun.uncurry(sum) 196 | uncurriedSum((3, 4)) == 7 197 | ]} 198 | ") 199 | let uncurry: (('a, 'b) => 'c, ('a, 'b)) => 'c 200 | 201 | @ocaml.doc(" Like {!curry} but for a {!Tuple3}. ") 202 | let curry3: ((('a, 'b, 'c)) => 'd, 'a, 'b, 'c) => 'd 203 | 204 | @ocaml.doc(" Like {!uncurry} but for a {!Tuple3}. ") 205 | let uncurry3: (('a, 'b, 'c) => 'd, ('a, 'b, 'c)) => 'd 206 | -------------------------------------------------------------------------------- /src/TableclothInt.res: -------------------------------------------------------------------------------- 1 | type t = int 2 | 3 | include TableclothComparator.Make({ 4 | type t = t 5 | 6 | let compare = (a, b) => compare(a, b) 7 | }) 8 | 9 | let minimumValue = Js.Int.min 10 | 11 | let maximumValue = Js.Int.max 12 | 13 | let zero = 0 14 | 15 | let one = 1 16 | 17 | let fromString = s => 18 | switch int_of_string(s) { 19 | | i => Some(i) 20 | | exception Failure(_) => None 21 | } 22 | 23 | let add = \"+" 24 | 25 | let subtract = \"-" 26 | 27 | let multiply = \"*" 28 | 29 | let divide = (n, ~by) => n / by 30 | 31 | let divideFloat = (n, ~by) => Js.Int.toFloat(n) /. Js.Int.toFloat(by) 32 | 33 | let power = (~base, ~exponent) => { 34 | let result = Js.Math.pow_float(~base=Js.Int.toFloat(base), ~exp=Js.Int.toFloat(exponent)) 35 | 36 | let result = if result > TableclothFloat.maximumSafeInteger { 37 | TableclothFloat.maximumSafeInteger 38 | } else if result < TableclothFloat.minimumSafeInteger { 39 | TableclothFloat.minimumSafeInteger 40 | } else { 41 | result 42 | } 43 | 44 | Js.Math.unsafe_trunc(result) 45 | } 46 | 47 | let negate = \"~-" 48 | 49 | let remainder = (n, ~by) => mod(n, by) 50 | 51 | let modulo = (n, ~by) => 52 | mod( 53 | if n <= 0 { 54 | abs(n) * 2 55 | } else { 56 | n 57 | }, 58 | by, 59 | ) 60 | 61 | let maximum = (a, b) => Js.Math.max_int(a, b) 62 | 63 | let minimum = (a, b) => Js.Math.min_int(a, b) 64 | 65 | let absolute = abs 66 | 67 | let isEven = n => mod(n, 2) == 0 68 | 69 | let isOdd = n => mod(n, 2) != 0 70 | 71 | let clamp = (n, ~lower, ~upper) => 72 | if upper < lower { 73 | raise(Invalid_argument("~lower must be less than or equal to ~upper")) 74 | } else { 75 | max(lower, min(upper, n)) 76 | } 77 | 78 | let inRange = (n, ~lower, ~upper) => 79 | if upper < lower { 80 | raise(Invalid_argument("~lower must be less than or equal to ~upper")) 81 | } else { 82 | n >= lower && n < upper 83 | } 84 | 85 | let toFloat = t => Js.Int.toFloat(t) 86 | 87 | let toString = t => Js.Int.toString(t) 88 | 89 | let equal = \"=" 90 | 91 | let compare = compare 92 | -------------------------------------------------------------------------------- /src/TableclothInt.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" An [int] is a whole number. 4 | 5 | Rescript's has a 32-bit {{: https://en.wikipedia.org/wiki/Signed_number_representations } signed } {{: https://en.wikipedia.org/wiki/Integer } integer}. 6 | Largegest [int] value is 2^31 - 1 === [2_147_483_647], and smallest is -2^31 - 1 === [-2_147_483_647] 7 | 8 | [int]s are subject to {{: https://en.wikipedia.org/wiki/Integer_overflow } overflow }, meaning that [Int.maximumValue + 1 == Int.minimumValue]. 9 | 10 | If you work with integers larger than {!minimumValue} and smaller than {!maximumValue} you can use the {!Int} module. 11 | If you need to work with larger numbers, concider using {!Float} since they are signed 64-bit [float]s and limited by 12 | [1.79E+308], or 1.7976931348623157 * 10^308 at the upper end and [5E-324] at the lower. 13 | 14 | Valid syntax for [int]s includes: 15 | {[ 16 | 0 17 | 42 18 | 9000 19 | 1_000_000 20 | 1_000_000 21 | 0xFF // 255 in hexadecimal 22 | 0x000A // 10 in hexadecimal 23 | ]} 24 | 25 | {e Historical Note: } The name [int] comes from the term {{: https://en.wikipedia.org/wiki/Integer } integer}. It appears 26 | that the [int] abbreviation was introduced in the programming language ALGOL 68. 27 | 28 | Today, almost all programming languages use this abbreviation. 29 | ") 30 | 31 | type t = int 32 | 33 | @@ocaml.text(" {1 Constants } ") 34 | 35 | @ocaml.doc(" The literal [0] as a named value. ") 36 | let zero: t 37 | 38 | @ocaml.doc(" The literal [1] as a named value. ") 39 | let one: t 40 | 41 | @ocaml.doc(" The maximum representable [int] on the current platform. ") 42 | let maximumValue: t 43 | 44 | @ocaml.doc(" The minimum representable [int] on the current platform. ") 45 | let minimumValue: t 46 | 47 | @@ocaml.text(" {1 Create} ") 48 | 49 | @ocaml.doc(" Attempt to parse a [string] into a [int]. 50 | 51 | {2 Examples} 52 | 53 | {[ 54 | Int.fromString(\"0\") == Some(0) 55 | Int.fromString(\"42\") == Some(42) 56 | Int.fromString(\"-3\") == Some(-3) 57 | Int.fromString(\"123_456\") == Some(123_456) 58 | Int.fromString(\"0xFF\") == Some(255) 59 | Int.fromString(\"0x00A\") == Some(10) 60 | Int.fromString(\"Infinity\") == None 61 | Int.fromString(\"NaN\") == None 62 | ]} 63 | ") 64 | let fromString: string => option 65 | 66 | @@ocaml.text(" {1 Operators} ") 67 | 68 | @ocaml.doc(" Add two {!Int} numbers. 69 | 70 | You {e cannot } add an [int] and a [float] directly though. 71 | 72 | See {!Float.add} for why, and how to overcome this limitation. 73 | 74 | {2 Examples} 75 | 76 | {[ 77 | Int.add(3002, 4004) == 7006 78 | ]} 79 | ") 80 | let add: (t, t) => t 81 | 82 | @ocaml.doc(" Subtract numbers. 83 | 84 | {2 Examples} 85 | 86 | {[ 87 | Int.subtract(4, 3) == 1 88 | ]} 89 | ") 90 | let subtract: (t, t) => t 91 | 92 | @ocaml.doc(" Multiply [int]s. 93 | 94 | {2 Examples} 95 | 96 | {[ 97 | Int.multiply(2, 7) == 14 98 | ]} 99 | ") 100 | let multiply: (t, t) => t 101 | 102 | @ocaml.doc(" Integer division. 103 | 104 | Notice that the remainder is discarded. 105 | 106 | {3 Exceptions} 107 | 108 | Throws [Division_by_zero] when the divisor is [0]. 109 | 110 | {2 Examples} 111 | 112 | {[ 113 | Int.divide(3, ~by=2) == 1 114 | ]} 115 | ") 116 | let divide: (t, ~by: t) => t 117 | 118 | @ocaml.doc(" Floating point division 119 | 120 | {2 Examples} 121 | 122 | {[ 123 | Int.divideFloat(3, ~by=2) == 1.5 124 | Int.divideFloat(27, ~by=5) == 5.25 125 | Int.divideFloat(8, ~by=4) == 2.0 126 | ]} 127 | ") 128 | let divideFloat: (t, ~by: t) => float 129 | 130 | @ocaml.doc(" Exponentiation, takes the base first, then the exponent. 131 | 132 | {2 Examples} 133 | 134 | {[ 135 | Int.power(~base=7, ~exponent=3) == 343 136 | ]} 137 | ") 138 | let power: (~base: t, ~exponent: t) => t 139 | 140 | @ocaml.doc(" Flips the 'sign' of an integer so that positive integers become negative and negative integers become positive. Zero stays as it is. 141 | 142 | {2 Examples} 143 | 144 | {[ 145 | Int.negate(8) == -8 146 | Int.negate(-7) == 7 147 | Int.negate(0) == 0 148 | ]} 149 | ") 150 | let negate: t => t 151 | 152 | @ocaml.doc(" Get the {{: https://en.wikipedia.org/wiki/Absolute_value } absolute value } of a number. 153 | 154 | {2 Examples} 155 | 156 | {[ 157 | Int.absolute(8) == 8 158 | Int.absolute(-7) == 7 159 | Int.absolute(0) == 0 160 | ]} 161 | ") 162 | let absolute: t => t 163 | 164 | @ocaml.doc(" Perform {{: https://en.wikipedia.org/wiki/Modular_arithmetic } modular arithmetic }. 165 | 166 | {b Note:} {!modulo} is not [%] JS operator. If you want [%], use {!remainder} 167 | 168 | If you intend to use [modulo] to detect even and odd numbers consider using {!Int.isEven} or {!Int.isOdd}. 169 | 170 | The [modulo] function works in the typical mathematical way when you run into negative numbers 171 | 172 | Use {!Int.remainder} for a different treatment of negative numbers. 173 | 174 | {2 Examples} 175 | 176 | {[ 177 | Int.modulo(-4, ~by=3) == 2 178 | Int.modulo(-3, ~by=3) == 0 179 | Int.modulo(-2, ~by=3) = 1 180 | Int.modulo(-1, ~by=3) == 2 181 | Int.modulo(0, ~by=3) == 0 182 | Int.modulo(1, ~by=3) == 1 183 | Int.modulo(2, ~by=3) == 2 184 | Int.modulo(3, ~by=3) == 0 185 | Int.modulo(4, ~by=3) == 1 186 | ]} 187 | ") 188 | let modulo: (t, ~by: t) => t 189 | 190 | @ocaml.doc(" Get the remainder after division. Works the same as [%] JS operator 191 | 192 | Use {!Int.modulo} for a different treatment of negative numbers. 193 | 194 | The sign of the result is the same as the sign 195 | of the dividend ([~by]) while with a {!modulo} the sign 196 | of the result is the same as the divisor ([t]). 197 | 198 | {2 Examples} 199 | 200 | {[ 201 | Array.map([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5], ~f=Int.remainder(~by=4)) == 202 | [-1, 0, -3, -2, -1, 0, 1, 2, 3, 0, 1] 203 | ]} 204 | ") 205 | let remainder: (t, ~by: t) => t 206 | 207 | @ocaml.doc(" Returns the larger of two [int]s. 208 | 209 | {2 Examples} 210 | 211 | {[ 212 | Int.maximum(7, 9) == 9 213 | Int.maximum(-4, -1) == -1 214 | ]} 215 | ") 216 | let maximum: (t, t) => t 217 | 218 | @ocaml.doc(" Returns the smaller of two [int]s. 219 | 220 | {2 Examples} 221 | 222 | {[ 223 | Int.minimum(7, 9) == 7 224 | Int.minimum(-4, -1) == -4 225 | ]} 226 | ") 227 | let minimum: (t, t) => t 228 | 229 | @@ocaml.text(" {1 Query} ") 230 | 231 | @ocaml.doc(" Check if an [int] is even. 232 | 233 | {2 Examples} 234 | 235 | {[ 236 | Int.isEven(8) == true 237 | Int.isEven(7) == false 238 | Int.isEven(0) == true 239 | ]} 240 | ") 241 | let isEven: t => bool 242 | 243 | @ocaml.doc(" Check if an [int] is odd. 244 | 245 | {2 Examples} 246 | 247 | {[ 248 | Int.isOdd(7) == true 249 | Int.isOdd(8) == false 250 | Int.isOdd(0) == false 251 | ]} 252 | ") 253 | let isOdd: t => bool 254 | 255 | @ocaml.doc(" Clamps [n] within the inclusive [lower] and [upper] bounds. 256 | 257 | {3 Exceptions} 258 | 259 | Throws an [Invalid_argument] exception if [lower > upper] 260 | 261 | {2 Examples} 262 | 263 | {[ 264 | Int.clamp(5, ~lower=0, ~upper=8) == 5 265 | Int.clamp(9, ~lower=0, ~upper=8) == 8 266 | Int.clamp(5, ~lower=-10, ~upper=-5) == -5 267 | ]} 268 | ") 269 | let clamp: (t, ~lower: t, ~upper: t) => t 270 | 271 | @ocaml.doc(" Checks if [n] is between [lower] and up to, but not including, [upper]. 272 | 273 | {3 Exceptions} 274 | 275 | Throws an [Invalid_argument] exception if [lower > upper] 276 | 277 | {2 Examples} 278 | 279 | {[ 280 | Int.inRange(3, ~lower=2, ~upper=4) == true 281 | Int.inRange(4, ~lower=5, ~upper=8) == false 282 | Int.inRange(-3, ~lower=-6, ~upper=-2) == true 283 | ]} 284 | 285 | ") 286 | let inRange: (t, ~lower: t, ~upper: t) => bool 287 | 288 | @@ocaml.text(" {1 Convert} ") 289 | 290 | @ocaml.doc(" Convert an [int] into a [float]. Useful when mixing {!Int} and {!Float} values like this: 291 | 292 | {2 Examples} 293 | 294 | {[ 295 | let halfOf = (number: int): float => Int.toFloat(number) /. 2. 296 | 297 | halfOf(7) == 3.5 298 | ]} 299 | ") 300 | let toFloat: t => float 301 | 302 | @ocaml.doc(" Convert an [int] into a [string] representation. 303 | 304 | Guarantees that 305 | 306 | {[ 307 | n->Int.toString->Int.fromString == Some(n) 308 | ]} 309 | 310 | {2 Examples} 311 | 312 | {[ 313 | Int.toString(3) == \"3\" 314 | Int.toString(-3) == \"-3\" 315 | Int.toString(0) == \"0\" 316 | ]} 317 | ") 318 | let toString: t => string 319 | 320 | @@ocaml.text(" {1 Compare} ") 321 | 322 | @ocaml.doc(" Test two [int]s for equality. ") 323 | let equal: (t, t) => bool 324 | 325 | @ocaml.doc(" Compare two [int]s. ") 326 | let compare: (t, t) => int 327 | 328 | @ocaml.doc(" The unique identity for {!Comparator}.") 329 | type identity 330 | 331 | let comparator: TableclothComparator.t 332 | -------------------------------------------------------------------------------- /src/TableclothList.res: -------------------------------------------------------------------------------- 1 | type t<'a> = list<'a> 2 | 3 | let empty = list{} 4 | 5 | let singleton = x => list{x} 6 | 7 | let fromArray = array => List.init(Array.length(array), i => array[i]) 8 | 9 | let range = (~from=0, to_) => 10 | if to_ < from { 11 | list{} 12 | } else { 13 | List.init(to_ - from, i => i + from) 14 | } 15 | 16 | let rec repeat = (element, ~times) => 17 | if times <= 0 { 18 | list{} 19 | } else { 20 | list{element, ...repeat(element, ~times=times - 1)} 21 | } 22 | 23 | let flatten = t => Belt.List.flatten(t) 24 | 25 | let reverse = t => Belt.List.reverse(t) 26 | 27 | let append = (a, b) => Belt.List.concat(a, b) 28 | 29 | let sum = (type a, t, module(M: TableclothContainer.Sum with type t = a)) => 30 | List.fold_left(M.add, M.zero, t) 31 | 32 | let map = (t, ~f) => Belt.List.map(t, a => f(a)) 33 | 34 | let flatMap = (t, ~f) => flatten(map(t, ~f)) 35 | 36 | let mapWithIndex = (list, ~f) => Belt.List.mapWithIndex(list, (a, b) => f(a, b)) 37 | 38 | let map2 = (a, b, ~f) => Belt.List.zipBy(a, b, (a, b) => f(a, b)) 39 | 40 | let zip = (a, b) => map2(a, b, ~f=(a, b) => (a, b)) 41 | 42 | let rec map3 = (a, b, c, ~f) => 43 | switch (a, b, c) { 44 | | (list{x, ...xs}, list{y, ...ys}, list{z, ...zs}) => list{f(x, y, z), ...map3(xs, ys, zs, ~f)} 45 | | _ => list{} 46 | } 47 | 48 | let rec last = l => 49 | switch l { 50 | | list{} => None 51 | | list{x} => Some(x) 52 | | list{_, ...rest} => last(rest) 53 | } 54 | 55 | let unzip = list => (List.map(((a, _)) => a, list), List.map(((_, b)) => b, list)) 56 | 57 | let includes = (t, value, ~equal) => Belt.List.has(t, value, (a, b) => equal(a, b)) 58 | 59 | let uniqueBy = (l: list<'a>, ~f: 'a => string): list<'a> => { 60 | let rec uniqueHelper = ( 61 | f: 'a => string, 62 | existing: Belt.Set.String.t, 63 | remaining: list<'a>, 64 | accumulator: list<'a>, 65 | ) => 66 | switch remaining { 67 | | list{} => reverse(accumulator) 68 | | list{first, ...rest} => 69 | let computedFirst = f(first) 70 | if Belt.Set.String.has(existing, computedFirst) { 71 | uniqueHelper(f, existing, rest, accumulator) 72 | } else { 73 | uniqueHelper( 74 | f, 75 | Belt.Set.String.add(existing, computedFirst), 76 | rest, 77 | list{first, ...accumulator}, 78 | ) 79 | } 80 | } 81 | 82 | uniqueHelper(f, Belt.Set.String.empty, l, list{}) 83 | } 84 | 85 | let find = (t, ~f) => Belt.List.getBy(t, a => f(a)) 86 | 87 | let getAt = (t, ~index) => Belt.List.get(t, index) 88 | 89 | let any = (t, ~f) => List.exists(a => f(a), t) 90 | 91 | let head = l => Belt.List.head(l) 92 | 93 | let drop = (t, ~count) => 94 | switch Belt.List.drop(t, count) { 95 | | None => 96 | if count <= 0 { 97 | t 98 | } else { 99 | list{} 100 | } 101 | | Some(v) => v 102 | } 103 | 104 | let take = (t, ~count) => 105 | switch Belt.List.take(t, count) { 106 | | None => 107 | if count <= 0 { 108 | list{} 109 | } else { 110 | t 111 | } 112 | | Some(v) => v 113 | } 114 | 115 | let initial = l => 116 | switch reverse(l) { 117 | | list{} => None 118 | | list{_, ...rest} => Some(reverse(rest)) 119 | } 120 | 121 | let filterMap = (t, ~f) => Belt.List.keepMap(t, a => f(a)) 122 | 123 | let filter = (t, ~f) => Belt.List.keep(t, a => f(a)) 124 | 125 | let filterWithIndex = (t, ~f) => Belt.List.keepWithIndex(t, (e, i) => f(i, e)) 126 | 127 | let partition = (t, ~f) => Belt.List.partition(t, a => f(a)) 128 | 129 | let fold = (t, ~initial, ~f) => Belt.List.reduce(t, initial, (a, b) => f(a, b)) 130 | 131 | let count = (t, ~f) => fold(t, ~initial=0, ~f=(total, element) => total + (f(element) ? 1 : 0)) 132 | 133 | let foldRight = (t, ~initial, ~f) => Belt.List.reduceReverse(t, initial, (a, b) => f(a, b)) 134 | 135 | let findIndex = (list, ~f) => { 136 | let rec loop = (i, l) => 137 | switch l { 138 | | list{} => None 139 | | list{x, ...rest} => 140 | if f(i, x) { 141 | Some(i, x) 142 | } else { 143 | loop(i + 1, rest) 144 | } 145 | } 146 | 147 | loop(0, list) 148 | } 149 | 150 | let splitAt = (t, ~index) => (take(~count=index, t), drop(~count=index, t)) 151 | 152 | let updateAt: (t<'a>, ~index: int, ~f: 'a => 'a) => t<'a> = (t, ~index, ~f) => 153 | Belt.List.mapWithIndex(t, (i, element) => 154 | if i == index { 155 | f(element) 156 | } else { 157 | element 158 | } 159 | ) 160 | 161 | let length = l => Belt.List.length(l) 162 | 163 | let rec dropWhile = (t, ~f) => 164 | switch t { 165 | | list{} => list{} 166 | | list{x, ...rest} => 167 | if f(x) { 168 | dropWhile(rest, ~f) 169 | } else { 170 | t 171 | } 172 | } 173 | 174 | let isEmpty = t => t == list{} 175 | 176 | let sliding = (~step=1, t, ~size) => { 177 | let rec loop = t => 178 | if isEmpty(t) { 179 | list{} 180 | } else { 181 | let sample = Belt.List.take(t, size) 182 | let rest = Belt.List.drop(t, step) 183 | switch (sample, rest) { 184 | | (None, _) => list{} 185 | | (Some(x), None) => list{x} 186 | | (Some(x), Some(xs)) => list{x, ...loop(xs)} 187 | } 188 | } 189 | 190 | loop(t) 191 | } 192 | 193 | let chunksOf = (t, ~size) => sliding(t, ~step=size, ~size) 194 | 195 | let cons = (t, element) => list{element, ...t} 196 | 197 | let takeWhile = (t, ~f) => { 198 | let rec takeWhileHelper = (acc, t) => 199 | switch t { 200 | | list{} => reverse(acc) 201 | | list{x, ...rest} => 202 | if f(x) { 203 | takeWhileHelper(list{x, ...acc}, rest) 204 | } else { 205 | reverse(acc) 206 | } 207 | } 208 | 209 | takeWhileHelper(list{}, t) 210 | } 211 | 212 | let all = (t, ~f) => Belt.List.every(t, a => f(a)) 213 | 214 | let tail = t => 215 | switch t { 216 | | list{} => None 217 | | list{_, ...rest} => Some(rest) 218 | } 219 | 220 | let removeAt = (t, ~index) => 221 | if index < 0 { 222 | t 223 | } else { 224 | let (front, back): (t<'a>, t<'a>) = splitAt(t, ~index) 225 | switch tail(back) { 226 | | None => t 227 | | Some(t) => append(front, t) 228 | } 229 | } 230 | 231 | let minimumBy = (~f: 'a => 'comparable, l: list<'a>): option<'a> => { 232 | let minBy = ((y, fy), x) => { 233 | let fx = f(x) 234 | if fx < fy { 235 | (x, fx) 236 | } else { 237 | (y, fy) 238 | } 239 | } 240 | 241 | switch l { 242 | | list{} => None 243 | | list{x} => Some(x) 244 | | list{x, ...rest} => Some(fst(fold(~f=minBy, ~initial=(x, f(x)), rest))) 245 | } 246 | } 247 | 248 | let maximumBy = (~f: 'a => 'comparable, l: list<'a>): option<'a> => { 249 | let maxBy = ((y, fy), x) => { 250 | let fx = f(x) 251 | if fx > fy { 252 | (x, fx) 253 | } else { 254 | (y, fy) 255 | } 256 | } 257 | 258 | switch l { 259 | | list{} => None 260 | | list{x} => Some(x) 261 | | list{x, ...rest} => Some(fst(fold(~f=maxBy, ~initial=(x, f(x)), rest))) 262 | } 263 | } 264 | 265 | let minimum = (t, ~compare) => 266 | fold(t, ~initial=None, ~f=(min, element) => 267 | switch min { 268 | | None => Some(element) 269 | | Some(value) => compare(element, value) < 0 ? Some(element) : min 270 | } 271 | ) 272 | 273 | let maximum = (t, ~compare) => 274 | fold(t, ~initial=None, ~f=(max, element) => 275 | switch max { 276 | | None => Some(element) 277 | | Some(value) => compare(element, value) > 0 ? Some(element) : max 278 | } 279 | ) 280 | 281 | let extent = (t, ~compare) => 282 | fold(t, ~initial=None, ~f=(current, element) => 283 | switch current { 284 | | None => Some(element, element) 285 | | Some(min, max) => 286 | Some(compare(element, min) < 0 ? element : min, compare(element, max) > 0 ? element : max) 287 | } 288 | ) 289 | 290 | let sort = (t, ~compare) => Belt.List.sort(t, (a, b) => compare(a, b)) 291 | 292 | let sortBy = (l: t<'a>, ~f: 'a => 'b): t<'a> => 293 | Belt.List.sort(l, (a, b) => { 294 | let a' = f(a) 295 | let b' = f(b) 296 | if a' == b' { 297 | 0 298 | } else if a' < b' { 299 | -1 300 | } else { 301 | 1 302 | } 303 | }) 304 | 305 | let groupi = (l, ~break) => { 306 | let groups = Belt.List.reduceWithIndex(l, list{}, (acc, x, i) => 307 | switch acc { 308 | | list{} => list{list{x}} 309 | | list{current_group, ...tl} => 310 | if break(i, Belt.List.headExn(current_group), x) { 311 | list{list{x}, current_group, ...tl} /* start new group */ 312 | } else { 313 | list{list{x, ...current_group}, ...tl} 314 | } 315 | } 316 | ) 317 | /* extend current group */ 318 | 319 | switch groups { 320 | | list{} => list{} 321 | | l => Belt.List.mapReverse(l, reverse) 322 | } 323 | } 324 | 325 | let groupWhile = (l, ~f) => groupi(l, ~break=(_, x, y) => f(x, y)) 326 | 327 | let insertAt = (t, ~index, ~value) => { 328 | let (front, back) = splitAt(t, ~index) 329 | append(front, list{value, ...back}) 330 | } 331 | 332 | let splitWhen = (t, ~f) => { 333 | let rec loop = (front, back) => 334 | switch back { 335 | | list{} => (t, list{}) 336 | | list{element, ...rest} => 337 | if f(element) { 338 | (reverse(front), back) 339 | } else { 340 | loop(list{element, ...front}, rest) 341 | } 342 | } 343 | 344 | loop(list{}, t) 345 | } 346 | 347 | let intersperse = (t, ~sep) => 348 | switch t { 349 | | list{} => list{} 350 | | list{x} => list{x} 351 | | list{x, ...rest} => 352 | list{x, ...foldRight(rest, ~initial=list{}, ~f=(acc, x) => list{sep, x, ...acc})} 353 | } 354 | 355 | let initialize = (length, ~f) => Belt.List.makeBy(length, a => f(a)) 356 | 357 | let forEach = (t, ~f): unit => Belt.List.forEach(t, a => f(a)) 358 | 359 | let forEachWithIndex = (t, ~f): unit => Belt.List.forEachWithIndex(t, (a, b) => f(a, b)) 360 | 361 | let toArray = t => Array.of_list(t) 362 | 363 | let join = (strings, ~sep) => Js.Array.joinWith(sep, toArray(strings)) 364 | 365 | let groupBy = (t, comparator, ~f) => 366 | fold(t, ~initial=TableclothMap.empty(comparator), ~f=(map, element) => { 367 | let key = f(element) 368 | TableclothMap.update(map, ~key, ~f=x => 369 | switch x { 370 | | None => Some(list{element}) 371 | | Some(elements) => Some(list{element, ...elements}) 372 | } 373 | ) 374 | }) 375 | 376 | let rec equal = (a, b, equalElement) => 377 | switch (a, b) { 378 | | (list{}, list{}) => true 379 | | (list{x, ...xs}, list{y, ...ys}) => equalElement(x, y) && equal(xs, ys, equalElement) 380 | | _ => false 381 | } 382 | 383 | let rec compare = (a, b, compareElement) => 384 | switch (a, b) { 385 | | (list{}, list{}) => 0 386 | | (list{}, _) => -1 387 | | (_, list{}) => 1 388 | | (list{x, ...xs}, list{y, ...ys}) => 389 | switch compareElement(x, y) { 390 | | 0 => compare(xs, ys, compareElement) 391 | | result => result 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/TableclothMap.res: -------------------------------------------------------------------------------- 1 | module Option = TableclothOption 2 | 3 | type t<'key, 'value, 'cmp> = Belt.Map.t<'key, 'value, 'cmp> 4 | 5 | let fromArray = (comparator: TableclothComparator.s<'key, 'id>, values: array<('key, 'v)>): t< 6 | 'key, 7 | 'value, 8 | 'id, 9 | > => Belt.Map.fromArray(values, ~id=Internal.toBeltComparator(comparator)) 10 | 11 | let empty = comparator => fromArray(comparator, []) 12 | 13 | let fromList = (comparator, l) => fromArray(comparator, Array.of_list(l)) 14 | 15 | let singleton = (comparator, ~key, ~value) => fromArray(comparator, [(key, value)]) 16 | 17 | let isEmpty = t => Belt.Map.isEmpty(t) 18 | 19 | let includes = (t, k) => Belt.Map.has(t, k) 20 | 21 | let length = t => Belt.Map.size(t) 22 | 23 | let add = (m, ~key, ~value) => Belt.Map.set(m, key, value) 24 | 25 | let remove = (t, k) => Belt.Map.remove(t, k) 26 | 27 | let get = (t, k) => Belt.Map.get(t, k) 28 | 29 | let update = (m, ~key, ~f) => Belt.Map.update(m, key, a => f(a)) 30 | 31 | let merge = (m1, m2, ~f) => Belt.Map.merge(m1, m2, (m, a, b) => f(m, a, b)) 32 | 33 | let map = (m, ~f) => Belt.Map.map(m, value => f(value)) 34 | 35 | let mapWithIndex = (t, ~f) => Belt.Map.mapWithKey(t, (a, b) => f(a, b)) 36 | 37 | let filter = (m, ~f) => Belt.Map.keep(m, (_, value) => f(value)) 38 | 39 | let filterMap = (m, ~f) => { 40 | let f' = ((key, value)) => f(~key, ~value)->Belt.Option.map(value' => (key, value')) 41 | m->Belt.Map.toArray->Belt.Array.keepMap(f')->Belt.Map.fromArray(~id=Belt.Map.getId(m)) 42 | } 43 | 44 | let partition = (m, ~f) => Belt.Map.partition(m, (key, value) => f(~key, ~value)) 45 | 46 | let find = (m, ~f) => Belt.Map.findFirstBy(m, (key, value) => f(~key, ~value)) 47 | 48 | let any = (m, ~f) => Belt.Map.some(m, (_, value) => f(value)) 49 | 50 | let all = (m, ~f) => Belt.Map.every(m, (_, value) => f(value)) 51 | 52 | let forEach = (m, ~f) => Belt.Map.forEach(m, (_, value) => f(value)) 53 | 54 | let forEachWithIndex = (m, ~f) => Belt.Map.forEach(m, (key, value) => f(~key, ~value)) 55 | 56 | let fold = (m, ~initial, ~f) => 57 | Belt.Map.reduce(m, initial, (acc, key, data) => f(acc, ~key, ~value=data)) 58 | 59 | let keys = m => Array.to_list(Belt.Map.keysToArray(m)) 60 | 61 | let values = m => Array.to_list(Belt.Map.valuesToArray(m)) 62 | 63 | let maximum = t => Belt.Map.maxKey(t) 64 | 65 | let minimum = t => Belt.Map.minKey(t) 66 | 67 | let extent = t => Option.both(minimum(t), maximum(t)) 68 | 69 | let toArray = t => Belt.Map.toArray(t) 70 | 71 | let toList = t => Belt.Map.toList(t) 72 | 73 | module Poly = { 74 | type identity 75 | 76 | type t<'k, 'v> = t<'k, 'v, identity> 77 | 78 | let fromArray = (type k v, a: array<(k, v)>): t => 79 | Belt.Map.fromArray( 80 | a, 81 | ~id=module( 82 | { 83 | type t = k 84 | 85 | type identity = identity 86 | 87 | let cmp = Obj.magic(Pervasives.compare) 88 | } 89 | ), 90 | ) 91 | 92 | let empty = () => fromArray([]) 93 | 94 | let fromList = l => fromArray(Array.of_list(l)) 95 | 96 | let singleton = (~key, ~value) => fromArray([(key, value)]) 97 | } 98 | 99 | module Int = { 100 | type identity 101 | 102 | type t<'value> = t 103 | 104 | let fromArray = a => Obj.magic(Poly.fromArray(a)) 105 | 106 | let empty = fromArray([]) 107 | 108 | let singleton = (~key, ~value) => fromArray([(key, value)]) 109 | 110 | let fromList = l => fromArray(Array.of_list(l)) 111 | } 112 | 113 | module String = { 114 | type identity 115 | 116 | type t<'value> = t 117 | 118 | let fromArray = a => Obj.magic(Poly.fromArray(a)) 119 | 120 | let empty = fromArray([]) 121 | 122 | let singleton = (~key, ~value) => fromArray([(key, value)]) 123 | 124 | let fromList = l => fromArray(Array.of_list(l)) 125 | } 126 | -------------------------------------------------------------------------------- /src/TableclothMap.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" A [Map] represents a unique mapping from keys to values. 4 | 5 | [Map] is an immutable data structure which means operations like {!Map.add} and {!Map.remove} do not modify the data structure, but return a new map with the desired changes. 6 | 7 | Since maps of [int]s and [string]s are so common the specialized {!Map.Int} and {!Map.String} modules are available, which offer a convenient way to construct new maps. 8 | 9 | Custom data types can be used with maps as long as the module satisfies the {!Comparator.S} interface. 10 | 11 | {[ 12 | module Point = { 13 | type t = (int, int) 14 | let compare = Tuple2.compare(~f=Int.compare, ~g=Int.compare) 15 | include Comparator.Make({ 16 | type t = t 17 | let compare = compare 18 | }) 19 | } 20 | 21 | type animal = 22 | | Cow 23 | | Pig 24 | | Alpacca 25 | 26 | let pointToAnimal = Map.fromArray( 27 | module(Point), 28 | [((0, 0), Alpacca), ((3, 4), Cow), ((6, 7), Pig)], 29 | ) 30 | ]} 31 | 32 | See the {!Comparator} module for a more details. 33 | ") 34 | 35 | type t<'key, 'value, 'cmp> = Belt.Map.t<'key, 'value, 'cmp> 36 | 37 | @@ocaml.text(" {1 Create} 38 | 39 | You can create sets of modules types which conform to the {!Comparator.S} signature by using {!empty}, {!singleton}, {!fromList} or {!fromArray}. 40 | 41 | Specialised versions of the {!empty}, {!singleton}, {!fromList} and {!fromArray} functions available in the {!Set.Int} and {!Set.String} sub-modules. 42 | ") 43 | 44 | @ocaml.doc(" A map with nothing in it. 45 | 46 | Often used as an intial value for functions like {!Array.fold}. 47 | 48 | {2 Examples} 49 | 50 | {[ 51 | Array.fold([\"Pear\", \"Orange\", \"Grapefruit\"], ~initial=Map.empty(module(Int)), ~f=( 52 | lengthToFruit, 53 | fruit, 54 | ) => Map.add(lengthToFruit, ~key=String.length(fruit), ~value=fruit))->Map.toArray == 55 | [(4, \"Pear\"), (6, \"Orange\"), (10, \"Grapefruit\")] 56 | ]} 57 | 58 | In this particular case you might want to use {!Array.groupBy} 59 | ") 60 | let empty: TableclothComparator.s<'key, 'identity> => t<'key, 'value, 'identity> 61 | 62 | @ocaml.doc(" Create a map from a key and value. 63 | 64 | {2 Examples} 65 | 66 | {[ 67 | Map.singleton(module(Int), ~key=1, ~value=\"Ant\")->Map.toArray == [(1, \"Ant\")] 68 | ]} 69 | ") 70 | let singleton: ( 71 | TableclothComparator.s<'key, 'identity>, 72 | ~key: 'key, 73 | ~value: 'value, 74 | ) => t<'key, 'value, 'identity> 75 | 76 | @ocaml.doc(" Create a map from an {!Array} of key-value tuples. ") 77 | let fromArray: ( 78 | TableclothComparator.s<'key, 'identity>, 79 | array<('key, 'value)>, 80 | ) => t<'key, 'value, 'identity> 81 | 82 | @ocaml.doc(" Create a map of a {!List} of key-value tuples. ") 83 | let fromList: ( 84 | TableclothComparator.s<'key, 'identity>, 85 | list<('key, 'value)>, 86 | ) => t<'key, 'value, 'identity> 87 | 88 | @@ocaml.text(" {1 Basic operations} ") 89 | 90 | @ocaml.doc(" Adds a new entry to a map. If [key] is allready present, its previous value is replaced with [value]. 91 | 92 | {2 Examples} 93 | 94 | {[ 95 | Map.add( 96 | Map.Int.fromArray([(1, \"Ant\"), (2, \"Bat\")]), 97 | ~key=3, 98 | ~value=\"Cat\", 99 | )->Map.toArray == [(1, \"Ant\"), (2, \"Bat\"), (3, \"Cat\")] 100 | 101 | Map.add( 102 | Map.Int.fromArray([(1, \"Ant\"), (2, \"Bat\")]), 103 | ~key=2, 104 | ~value=\"Bug\", 105 | )->Map.toArray == [(1, \"Ant\"), (2, \"Bug\")] 106 | ]} 107 | ") 108 | let add: (t<'key, 'value, 'id>, ~key: 'key, ~value: 'value) => t<'key, 'value, 'id> 109 | 110 | @ocaml.doc(" Removes a key-value pair from a map based on they provided key. 111 | 112 | {2 Examples} 113 | 114 | {[ 115 | let animalPopulations = Map.String.fromArray([ 116 | (\"Elephant\", 3_156), 117 | (\"Mosquito\", 56_123_156), 118 | (\"Rhino\", 3), 119 | (\"Shrew\", 56_423), 120 | ]) 121 | Map.remove(animalPopulations, \"Mosquito\")->Map.toArray 122 | == [(\"Elephant\", 3_156), (\"Rhino\", 3), (\"Shrew\", 56_423)] 123 | ]} 124 | ") 125 | let remove: (t<'key, 'value, 'id>, 'key) => t<'key, 'value, 'id> 126 | 127 | @ocaml.doc(" Get the value associated with a key. If the key is not present in the map, returns [None]. 128 | 129 | {2 Examples} 130 | 131 | {[ 132 | let animalPopulations = Map.String.fromArray([ 133 | (\"Elephant\", 3_156), 134 | (\"Mosquito\", 56_123_156), 135 | (\"Rhino\", 3), 136 | (\"Shrew\", 56_423), 137 | ]) 138 | Map.get(animalPopulations, \"Shrew\") == Some(56_423) 139 | ]} 140 | ") 141 | let get: (t<'key, 'value, 'id>, 'key) => option<'value> 142 | 143 | @ocaml.doc(" Update the value for a specific key using [f]. If [key] is not present in the map [f] will be called with [None]. 144 | 145 | {2 Examples} 146 | 147 | {[ 148 | let animalPopulations = Map.String.fromArray([ 149 | (\"Elephant\", 3_156), 150 | (\"Mosquito\", 56_123_156), 151 | (\"Rhino\", 3), 152 | (\"Shrew\", 56_423), 153 | ]) 154 | 155 | Map.update(animalPopulations, ~key=\"Hedgehog\", ~f=population => 156 | switch population { 157 | | None => Some(1) 158 | | Some(count) => Some(count + 1) 159 | } 160 | )->Map.toArray == 161 | [ 162 | (\"Elephant\", 3_156), 163 | (\"Hedgehog\", 1), 164 | (\"Mosquito\", 56_123_156), 165 | (\"Rhino\", 3), 166 | (\"Shrew\", 56_423), 167 | ] 168 | ]} 169 | ") 170 | let update: ( 171 | t<'key, 'value, 'id>, 172 | ~key: 'key, 173 | ~f: option<'value> => option<'value>, 174 | ) => t<'key, 'value, 'id> 175 | 176 | @@ocaml.text(" {1 Query} ") 177 | 178 | @ocaml.doc(" Determine if a map is empty. ") 179 | let isEmpty: t<_, _, _> => bool 180 | 181 | @ocaml.doc(" Returns the number of key-value pairs present in the map. 182 | 183 | {2 Examples} 184 | 185 | {[ 186 | Map.Int.fromArray([(1, \"Hornet\"), (3, \"Marmot\")])->Map.length == 2 187 | ]} 188 | ") 189 | let length: t<_, _, _> => int 190 | 191 | @ocaml.doc(" Determine if [f] returns [true] for [any] values in a map. ") 192 | let any: (t<_, 'value, _>, ~f: 'value => bool) => bool 193 | 194 | @ocaml.doc(" Determine if [f] returns [true] for [all] values in a map. ") 195 | let all: (t<_, 'value, _>, ~f: 'value => bool) => bool 196 | 197 | @ocaml.doc(" Returns, as an {!Option} the first key-value pair for which [f] evaluates to [true]. 198 | 199 | If [f] doesn't return [true] for any of the elements [find] will return [None]. 200 | 201 | Searches starting from the smallest {b key} 202 | 203 | {2 Examples} 204 | 205 | {[ 206 | Map.String.fromArray([ 207 | (\"Elephant\", 3_156), 208 | (\"Mosquito\", 56_123_156), 209 | (\"Rhino\", 3), 210 | (\"Shrew\", 56_423), 211 | ])->Map.find(~f=(~key, ~value) => value > 10_000) 212 | == Some(\"Mosquito\", 56_123_156) 213 | ]} 214 | ") 215 | let find: (t<'key, 'value, _>, ~f: (~key: 'key, ~value: 'value) => bool) => option<('key, 'value)> 216 | 217 | @ocaml.doc(" Determine if a map includes [key]. ") 218 | let includes: (t<'key, _, _>, 'key) => bool 219 | 220 | @ocaml.doc(" Returns, as an {!Option}, the smallest {b key } in the map. 221 | 222 | Returns [None] if the map is empty. 223 | 224 | {2 Examples} 225 | 226 | {[ 227 | Map.Int.fromArray([(8, \"Pigeon\"), (1, \"Hornet\"), (3, \"Marmot\")]) 228 | ->Map.minimum == Some(1) 229 | ]} 230 | ") 231 | let minimum: t<'key, _, _> => option<'key> 232 | 233 | @ocaml.doc(" Returns the largest {b key } in the map. 234 | 235 | Returns [None] if the map is empty. 236 | 237 | {2 Examples} 238 | 239 | {[ 240 | Map.Int.fromArray([(8, \"Pigeon\"), (1, \"Hornet\"), (3, \"Marmot\")]) 241 | ->Map.maximum == Some(8) 242 | ]} 243 | ") 244 | let maximum: t<'key, _, _> => option<'key> 245 | 246 | @ocaml.doc(" Returns, as an {!Option}, a {!Tuple2} of the [(minimum, maximum)] {b key}s in the map. 247 | 248 | Returns [None] if the map is empty. 249 | 250 | {2 Examples} 251 | 252 | {[ 253 | Map.Int.fromArray([(8, \"Pigeon\"), (1, \"Hornet\"), (3, \"Marmot\")]) 254 | ->Map.extent == Some(1, 8) 255 | ]} 256 | ") 257 | let extent: t<'key, _, _> => option<('key, 'key)> 258 | 259 | @@ocaml.text(" {1 Combine} ") 260 | 261 | @ocaml.doc(" Combine two maps. 262 | 263 | You provide a function [f] which is provided the key and the optional 264 | value from each map and needs to account for the three possibilities: 265 | 266 | - Only the 'left' map includes a value for the key. 267 | - Both maps contain a value for the key. 268 | - Only the 'right' map includes a value for the key. 269 | 270 | You then traverse all the keys, building up whatever you want. 271 | 272 | {2 Examples} 273 | 274 | {[ 275 | let animalToPopulation = Map.String.fromArray([(\"Elephant\", 3_156), (\"Shrew\", 56_423)]) 276 | 277 | let animalToPopulationGrowthRate = Map.String.fromArray([ 278 | (\"Elephant\", 0.88), 279 | (\"Squirrel\", 1.2), 280 | (\"Python\", 4.0), 281 | ]) 282 | 283 | Map.merge(animalToPopulation, animalToPopulationGrowthRate, ~f=(_animal, population, growth) => 284 | switch Option.both(population, growth) { 285 | | Some(population, growth) => Some(Float.fromInt(population) *. growth) 286 | | None => None 287 | } 288 | )->Map.toArray 289 | == [(\"Elephant\", 2777.28)] 290 | ]} 291 | ") 292 | let merge: ( 293 | t<'key, 'v1, 'id>, 294 | t<'key, 'v2, 'id>, 295 | ~f: ('key, option<'v1>, option<'v2>) => option<'v3>, 296 | ) => t<'key, 'v3, 'id> 297 | 298 | @@ocaml.text(" {1 Transform} ") 299 | 300 | @ocaml.doc(" Apply a function to all values in a dictionary. 301 | 302 | {2 Examples} 303 | 304 | {[ 305 | Map.String.fromArray([(\"Elephant\", 3_156), (\"Shrew\", 56_423)]) 306 | ->Map.map(~f=Int.toString) 307 | ->Map.toArray == [(\"Elephant\", \"3156\"), (\"Shrew\", \"56423\")] 308 | ]} 309 | ") 310 | let map: (t<'key, 'value, 'id>, ~f: 'value => 'b) => t<'key, 'b, 'id> 311 | 312 | @ocaml.doc(" Like {!map} but [f] is also called with each values corresponding key. ") 313 | let mapWithIndex: (t<'key, 'value, 'id>, ~f: ('key, 'value) => 'b) => t<'key, 'b, 'id> 314 | 315 | @ocaml.doc(" Keep elements that [f] returns [true] for. 316 | 317 | {2 Examples} 318 | 319 | {[ 320 | Map.String.fromArray([(\"Elephant\", 3_156), (\"Shrew\", 56_423)]) 321 | ->Map.filter(~f=population => population > 10_000) 322 | ->Map.toArray 323 | == [(\"Shrew\", 56423)] 324 | ]} 325 | ") 326 | let filter: (t<'key, 'value, 'id>, ~f: 'value => bool) => t<'key, 'value, 'id> 327 | 328 | @ocaml.doc(" Mombine {!map} and {!filter} into a single pass. 329 | 330 | The output list only contains elements for which [f] returns [Some]. 331 | ") 332 | let filterMap: ( 333 | t<'key, 'value, 'id>, 334 | ~f: (~key: 'key, ~value: 'value) => option<'b>, 335 | ) => t<'key, 'b, 'id> 336 | 337 | @ocaml.doc(" Divide a map into two, the first map will contain the key-value pairs that [f] returns [true] for, pairs that [f] returns [false] for will end up in the second. 338 | 339 | {2 Examples} 340 | 341 | {[ 342 | let (endangered, notEndangered) = 343 | Map.String.fromArray([ 344 | (\"Elephant\", 3_156), 345 | (\"Mosquito\", 56_123_156), 346 | (\"Rhino\", 3), 347 | (\"Shrew\", 56_423), 348 | ])->Map.partition(~f=(~key as _, ~value as population) => population < 10_000) 349 | 350 | endangered->Map.toArray == [(\"Elephant\", 3_156), (\"Rhino\", 3)] 351 | 352 | notEndangered->Map.toArray == [(\"Mosquito\", 56_123_156), (\"Shrew\", 56_423)] 353 | ]} 354 | ") 355 | let partition: ( 356 | t<'key, 'value, 'id>, 357 | ~f: (~key: 'key, ~value: 'value) => bool, 358 | ) => (t<'key, 'value, 'id>, t<'key, 'value, 'id>) 359 | 360 | @ocaml.doc(" Like {!Array.fold} but [f] is also called with both the [key] and [value]. ") 361 | let fold: (t<'key, 'value, _>, ~initial: 'a, ~f: ('a, ~key: 'key, ~value: 'value) => 'a) => 'a 362 | 363 | @@ocaml.text(" {1 Iterate} ") 364 | 365 | @ocaml.doc(" Runs a function [f] against each {b value} in the map. ") 366 | let forEach: (t<_, 'value, _>, ~f: 'value => unit) => unit 367 | 368 | @ocaml.doc(" Like {!Map.forEach} except [~f] is also called with the corresponding key. ") 369 | let forEachWithIndex: (t<'key, 'value, _>, ~f: (~key: 'key, ~value: 'value) => unit) => unit 370 | 371 | @@ocaml.text(" {1 Convert} ") 372 | 373 | @ocaml.doc(" Get a {!List} of all of the keys in a map. 374 | 375 | {2 Examples} 376 | 377 | {[ 378 | Map.String.fromArray([ 379 | (\"Elephant\", 3_156), 380 | (\"Mosquito\", 56_123_156), 381 | (\"Rhino\", 3), 382 | (\"Shrew\", 56_423), 383 | ])->Map.keys 384 | == list{\"Elephant\", \"Mosquito\", \"Rhino\", \"Shrew\"} 385 | ]} 386 | ") 387 | let keys: t<'key, _, _> => list<'key> 388 | 389 | @ocaml.doc(" Get a {!List} of all of the values in a map. 390 | 391 | {2 Examples} 392 | 393 | {[ 394 | Map.String.fromArray([ 395 | (\"Elephant\", 3_156), 396 | (\"Mosquito\", 56_123_156), 397 | (\"Rhino\", 3), 398 | (\"Shrew\", 56_423), 399 | ])->Map.values 400 | == list{3_156, 56_123_156, 3, 56_423} 401 | ]} 402 | ") 403 | let values: t<_, 'value, _> => list<'value> 404 | 405 | @ocaml.doc(" Get an {!Array} of all of the key-value pairs in a map. ") 406 | let toArray: t<'key, 'value, _> => array<('key, 'value)> 407 | 408 | @ocaml.doc(" Get a {!List} of all of the key-value pairs in a map. ") 409 | let toList: t<'key, 'value, _> => list<('key, 'value)> 410 | 411 | @ocaml.doc( 412 | " Construct a Map which can be keyed by any data type using the polymorphic [compare] function. " 413 | ) 414 | module Poly: { 415 | type identity 416 | 417 | type t<'key, 'value> = t<'key, 'value, identity> 418 | 419 | @ocaml.doc(" A map with nothing in it. ") 420 | let empty: unit => t<'key, 'value> 421 | 422 | @ocaml.doc(" Create a map from a key and value. 423 | 424 | {2 Examples} 425 | 426 | {[ 427 | Map.Poly.singleton(~key=false, ~value=1)->Map.toArray == [(false, 1)] 428 | ]} 429 | ") 430 | let singleton: (~key: 'key, ~value: 'value) => t<'key, 'value> 431 | 432 | @ocaml.doc(" Create a map from an {!Array} of key-value tuples. ") 433 | let fromArray: array<('key, 'value)> => t<'key, 'value> 434 | 435 | @ocaml.doc(" Create a map from a {!List} of key-value tuples. ") 436 | let fromList: list<('key, 'value)> => t<'key, 'value> 437 | } 438 | 439 | @ocaml.doc(" Construct a Map with {!Int}s for keys. ") 440 | module Int: { 441 | type identity 442 | 443 | type t<'value> = t 444 | 445 | @ocaml.doc(" A map with nothing in it. ") 446 | let empty: t<'value> 447 | 448 | @ocaml.doc(" Create a map from a key and value. 449 | 450 | {2 Examples} 451 | 452 | {[ 453 | Map.Int.singleton(~key=1, ~value=\"Ant\")->Map.toArray == [(1, \"Ant\")] 454 | ]} 455 | ") 456 | let singleton: (~key: int, ~value: 'value) => t<'value> 457 | 458 | @ocaml.doc(" Create a map from an {!Array} of key-value tuples. ") 459 | let fromArray: array<(int, 'value)> => t<'value> 460 | 461 | @ocaml.doc(" Create a map of a {!List} of key-value tuples. ") 462 | let fromList: list<(int, 'value)> => t<'value> 463 | } 464 | 465 | @ocaml.doc(" Construct a Map with {!String}s for keys. ") 466 | module String: { 467 | type identity 468 | 469 | type t<'value> = t 470 | 471 | @ocaml.doc(" A map with nothing in it. ") 472 | let empty: t<'value> 473 | 474 | @ocaml.doc(" Create a map from a key and value. 475 | 476 | {2 Examples} 477 | 478 | {[ 479 | Map.String.singleton(~key=\"Ant\", ~value=1)->Map.toArray == [(\"Ant\", 1)] 480 | ]} 481 | ") 482 | let singleton: (~key: string, ~value: 'value) => t<'value> 483 | 484 | @ocaml.doc(" Create a map from an {!Array} of key-value tuples. ") 485 | let fromArray: array<(string, 'value)> => t<'value> 486 | 487 | @ocaml.doc(" Create a map from a {!List} of key-value tuples. ") 488 | let fromList: list<(string, 'value)> => t<'value> 489 | } 490 | -------------------------------------------------------------------------------- /src/TableclothOption.res: -------------------------------------------------------------------------------- 1 | type t<'a> = option<'a> 2 | 3 | let some = a => Some(a) 4 | 5 | let isSome = t => Belt.Option.isSome(t) 6 | 7 | let isNone = t => Belt.Option.isNone(t) 8 | 9 | let or_ = (ta, tb) => isSome(ta) ? ta : tb 10 | 11 | let orElse = (ta, tb) => isSome(tb) ? tb : ta 12 | 13 | let and_ = (ta, tb) => isSome(ta) ? tb : ta 14 | 15 | let andThen = (t, ~f) => 16 | switch t { 17 | | None => None 18 | | Some(x) => f(x) 19 | } 20 | 21 | let flatten = x => 22 | switch x { 23 | | Some(option) => option 24 | | None => None 25 | } 26 | 27 | let both = (a, b) => 28 | switch (a, b) { 29 | | (Some(a), Some(b)) => Some(a, b) 30 | | _ => None 31 | } 32 | 33 | let map = (t, ~f) => Belt.Option.map(t, a => f(a)) 34 | 35 | let map2 = (a, b, ~f) => 36 | switch (a, b) { 37 | | (Some(a), Some(b)) => Some(f(a, b)) 38 | | _ => None 39 | } 40 | 41 | let unwrap = (t, ~default) => Belt.Option.getWithDefault(t, default) 42 | 43 | let unwrapOrFailWith = (t, ~exn) => 44 | switch t { 45 | | Some(value) => value 46 | | None => raise(exn) 47 | } 48 | 49 | let unwrapUnsafe = t => 50 | unwrapOrFailWith(~exn=Invalid_argument("Option.unwrapUnsafe called with None"), t) 51 | 52 | let toArray = t => 53 | switch t { 54 | | None => [] 55 | | Some(value) => [value] 56 | } 57 | 58 | let toList = t => 59 | switch t { 60 | | None => list{} 61 | | Some(value) => list{value} 62 | } 63 | 64 | let tap = (t, ~f) => 65 | switch t { 66 | | None => () 67 | | Some(x) => f(x) 68 | } 69 | 70 | let equal = (a, b, equal) => 71 | switch (a, b) { 72 | | (None, None) => true 73 | | (Some(a'), Some(b')) => equal(a', b') 74 | | _ => false 75 | } 76 | 77 | let compare = (a, b, ~f as compare) => 78 | switch (a, b) { 79 | | (None, None) => 0 80 | | (Some(a'), Some(b')) => compare(a', b') 81 | | (None, Some(_)) => -1 82 | | (Some(_), None) => 1 83 | } 84 | -------------------------------------------------------------------------------- /src/TableclothOption.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" {!Option} represents a value which may not be present. 4 | 5 | It is a variant containing the [Some('a)] and [None] constructors 6 | 7 | {[ 8 | type t<'a> = 9 | | Some('a) 10 | | None 11 | ]} 12 | 13 | Many other languages use [null] or [nil] to represent something similar. 14 | 15 | {!Option} values are very common and they are used in a number of ways: 16 | - Initial values 17 | - Optional function arguments 18 | - Optional record fields 19 | - Return values for functions that are not defined over their entire input range (partial functions). 20 | - Return value for otherwise reporting simple errors, where [None] is returned on error. 21 | 22 | Lots of functions in [Tablecloth] return options, one you have one you can 23 | work with the value it might contain by: 24 | 25 | - Pattern matching 26 | - Using {!map} or {!andThen} 27 | - Unwrapping it using {!unwrap} 28 | - Converting a [None] into an exception using{!unwrapUnsafe} 29 | 30 | If the function you are writing can fail in a variety of ways, use a {!Result} instead to 31 | better communicate with the caller. 32 | 33 | If a function only fails in unexpected, unrecoverable ways, maybe you want raise exception. 34 | ") 35 | 36 | type t<'a> = option<'a> 37 | 38 | @ocaml.doc(" A function version of the [Some] constructor. 39 | 40 | In most situations you just want to use the [Some] constructor directly. 41 | 42 | Note that when using the Rescript syntax you {b can} use fast pipe ([->]) with variant constructors, so you don't need this function. 43 | 44 | See the {{: https://rescript-lang.org/docs/manual/latest/pipe#pipe-into-variants} Reason docs } for more. 45 | 46 | {2 Examples} 47 | 48 | {[ 49 | String.reverse(\"desserts\")->Option.some == Some(\"stressed\") 50 | String.reverse(\"desserts\")->Some == Some(\"stressed\") 51 | ]} 52 | ") 53 | let some: 'a => option<'a> 54 | 55 | @ocaml.doc(" Returns [None] if the first argument is [None], otherwise return the second argument. 56 | 57 | Unlike the built in [&&] operator, the [and_] function does not short-circuit. 58 | 59 | When you call [and_], both arguments are evaluated before being passed to the function. 60 | 61 | {2 Examples} 62 | 63 | {[ 64 | Option.and_(Some(11), Some(22)) == Some(22) 65 | Option.and_(None, Some(22)) == None 66 | Option.and_(Some(11), None) == None 67 | Option.and_(None, None) == None 68 | ]} 69 | ") 70 | let and_: (t<'a>, t<'a>) => t<'a> 71 | 72 | @ocaml.doc(" Return the first argument if it {!isSome}, otherwise return the second. 73 | 74 | Unlike the built in [||] operator, the [or_] function does not short-circuit. 75 | When you call [or_], both arguments are evaluated before being passed to the function. 76 | 77 | {2 Examples} 78 | 79 | {[ 80 | Option.or_(Some(11), Some(22)) == Some(11) 81 | Option.or_(None, Some(22)) == Some(22) 82 | Option.or_(Some(11), None) == Some(11) 83 | Option.or_(None, None) == None 84 | ]} 85 | ") 86 | let or_: (t<'a>, t<'a>) => t<'a> 87 | 88 | @ocaml.doc(" Return the second argument if it {!isSome}, otherwise return the first. 89 | 90 | Like {!or_} but in reverse. Useful when using the [|>] operator 91 | 92 | {2 Examples} 93 | 94 | {[ 95 | Option.orElse(Some(11), Some(22)) == Some(22) 96 | Option.orElse(None, Some(22)) == Some(22) 97 | Option.orElse(Some(11), None) == Some(11) 98 | Option.orElse(None, None) == None 99 | ]} 100 | ") 101 | let orElse: (t<'a>, t<'a>) => t<'a> 102 | 103 | @ocaml.doc(" Transform two options into an option of a {!Tuple2}. 104 | 105 | Returns None if either of the aguments is None. 106 | 107 | {2 Examples} 108 | 109 | {[ 110 | Option.both(Some(3004), Some(\"Ant\")) == Some(3004, \"Ant\") 111 | Option.both(Some(3004), None) == None 112 | Option.both(None, Some(\"Ant\")) == None 113 | Option.both(None, None) == None 114 | ]} 115 | ") 116 | let both: (t<'a>, t<'b>) => t<('a, 'b)> 117 | 118 | @ocaml.doc(" Flatten two optional layers into a single optional layer. 119 | 120 | {2 Examples} 121 | 122 | {[ 123 | Option.flatten(Some(Some(4))) == Some(4) 124 | Option.flatten(Some(None)) == None 125 | Option.flatten(None) == None 126 | ]} 127 | ") 128 | let flatten: t> => t<'a> 129 | 130 | @ocaml.doc(" Transform the value inside an option. 131 | 132 | Leaves [None] untouched. 133 | 134 | {2 Examples} 135 | 136 | {[ 137 | Option.map(~f=x => x * x, Some(9)) == Some(81) 138 | Option.map(~f=Int.toString, Some(9)) == Some(\"9\") 139 | Option.map(~f=x => x * x, None) == None 140 | ]} 141 | ") 142 | let map: (t<'a>, ~f: 'a => 'b) => t<'b> 143 | 144 | @ocaml.doc(" Combine two {!Option}s. 145 | 146 | If both options are [Some] returns, as [Some] the result of running [f] on both values. 147 | 148 | If either value is [None], returns [None]. 149 | 150 | {2 Examples} 151 | 152 | {[ 153 | Option.map2(Some(3), Some(4), ~f=Int.add) == Some(7) 154 | Option.map2(Some(3), Some(4), ~f=Tuple.make) == Some(3, 4) 155 | Option.map2(Some(3), None, ~f=Int.add) == None 156 | Option.map2(None, Some(4), ~f=Int.add) == None 157 | ]} 158 | ") 159 | let map2: (t<'a>, t<'b>, ~f: ('a, 'b) => 'c) => t<'c> 160 | 161 | @ocaml.doc(" Chain together many computations that may not return a value. 162 | 163 | It is helpful to see its definition: 164 | 165 | {[ 166 | let andThen = (t, ~f) => 167 | switch t { 168 | | Some(x) => f(x) 169 | | None => None 170 | } 171 | ]} 172 | 173 | This means we only continue with the callback if we have a value. 174 | 175 | For example, say you need to parse some user input as a month: 176 | 177 | {[ 178 | let toValidMonth = (month) => 179 | if 1 <= month && month <= 12 { 180 | Some(month) 181 | } else { 182 | None 183 | } 184 | 185 | let userInput = \"5\" 186 | 187 | Int.fromString(userInput)->Option.andThen(~f=toValidMonth) 188 | ]} 189 | 190 | If [Int.fromString] produces [None] (because the [userInput] was not an 191 | integer) this entire chain of operations will short-circuit and result in 192 | [None]. If [toValidMonth] results in [None], again the chain of 193 | computations will result in [None]. 194 | 195 | {2 Examples} 196 | 197 | {[ 198 | Option.andThen(Some([1, 2, 3]), ~f=Array.first) == Some(1) 199 | Option.andThen(Some([]), ~f=Array.first) == None 200 | ]} 201 | ") 202 | let andThen: (t<'a>, ~f: 'a => t<'b>) => t<'b> 203 | 204 | @ocaml.doc(" Unwrap an [option<'a>] returning [default] if called with [None]. 205 | 206 | This comes in handy when paired with functions like {!Map.get}, 207 | {!Array.first} or {!List.head} which return an {!Option}. 208 | 209 | {b Note:} This can be overused! Many cases are better handled using pattern matching, {!map} or {!andThen}. 210 | 211 | {2 Examples} 212 | 213 | {[ 214 | Option.unwrap(Some(42), ~default=99) == 42 215 | Option.unwrap(None, ~default=99) == 99 216 | Option.unwrap(Map.get(Map.String.empty, \"Tom\"), ~default=\"unknown\") == \"unknown\" 217 | ]} 218 | ") 219 | let unwrap: (t<'a>, ~default: 'a) => 'a 220 | 221 | @ocaml.doc(" Unwrap an [option('a)] returning the enclosed ['a]. 222 | 223 | {b Note} in most situations it is better to use pattern matching, {!unwrap}, {!map} or {!andThen}. 224 | Can you structure your code slightly differently to avoid potentially raising an exception? 225 | 226 | {3 Exceptions} 227 | 228 | Raises an [Invalid_argument] exception if called with [None] 229 | 230 | {2 Examples} 231 | 232 | {[ 233 | Array.first([1, 2, 3])->Option.unwrapUnsafe == 1 234 | Array.first([])->Option.unwrapUnsafe // will raise Invalid_argument 235 | ]} 236 | ") 237 | let unwrapUnsafe: t<'a> => 'a 238 | 239 | @ocaml.doc(" Check if an {!Option} is a [Some]. 240 | 241 | In most situtations you should just use pattern matching instead. 242 | 243 | {2 Examples} 244 | 245 | {[ 246 | Option.isSome(Some(3004)) == true 247 | Option.isSome(None) == false 248 | ]} 249 | ") 250 | let isSome: t<'a> => bool 251 | 252 | @ocaml.doc(" Check if an {!Option} is a [None]. 253 | 254 | In most situtations you should just use pattern matching instead. 255 | 256 | {2 Examples} 257 | 258 | {[ 259 | Option.isNone(Some(3004)) == false 260 | Option.isNone(None) == true 261 | ]} 262 | ") 263 | let isNone: t<'a> => bool 264 | 265 | @ocaml.doc(" Run a function against an [Some(value)], ignores [None]s. 266 | 267 | {2 Examples} 268 | 269 | {[ 270 | Option.tap(Some(\"Dog\"), ~f=Js.log) 271 | (* logs \"Dog\" *) 272 | ]} 273 | ") 274 | let tap: (t<'a>, ~f: 'a => unit) => unit 275 | 276 | @ocaml.doc(" Convert an option to an {!Array}. 277 | 278 | [None] is represented as an empty array and [Some] is represented as an array of one element. 279 | 280 | {2 Examples} 281 | 282 | {[ 283 | Option.toArray(Some(3004)) == [3004] 284 | Option.toArray(None) == [ 285 | ]} 286 | ") 287 | let toArray: t<'a> => array<'a> 288 | 289 | @ocaml.doc(" Convert an option to a {!List}. 290 | 291 | [None] is represented as an empty list and [Some] is represented as a list of one element. 292 | 293 | {2 Examples} 294 | 295 | {[ 296 | Option.toList(Some(3004)) == list{3004} 297 | Option.toList(None) == list{} 298 | ]} 299 | ") 300 | let toList: t<'a> => list<'a> 301 | 302 | @@ocaml.text(" {1 Compare} ") 303 | 304 | @ocaml.doc(" Test two optional values for equality using the provided function. 305 | 306 | {2 Examples} 307 | 308 | {[ 309 | Option.equal(Some(1), Some(1), Int.equal) == true 310 | Option.equal(Some(1), Some(3), Int.equal) == false 311 | Option.equal(Some(1), None, Int.equal) == false 312 | Option.equal(None, None, Int.equal) == true 313 | ]} 314 | ") 315 | let equal: (t<'a>, t<'a>, ('a, 'a) => bool) => bool 316 | 317 | @ocaml.doc(" Compare two optional values using the provided [f] function. 318 | 319 | A [None] is \"less\" than a [Some]. 320 | 321 | {2 Examples} 322 | 323 | {[ 324 | Option.compare(Some(1), Some(3), ~f=Int.compare) == -1 325 | Option.compare(Some(1), None, ~f=Int.compare) == 1 326 | Option.compare(None, None, ~f=Int.compare) == 0 327 | ]} 328 | ") 329 | let compare: (t<'a>, t<'a>, ~f: ('a, 'a) => int) => int 330 | -------------------------------------------------------------------------------- /src/TableclothResult.res: -------------------------------------------------------------------------------- 1 | type t<'ok, 'error> = result<'ok, 'error> 2 | 3 | let ok = a => Ok(a) 4 | 5 | let error = e => Error(e) 6 | 7 | let fromOption = (ma, ~error) => 8 | switch ma { 9 | | None => Error(error) 10 | | Some(right) => Ok(right) 11 | } 12 | 13 | let isError = t => Belt.Result.isError(t) 14 | 15 | let isOk = t => Belt.Result.isOk(t) 16 | 17 | let both = (a, b) => 18 | switch (a, b) { 19 | | (Ok(a'), Ok(b')) => Ok(a', b') 20 | | (Error(a'), _) => Error(a') 21 | | (_, Error(b')) => Error(b') 22 | } 23 | 24 | let flatten = a => 25 | switch a { 26 | | Ok(a') => a' 27 | | Error(error) => Error(error) 28 | } 29 | 30 | let or_ = (a, b) => 31 | switch a { 32 | | Ok(_) => a 33 | | _ => b 34 | } 35 | 36 | let orElse = (a, b) => 37 | switch b { 38 | | Ok(_) => b 39 | | _ => a 40 | } 41 | 42 | let or_else = (a, b) => 43 | switch b { 44 | | Ok(_) => b 45 | | _ => a 46 | } 47 | 48 | let and_ = (a, b) => 49 | switch a { 50 | | Ok(_) => b 51 | | _ => a 52 | } 53 | 54 | let unwrap = (t, ~default) => Belt.Result.getWithDefault(t, default) 55 | 56 | let unwrapLazy = (t, ~default) => 57 | switch t { 58 | | Ok(t') => t' 59 | | Error(_) => Lazy.force(default) 60 | } 61 | 62 | let unwrapUnsafe = t => Belt.Result.getExn(t) 63 | 64 | let unwrapError = (t, ~default) => 65 | switch t { 66 | | Ok(_) => default 67 | | Error(value) => value 68 | } 69 | 70 | let map2 = (a, b, ~f) => 71 | switch (a, b) { 72 | | (Ok(a), Ok(b)) => Ok(f(a, b)) 73 | | (Error(a), _) => Error(a) 74 | | (_, Error(b)) => Error(b) 75 | } 76 | 77 | let values = t => List.fold_right((c, d) => map2(c, d, ~f=(a, b) => list{a, ...b}), t, Ok(list{})) 78 | 79 | let combine = (l: list>): result, 'error> => 80 | TableclothList.foldRight( 81 | ~f=(accum: result, 'error>, value: result<'ok, 'error>): result, 'error> => 82 | map2(~f=(head: 'ok, list: list<'ok>) => list{head, ...list}, value, accum), 83 | ~initial=Ok(list{}), 84 | l, 85 | ) 86 | 87 | let map = (t, ~f) => Belt.Result.map(t, a => f(a)) 88 | 89 | let mapError = (t, ~f) => 90 | switch t { 91 | | Error(error) => Error(f(error)) 92 | | Ok(value) => Ok(value) 93 | } 94 | 95 | let toOption = r => 96 | switch r { 97 | | Ok(v) => Some(v) 98 | | Error(_) => None 99 | } 100 | 101 | let andThen = (t, ~f) => Belt.Result.flatMap(t, a => f(a)) 102 | 103 | let attempt = f => 104 | switch f() { 105 | | value => Ok(value) 106 | | exception error => Error(error) 107 | } 108 | 109 | let tap = (t, ~f) => 110 | switch t { 111 | | Ok(a) => f(a) 112 | | _ => () 113 | } 114 | 115 | let equal = (a, b, equalOk, equalError) => 116 | switch (a, b) { 117 | | (Error(a'), Error(b')) => equalError(a', b') 118 | | (Ok(a'), Ok(b')) => equalOk(a', b') 119 | | _ => false 120 | } 121 | 122 | let compare = ( 123 | a: t<'ok, 'error>, 124 | b: t<'ok, 'error>, 125 | ~f as compareOk: ('ok, 'ok) => int, 126 | ~g as compareError: ('error, 'error) => int, 127 | ): int => 128 | switch (a, b) { 129 | | (Error(a'), Error(b')) => compareError(a', b') 130 | | (Ok(a'), Ok(b')) => compareOk(a', b') 131 | | (Error(_), Ok(_)) => -1 132 | | (Ok(_), Error(_)) => 1 133 | } 134 | -------------------------------------------------------------------------------- /src/TableclothResult.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" A {!Result} is used to represent a computation which may fail. 4 | 5 | A [Result] is a variant, which has a constructor for successful results 6 | [Ok('ok)], and one for unsuccessful results ([Error('error)]). 7 | 8 | {[ 9 | type t<'ok, 'error> = 10 | | Ok('ok) 11 | | Error('error) 12 | ]} 13 | 14 | Here is how you would annotate a [Result] variable whose [Ok] 15 | variant is an integer and whose [Error] variant is a string: 16 | 17 | {[ 18 | let ok: Result.t = Ok(3) 19 | let error: Result.t = Error(\"This computation failed!\") 20 | ]} 21 | 22 | {b Note} The ['error] case can be of {b any} type and while [string] is very common you could also use: 23 | - [Array.t(string)] to allow errors to be accumulated 24 | - [exn], in which case the result type just makes exceptions explicit in the return type 25 | - A variant or polymorphic variant, with one case per possible error. This is means each error can be dealt with explicitly. See {{: https://dev.to/kevanstannard/exploring-rescript-exception-handling-57o3 } this excellent article} for more information on this approach. 26 | 27 | If the function you are writing can only fail in a single obvious way, maybe you want an {!Option} instead. 28 | ") 29 | 30 | type t<'ok, 'error> = result<'ok, 'error> 31 | 32 | @@ocaml.text(" {1 Create} ") 33 | 34 | @ocaml.doc(" A function alternative to the [Ok] constructor which can be used in places where 35 | the constructor isn't permitted or functions like {!List.map}. 36 | 37 | {2 Examples} 38 | 39 | {[ 40 | String.reverse(\"desserts\") ->Result.ok == Ok(\"stressed\") 41 | Array.map([1, 2, 3], ~f=Result.ok) == [Ok(1), Ok(2), Ok(3)] 42 | ]} 43 | ") 44 | let ok: 'ok => t<'ok, 'error> 45 | 46 | @ocaml.doc(" A function alternative to the [Error] constructor which can be used in places where 47 | the constructor isn't permitted such as at the of a {!Fun.pipe} or functions like {!List.map}. 48 | 49 | {b Note} 50 | 51 | In Rescript you {b can} use constructors with the fast pipe ([->]). 52 | 53 | {[ 54 | 5->Ok == Ok(5) 55 | ]} 56 | 57 | See the {{: https://reasonml.github.io/docs/en/pipe-first#pipe-into-variants} Rescript docs } for more. 58 | 59 | {2 Examples} 60 | 61 | {[ 62 | Int.negate(3)->Result.error == Error(-3) 63 | Array.map([1, 2, 3], ~f=Result.error) == [Error(1), Error(2), Error(3)] 64 | ]} 65 | ") 66 | let error: 'error => t<'ok, 'error> 67 | 68 | @ocaml.doc(" Run the provided function and wrap the returned value in a {!Result}, catching any exceptions raised. 69 | 70 | {2 Examples} 71 | 72 | {[ 73 | Result.attempt(() => 5 / 0) // returns Error(Division_by_zero) 74 | 75 | Array.map([1, 2, 3], ~f=Result.ok) == [Ok(1), Ok(2), Ok(3)] 76 | 77 | let numbers = [1, 2, 3] 78 | Result.attempt(() => numbers[3]) // returns Error(Assert_failure) 79 | ]} 80 | ") 81 | let attempt: (unit => 'ok) => t<'ok, exn> 82 | 83 | @ocaml.doc(" Convert an {!Option} to a {!Result} where a [Some(value)] becomes [Ok(value)] and a [None] becomes [Error(error)]. 84 | 85 | {2 Examples} 86 | 87 | {[ 88 | Result.fromOption(Some(84), ~error=\"Greater than 100\") == Ok(84) 89 | 90 | Result.fromOption(None, ~error=\"Greater than 100\") == Error(\"Greater than 100\") 91 | ]} 92 | ") 93 | let fromOption: (option<'ok>, ~error: 'error) => t<'ok, 'error> 94 | 95 | @ocaml.doc(" Check if a {!Result} is an [Ok]. 96 | 97 | Useful when you want to perform some side effect based on the presence of 98 | an [Ok] like logging. 99 | 100 | {b Note} if you need access to the contained value rather than doing 101 | [Result.isOk] followed by {!Result.unwrapUnsafe} its safer and just as 102 | convenient to use pattern matching directly or use one of {!Result.andThen} 103 | or {!Result.map} 104 | 105 | {2 Examples} 106 | 107 | {[ 108 | Result.isOk(Ok(3)) == true 109 | Result.isOk(Error(3)) == false 110 | ]} 111 | ") 112 | let isOk: t<_, _> => bool 113 | 114 | @ocaml.doc(" Check if a {!Result} is an [Error]. 115 | 116 | Useful when you want to perform some side effect based on the presence of 117 | an [Error] like logging. 118 | 119 | {b Note} if you need access to the contained value rather than doing 120 | {!Result.isOk} followed by {!Result.unwrapUnsafe} its safer and just as 121 | convenient to use pattern matching directly or use one of {!Result.andThen} 122 | or {!Result.map} 123 | 124 | {2 Examples} 125 | 126 | {[ 127 | Result.isError(Ok(3)) == false 128 | Result.isError(Error(3)) == true 129 | ]} 130 | ") 131 | let isError: t<_, _> => bool 132 | 133 | @ocaml.doc(" Returns the first argument if it {!isError}, otherwise return the second argument. 134 | 135 | Unlike the {!Bool.and_} operator, the [and_] function does not short-circuit. 136 | When you call [and_], both arguments are evaluated before being passed to the function. 137 | 138 | {2 Examples} 139 | 140 | {[ 141 | Result.and_(Ok(\"Antelope\"), Ok(\"Salmon\")) == Ok(\"Salmon\") 142 | 143 | Result.and_(Error(#UnexpectedBird(\"Finch\")), Ok(\"Salmon\")) 144 | == Error(#UnexpectedBird(\"Finch\")) 145 | 146 | Result.and_(Ok(\"Antelope\"), Error(#UnexpectedBird(\"Finch\"))) 147 | == Error(#UnexpectedBird(\"Finch\")) 148 | 149 | Result.and_(Error(#UnexpectedInvertebrate(\"Honey Bee\")), Error(#UnexpectedBird(\"Finch\"))) 150 | == Error(#UnexpectedInvertebrate(\"Honey Bee\")) 151 | ]} 152 | ") 153 | let and_: (t<'ok, 'error>, t<'ok, 'error>) => t<'ok, 'error> 154 | 155 | @ocaml.doc(" Return the first argument if it {!isOk}, otherwise return the second. 156 | 157 | Unlike the built in [||] operator, the [or_] function does not short-circuit. 158 | When you call [or_], both arguments are evaluated before being passed to the function. 159 | 160 | {2 Examples} 161 | 162 | {[ 163 | Result.or_(Ok(\"Boar\"), Ok(\"Gecko\")) == Ok(\"Boar\") 164 | Result.or_(Error(#UnexpectedInvertebrate(\"Periwinkle\")), Ok(\"Gecko\")) == Ok(\"Gecko\") 165 | Result.or_(Ok(\"Boar\"), Error(#UnexpectedInvertebrate(\"Periwinkle\"))) == Ok(\"Boar\") 166 | 167 | Result.or_(Error(#UnexpectedInvertebrate(\"Periwinkle\")), Error(#UnexpectedBird(\"Robin\"))) 168 | == Error(#UnexpectedBird(\"Robin\")) 169 | ]} 170 | ") 171 | let or_: (t<'ok, 'error>, t<'ok, 'error>) => t<'ok, 'error> 172 | 173 | @ocaml.doc(" Return the second argument if it {!isOk}, otherwise return the first. 174 | 175 | Like {!or_} but in reverse. Useful when using the [|>] operator 176 | 177 | {2 Examples} 178 | 179 | {[Result.orElse (Ok \"Boar\") (Ok \"Gecko\") = (Ok \"Gecko\")]} 180 | 181 | {[Result.orElse (Error (`UnexpectedInvertabrate \"Periwinkle\")) (Ok \"Gecko\") = (Ok \"Gecko\")]} 182 | 183 | {[Result.orElse (Ok \"Boar\") (Error (`UnexpectedInvertabrate \"Periwinkle\")) = (Ok \"Boar\") ]} 184 | 185 | {[Result.orElse (Error (`UnexpectedInvertabrate \"Periwinkle\")) (Error (`UnexpectedBird \"Robin\")) = (Error (`UnexpectedInvertabrate \"Periwinkle\"))]} 186 | ") 187 | let orElse: (t<'ok, 'error>, t<'ok, 'error>) => t<'ok, 'error> 188 | 189 | let or_else: (t<'ok, 'error>, t<'ok, 'error>) => t<'ok, 'error> 190 | 191 | @ocaml.doc(" Combine two results, if both are [Ok] returns an [Ok] containing a {!Tuple2} of the values. 192 | 193 | If either is an [Error], returns the first [Error]. 194 | 195 | The same as writing [Result.map2(~f=Tuple2.make)]. 196 | 197 | {2 Examples} 198 | 199 | {[ 200 | Result.both(Ok(\"Badger\"), Ok(\"Rhino\")) == Ok(\"Dog\", \"Rhino\") 201 | 202 | Result.both(Error(#UnexpectedBird(\"Flamingo\")), Ok(\"Rhino\")) 203 | == Error(#UnexpectedBird(\"Flamingo\")) 204 | 205 | Result.both(Ok(\"Badger\"), Error(#UnexpectedInvertebrate(\"Blue ringed octopus\"))) 206 | == Error(#UnexpectedInvertebrate(\"Blue ringed octopus\")) 207 | 208 | Result.both( 209 | Error(#UnexpectedBird(\"Flamingo\")), 210 | Error(#UnexpectedInvertebrate(\"Blue ringed octopus\")), 211 | ) == Error(#UnexpectedBird(\"Flamingo\")) 212 | ]} 213 | ") 214 | let both: (t<'a, 'error>, t<'b, 'error>) => t<('a, 'b), 'error> 215 | 216 | @ocaml.doc(" Collapse a nested result, removing one layer of nesting. 217 | 218 | {2 Examples} 219 | 220 | {[ 221 | Result.flatten(Ok(Ok(2))) == Ok(2) 222 | 223 | Result.flatten(Ok(Error(#UnexpectedBird(\"Peregrin falcon\")))) 224 | == Error(#UnexpectedBird(\"Peregrin falcon\")) 225 | 226 | Result.flatten(Error(#UnexpectedInvertebrate(\"Woodlouse\"))) 227 | == Error(#UnexpectedInvertebrate(\"Woodlouse\")) 228 | ]} 229 | ") 230 | let flatten: t, 'error> => t<'ok, 'error> 231 | 232 | @ocaml.doc(" Unwrap a Result using the [~default] value in case of an [Error]. 233 | 234 | {2 Examples} 235 | 236 | {[ 237 | Result.unwrap(Ok(12), ~default=0) == 12 238 | Result.unwrap(Error(#UnexpectedBird(\"Ostrich\")), ~default=0) == 0 239 | ]} 240 | ") 241 | let unwrap: (t<'ok, 'error>, ~default: 'ok) => 'ok 242 | 243 | @ocaml.doc(" Unwrap a Result using the [Lazy.force default] value in case of an [Error] 244 | 245 | {2 Examples} 246 | 247 | {[Result.unwrapLazy ~default:(lazy 0) (Ok 12) = 12]} 248 | 249 | {[Result.unwrapLazy ~default:(lazy 0) ((Error (`UnexpectedBird \"Ostrich\"))) = 0]} 250 | ") 251 | let unwrapLazy: (t<'ok, 'error>, ~default: Lazy.t<'ok>) => 'ok 252 | 253 | @ocaml.doc(" Unwrap a Result, raising an exception in case of an [Error]. 254 | 255 | {3 Exceptions} 256 | 257 | Raises an [Not_found] exception. 258 | 259 | {2 Examples} 260 | 261 | {[ 262 | Result.unwrapUnsafe(Ok(12)) == 12 263 | Result.unwrapUnsafe(Error(\"bad\")) // raises Not_found 264 | ]} 265 | ") 266 | let unwrapUnsafe: t<'ok, _> => 'ok 267 | 268 | @ocaml.doc(" Like {!Result.unwrap} but unwraps an [Error] value instead. 269 | 270 | {2 Examples} 271 | 272 | {[ 273 | Result.unwrapError( 274 | Error(#UnexpectedBird(\"Swallow\")), 275 | ~default=#UnexpectedInvertebrate(\"Ladybird\"), 276 | ) == #UnexpectedBird(\"Swallow\") 277 | 278 | Result.unwrapError(Ok(5), ~default=#UnexpectedInvertebrate(\"Ladybird\")) 279 | == #UnexpectedInvertebrate(\"Ladybird\") 280 | ]} 281 | ") 282 | let unwrapError: (t<'ok, 'error>, ~default: 'error) => 'error 283 | 284 | @ocaml.doc(" Combine two Results. 285 | 286 | If one of the results is an [Error], that becomes the return result. 287 | 288 | If both are [Error] values, returns its first. 289 | 290 | {2 Examples} 291 | 292 | {[ 293 | Result.map2(Ok(7), Ok(3), ~f=Int.add) == Ok(10) 294 | Result.map2(Error(\"A\"), Ok(3), ~f=Int.add) == Error(\"A\") 295 | Result.map2(Ok(7), Error(\"B\"), ~f=Int.add) == Error(\"B\") 296 | Result.map2(Error(\"A\"), Error(\"B\"), ~f=Int.add) == Error(\"A\") 297 | ]} 298 | ") 299 | let map2: (t<'a, 'error>, t<'b, 'error>, ~f: ('a, 'b) => 'c) => t<'c, 'error> 300 | 301 | @ocaml.doc(" If all of the elements of a list are [Ok], returns an [Ok] of the the list of unwrapped values. 302 | 303 | If {b any} of the elements are an [Error], the first one encountered is returned. 304 | 305 | {2 Examples} 306 | 307 | {[ 308 | Result.values(list{Ok(1), Ok(2), Ok(3), Ok(4)}) == Ok(list{1, 2, 3, 4}) 309 | Result.values(list{Ok(1), Error(\"two\"), Ok(3), Error(\"four\")}) == Error(\"two\") 310 | ]} 311 | ") 312 | let values: list> => t, 'error> 313 | 314 | @ocaml.doc(" 315 | [Result.combine(results)] takes a list of [Result] values. If all 316 | the elements in [results] are of the form [Ok x], then [Result.combine] 317 | creates a list [xs] of all the values extracted from their [Ok]s, and returns 318 | [Ok xs] 319 | 320 | If any of the elements in [results] are of the form [Error err], 321 | the first of them is returned as the result of [Result.combine]. 322 | 323 | {2 Examples} 324 | 325 | {[ 326 | Result.combine(list{Ok(1), Ok(2), Ok(3), Ok(4)}) == Ok(list{1, 2, 3, 4}) 327 | Result.combine(list{Ok(1), Error(\"two\"), Ok(3), Error(\"four\")}) == Error(\"two\") 328 | ]} 329 | ") 330 | let combine: list> => result, 'error> 331 | 332 | @ocaml.doc(" Transforms the ['ok] in a result using [f]. Leaves the ['error] untouched. 333 | 334 | {2 Examples} 335 | 336 | {[ 337 | Result.map(Ok(3), ~f=Int.add(1)) == Ok(9) 338 | Result.map(Error(\"three\"), ~f=Int.add(1)) == Error(\"three\") 339 | ]} 340 | ") 341 | let map: (t<'a, 'error>, ~f: 'a => 'b) => t<'b, 'error> 342 | 343 | @ocaml.doc(" Transforms the value in an [Error] using [f]. Leaves an [Ok] untouched. 344 | 345 | {2 Examples} 346 | 347 | {[ 348 | Result.mapError(Ok(3), ~f=String.reverse) == Ok(3) 349 | Result.mapError(Error(\"bad\"), ~f=String.reverse) == Error(\"dab\") 350 | ]} 351 | ") 352 | let mapError: (t<'ok, 'a>, ~f: 'a => 'b) => t<'ok, 'b> 353 | 354 | @ocaml.doc(" Run a function which may fail on a result. 355 | 356 | Short-circuits of called with an [Error]. 357 | 358 | {2 Examples} 359 | 360 | {[ 361 | let reciprical = (x: float): Result.t => 362 | if x == 0.0 { 363 | Error(\"Divide by zero\") 364 | } else { 365 | Ok(1.0 /. x) 366 | } 367 | 368 | let root = (x: float): Result.t => 369 | if x < 0.0 { 370 | Error(\"Cannot be negative\") 371 | } else { 372 | Ok(Float.squareRoot(x)) 373 | } 374 | 375 | Result.andThen(Ok(4.0), ~f=reciprical) == Ok(0.25) 376 | Result.andThen(Error(\"Missing number!\"), ~f=reciprical) == Error(\"Missing number!\") 377 | Result.andThen(Ok(0.0), ~f=reciprical) == Error(\"Divide by zero\") 378 | Result.andThen(Ok(4.0), ~f=root)->Result.andThen(~f=reciprical) == Ok(0.5) 379 | Result.andThen(Ok(-2.0), ~f=root)->Result.andThen(~f=reciprical) == Error(\"Cannot be negative\") 380 | Result.andThen(Ok(0.0), ~f=root)->Result.andThen(~f=reciprical) == Error(\"Divide by zero\") 381 | ]} 382 | ") 383 | let andThen: (t<'a, 'error>, ~f: 'a => t<'b, 'error>) => t<'b, 'error> 384 | 385 | @ocaml.doc(" Run a function against an [Ok(value)], ignores [Error]s. 386 | 387 | {2 Examples} 388 | 389 | {[ 390 | Result.tap(Ok(\"Dog\"), ~f=Js.log) 391 | (* logs \"Dog\" *) 392 | ]} 393 | ") 394 | let tap: (t<'ok, _>, ~f: 'ok => unit) => unit 395 | 396 | @@ocaml.text(" {1 Convert} ") 397 | 398 | @ocaml.doc(" Convert a {!Result} to an {!Option}. 399 | 400 | An [Ok x] becomes [Some x] 401 | 402 | An [Error _] becomes [None] 403 | 404 | {2 Examples} 405 | 406 | {[ 407 | Result.toOption(Ok(42)) == Some(42) 408 | Result.toOption(Error(\"Missing number!\")) == None 409 | ]} 410 | ") 411 | let toOption: t<'ok, _> => option<'ok> 412 | 413 | @@ocaml.text(" {1 Compare} ") 414 | 415 | @ocaml.doc(" Test two results for equality using the provided functions. 416 | 417 | {2 Examples} 418 | 419 | {[ 420 | Result.equal(Ok(3), Ok(3), Int.equal, String.equal) == true 421 | Result.equal(Ok(3), Ok(4), Int.equal, String.equal) == false 422 | Result.equal(Error(\"Fail\"), Error(\"Fail\"), Int.equal, String.equal) == true 423 | Result.equal(Error(\"Expected error\"), Error(\"Unexpected error\"), Int.equal, String.equal) == false 424 | Result.equal(Error(\"Fail\"), Ok(4), Int.equal, String.equal) == false 425 | ]} 426 | ") 427 | let equal: (t<'ok, 'error>, t<'ok, 'error>, ('ok, 'ok) => bool, ('error, 'error) => bool) => bool 428 | 429 | @ocaml.doc(" Compare results for using the provided functions. 430 | [f] will be used to compare [Ok]'s and [g] will be used on [Error]s. 431 | 432 | In the case when one of the results is an [Error] and one is [Ok], [Error]s are considered 'less' then [Ok]s. 433 | 434 | {2 Examples} 435 | 436 | {[ 437 | Result.compare(Ok(3), Ok(3), ~f=Int.compare, ~g=String.compare) == 0 438 | Result.compare(Ok(3), Ok(4), ~f=Int.compare, ~g=String.compare) == -1 439 | Result.compare(Error(\"Fail\"), Error(\"Fail\"), ~f=Int.compare, ~g=String.compare) == 0 440 | Result.compare(Error(\"Fail\"), Ok(4), ~f=Int.compare, ~g=String.compare) == -1 441 | Result.compare(Ok(4), Error(\"Fail\"), ~f=Int.compare, ~g=String.compare) == 1 442 | 443 | Result.compare( 444 | Error(\"Expected error\"), 445 | Error(\"Unexpected error\"), 446 | ~f=Int.compare, 447 | ~g=String.compare 448 | ) == -1 449 | ]} 450 | ") 451 | let compare: ( 452 | t<'ok, 'error>, 453 | t<'ok, 'error>, 454 | ~f: ('ok, 'ok) => int, 455 | ~g: ('error, 'error) => int, 456 | ) => int 457 | 458 | -------------------------------------------------------------------------------- /src/TableclothSet.res: -------------------------------------------------------------------------------- 1 | type t<'a, 'id> = Belt.Set.t<'a, 'id> 2 | 3 | let empty = comparator => Belt.Set.make(~id=Internal.toBeltComparator(comparator)) 4 | 5 | let singleton = (element: 'a, comparator: TableclothComparator.s<'a, 'identity>): t< 6 | 'a, 7 | 'identity, 8 | > => Belt.Set.fromArray(~id=Internal.toBeltComparator(comparator), [element]) 9 | 10 | let fromArray = (elements: array<'a>, comparator: TableclothComparator.s<'a, 'identity>): t< 11 | 'a, 12 | 'identity, 13 | > => Belt.Set.fromArray(~id=Internal.toBeltComparator(comparator), elements) 14 | 15 | let fromList = (elements: list<'a>, comparator: TableclothComparator.s<'a, 'identity>): t< 16 | 'a, 17 | 'identity, 18 | > => Belt.Set.fromArray(~id=Internal.toBeltComparator(comparator), Array.of_list(elements)) 19 | 20 | let length = t => Belt.Set.size(t) 21 | 22 | let isEmpty = t => Belt.Set.isEmpty(t) 23 | 24 | let includes = (t, a) => Belt.Set.has(t, a) 25 | 26 | let add = (t, a) => Belt.Set.add(t, a) 27 | 28 | let remove = (t, a) => Belt.Set.remove(t, a) 29 | 30 | let difference = (t, a) => Belt.Set.diff(t, a) 31 | 32 | let intersection = (t, a) => Belt.Set.intersect(t, a) 33 | 34 | let union = (t, a) => Belt.Set.union(t, a) 35 | 36 | let filter = (s, ~f) => Belt.Set.keep(s, a => f(a)) 37 | 38 | let partition = (s, ~f) => Belt.Set.partition(s, a => f(a)) 39 | 40 | let find = (s, ~f) => Belt.Set.toArray(s)->Belt.Array.getBy(a => f(a)) 41 | 42 | let all = (s, ~f) => Belt.Set.every(s, a => f(a)) 43 | 44 | let any = (s, ~f) => Belt.Set.some(s, a => f(a)) 45 | 46 | let forEach = (s, ~f) => Belt.Set.forEach(s, a => f(a)) 47 | 48 | let fold = (s, ~initial, ~f) => Belt.Set.reduce(s, initial, (a, b) => f(a, b)) 49 | 50 | let toArray = t => Belt.Set.toArray(t) 51 | 52 | let toList = t => Belt.Set.toList(t) 53 | 54 | module Poly = { 55 | type identity 56 | 57 | type t<'a> = t<'a, identity> 58 | 59 | let fromArray = (type a, a: array): t => 60 | Belt.Set.fromArray( 61 | a, 62 | ~id=module( 63 | { 64 | type t = a 65 | 66 | type identity = identity 67 | 68 | let cmp = Obj.magic(Pervasives.compare) 69 | } 70 | ), 71 | ) 72 | 73 | let fromList = l => fromArray(Array.of_list(l)) 74 | 75 | let empty = () => fromArray([]) 76 | 77 | let singleton = a => fromArray([a]) 78 | } 79 | 80 | module Int = { 81 | type identity 82 | 83 | type t = t 84 | 85 | let fromArray = a => Obj.magic(Poly.fromArray(a)) 86 | 87 | let empty = fromArray([]) 88 | 89 | let singleton = a => fromArray([a]) 90 | 91 | let fromList = l => fromArray(Array.of_list(l)) 92 | } 93 | 94 | module String = { 95 | type identity 96 | 97 | type t = t 98 | 99 | let fromArray = a => Obj.magic(Poly.fromArray(a)) 100 | 101 | let empty = fromArray([]) 102 | 103 | let singleton = a => fromArray([a]) 104 | 105 | let fromList = l => fromArray(Array.of_list(l)) 106 | } 107 | -------------------------------------------------------------------------------- /src/TableclothSet.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" A {!Set} represents a collection of unique values. 4 | 5 | [Set] is an immutable data structure which means operations like {!Set.add} and {!Set.remove} do not modify the data structure, but return a new set with the desired changes. 6 | 7 | Since sets of [int]s and [string]s are so common the specialised {!Set.Int} and {!Set.String} modules are available which offer a convenient way to construct new sets. 8 | 9 | Custom data types can be used with sets as long as the module satisfies the {!Comparator.S} interface. 10 | 11 | {[ 12 | module Point = { 13 | type rec t = (int, int) 14 | let compare = Tuple2.compare(~f=Int.compare, ~g=Int.compare) 15 | include Comparator.Make({ 16 | type t = t 17 | let compare = compare 18 | }) 19 | } 20 | 21 | let points = Set.fromArray(module(Point), [(0, 0), (3, 4), (6, 7)]) 22 | ]} 23 | 24 | See the {!Comparator} module for a more details. 25 | ") 26 | 27 | type t<'a, 'id> = Belt.Set.t<'a, 'id> 28 | 29 | @@ocaml.text(" {1 Create} 30 | 31 | You can create a Set by providing a module conform to the {!Comparator.S} signature by using {!empty}, {!singleton}, {!fromList} or {!fromArray}. 32 | 33 | Specialised versions of the {!empty}, {!singleton}, {!fromList} and {!fromArray} functions available in the {!Set.Int} and {!Set.String} sub-modules. 34 | ") 35 | 36 | @ocaml.doc(" A set with nothing in it. 37 | 38 | Often used as an initial value for functions like {!Array.fold} 39 | 40 | {2 Examples} 41 | 42 | {[ 43 | Array.fold( 44 | ['m', 'i', 's', 's', 'i', 's', 's', 'i', 'p', 'p', 'i'], 45 | ~initial=Set.empty(module(Char)), 46 | ~f=Set.add, 47 | )->Set.toArray 48 | == ['i', 'm', 'p', 's'] 49 | ]} 50 | ") 51 | let empty: TableclothComparator.s<'a, 'identity> => t<'a, 'identity> 52 | 53 | @ocaml.doc(" Create a set from a single {!Int}. 54 | 55 | {2 Examples} 56 | 57 | {[ 58 | Set.singleton(7, module(Int)) |> Set.toArray == [7] 59 | ]} 60 | ") 61 | let singleton: ('a, TableclothComparator.s<'a, 'identity>) => t<'a, 'identity> 62 | 63 | @ocaml.doc(" Create a set from an {!Array}. 64 | 65 | {2 Examples} 66 | 67 | {[ 68 | Set.fromArray([\"Ant\", \"Bat\", \"Bat\", \"Goldfish\"], module(String))->Set.toArray 69 | == [\"Ant\", \"Bat\", \"Goldfish\"] 70 | ]} 71 | ") 72 | let fromArray: (array<'a>, TableclothComparator.s<'a, 'identity>) => t<'a, 'identity> 73 | 74 | @ocaml.doc(" Create a set from a {!List}. 75 | 76 | {2 Examples} 77 | 78 | {[ 79 | Set.fromList(list{'A', 'B', 'B', 'G'}, module(Char))->Set.toArray == ['A', 'B', 'G'] 80 | ]} 81 | ") 82 | let fromList: (list<'a>, TableclothComparator.s<'a, 'identity>) => t<'a, 'identity> 83 | 84 | @@ocaml.text(" {1 Basic operations} ") 85 | 86 | @ocaml.doc(" Insert a value into a set. 87 | 88 | {2 Examples} 89 | 90 | {[ 91 | Set.add(Set.Int.fromArray([1, 2]), 3) -> Set.toArray == [1, 2, 3] 92 | Set.add(Set.Int.fromArray([1, 2]), 2) -> Set.toArray == [1, 2] 93 | ]} 94 | ") 95 | let add: (t<'a, 'id>, 'a) => t<'a, 'id> 96 | 97 | @ocaml.doc(" Remove a value from a set, if the set doesn't contain the value anyway, returns the original set. 98 | 99 | {2 Examples} 100 | 101 | {[ 102 | Set.remove(Set.Int.fromArray([1, 2]), 2)->Set.toArray == [1] 103 | 104 | let originalSet = Set.Int.fromArray([1, 2]) 105 | let newSet = Set.remove(originalSet, 3) 106 | originalSet == newSet 107 | ]} 108 | ") 109 | let remove: (t<'a, 'id>, 'a) => t<'a, 'id> 110 | 111 | @ocaml.doc(" Determine if a value is in a set. 112 | 113 | {2 Examples} 114 | 115 | {[ 116 | Set.includes(Set.String.fromArray([\"Ant\", \"Bat\", \"Cat\"]), \"Bat\") == true 117 | ]} 118 | ") 119 | let includes: (t<'a, _>, 'a) => bool 120 | 121 | @ocaml.doc(" Determine the number of elements in a set. 122 | 123 | {2 Examples} 124 | 125 | {[ 126 | Set.length(Set.Int.fromArray([1, 2, 3])) == 3 127 | ]} 128 | ") 129 | let length: t<_, _> => int 130 | 131 | @ocaml.doc(" Returns, as an {!Option}, the first element for which [f] evaluates to [true]. 132 | If [f] doesn't return [true] for any of the elements [find] will return [None]. 133 | 134 | {2 Examples} 135 | 136 | {[ 137 | Set.find(Set.Int.fromArray([1, 3, 4, 8]), ~f=Int.isEven) == Some(4) 138 | Set.find(Set.Int.fromArray([0, 2, 4, 8]), ~f=Int.isOdd) == None 139 | Set.find(Set.Int.empty, ~f=Int.isEven) == None 140 | ]} 141 | ") 142 | let find: (t<'value, _>, ~f: 'value => bool) => option<'value> 143 | 144 | @@ocaml.text(" {1 Query} ") 145 | 146 | @ocaml.doc(" Check if a set is empty. 147 | 148 | {2 Examples} 149 | 150 | {[ 151 | Set.isEmpty(Set.Int.empty) == true 152 | Set.isEmpty(Set.Int.singleton(4)) == false 153 | ]} 154 | ") 155 | let isEmpty: t<_, _> => bool 156 | 157 | @ocaml.doc(" Determine if [f] returns true for [any] values in a set. 158 | 159 | {2 Examples} 160 | 161 | {[ 162 | Set.any(Set.Int.fromArray([2, 3]), ~f=Int.isEven) == true 163 | Set.any(Set.Int.fromArray([1, 3]), ~f=Int.isEven) == false 164 | Set.any(Set.Int.fromArray([]), ~f=Int.isEven) == false 165 | ]} 166 | ") 167 | let any: (t<'value, _>, ~f: 'value => bool) => bool 168 | 169 | @ocaml.doc(" Determine if [f] returns true for [all] values in a set. 170 | 171 | {2 Examples} 172 | 173 | {[ 174 | Set.all(Set.Int.fromArray([2, 4]), ~f=Int.isEven) == true 175 | Set.all(Set.Int.fromArray([2, 3]), ~f=Int.isEven) == false 176 | Set.all(Set.Int.empty, ~f=Int.isEven) == true 177 | ]} 178 | ") 179 | let all: (t<'value, _>, ~f: 'value => bool) => bool 180 | 181 | @@ocaml.text(" {1 Combine} ") 182 | 183 | @ocaml.doc(" Returns a new set with the values from the first set which are not in the second set. 184 | 185 | {2 Examples} 186 | 187 | {[ 188 | Set.difference( 189 | Set.Int.fromArray([1, 2, 5]), 190 | Set.Int.fromArray([2, 3, 4]) 191 | )->Set.toArray == [1, 5] 192 | 193 | Set.difference( 194 | Set.Int.fromArray([2, 3, 4]), 195 | Set.Int.fromArray([1, 2, 5]) 196 | )->Set.toArray == [3, 4] 197 | ]} 198 | ") 199 | let difference: (t<'a, 'id>, t<'a, 'id>) => t<'a, 'id> 200 | 201 | @ocaml.doc(" Get the intersection of two sets. Keeps values that appear in both sets. 202 | 203 | {2 Examples} 204 | 205 | {[ 206 | Set.intersection( 207 | Set.Int.fromArray([1, 2, 5]), 208 | Set.Int.fromArray([2, 3, 4]) 209 | )->Set.toArray == [2] 210 | ]} 211 | ") 212 | let intersection: (t<'a, 'id>, t<'a, 'id>) => t<'a, 'id> 213 | 214 | @ocaml.doc(" Get the union of two sets. Keep all values. 215 | 216 | {2 Examples} 217 | 218 | {[ 219 | Set.union( 220 | Set.Int.fromArray([1, 2, 5]), 221 | Set.Int.fromArray([2, 3, 4]) 222 | )->Set.toArray == [1, 2, 3, 4, 5] 223 | ]} 224 | ") 225 | let union: (t<'a, 'id>, t<'a, 'id>) => t<'a, 'id> 226 | 227 | @@ocaml.text(" {1 Transform} ") 228 | 229 | @ocaml.doc(" Keep elements that [f] returns [true] for. 230 | 231 | {2 Examples} 232 | 233 | {[ 234 | Set.filter(Set.Int.fromArray([1, 2, 3]), ~f=Int.isEven)->Set.toArray == [2] 235 | ]} 236 | ") 237 | let filter: (t<'a, 'id>, ~f: 'a => bool) => t<'a, 'id> 238 | 239 | @ocaml.doc(" Divide a set into two according to [f]. The first set will contain the values 240 | that [f] returns [true] for, values that [f] returns [false] for will end up in the second. 241 | 242 | {2 Examples} 243 | 244 | {[ 245 | let numbers = Set.Int.fromArray([1, 1, 5, 6, 5, 7, 9, 8]) 246 | let (evens, odds) = Set.partition(numbers, ~f=Int.isEven) 247 | Set.toArray(evens) == [6, 8] 248 | Set.toArray(odds) == [1, 5, 7, 9] 249 | ]} 250 | ") 251 | let partition: (t<'a, 'id>, ~f: 'a => bool) => (t<'a, 'id>, t<'a, 'id>) 252 | 253 | @ocaml.doc(" Transform a set into a value which is result of running each element in the set through [f], 254 | where each successive invocation is supplied the return value of the previous. 255 | 256 | See {!Array.fold} for a more in-depth explanation. 257 | 258 | {2 Examples} 259 | 260 | {[ 261 | Set.fold(Set.Int.fromArray([1, 2, 3, 4], ~initial=1, ~f=Int.multiply)) == 24 262 | ]} 263 | ") 264 | let fold: (t<'a, _>, ~initial: 'b, ~f: ('b, 'a) => 'b) => 'b 265 | 266 | @ocaml.doc(" Runs a function [f] against each element of the set. ") 267 | let forEach: (t<'a, _>, ~f: 'a => unit) => unit 268 | 269 | @@ocaml.text(" {1 Convert} ") 270 | 271 | @ocaml.doc(" Converts a set into an {!Array} ") 272 | let toArray: t<'a, _> => array<'a> 273 | 274 | @ocaml.doc(" Converts a set into a {!List}. ") 275 | let toList: t<'a, _> => list<'a> 276 | 277 | @ocaml.doc( 278 | " Construct sets which can hold any data type using the polymorphic [compare] function. " 279 | ) 280 | module Poly: { 281 | type identity 282 | 283 | type t<'a> = t<'a, identity> 284 | 285 | @ocaml.doc(" The empty set. 286 | 287 | A great starting point. 288 | ") 289 | let empty: unit => t<'a> 290 | 291 | @ocaml.doc(" Create a set of a single value 292 | 293 | {2 Examples} 294 | 295 | {[ 296 | Set.Poly.singleton((5, \"Emu\"))->Set.toArray == [(5, \"Emu\")] 297 | ]} 298 | ") 299 | let singleton: 'a => t<'a> 300 | 301 | @ocaml.doc(" Create a set from an {!Array} 302 | 303 | {2 Examples} 304 | 305 | {[ 306 | Set.Poly.fromArray([(1, \"Ant\"), (2, \"Bat\"), (2, \"Bat\")])->Set.toArray 307 | == [(1, \"Ant\"), (2, \"Bat\")] 308 | ]} 309 | ") 310 | let fromArray: array<'a> => t<'a> 311 | 312 | @ocaml.doc(" Create a set from a {!List} 313 | 314 | {2 Examples} 315 | 316 | {[ 317 | Set.Poly.fromList(list{(1, \"Ant\"), (2, \"Bat\"), (2, \"Bat\")})->Set.toArray 318 | == [(1, \"Ant\"), (2, \"Bat\")] 319 | ]} 320 | ") 321 | let fromList: list<'a> => t<'a> 322 | } 323 | 324 | @ocaml.doc(" Construct sets of {!Int}s ") 325 | module Int: { 326 | type identity 327 | 328 | type t = t 329 | 330 | @ocaml.doc(" A set with nothing in it. ") 331 | let empty: t 332 | 333 | @ocaml.doc(" Create a set from a single {!Int} 334 | 335 | {2 Examples} 336 | 337 | {[ 338 | Set.Int.singleton(5)->Set.toArray == [5] 339 | ]} 340 | ") 341 | let singleton: int => t 342 | 343 | @ocaml.doc(" Create a set from an {!Array} 344 | 345 | {2 Examples} 346 | 347 | {[ 348 | Set.Int.fromArray([1, 2, 3, 3, 2, 1, 7])->Set.toArray == [1, 2, 3, 7] 349 | ]} 350 | ") 351 | let fromArray: array => t 352 | 353 | @ocaml.doc(" Create a set from a {!List} 354 | 355 | {2 Examples} 356 | 357 | {[ 358 | Set.Int.fromList(list{1, 2, 3, 3, 2, 1, 7})->Set.toArray == [1, 2, 3, 7] 359 | ]} 360 | ") 361 | let fromList: list => t 362 | } 363 | 364 | @ocaml.doc(" Construct sets of {!String}s ") 365 | module String: { 366 | type identity 367 | 368 | type t = t 369 | 370 | @ocaml.doc(" A set with nothing in it. ") 371 | let empty: t 372 | 373 | @ocaml.doc(" Create a set of a single {!String}. 374 | 375 | {2 Examples} 376 | 377 | {[ 378 | Set.String.singleton(\"Bat\")->Set.toArray == [\"Bat\"] 379 | ]} 380 | ") 381 | let singleton: string => t 382 | 383 | @ocaml.doc(" Create a set from an {!Array}. 384 | 385 | {2 Examples} 386 | 387 | {[ 388 | Set.String.fromArray([\"a\", \"b\", \"g\", \"b\", \"g\", \"a\", \"a\"])->Set.toArray == [\"a\", \"b\", \"g\"] 389 | ]} 390 | ") 391 | let fromArray: array => t 392 | 393 | @ocaml.doc(" Create a set from a {!List}. 394 | 395 | {2 Examples} 396 | 397 | {[ 398 | Set.String.fromList([\"a\", \"b\", \"g\", \"b\", \"g\", \"a\", \"a\"])->Set.toArray == [\"a\", \"b\", \"g\"] 399 | ]} 400 | ") 401 | let fromList: list => t 402 | } 403 | 404 | -------------------------------------------------------------------------------- /src/TableclothString.res: -------------------------------------------------------------------------------- 1 | type t = string 2 | 3 | include TableclothComparator.Make({ 4 | type t = t 5 | 6 | let compare = (a, b) => compare(a, b) 7 | }) 8 | 9 | let initialize = (length, ~f) => 10 | Js.Array.joinWith("", Array.init(length, index => TableclothChar.toString(f(index)))) 11 | 12 | let get = (string: string, index: int) => String.get(string, index) 13 | 14 | let getAt = (string: string, ~index: int) => 15 | if index < 0 || index >= Js.String.length(string) { 16 | None 17 | } else { 18 | Some(String.get(string, index)) 19 | } 20 | 21 | let fromArray = characters => 22 | Js.Array.joinWith( 23 | "", 24 | Array.map(character => Js.String.fromCharCode(TableclothChar.toCode(character)), characters), 25 | ) 26 | 27 | let fromList = t => 28 | Js.Array.joinWith( 29 | "", 30 | Array.map( 31 | character => Js.String.fromCharCode(TableclothChar.toCode(character)), 32 | Array.of_list(t), 33 | ), 34 | ) 35 | 36 | let fromChar = c => Js.String.fromCharCode(TableclothChar.toCode(c)) 37 | 38 | let indexOf = (haystack, needle): option => { 39 | let result = Js.String.indexOf(needle, haystack) 40 | if result == -1 { 41 | None 42 | } else { 43 | Some(result) 44 | } 45 | } 46 | 47 | let indexOfRight = (haystack, needle): option => { 48 | let result = Js.String.lastIndexOf(needle, haystack) 49 | if result == -1 { 50 | None 51 | } else { 52 | Some(result) 53 | } 54 | } 55 | 56 | let isEmpty = t => t == "" 57 | 58 | let length = t => Js.String.length(t) 59 | 60 | let uncons = s => 61 | switch s { 62 | | "" => None 63 | | s => Some(String.get(s, 0), String.sub(s, 1, Js.String.length(s) - 1)) 64 | } 65 | 66 | let dropLeft = (s, ~count) => Js.String.substr(~from=count, s) 67 | 68 | let dropRight = (s, ~count) => 69 | if count < 1 { 70 | s 71 | } else { 72 | Js.String.slice(~from=0, ~to_=-count, s) 73 | } 74 | 75 | let split = (t, ~on) => Array.to_list(Js.String.split(on, t)) 76 | 77 | let endsWith = (t, ~suffix) => Js.String.endsWith(suffix, t) 78 | 79 | let startsWith = (t, ~prefix) => Js.String.startsWith(prefix, t) 80 | 81 | let trim = t => Js.String.trim(t) 82 | 83 | @send external trimLeft: string => string = "trimStart" 84 | 85 | @send external trimRight: string => string = "trimEnd" 86 | 87 | @send external padLeft: (string, int, string) => string = "padStart" 88 | 89 | let padLeft = (string, count, ~with_) => padLeft(string, count, with_) 90 | 91 | @send external padRight: (string, int, string) => string = "padEnd" 92 | 93 | let padRight = (string, count, ~with_) => padRight(string, count, with_) 94 | 95 | let toLowercase = t => Js.String.toLowerCase(t) 96 | 97 | let toUppercase = t => Js.String.toUpperCase(t) 98 | 99 | let uncapitalize = str => 100 | Js.String.toLowerCase(Js.String.charAt(0, str)) ++ Js.String.sliceToEnd(~from=1, str) 101 | 102 | let capitalize = str => 103 | Js.String.toUpperCase(Js.String.charAt(0, str)) ++ Js.String.sliceToEnd(~from=1, str) 104 | 105 | let isCapitalized = s => s == capitalize(s) 106 | 107 | let includes = (t, ~substring) => Js.String.includes(substring, t) 108 | 109 | let repeat = (s, ~count) => Js.String.repeat(count, s) 110 | 111 | let reverse = s => Js.Array.joinWith("", Js.Array.reverseInPlace(Js.String.split("", s))) 112 | 113 | let toArray = (t: string): array => 114 | Js.Array.map( 115 | characterString => Belt.Option.getExn(TableclothChar.fromString(characterString)), 116 | Js.Array.from(Js.String.castToArrayLike(t)), 117 | ) 118 | 119 | let toList = (s: string): list => Belt.List.fromArray(toArray(s)) 120 | 121 | let slice = (~to_=?, t: string, ~from): string => 122 | Js.String.slice(~from, ~to_=Belt.Option.getWithDefault(to_, length(t)), t) 123 | 124 | let insertAt = (t, ~index, ~value) => 125 | Js.String.slice(~from=0, ~to_=index, t) ++ (value ++ Js.String.sliceToEnd(~from=index, t)) 126 | 127 | let forEach = (t, ~f) => Js.Array.forEach(a => f(a), toArray(t)) 128 | 129 | let fold = (t, ~initial, ~f) => Belt.Array.reduce(toArray(t), initial, (a, ch) => f(a, ch)) 130 | 131 | let equal = \"=" 132 | 133 | let compare = compare 134 | -------------------------------------------------------------------------------- /src/TableclothString.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" Functions for working with [\"strings\"] ") 4 | 5 | type t = string 6 | 7 | @@ocaml.text(" {1 Create} 8 | 9 | Strings literals are created with the [\"double quotes\"], [`backticks`] syntax. 10 | {b Warning} If string contains non-ASCII characters, use [`backticks`] 11 | 12 | ") 13 | 14 | @ocaml.doc(" Converts the given character to an equivalent string of length one. ") 15 | let fromChar: char => string 16 | 17 | @ocaml.doc(" Create a string from an {!Array} of characters. 18 | 19 | Note that these must be individual characters in single quotes, not strings of length one. 20 | 21 | {2 Examples} 22 | 23 | {[ 24 | String.fromArray([]) == \"\" 25 | String.fromArray(['a', 'b', 'c']) == \"abc\" 26 | ]} 27 | ") 28 | let fromArray: array => string 29 | 30 | @ocaml.doc(" Create a string from a {!List} of characters. 31 | 32 | Note that these must be individual characters in single quotes, not strings of length one. 33 | 34 | {2 Examples} 35 | 36 | {[ 37 | String.fromList(list{}) == \"\" 38 | String.fromList(list{'a', 'b', 'c'}) == \"abc\" 39 | ]} 40 | ") 41 | let fromList: list => string 42 | 43 | @ocaml.doc(" Create a string by repeating a string [count] time. 44 | 45 | {3 Exceptions} 46 | 47 | If [count] is negative, [String.repeat] throws a [RangeError] exception. 48 | 49 | {2 Examples} 50 | 51 | {[ 52 | String.repeat(\"ok\", ~count=3) == \"okokok\" 53 | String.repeat(\"\", ~count=3) == \"\" 54 | String.repeat(\"ok\", ~count=0) == \"\" 55 | ]} 56 | ") 57 | let repeat: (string, ~count: int) => string 58 | 59 | @ocaml.doc(" Create a string by providing a length and a function to choose characters. 60 | 61 | Returns an empty string if the length is negative. 62 | 63 | {2 Examples} 64 | 65 | {[ 66 | String.initialize(8, ~f=Fun.constant('9')) == \"99999999\" 67 | ]} 68 | ") 69 | let initialize: (int, ~f: int => char) => string 70 | 71 | @@ocaml.text(" {1 Basic operations} ") 72 | 73 | @ocaml.doc(" Get the character at the specified index 74 | 75 | {3 Exceptions} 76 | 77 | If index out of range, throws a [Invalid_argument] exception. 78 | Concider using {!getAt}, it returns an [option] 79 | 80 | {2 Examples} 81 | 82 | {[ 83 | String.get(\"stressed\", 1) == 't' 84 | ]} 85 | 86 | ") 87 | let get: (string, int) => char 88 | 89 | @ocaml.doc(" Get the character at [~index] ") 90 | let getAt: (string, ~index: int) => option 91 | 92 | @ocaml.doc(" Reverse a string 93 | 94 | {2 Examples} 95 | 96 | {[ 97 | String.reverse(\"stressed\") == \"desserts\" 98 | ]} 99 | ") 100 | let reverse: string => string 101 | 102 | @ocaml.doc(" Extract a substring from the specified indicies. 103 | 104 | See {!Array.slice}. 105 | ") 106 | let slice: (~to_: int=?, string, ~from: int) => string 107 | 108 | @@ocaml.text(" {1 Query} ") 109 | 110 | @ocaml.doc(" Check if a string is empty ") 111 | let isEmpty: string => bool 112 | 113 | @ocaml.doc(" Returns the length of the given string. 114 | 115 | {2 Examples} 116 | 117 | {[ 118 | String.length(\"abc\") == 3 119 | ]} 120 | ") 121 | let length: string => int 122 | 123 | @ocaml.doc(" See if the string starts with [prefix]. 124 | 125 | {2 Examples} 126 | 127 | {[ 128 | String.startsWith(\"theory\", ~prefix=\"the\") == true 129 | String.startsWith(\"theory\", ~prefix=\"ory\") == false 130 | ]} 131 | ") 132 | let startsWith: (string, ~prefix: string) => bool 133 | 134 | @ocaml.doc(" See if the string ends with [suffix]. 135 | 136 | {2 Examples} 137 | 138 | {[ 139 | String.endsWith(\"theory\", ~suffix=\"the\") == false 140 | String.endsWith(\"theory\", ~suffix=\"ory\") == true 141 | ]} 142 | ") 143 | let endsWith: (string, ~suffix: string) => bool 144 | 145 | @ocaml.doc(" Check if one string appears within another 146 | 147 | {2 Examples} 148 | 149 | {[ 150 | String.includes(\"team\", ~substring=\"tea\") == true 151 | String.includes(\"team\", ~substring=\"i\") == false 152 | String.includes(\"ABC\", ~substring=\"\") == true 153 | ]} 154 | ") 155 | let includes: (string, ~substring: string) => bool 156 | 157 | @ocaml.doc(" Test if the first letter of a string is upper case. 158 | 159 | {2 Examples} 160 | 161 | {[ 162 | String.isCapitalized(\"Anastasia\") == true 163 | String.isCapitalized(\"\") == false 164 | ]} 165 | ") 166 | let isCapitalized: string => bool 167 | 168 | @ocaml.doc(" Drop [count] characters from the left side of a string. 169 | 170 | {2 Examples} 171 | 172 | {[ 173 | String.dropLeft(\"abcdefg\", ~count=3) == \"defg\" 174 | String.dropLeft(\"abcdefg\", ~count=0) == \"abcdefg\" 175 | String.dropLeft(\"abcdefg\", ~count=7) == \"\" 176 | String.dropLeft(\"abcdefg\", ~count=-2) == \"fg\" 177 | String.dropLeft(\"abcdefg\", ~count=8) == \"\" 178 | ]} 179 | ") 180 | let dropLeft: (string, ~count: int) => string 181 | 182 | @ocaml.doc(" Drop [count] characters from the right side of a string. 183 | 184 | {2 Examples} 185 | 186 | {[ 187 | String.dropRight(\"abcdefg\", ~count=3) == \"abcd\" 188 | String.dropRight(\"abcdefg\", ~count=0) == \"abcdefg\" 189 | String.dropRight(\"abcdefg\", ~count=7) == \"\" 190 | String.dropRight(\"abcdefg\", ~count=-2) == \"abcdefg\" 191 | String.dropRight(\"abcdefg\", ~count=8) == \"\" 192 | ]} 193 | ") 194 | let dropRight: (string, ~count: int) => string 195 | 196 | @ocaml.doc(" Returns the index of the first occurrence of [string] or None if string has no occurences of [string] 197 | 198 | {2 Examples} 199 | 200 | {[ 201 | String.indexOf(\"Hello World World\", \"World\") == Some(6) 202 | String.indexOf(\"Hello World World\", \"Bye\") == None 203 | ]} 204 | ") 205 | let indexOf: (string, string) => option 206 | 207 | @ocaml.doc(" Returns the index of the last occurrence of [string] or None if string has no occurences of [string] 208 | 209 | {2 Examples} 210 | 211 | {[ 212 | String.indexOfRight(\"Hello World World\", \"World\") == Some(12) 213 | String.indexOfRight(\"Hello World World\", \"Bye\") == None 214 | ]} 215 | ") 216 | let indexOfRight: (string, string) => option 217 | 218 | @ocaml.doc(" Insert a string at [index]. 219 | 220 | The character previously at index will now follow the inserted string. 221 | 222 | {2 Examples} 223 | 224 | {[ 225 | String.insertAt(\"abcde\", ~value=\"**\", ~index=2) == \"ab**cde\" 226 | String.insertAt(\"abcde\", ~value=\"**\", ~index=0) == \"**abcde\" 227 | String.insertAt(\"abcde\", ~value=\"**\", ~index=5) == \"abcde**\" 228 | String.insertAt(\"abcde\", ~value=\"**\", ~index=-2) == \"abc**de\" 229 | String.insertAt(\"abcde\", ~value=\"**\", ~index=-9) == \"**abcde\" 230 | String.insertAt(\"abcde\", ~value=\"**\", ~index=9) == \"abcde**\" 231 | ]} 232 | ") 233 | let insertAt: (string, ~index: int, ~value: t) => string 234 | 235 | @ocaml.doc(" Converts all upper case letters to lower case. 236 | 237 | {2 Examples} 238 | 239 | {[ 240 | String.toLowercase(\"AaBbCc123\") == \"aabbcc123\" 241 | ]} 242 | ") 243 | let toLowercase: string => string 244 | 245 | @ocaml.doc(" Converts all lower case letters to upper case. 246 | 247 | {2 Examples} 248 | 249 | {[ 250 | String.toUppercase(\"AaBbCc123\") == \"AABBCC123\" 251 | ]} 252 | ") 253 | let toUppercase: string => string 254 | 255 | @ocaml.doc(" Converts the first letter to lower case if it is upper case. 256 | 257 | {2 Examples} 258 | 259 | {[ 260 | String.uncapitalize(\"Anastasia\") == \"anastasia\" 261 | ]} 262 | ") 263 | let uncapitalize: string => string 264 | 265 | @ocaml.doc(" Converts the first letter of [s] to lowercase if it is upper case. 266 | 267 | {2 Examples} 268 | 269 | {[ 270 | String.capitalize(\"den\") == \"Den\" 271 | ]} 272 | ") 273 | let capitalize: string => string 274 | 275 | @ocaml.doc(" Removes leading and trailing {{!Char.isWhitespace} whitespace} from a string 276 | 277 | {2 Examples} 278 | 279 | {[ 280 | String.trim(\" abc \") == \"abc\" 281 | String.trim(\" abc def \") == \"abc def\" 282 | String.trim(\"\r\n\t abc \n\n\") == \"abc\" 283 | ]} 284 | ") 285 | let trim: string => string 286 | 287 | @ocaml.doc(" Like {!trim} but only drops characters from the beginning of the string. ") 288 | let trimLeft: string => string 289 | 290 | @ocaml.doc(" Like {!trim} but only drops characters from the end of the string. ") 291 | let trimRight: string => string 292 | 293 | @ocaml.doc(" Pad a string up to a minimum length. 294 | 295 | If the string is shorted than the proivded length, adds [with_] 296 | to the left of the string until the minimum length is met. 297 | 298 | {2 Examples} 299 | 300 | {[ 301 | String.padLeft(\"5\", 3, ~with_=\"0\") == \"005\" 302 | ]} 303 | ") 304 | let padLeft: (string, int, ~with_: string) => string 305 | 306 | @ocaml.doc(" Pad a string up to a minimum length. 307 | 308 | If the string is shorted than the proivded length, adds [with_] 309 | to the left of the string until the minimum length is met. 310 | 311 | {2 Examples} 312 | 313 | {[ 314 | String.padRight(\"Ahh\", 7, ~with_=\"h\") == \"Ahhhhhh\" 315 | ]} 316 | ") 317 | let padRight: (string, int, ~with_: string) => string 318 | 319 | @ocaml.doc(" Returns, as an {!Option}, a tuple containing the first {!Char} and the remaining String. 320 | 321 | If given an empty string, returns [None]. 322 | 323 | {2 Examples} 324 | 325 | {[ 326 | String.uncons(\"abcde\") == Some('a', \"bcde\") 327 | String.uncons(\"a\") == Some('a', \"\") 328 | String.uncons(\"\") == None 329 | ]} 330 | ") 331 | let uncons: string => option<(char, string)> 332 | 333 | @ocaml.doc(" Divide a string into a list of strings, splitting whenever [on] is encountered. 334 | 335 | {2 Examples} 336 | 337 | {[ 338 | String.split(\"a/b/c\", ~on=\"/\") == list{\"a\", \"b\", \"c\"} 339 | String.split(\"a--b--c\", ~on=\"--\") == list{\"a\", \"b\", \"c\"} 340 | String.split(\"abc\", ~on=\"/\") == list{\"abc\"} 341 | String.split(\"\", ~on=\"/\") == list{\"\"} 342 | String.split(\"abc\", ~on=\"\") == list{\"a\", \"b\", \"c\"} 343 | ]} 344 | ") 345 | let split: (string, ~on: string) => list 346 | 347 | @@ocaml.text(" {1 Iterate} ") 348 | 349 | @ocaml.doc(" Run [f] on each character in a string. ") 350 | let forEach: (string, ~f: char => unit) => unit 351 | 352 | @ocaml.doc(" Like {!Array.fold} but the elements are {!Char}s ") 353 | let fold: (string, ~initial: 'a, ~f: ('a, char) => 'a) => 'a 354 | 355 | @@ocaml.text(" {1 Convert} ") 356 | 357 | @ocaml.doc(" Returns an {!Array} of the individual characters in the given string. 358 | 359 | {2 Examples} 360 | 361 | {[ 362 | String.toArray(\"\") == [] 363 | String.toArray(\"abc\") == ['a', 'b', 'c'] 364 | ]} 365 | ") 366 | let toArray: string => array 367 | 368 | @ocaml.doc(" Returns a {!List} of the individual characters in the given string. 369 | 370 | {2 Examples} 371 | 372 | {[ 373 | String.toList(\"\") == list{} 374 | String.toList(\"abc\") == list{'a', 'b', 'c'} 375 | ]} 376 | ") 377 | let toList: string => list 378 | 379 | @@ocaml.text(" {1 Compare} ") 380 | 381 | @ocaml.doc(" Test two string for equality. ") 382 | let equal: (string, string) => bool 383 | 384 | @ocaml.doc(" Compare two strings. Strings use 'dictionary' ordering. 385 | 1 386 | Also known as {{: https://en.wikipedia.org/wiki/Lexicographical_order } lexicographical ordering }. 387 | 388 | {2 Examples} 389 | 390 | {[ 391 | String.compare(\"Z\", \"A\") == 1 392 | String.compare(\"Be\", \"Bee\") == -1 393 | String.compare(\"Pear\", \"pear\") == 1 394 | String.compare(\"Peach\", \"Peach\") == 0 395 | ]} 396 | ") 397 | let compare: (string, string) => int 398 | 399 | @ocaml.doc(" The unique identity for {!Comparator} ") 400 | type identity 401 | 402 | let comparator: TableclothComparator.t 403 | 404 | -------------------------------------------------------------------------------- /src/TableclothTuple2.res: -------------------------------------------------------------------------------- 1 | type t<'a, 'b> = ('a, 'b) 2 | 3 | let make = (a, b) => (a, b) 4 | 5 | let fromArray = array => 6 | switch array { 7 | | [] | [_] => None 8 | | [a, b] => Some(a, b) 9 | | _ => Some(array[0], array[1]) 10 | } 11 | 12 | let fromList = list => 13 | switch list { 14 | | list{} | list{_} => None 15 | | list{a, b, ..._rest} => Some(a, b) 16 | } 17 | 18 | let first = ((a, _)) => a 19 | 20 | let second = ((_, b)) => b 21 | 22 | let mapFirst = ((a, b), ~f) => (f(a), b) 23 | 24 | let mapSecond = ((a, b), ~f) => (a, f(b)) 25 | 26 | let mapEach = ((a, b), ~f, ~g) => (f(a), g(b)) 27 | 28 | let mapAll = ((a1, a2), ~f) => (f(a1), f(a2)) 29 | 30 | let swap = ((a, b)) => (b, a) 31 | 32 | let toArray = ((a, b)) => [a, b] 33 | 34 | let toList = ((a, b)) => list{a, b} 35 | 36 | let equal = ((a, b), (a', b'), equalFirst, equalSecond) => equalFirst(a, a') && equalSecond(b, b') 37 | 38 | let compare = ((a, b), (a', b'), ~f as compareFirst, ~g as compareSecond) => 39 | switch compareFirst(a, a') { 40 | | 0 => compareSecond(b, b') 41 | | result => result 42 | } 43 | -------------------------------------------------------------------------------- /src/TableclothTuple2.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" Functions for manipulating pairs of values ") 4 | 5 | type t<'a, 'b> = ('a, 'b) 6 | 7 | @@ocaml.text(" {1 Create} ") 8 | 9 | @ocaml.doc(" Create a two-tuple with the given values. 10 | 11 | The values do not have to be of the same type. 12 | 13 | {2 Examples} 14 | 15 | {[ 16 | Tuple2.make(3, \"Clementine\") == (3, \"Clementine\") 17 | ]} 18 | ") 19 | let make: ('a, 'b) => ('a, 'b) 20 | 21 | @ocaml.doc(" Create a tuple from the first two elements of an {!Array}. 22 | 23 | If the array is longer than two elements, the extra elements are ignored. 24 | 25 | If the array is less than two elements, returns [None]. 26 | 27 | {2 Examples} 28 | 29 | {[ 30 | Tuple2.fromArray([1, 2]) == Some(1, 2) 31 | Tuple2.fromArray([1]) == None 32 | Tuple2.fromArray([4, 5, 6]) == Some(4, 5) 33 | ]} 34 | ") 35 | let fromArray: array<'a> => option<('a, 'a)> 36 | 37 | @ocaml.doc(" Create a tuple from the first two elements of a {!List}. 38 | 39 | If the list is longer than two elements, the extra elements are ignored. 40 | 41 | If the list is less than two elements, returns [None]. 42 | 43 | {2 Examples} 44 | 45 | {[ 46 | Tuple2.fromList(list{1, 2}) == Some(1, 2) 47 | Tuple2.fromList(list{1}) == None 48 | Tuple2.fromList(list{4, 5, 6}) == Some(4, 5) 49 | ]} 50 | ") 51 | let fromList: list<'a> => option<('a, 'a)> 52 | 53 | @ocaml.doc(" Extract the first value from a tuple. 54 | 55 | {2 Examples} 56 | 57 | {[ 58 | Tuple2.first((3, 4)) == 3 59 | Tuple2.first((\"john\", \"doe\")) == \"john\" 60 | ]} 61 | ") 62 | let first: (('a, 'b)) => 'a 63 | 64 | @ocaml.doc(" Extract the second value from a tuple. 65 | 66 | {2 Examples} 67 | 68 | {[ 69 | Tuple2.second((3, 4)) == 4 70 | Tuple2.second((\"john\", \"doe\")) == \"doe\" 71 | ]} 72 | ") 73 | let second: (('a, 'b)) => 'b 74 | 75 | @@ocaml.text(" {1 Transform} ") 76 | 77 | @ocaml.doc(" Transform the {!first} value in a tuple. 78 | 79 | {2 Examples} 80 | 81 | {[ 82 | Tuple2.mapFirst((\"stressed\", 16), ~f=String.reverse) == (\"desserts\", 16) 83 | Tuple2.mapFirst((\"stressed\", 16), ~f=String.length) == (8, 16) 84 | ]} 85 | ") 86 | let mapFirst: (('a, 'b), ~f: 'a => 'x) => ('x, 'b) 87 | 88 | @ocaml.doc(" Transform the second value in a tuple. 89 | 90 | {2 Examples} 91 | 92 | {[ 93 | Tuple2.mapSecond((\"stressed\", 16.), ~f=Float.squareRoot) == (\"stressed\", 4.) 94 | Tuple2.mapSecond(~f=Int.negate, (\"stressed\", 16)) == (\"stressed\", -16) 95 | ]} 96 | ") 97 | let mapSecond: (('a, 'b), ~f: 'b => 'c) => ('a, 'c) 98 | 99 | @ocaml.doc(" Transform both values of a tuple, using [f] for the first value and [g] for the second. 100 | 101 | {2 Examples} 102 | 103 | {[ 104 | Tuple2.mapEach((\"stressed\", 16.), ~f=String.reverse, ~g=Float.squareRoot) == (\"desserts\", 4.) 105 | Tuple2.mapEach(~f=String.length, ~g=Int.negate, (\"stressed\", 16)) == (8, -16) 106 | ]} 107 | ") 108 | let mapEach: (('a, 'b), ~f: 'a => 'x, ~g: 'b => 'y) => ('x, 'y) 109 | 110 | @ocaml.doc(" Transform both of the values of a tuple using the same function. 111 | 112 | [mapAll] can only be used on tuples which have the same type for each value. 113 | 114 | {2 Examples} 115 | 116 | {[ 117 | Tuple2.mapAll(~f=Int.add(1), (3, 4)) == (4, 5) 118 | Tuple2.mapAll((\"was\", \"stressed\"), ~f=String.length) == (3, 8) 119 | ]} 120 | ") 121 | let mapAll: (('a, 'a), ~f: 'a => 'b) => ('b, 'b) 122 | 123 | @ocaml.doc(" Switches the first and second values of a tuple. 124 | 125 | {2 Examples} 126 | 127 | {[ 128 | Tuple2.swap((3, 4)) == (4, 3) 129 | Tuple2.swap((\"stressed\", 16)) == (16, \"stressed\") 130 | ]} 131 | ") 132 | let swap: (('a, 'b)) => ('b, 'a) 133 | 134 | @@ocaml.text(" {1 Convert} ") 135 | 136 | @ocaml.doc(" Turns a tuple into an {!Array} of length two. 137 | 138 | This function can only be used on tuples which have the same type for each value. 139 | 140 | {2 Examples} 141 | 142 | {[ 143 | Tuple2.toArray((3, 4)) == [3, 4] 144 | Tuple2.toArray((\"was\", \"stressed\")) == [\"was\", \"stressed\"] 145 | ]} 146 | ") 147 | let toArray: (('a, 'a)) => array<'a> 148 | 149 | @ocaml.doc(" Turns a tuple into a list of length two. This function can only be used on tuples which have the same type for each value. 150 | 151 | {2 Examples} 152 | 153 | {[ 154 | Tuple2.toList((3, 4)) == list{3, 4} 155 | Tuple2.toList((\"was\", \"stressed\")) == list{\"was\", \"stressed\"} 156 | ]} 157 | ") 158 | let toList: (('a, 'a)) => list<'a> 159 | 160 | @ocaml.doc(" Test two {!Tuple2}s for equality, using the provided functions to test the 161 | first and second components. 162 | 163 | {2 Examples} 164 | 165 | {[ 166 | Tuple2.equal((1, \"Fox\"), (1, \"Fox\"), Int.equal, String.equal) == true 167 | Tuple2.equal((1, \"Fox\"), (2, \"Hen\"), Int.equal, String.equal) == false 168 | ]} 169 | ") 170 | let equal: (t<'a, 'b>, t<'a, 'b>, ('a, 'a) => bool, ('b, 'b) => bool) => bool 171 | 172 | @ocaml.doc(" Compare two {!Tuple2}s, using the provided [f] function to compare the first components. 173 | Then, if the first components are equal, the second components are compared with [g]. 174 | 175 | {2 Examples} 176 | 177 | {[ 178 | Tuple2.compare((1, \"Fox\"), (1, \"Fox\"), ~f=Int.compare, ~g=String.compare) == 0 179 | Tuple2.compare((1, \"Fox\"), (1, \"Eel\"), ~f=Int.compare, ~g=String.compare) == 1 180 | Tuple2.compare((1, \"Fox\"), (2, \"Hen\"), ~f=Int.compare, ~g=String.compare) == -1 181 | ]} 182 | ") 183 | let compare: (t<'a, 'b>, t<'a, 'b>, ~f: ('a, 'a) => int, ~g: ('b, 'b) => int) => int 184 | -------------------------------------------------------------------------------- /src/TableclothTuple3.res: -------------------------------------------------------------------------------- 1 | type t<'a, 'b, 'c> = ('a, 'b, 'c) 2 | 3 | let make = (a, b, c) => (a, b, c) 4 | 5 | let fromArray = array => 6 | switch array { 7 | | [] | [_] | [_, _] => None 8 | | [a, b, c] => Some(a, b, c) 9 | | _ => Some(array[0], array[1], array[2]) 10 | } 11 | 12 | let fromList = list => 13 | switch list { 14 | | list{} | list{_} | list{_, _} => None 15 | | list{a, b, c, ..._rest} => Some(a, b, c) 16 | } 17 | 18 | let first = ((a, _, _)) => a 19 | 20 | let second = ((_, b, _)) => b 21 | 22 | let third = ((_, _, c)) => c 23 | 24 | let initial = ((a, b, _)) => (a, b) 25 | 26 | let tail = ((_, b, c)) => (b, c) 27 | 28 | let mapFirst = ((a, b, c), ~f) => (f(a), b, c) 29 | 30 | let mapSecond = ((a, b, c), ~f) => (a, f(b), c) 31 | 32 | let mapThird = ((a, b, c), ~f) => (a, b, f(c)) 33 | 34 | let mapEach = ((a, b, c), ~f, ~g, ~h) => (f(a), g(b), h(c)) 35 | 36 | let mapAll = ((a1, a2, a3), ~f) => (f(a1), f(a2), f(a3)) 37 | 38 | let rotateLeft = ((a, b, c)) => (b, c, a) 39 | 40 | let rotateRight = ((a, b, c)) => (c, a, b) 41 | 42 | let toArray = ((a, b, c)) => [a, b, c] 43 | 44 | let toList = ((a, b, c)) => list{a, b, c} 45 | 46 | let equal = ((a, b, c), (a', b', c'), equalFirst, equalSecond, equalThird) => 47 | equalFirst(a, a') && (equalSecond(b, b') && equalThird(c, c')) 48 | 49 | let compare = ( 50 | (a, b, c), 51 | (a', b', c'), 52 | ~f as compareFirst, 53 | ~g as compareSecond, 54 | ~h as compareThird, 55 | ) => 56 | switch compareFirst(a, a') { 57 | | 0 => 58 | switch compareSecond(b, b') { 59 | | 0 => compareThird(c, c') 60 | | result => result 61 | } 62 | | result => result 63 | } 64 | -------------------------------------------------------------------------------- /src/TableclothTuple3.resi: -------------------------------------------------------------------------------- 1 | @@ocaml.text(" ") 2 | 3 | @@ocaml.text(" Functions for manipulating trios of values ") 4 | 5 | type t<'a, 'b, 'c> = ('a, 'b, 'c) 6 | 7 | @@ocaml.text(" {1 Create} ") 8 | 9 | @ocaml.doc(" Create a {!Tuple3}. 10 | 11 | {2 Examples} 12 | 13 | {[ 14 | Tuple3.make(3, \"cat\", false) == (3, \"cat\", false) 15 | 16 | Array.map3(~f=Tuple3.make, [1, 2, 3], ['a', 'b', 'c'], [4., 5., 6.]) 17 | == [ 18 | (1, 'a', 4.), 19 | (2, 'b', 5.), 20 | (3, 'c', 6.), 21 | ] 22 | ]} 23 | ") 24 | let make: ('a, 'b, 'c) => ('a, 'b, 'c) 25 | 26 | @ocaml.doc(" Create a tuple from the first two elements of an {!Array}. 27 | 28 | If the array is longer than three elements, the extra elements are ignored. 29 | 30 | If the array is less than three elements, returns [None] 31 | 32 | {2 Examples} 33 | 34 | {[ 35 | Tuple3.fromArray([1, 2, 3]) == Some(1, 2, 3) 36 | Tuple3.fromArray([1, 2]) == None 37 | Tuple3.fromArray([4, 5, 6, 7]) == Some(4, 5, 6) 38 | ]} 39 | ") 40 | let fromArray: array<'a> => option<('a, 'a, 'a)> 41 | 42 | @ocaml.doc(" Create a tuple from the first two elements of a {!List}. 43 | 44 | If the list is longer than two elements, the extra elements are ignored. 45 | 46 | If the list is less than two elements, returns [None] 47 | 48 | {2 Examples} 49 | 50 | {[ 51 | Tuple3.fromList(list{1, 2, 3}) == Some(1, 2, 3) 52 | Tuple3.fromList(list{1, 2}) == None 53 | Tuple3.fromList(list{4, 5, 6, 7}) == Some(4, 5, 6) 54 | ]} 55 | ") 56 | let fromList: list<'a> => option<('a, 'a, 'a)> 57 | 58 | @ocaml.doc(" Extract the first value from a tuple. 59 | 60 | {2 Examples} 61 | 62 | {[ 63 | Tuple3.first((3, 4, 5)) == 3 64 | Tuple3.first((\"john\", \"danger\", \"doe\")) == \"john\" 65 | ]} 66 | ") 67 | let first: (('a, 'b, 'c)) => 'a 68 | 69 | @ocaml.doc(" Extract the second value from a tuple. 70 | 71 | {2 Examples} 72 | 73 | {[ 74 | Tuple3.second((3, 4, 5)) == 4 75 | Tuple3.second((\"john\", \"danger\", \"doe\")) == \"danger\" 76 | ]} 77 | ") 78 | let second: (('a, 'b, 'c)) => 'b 79 | 80 | @ocaml.doc(" Extract the third value from a tuple. 81 | 82 | {2 Examples} 83 | 84 | {[ 85 | Tuple3.third((3, 4, 5)) == 5 86 | Tuple3.third((\"john\", \"danger\", \"doe\")) == \"doe\" 87 | ]} 88 | ") 89 | let third: (('a, 'b, 'c)) => 'c 90 | 91 | @ocaml.doc(" Extract the first and second values of a {!Tuple3} as a {!Tuple2}. 92 | 93 | {2 Examples} 94 | 95 | {[ 96 | Tuple3.initial((3, \"stressed\", false)) == (3, \"stressed\") 97 | Tuple3.initial((\"john\", 16, true)) == (\"john\", 16) 98 | ]} 99 | ") 100 | let initial: (('a, 'b, 'c)) => ('a, 'b) 101 | 102 | @ocaml.doc(" Extract the second and third values of a {!Tuple3} as a {!Tuple2}. 103 | 104 | {2 Examples} 105 | 106 | {[ 107 | Tuple3.tail((3, \"stressed\", false)) == (\"stressed\", false) 108 | Tuple3.tail((\"john\", 16, true)) == (16, true) 109 | ]} 110 | ") 111 | let tail: (('a, 'b, 'c)) => ('b, 'c) 112 | 113 | @@ocaml.text(" {1 Modify} ") 114 | 115 | @ocaml.doc(" Move each value in the tuple one position to the left, moving the value in the first position into the last position. 116 | 117 | {2 Examples} 118 | 119 | {[ 120 | Tuple3.rotateLeft((3, 4, 5)) == (4, 5, 3) 121 | Tuple3.rotateLeft((\"was\", \"stressed\", \"then\")) == (\"stressed\", \"then\", \"was\") 122 | ]} 123 | ") 124 | let rotateLeft: (('a, 'b, 'c)) => ('b, 'c, 'a) 125 | 126 | @ocaml.doc(" Move each value in the tuple one position to the right, moving the value in the last position into the first position. 127 | 128 | {2 Examples} 129 | 130 | {[ 131 | Tuple3.rotateRight((3, 4, 5)) == (5, 3, 4) 132 | Tuple3.rotateRight((\"was\", \"stressed\", \"then\")) == (\"then\", \"was\", \"stressed\") 133 | ]} 134 | ") 135 | let rotateRight: (('a, 'b, 'c)) => ('c, 'a, 'b) 136 | 137 | @ocaml.doc(" Transform the first value in a tuple. 138 | 139 | {2 Examples} 140 | 141 | {[ 142 | Tuple3.mapFirst((\"stressed\", 16, false), ~f=String.reverse) == (\"desserts\", 16, false) 143 | Tuple3.mapFirst((\"stressed\", 16, false), ~f=String.length) == (8, 16, false) 144 | ]} 145 | ") 146 | let mapFirst: (('a, 'b, 'c), ~f: 'a => 'x) => ('x, 'b, 'c) 147 | 148 | @ocaml.doc(" Transform the second value in a tuple. 149 | 150 | {2 Examples} 151 | 152 | {[ 153 | Tuple3.mapSecond((\"stressed\", 16., false), ~f=Float.squareRoot) == (\"stressed\", 4., false) 154 | Tuple3.mapSecond(~f=Int.negate, (\"stressed\", 16, false)) == (\"stressed\", -16, false) 155 | ]} 156 | ") 157 | let mapSecond: (('a, 'b, 'c), ~f: 'b => 'y) => ('a, 'y, 'c) 158 | 159 | @ocaml.doc(" Transform the third value in a tuple. 160 | 161 | {2 Examples} 162 | 163 | {[ 164 | Tuple3.mapThird((\"stressed\", 16, false), ~f=Bool.not) == (\"stressed\", 16, true) 165 | ]} 166 | ") 167 | let mapThird: (('a, 'b, 'c), ~f: 'c => 'z) => ('a, 'b, 'z) 168 | 169 | @ocaml.doc(" Transform each value in a tuple by applying [f] to the {!first} value, [g] to the {!second} value and [h] to the {!third} value. 170 | 171 | {2 Examples} 172 | 173 | {[ 174 | Tuple3.mapEach( 175 | (\"stressed\", 16., false) 176 | ~f=String.reverse, 177 | ~g=Float.squareRoot, 178 | ~h=Bool.not) 179 | == (\"desserts\", 4., true) 180 | ]} 181 | ") 182 | let mapEach: (('a, 'b, 'c), ~f: 'a => 'x, ~g: 'b => 'y, ~h: 'c => 'z) => ('x, 'y, 'z) 183 | 184 | @ocaml.doc(" Transform all the values of a tuple using the same function. 185 | 186 | [mapAll] can only be used on tuples which have the same type for each value. 187 | 188 | {2 Examples} 189 | 190 | {[ 191 | Tuple3.mapAll((9., 16., 25.), ~f=Float.squareRoot) == (3., 4., 5.) 192 | Tuple3.mapAll((\"was\", \"stressed\", \"then\"), ~f=String.length) == (3, 8, 4) 193 | ]} 194 | ") 195 | let mapAll: (('a, 'a, 'a), ~f: 'a => 'b) => ('b, 'b, 'b) 196 | 197 | @ocaml.doc(" Turns a tuple into a {!List} of length three. 198 | 199 | This function can only be used on tuples which have the same type for each value. 200 | 201 | {2 Examples} 202 | 203 | {[ 204 | Tuple3.toArray((3, 4, 5)) == [3, 4, 5] 205 | Tuple3.toArray((\"was\", \"stressed\", \"then\")) == [\"was\", \"stressed\", \"then\"] 206 | ]} 207 | ") 208 | let toArray: (('a, 'a, 'a)) => array<'a> 209 | 210 | @ocaml.doc(" Turns a tuple into a {!List} of length three. 211 | 212 | This function can only be used on tuples which have the same type for each value. 213 | 214 | {2 Examples} 215 | 216 | {[ 217 | Tuple3.toList((3, 4, 5)) == list{3, 4, 5} 218 | Tuple3.toList((\"was\", \"stressed\", \"then\")) == list{\"was\", \"stressed\", \"then\"} 219 | ]} 220 | ") 221 | let toList: (('a, 'a, 'a)) => list<'a> 222 | 223 | @ocaml.doc(" Test two {!Tuple3}s for equality, using the provided functions to test the 224 | first, second and third components. 225 | 226 | {2 Examples} 227 | 228 | {[ 229 | Tuple3.equal((1, \"Fox\", 'j'), (1, \"Fox\", 'k'), Int.equal, String.equal, Char.equal) == false 230 | Tuple3.equal((1, \"Fox\", 'j'), (2, \"Hen\", 'j'), Int.equal, String.equal, Char.equal) == false 231 | ]} 232 | ") 233 | let equal: ( 234 | t<'a, 'b, 'c>, 235 | t<'a, 'b, 'c>, 236 | ('a, 'a) => bool, 237 | ('b, 'b) => bool, 238 | ('c, 'c) => bool, 239 | ) => bool 240 | 241 | @ocaml.doc(" Compare two {!Tuple3}s, using [f] to compare the first 242 | components then, if the first components are equal, the second components are compared with [g], 243 | then the third components are compared with [h]. 244 | 245 | {2 Examples} 246 | 247 | {[ 248 | Tuple3.compare((1, \"Fox\", 'j'), (1, \"Fox\", 'j'), ~f=Int.compare, ~g=String.compare, ~h=Char.compare) == 0 249 | Tuple3.compare((1, \"Fox\", 'j'), (1, \"Eel\", 'j'), ~f=Int.compare, ~g=String.compare, ~h=Char.compare) == 1 250 | Tuple3.compare((1, \"Fox\", 'j'), (2, \"Fox\", 'm'), ~f=Int.compare, ~g=String.compare, ~h=Char.compare) == -1 251 | ]} 252 | ") 253 | let compare: ( 254 | t<'a, 'b, 'c>, 255 | t<'a, 'b, 'c>, 256 | ~f: ('a, 'a) => int, 257 | ~g: ('b, 'b) => int, 258 | ~h: ('c, 'c) => int, 259 | ) => int 260 | 261 | -------------------------------------------------------------------------------- /test/BoolTest.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | open Jest 3 | open Expect 4 | 5 | open Bool 6 | describe("fromInt", () => { 7 | test("converts zero to Some(false)", () => expect(fromInt(0))->toEqual(Some(false))) 8 | test("converts one to Some(true)", () => expect(fromInt(1))->toEqual(Some(true))) 9 | testAll( 10 | "converts everything else to None", 11 | list{Int.minimumValue, -2, -1, 2, Int.maximumValue}, 12 | int => expect(fromInt(int))->toEqual(None), 13 | ) 14 | }) 15 | describe("fromString", () => { 16 | test("converts string to Some(true)", () => expect(fromString("true"))->toEqual(Some(true))) 17 | test("converts string to Some(false)", () => expect(fromString("false"))->toEqual(Some(false))) 18 | test("capital True returns None", () => expect(fromString("True"))->toEqual(None)) 19 | test("non-string returns None", () => expect(fromString("1"))->toEqual(None)) 20 | }) 21 | 22 | describe("xor", () => { 23 | test("Returns [true] for xor of args true true", () => expect(xor(true, true))->toEqual(false)) 24 | test("Returns [true] for xor of args true false]", () => expect(xor(true, false))->toEqual(true)) 25 | test("Returns [true] for xor of args false true", () => expect(xor(false, true))->toEqual(true)) 26 | test("Returns [false] for xor of args false false", () => 27 | expect(xor(false, false))->toEqual(false) 28 | ) 29 | }) 30 | 31 | describe("not", () => { 32 | test("Returns negation of true, returns false", () => expect(!true)->toEqual(false)) 33 | test("Returns negation of false, returns true", () => expect(!false)->toEqual(true)) 34 | }) 35 | 36 | describe("toString", () => { 37 | test("Returns string of bool, returns true as string", () => 38 | expect(toString(true))->toEqual("true") 39 | ) 40 | test("Returns string of bool, returns false as string", () => 41 | expect(toString(false))->toEqual("false") 42 | ) 43 | }) 44 | 45 | describe("toInt", () => { 46 | test("Returns 1 for arg true", () => expect(toInt(true))->toEqual(1)) 47 | test("Returns 0 for arg false", () => expect(toInt(false))->toEqual(0)) 48 | }) 49 | describe("equal", () => { 50 | test("Returns true for equal args true true", () => expect(equal(true, true))->toEqual(true)) 51 | test("Returns true equal for args false false", () => expect(equal(false, false))->toEqual(true)) 52 | test("Returns false for inqueal args true false", () => 53 | expect(equal(true, false))->toEqual(false) 54 | ) 55 | }) 56 | 57 | describe("compare", () => { 58 | test("Returns int 0 to describe comparison of args true true", () => 59 | expect(compare(true, true))->toEqual(0) 60 | ) 61 | test("Returns int 1 to describe comparison of args true false", () => 62 | expect(compare(true, false))->toEqual(1) 63 | ) 64 | test("Returns int -1 to describe comparison of args false true", () => 65 | expect(compare(false, true))->toEqual(-1) 66 | ) 67 | test("Returns int 0 to describe comparison of args false false", () => 68 | expect(compare(false, false))->toEqual(0) 69 | ) 70 | }) 71 | -------------------------------------------------------------------------------- /test/CharTest.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | 3 | open Jest 4 | open Expect 5 | 6 | open Char 7 | test("toCode", () => expect(toCode('a'))->toEqual(97)) 8 | describe("fromCode", () => { 9 | test("valid ASCII codes return the corresponding character", () => 10 | expect(fromCode(97))->toEqual(Some('a')) 11 | ) 12 | test("negative integers return None", () => expect(fromCode(-1))->toEqual(None)) 13 | test("integers greater than 255 return None", () => expect(fromCode(256))->toEqual(None)) 14 | }) 15 | test("toString", () => expect(toString('a'))->toEqual("a")) 16 | describe("fromString", () => { 17 | test("one-length string return Some", () => expect(fromString("a"))->toEqual(Some('a'))) 18 | test("multi character strings return None", () => expect(fromString("abc"))->toEqual(None)) 19 | test("zero length strings return None", () => expect(fromString(""))->toEqual(None)) 20 | }) 21 | describe("toLowercase", () => { 22 | test("converts uppercase ASCII characters to lowercase", () => 23 | expect(toLowercase('A'))->toEqual('a') 24 | ) 25 | test("perserves lowercase characters", () => expect(toLowercase('a'))->toEqual('a')) 26 | test("perserves non-alphabet characters", () => expect(toLowercase('7'))->toEqual('7')) 27 | test("perserves non-ASCII characters", () => expect(toLowercase('\233'))->toEqual('\233')) 28 | }) 29 | describe("toUppercase", () => { 30 | test("converts lowercase ASCII characters to uppercase", () => 31 | expect(toUppercase('a'))->toEqual('A') 32 | ) 33 | test("perserves uppercase characters", () => expect(toUppercase('A'))->toEqual('A')) 34 | test("perserves non-alphabet characters", () => expect(toUppercase('7'))->toEqual('7')) 35 | test("perserves non-ASCII characters", () => expect(toUppercase('\233'))->toEqual('\233')) 36 | }) 37 | describe("toDigit", () => { 38 | test("toDigit - converts ASCII characters representing digits into integers", () => 39 | expect(toDigit('0'))->toEqual(Some(0)) 40 | ) 41 | test("toDigit - converts ASCII characters representing digits into integers", () => 42 | expect(toDigit('8'))->toEqual(Some(8)) 43 | ) 44 | test("toDigit - converts ASCII characters representing digits into integers", () => 45 | expect(toDigit('a'))->toEqual(None) 46 | ) 47 | }) 48 | describe("isLowercase", () => { 49 | test("returns true for any lowercase character", () => expect(isLowercase('a'))->toEqual(true)) 50 | test("returns false for all other characters", () => expect(isLowercase('7'))->toEqual(false)) 51 | test("returns false for non-ASCII characters", () => expect(isLowercase('\236'))->toEqual(false)) 52 | }) 53 | describe("isUppercase", () => { 54 | test("returns true for any uppercase character", () => expect(isUppercase('A'))->toEqual(true)) 55 | test("returns false for all other characters", () => expect(isUppercase('7'))->toEqual(false)) 56 | test("returns false for non-ASCII characters", () => expect(isLowercase('\237'))->toEqual(false)) 57 | }) 58 | describe("isLetter", () => { 59 | test("returns true for any ASCII alphabet character", () => expect(isLetter('A'))->toEqual(true)) 60 | testAll("returns false for all other characters", list{'7', ' ', '\n', '\011', '\236'}, char => 61 | expect(isLetter(char))->toEqual(false) 62 | ) 63 | }) 64 | describe("isDigit", () => { 65 | testAll( 66 | "returns true for digits 0-9", 67 | list{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}, 68 | digit => expect(isDigit(digit))->toEqual(true), 69 | ) 70 | test("returns false for all other characters", () => expect(isDigit('a'))->toEqual(false)) 71 | }) 72 | describe("isAlphanumeric", () => { 73 | test("returns true for any alphabet or digit character", () => 74 | expect(isAlphanumeric('A'))->toEqual(true) 75 | ) 76 | test("returns false for all other characters", () => expect(isAlphanumeric('?'))->toEqual(false)) 77 | }) 78 | describe("isPrintable", () => { 79 | test("returns true for a printable character", () => expect(isPrintable('~'))->toEqual(true)) 80 | test("returns false for non-printable character", () => 81 | expect(fromCode(31)->Option.map(~f=isPrintable))->toEqual(Some(false)) 82 | ) 83 | }) 84 | describe("isWhitespace", () => { 85 | test("returns true for any whitespace character", () => expect(isWhitespace(' '))->toEqual(true)) 86 | test("returns false for a non-whitespace character", () => 87 | expect(isWhitespace('a'))->toEqual(false) 88 | ) 89 | }) 90 | -------------------------------------------------------------------------------- /test/ComparatorTest.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | 3 | open Jest 4 | open Expect 5 | 6 | module Book = { 7 | type t = { 8 | isbn: string, 9 | title: string, 10 | } 11 | } 12 | 13 | module BookByIsbn = { 14 | type t = Book.t 15 | 16 | include Comparator.Make({ 17 | type t = t 18 | 19 | let compare = (bookA: Book.t, bookB: Book.t) => String.compare(bookA.isbn, bookB.isbn) 20 | }) 21 | } 22 | 23 | module BookByTitle = { 24 | type t = Book.t 25 | 26 | include Comparator.Make({ 27 | type t = t 28 | 29 | let compare = (bookA: Book.t, bookB: Book.t) => String.compare(bookA.title, bookB.title) 30 | }) 31 | } 32 | 33 | module BookByIsbnThenTitle = { 34 | type t = Book.t 35 | 36 | include Comparator.Make({ 37 | type t = t 38 | 39 | let compare = (bookA: Book.t, bookB: Book.t) => { 40 | let isbnComparison = String.compare(bookA.isbn, bookB.isbn) 41 | if isbnComparison == 0 { 42 | String.compare(bookA.title, bookB.title) 43 | } else { 44 | isbnComparison 45 | } 46 | } 47 | }) 48 | } 49 | 50 | let book = (a: Book.t, b: Book.t): bool => a == b 51 | 52 | let mobyDick: Book.t = {isbn: "9788460767923", title: "Moby Dick or The Whale"} 53 | 54 | let mobyDickReissue: Book.t = {isbn: "9788460767924", title: "Moby Dick or The Whale"} 55 | 56 | let frankenstein: Book.t = {isbn: "9781478198406", title: "Frankenstein"} 57 | 58 | let frankensteinAlternateTitle: Book.t = {isbn: "9781478198406", title: "The Modern Prometheus"} 59 | 60 | describe("Make", () => 61 | test("module documentation example", () => { 62 | let result: list = 63 | Set.fromList(list{frankenstein, frankensteinAlternateTitle}, module(BookByIsbn))->Set.toList 64 | 65 | expect(result)->toEqual(list{frankenstein}) 66 | }) 67 | ) 68 | describe("make", () => 69 | test("module documentation example", () => { 70 | let result: list = Set.toList( 71 | Set.fromList(list{mobyDick, mobyDickReissue}, module(BookByTitle)), 72 | ) 73 | 74 | expect(result)->toEqual(list{mobyDick}) 75 | }) 76 | ) 77 | -------------------------------------------------------------------------------- /test/FunTest.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | 3 | open Jest 4 | open Expect 5 | 6 | test("identity", () => expect(Fun.identity(1))->toEqual(1)) 7 | test("ignore", () => expect(Fun.ignore(1))->toEqual()) 8 | test("constant", () => expect(Fun.constant(1, 2))->toEqual(1)) 9 | test("sequence", () => expect(Fun.sequence(1, 2))->toEqual(2)) 10 | test("flip", () => expect(Fun.flip(Int.subtract, 2, 4))->toEqual(2)) 11 | test("negate", () => { 12 | let num = 5 13 | let greaterThanFour = n => n > 4 14 | expect(Fun.negate(greaterThanFour, num))->toEqual(false) 15 | }) 16 | test("apply", () => expect(Fun.apply(a => a + 1, 1))->toEqual(2)) 17 | let increment = x => x + 1 18 | let double = x => x * 2 19 | test("compose", () => expect(Fun.compose(1, increment, double))->toEqual(4)) 20 | test("composeRight", () => expect(Fun.composeRight(1, increment, double))->toEqual(3)) 21 | test("tap", () => 22 | expect( 23 | Fun.tap( 24 | ~f=Array.reverse, 25 | Fun.tap(~f=numbers => ignore(numbers[1] = 0), Array.filter([1, 3, 2, 5, 4], ~f=Int.isEven)), 26 | ), 27 | )->toEqual([0, 2]) 28 | ) 29 | 30 | test("curry", () => expect(Fun.curry(((a, b)) => a / b, 8, 4))->toEqual(2)) 31 | test("uncurry", () => expect(Fun.uncurry((a, b) => a / b, (8, 4)))->toEqual(2)) 32 | test("curry3", () => { 33 | let tupleAdder = ((a, b, c)) => a + b + c 34 | expect(Fun.curry3(tupleAdder, 3, 4, 5))->toEqual(12) 35 | }) 36 | test("uncurry3", () => { 37 | let curriedAdder = (a, b, c) => a + b + c 38 | expect(Fun.uncurry3(curriedAdder, (3, 4, 5)))->toEqual(12) 39 | }) 40 | -------------------------------------------------------------------------------- /test/IntTest.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | 3 | open Jest 4 | open Expect 5 | 6 | test("zero", () => expect(Int.zero)->toEqual(0)) 7 | test("one", () => expect(Int.one)->toEqual(1)) 8 | test("minimumValue", () => expect(Int.minimumValue - 1)->toEqual(Int.maximumValue)) 9 | test("maximumValue", () => expect(Int.maximumValue + 1)->toEqual(Int.minimumValue)) 10 | describe("add", () => test("add", () => expect(Int.add(3002, 4004))->toEqual(7006))) 11 | describe("subtract", () => test("subtract", () => expect(Int.subtract(4, 3))->toEqual(1))) 12 | describe("multiply", () => test("multiply", () => expect(Int.multiply(2, 7))->toEqual(14))) 13 | describe("divide", () => { 14 | test("divide", () => expect(Int.divide(3, ~by=2))->toEqual(1)) 15 | test("division by zero", () => toThrow(expect(() => Int.divide(3, ~by=0)))) 16 | test("divide", () => 17 | expect({ 18 | open Int 19 | divide(27, ~by=5) 20 | })->toEqual(5) 21 | ) 22 | test("divideFloat", () => 23 | expect({ 24 | open Int 25 | divideFloat(3, ~by=2) 26 | })->toEqual(1.5) 27 | ) 28 | test("divideFloat", () => 29 | expect({ 30 | open Int 31 | divideFloat(27, ~by=5) 32 | })->toEqual(5.4) 33 | ) 34 | test("divideFloat", () => 35 | expect({ 36 | open Int 37 | divideFloat(8, ~by=4) 38 | })->toEqual(2.0) 39 | ) 40 | test("divideFloat by 0", () => 41 | expect( 42 | { 43 | open Int 44 | divideFloat(8, ~by=0) 45 | } == Float.infinity, 46 | )->toEqual(true) 47 | ) 48 | test("divideFloat 0", () => 49 | expect( 50 | { 51 | open Int 52 | divideFloat(-8, ~by=0) 53 | } == Float.negativeInfinity, 54 | )->toEqual(true) 55 | ) 56 | }) 57 | describe("power", () => { 58 | test("power", () => expect(Int.power(~base=7, ~exponent=3))->toEqual(343)) 59 | test("0 base", () => expect(Int.power(~base=0, ~exponent=3))->toEqual(0)) 60 | test("0 exponent", () => expect(Int.power(~base=7, ~exponent=0))->toEqual(1)) 61 | }) 62 | describe("negate", () => { 63 | test("positive number", () => expect(Int.negate(8))->toEqual(-8)) 64 | test("negative number", () => expect(Int.negate(-7))->toEqual(7)) 65 | test("zero", () => expect(Int.negate(0))->toEqual(-0)) 66 | }) 67 | describe("modulo", () => 68 | test("documentation examples", () => 69 | expect(Array.map([-4, -3, -2, -1, 0, 1, 2, 3, 4], ~f=a => Int.modulo(a, ~by=3)))->toEqual([ 70 | 2, 71 | 0, 72 | 1, 73 | 2, 74 | 0, 75 | 1, 76 | 2, 77 | 0, 78 | 1, 79 | ]) 80 | ) 81 | ) 82 | describe("remainder", () => 83 | test("documentation examples", () => 84 | expect(Array.map([-4, -2, -1, 0, 1, 2, 3, 4], ~f=a => Int.remainder(a, ~by=3)))->toEqual([ 85 | -1, 86 | -2, 87 | -1, 88 | 0, 89 | 1, 90 | 2, 91 | 0, 92 | 1, 93 | ]) 94 | ) 95 | ) 96 | describe("absolute", () => { 97 | test("positive number", () => expect(Int.absolute(8))->toEqual(8)) 98 | test("negative number", () => expect(Int.absolute(-7))->toEqual(7)) 99 | test("zero", () => expect(Int.absolute(0))->toEqual(0)) 100 | }) 101 | 102 | describe("minimum", () => { 103 | test("positive numbers", () => expect(Int.minimum(8, 18))->toEqual(8)) 104 | test("with zero", () => expect(Int.minimum(5, 0))->toEqual(0)) 105 | test("negative numbers", () => expect(Int.minimum(-4, -1))->toEqual(-4)) 106 | }) 107 | 108 | describe("maximum", () => { 109 | test("positive numbers", () => expect(Int.maximum(8, 18))->toEqual(18)) 110 | test("with zero", () => expect(Int.maximum(5, 0))->toEqual(5)) 111 | test("negative numbers", () => expect(Int.maximum(-4, -1))->toEqual(-1)) 112 | }) 113 | 114 | describe("isEven", () => { 115 | test("even number", () => expect(Int.isEven(8))->toEqual(true)) 116 | test("odd number", () => expect(Int.isEven(9))->toEqual(false)) 117 | test("zero even", () => expect(Int.isEven(0))->toEqual(true)) 118 | }) 119 | 120 | describe("isOdd", () => { 121 | test("even number", () => expect(Int.isOdd(8))->toEqual(false)) 122 | test("odd number", () => expect(Int.isOdd(9))->toEqual(true)) 123 | test("zero even", () => expect(Int.isOdd(0))->toEqual(false)) 124 | }) 125 | 126 | describe("clamp", () => { 127 | test("in range", () => expect(Int.clamp(~lower=0, ~upper=8, 5))->toEqual(5)) 128 | test("above range", () => expect(Int.clamp(~lower=0, ~upper=8, 9))->toEqual(8)) 129 | test("below range", () => expect(Int.clamp(~lower=2, ~upper=8, 1))->toEqual(2)) 130 | test("above negative range", () => expect(Int.clamp(~lower=-10, ~upper=-5, 5))->toEqual(-5)) 131 | test("below negative range", () => expect(Int.clamp(~lower=-10, ~upper=-5, -15))->toEqual(-10)) 132 | test("invalid arguments", () => toThrow(expect(() => Int.clamp(~lower=7, ~upper=1, 3)))) 133 | }) 134 | describe("inRange", () => { 135 | test("in range", () => expect(Int.inRange(~lower=2, ~upper=4, 3))->toEqual(true)) 136 | test("above range", () => expect(Int.inRange(~lower=2, ~upper=4, 8))->toEqual(false)) 137 | test("below range", () => expect(Int.inRange(~lower=2, ~upper=4, 1))->toEqual(false)) 138 | test("equal to ~upper", () => expect(Int.inRange(~lower=1, ~upper=2, 2))->toEqual(false)) 139 | test("negative range", () => expect(Int.inRange(~lower=-7, ~upper=-5, -6))->toEqual(true)) 140 | test("invalid arguments", () => toThrow(expect(() => Int.inRange(~lower=7, ~upper=1, 3)))) 141 | }) 142 | describe("toFloat", () => { 143 | test("5", () => expect(Int.toFloat(5))->toEqual(5.)) 144 | test("0", () => expect(Int.toFloat(0))->toEqual(0.)) 145 | test("-7", () => expect(Int.toFloat(-7))->toEqual(-7.)) 146 | }) 147 | describe("fromString", () => { 148 | test("0", () => expect(Int.fromString("0"))->toEqual(Some(0))) 149 | test("-0", () => expect(Int.fromString("-0"))->toEqual(Some(-0))) 150 | test("42", () => expect(Int.fromString("42"))->toEqual(Some(42))) 151 | test("123_456", () => expect(Int.fromString("123_456"))->toEqual(Some(123_456))) 152 | test("-42", () => expect(Int.fromString("-42"))->toEqual(Some(-42))) 153 | test("0XFF", () => expect(Int.fromString("0XFF"))->toEqual(Some(255))) 154 | test("0X000A", () => expect(Int.fromString("0X000A"))->toEqual(Some(10))) 155 | test("Infinity", () => expect(Int.fromString("Infinity"))->toEqual(None)) 156 | test("-Infinity", () => expect(Int.fromString("-Infinity"))->toEqual(None)) 157 | test("NaN", () => expect(Int.fromString("NaN"))->toEqual(None)) 158 | test("abc", () => expect(Int.fromString("abc"))->toEqual(None)) 159 | test("--4", () => expect(Int.fromString("--4"))->toEqual(None)) 160 | test("empty string", () => expect(Int.fromString(" "))->toEqual(None)) 161 | }) 162 | describe("toString", () => { 163 | test("positive number", () => expect(Int.toString(1))->toEqual("1")) 164 | test("negative number", () => expect(Int.toString(-1))->toEqual("-1")) 165 | }) 166 | -------------------------------------------------------------------------------- /test/JestStubs.res: -------------------------------------------------------------------------------- 1 | module Coordinate = { 2 | type t = (int, int) 3 | } 4 | 5 | module Student = { 6 | type t = { 7 | id: int, 8 | name: string, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/MapTest.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | 3 | open Jest 4 | open Expect 5 | 6 | module Coordinate: { 7 | type t = (int, int) 8 | 9 | let compare: (t, t) => int 10 | 11 | type identity 12 | 13 | let comparator: Tablecloth.Comparator.comparator 14 | } = { 15 | module T = { 16 | type t = (int, int) 17 | 18 | let compare = (a, b) => Tuple2.compare(a, b, ~f=Int.compare, ~g=Int.compare) 19 | } 20 | 21 | include T 22 | include Tablecloth.Comparator.Make(T) 23 | } 24 | 25 | describe("fromArray", () => { 26 | test("returns map of key value pairs from array of pairs with ordered type key String", () => { 27 | let fromArrayMap = Map.fromArray( 28 | module(String), 29 | [("Cat", 4), ("Owl", 2), ("Fox", 5), ("Frog", 12), ("Camel", 2)], 30 | ) 31 | 32 | let ansList = Map.toList(fromArrayMap) 33 | 34 | ansList->expect->toEqual(list{("Camel", 2), ("Cat", 4), ("Fox", 5), ("Frog", 12), ("Owl", 2)}) 35 | }) 36 | test("returns empty map from empty array of key value pairs", () => { 37 | let fromArrayMap = Map.fromArray(module(String), []) 38 | let ansList = Map.toList(fromArrayMap) 39 | expect(ansList)->toEqual(list{}) 40 | }) 41 | }) 42 | 43 | describe("singleton", () => 44 | test("key value pair", () => { 45 | let singletonMap = Map.singleton(module(Int), ~key=1, ~value="Ant") 46 | 47 | let ans = Map.toList(singletonMap) 48 | expect(ans)->toEqual(list{(1, "Ant")}) 49 | }) 50 | ) 51 | 52 | describe("empty", () => 53 | test("has length zero", () => { 54 | expect(Map.length(Tablecloth.Map.empty(module(Char))))->toEqual(0) 55 | }) 56 | ) 57 | describe("Poly.fromList", () => 58 | test("creates a map from a list", () => { 59 | let map = Map.Poly.fromList(list{(#Ant, "Ant"), (#Bat, "Bat")}) 60 | expect(Map.get(map, #Ant))->toEqual(Some("Ant")) 61 | }) 62 | ) 63 | describe("Int.fromList", () => 64 | test("creates a map from a list", () => { 65 | let map = Map.Int.fromList(list{(1, "Ant"), (2, "Bat")}) 66 | expect(Map.get(map, 1))->toEqual(Some("Ant")) 67 | }) 68 | ) 69 | describe("String.fromList", () => 70 | test("creates a map from a list", () => { 71 | let map = Map.String.fromList(list{("Ant", 1), ("Bat", 1)}) 72 | expect(Map.get(map, "Ant"))->toEqual(Some(1)) 73 | }) 74 | ) 75 | 76 | describe("filterMap", () => 77 | test("maps values by their results and filters out items with no result", () => { 78 | let filterMapMap = Map.String.fromArray([ 79 | ("Cat", 4), 80 | ("Owl", 2), 81 | ("Fox", 5), 82 | ("Frog", 12), 83 | ("Camel", 2), 84 | ]) 85 | 86 | let ansList = 87 | filterMapMap 88 | ->Map.filterMap( 89 | ~f=(~key as _, ~value) => 90 | if value->Int.isEven { 91 | Some(value / 2) 92 | } else { 93 | None 94 | }, 95 | ) 96 | ->Map.toList 97 | 98 | expect(ansList)->toEqual(list{("Camel", 1), ("Cat", 2), ("Frog", 6), ("Owl", 1)}) 99 | }) 100 | ) 101 | -------------------------------------------------------------------------------- /test/OptionTest.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | 3 | open Jest 4 | open Expect 5 | 6 | describe("unwrapUnsafe", () => { 7 | test("returns the wrapped value for a Some", () => 8 | expect(Option.unwrapUnsafe(Some(1)))->toEqual(1) 9 | ) 10 | test("raises for a None", () => toThrow(expect(() => ignore(Option.unwrapUnsafe(None))))) 11 | 12 | // FIXME 13 | // ( 14 | // Invalid_argument("Option.unwrapUnsafe called with None"), 15 | //) 16 | }) 17 | describe("and_", () => { 18 | test("returns second argument", () => expect(Option.and_(Some(1), Some(15)))->toEqual(Some(15))) 19 | 20 | test("returns none", () => expect(Option.and_(None, Some(15)))->toEqual(None)) 21 | 22 | test("returns none", () => expect(Option.and_(Some(1), None))->toEqual(None)) 23 | 24 | test("returns none", () => expect(Option.and_(None, None))->toEqual(None)) 25 | }) 26 | 27 | describe("or_", () => { 28 | test("returns first argument", () => expect(Option.or_(Some(1), Some(15)))->toEqual(Some(1))) 29 | 30 | test("returns second argument some", () => expect(Option.or_(None, Some(15)))->toEqual(Some(15))) 31 | 32 | test("returns first argument some", () => expect(Option.or_(Some(1), None))->toEqual(Some(1))) 33 | 34 | test("returns none", () => expect(Option.or_(None, None))->toEqual(None)) 35 | }) 36 | 37 | describe("orElse", () => { 38 | test("returns second argument", () => expect(Option.orElse(Some(1), Some(15)))->toEqual(Some(15))) 39 | 40 | test("returns second argument", () => expect(Option.orElse(None, Some(15)))->toEqual(Some(15))) 41 | 42 | test("returns first argument some", () => expect(Option.orElse(Some(1), None))->toEqual(Some(1))) 43 | 44 | test("returns none", () => expect(Option.orElse(None, None))->toEqual(None)) 45 | }) 46 | 47 | describe("both", () => { 48 | test("returns both as pair", () => 49 | expect(Option.both(Some(3004), Some("Ant")))->toEqual(Some(3004, "Ant")) 50 | ) 51 | 52 | test("returns none", () => expect(Option.both(None, Some("Ant")))->toEqual(None)) 53 | 54 | test("returns none", () => expect(Option.both(Some(3004), None))->toEqual(None)) 55 | 56 | test("returns none", () => expect(Option.both(None, None))->toEqual(None)) 57 | }) 58 | 59 | describe("flatten", () => { 60 | test("returns option layers as single option layer", () => 61 | expect(Option.flatten(Some(Some(4))))->toEqual(Some(4)) 62 | ) 63 | 64 | test("returns none", () => expect(Option.flatten(Some(None)))->toEqual(None)) 65 | 66 | test("returns none", () => expect(Option.flatten(None))->toEqual(None)) 67 | }) 68 | 69 | describe("map", () => { 70 | test("returns transformed value from inside option arg", () => 71 | expect(Option.map(~f=x => x * x, Some(9)))->toEqual(Some(81)) 72 | ) 73 | 74 | test("returns transformed value from inside option arg", () => 75 | expect(Option.map(~f=Int.toString, Some(9)))->toEqual(Some("9")) 76 | ) 77 | 78 | test("returns none", () => expect(Option.map(~f=x => x * x, None))->toEqual(None)) 79 | }) 80 | 81 | describe("map2", () => { 82 | test("returns transformed value from two option arg", () => 83 | expect(Option.map2(Some(3), Some(4), ~f=Int.add))->toEqual(Some(7)) 84 | ) 85 | 86 | test("returns none", () => expect(Option.map2(Some(3), None, ~f=Int.add))->toEqual(None)) 87 | test("returns none", () => expect(Option.map2(None, Some(4), ~f=Int.add))->toEqual(None)) 88 | }) 89 | 90 | describe("andThen", () => { 91 | test("returns result of callback", () => 92 | expect(Option.andThen(Some(list{1, 2, 3}), ~f=List.head))->toEqual(Some(1)) 93 | ) 94 | 95 | test("returns none", () => expect(Option.andThen(Some(list{}), ~f=List.head))->toEqual(None)) 96 | }) 97 | 98 | describe("unwrap", () => { 99 | test("returns unwrapped [option('a)]", () => 100 | expect(Option.unwrap(~default=99, Some(42)))->toEqual(42) 101 | ) 102 | 103 | test("returns default", () => expect(Option.unwrap(~default=99, None))->toEqual(99)) 104 | }) 105 | 106 | describe("isSome", () => { 107 | test("returns true if is a Some", () => expect(Option.isSome(Some(3004)))->toEqual(true)) 108 | 109 | test("returns false if is a None", () => expect(Option.isSome(None))->toEqual(false)) 110 | }) 111 | 112 | describe("isNone", () => { 113 | test("returns false if is a Some", () => expect(Option.isNone(Some(3004)))->toEqual(false)) 114 | 115 | test("returns true if is a None", () => expect(Option.isNone(None))->toEqual(true)) 116 | }) 117 | 118 | describe("toArray", () => { 119 | test("returns option as array", () => expect(Option.toArray(Some(3004)))->toEqual([3004])) 120 | 121 | test("returns empty array if None", () => expect(Option.toArray(None))->toEqual([])) 122 | }) 123 | 124 | describe("toList", () => { 125 | test("returns option as list", () => expect(Option.toList(Some(3004)))->toEqual(list{3004})) 126 | 127 | test("returns empty list if None", () => expect(Option.toList(None))->toEqual(list{})) 128 | }) 129 | 130 | describe("equal", () => { 131 | test("returns bool true if options are equal", () => 132 | expect(Option.equal(Some(1), Some(1), Int.equal))->toEqual(true) 133 | ) 134 | 135 | test("returns bool true if options are equal", () => 136 | expect(Option.equal(Some(1), Some(3), Int.equal))->toEqual(false) 137 | ) 138 | 139 | test("returns bool true if options are equal", () => 140 | expect(Option.equal(Some(1), None, Int.equal))->toEqual(false) 141 | ) 142 | test("returns bool true if options are equal", () => 143 | expect(Option.equal(None, None, Int.equal))->toEqual(true) 144 | ) 145 | }) 146 | 147 | describe("compare", () => { 148 | test("returns comparative value -1, 0, or 1", () => 149 | expect(Option.compare(Some(1), Some(3), ~f=Int.compare))->toEqual(-1) 150 | ) 151 | 152 | test("returns comparative value -1, 0, or 1", () => 153 | expect(Option.compare(Some(1), None, ~f=Int.compare))->toEqual(1) 154 | ) 155 | 156 | test("returns comparative value -1, 0, or 1", () => 157 | expect(Option.compare(None, None, ~f=Int.compare))->toEqual(0) 158 | ) 159 | }) 160 | -------------------------------------------------------------------------------- /test/ResultTest.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | 3 | open Jest 4 | open Expect 5 | 6 | open Result 7 | describe("ok", () => { 8 | test("returns ok type", () => 9 | expect(Result.ok(String.reverse("desserts")))->toEqual(Ok("stressed")) 10 | ) 11 | test("returns ok type", () => 12 | expect(List.map(list{1, 2, 3}, ~f=Result.ok))->toEqual(list{Ok(1), Ok(2), Ok(3)}) 13 | ) 14 | }) 15 | 16 | describe("error", () => { 17 | test("returns error type", () => expect(Result.error(Int.negate(3)))->toEqual(Error(-3))) 18 | test("returns error type", () => 19 | expect(List.map(list{1, 2, 3}, ~f=Result.error))->toEqual(list{Error(1), Error(2), Error(3)}) 20 | ) 21 | }) 22 | 23 | describe("fromOption", () => { 24 | test("maps None into Error", () => 25 | expect(fromOption(~error="error message", None))->toEqual(Error("error message")) 26 | ) 27 | test("maps Some into Ok", () => 28 | expect(fromOption(~error="error message", Some(10)))->toEqual(Ok(10)) 29 | ) 30 | }) 31 | 32 | describe("isOk", () => { 33 | test("returns true if result is Ok", () => expect(Result.isOk(Ok(3)))->toEqual(true)) 34 | test("returns false if result is Error", () => expect(Result.isOk(Error(3)))->toEqual(false)) 35 | }) 36 | 37 | describe("isError", () => { 38 | test("returns false if result is Ok", () => expect(Result.isError(Ok(3)))->toEqual(false)) 39 | test("returns true if result is Error", () => expect(Result.isError(Error(3)))->toEqual(true)) 40 | }) 41 | 42 | describe("and_", () => { 43 | test("returns second arg if both are Ok", () => 44 | expect(Result.and_(Ok("Antelope"), Ok("Salmon")))->toEqual(Ok("Salmon")) 45 | ) 46 | test("returns first error arg", () => 47 | expect(Result.and_(Error("Finch"), Ok("Salmon")))->toEqual(Error("Finch")) 48 | ) 49 | 50 | test("returns first error arg", () => 51 | expect(Result.and_(Ok("Antelope"), Error("Finch")))->toEqual(Error("Finch")) 52 | ) 53 | 54 | test("returns first error arg", () => 55 | expect(Result.and_(Error("Honey bee"), Error("Finch")))->toEqual(Error("Honey bee")) 56 | ) 57 | }) 58 | 59 | describe("or_", () => { 60 | test("returns first arg if both are Ok", () => 61 | expect(Result.or_(Ok("Boar"), Ok("Gecko")))->toEqual(Ok("Boar")) 62 | ) 63 | test("returns ok arg", () => 64 | expect(Result.or_(Error("Periwinkle"), Ok("Gecko")))->toEqual(Ok("Gecko")) 65 | ) 66 | 67 | test("returns ok arg", () => 68 | expect(Result.or_(Ok("Boar"), Error("Periwinkle")))->toEqual(Ok("Boar")) 69 | ) 70 | 71 | test("returns second arg", () => 72 | expect(Result.or_(Error("Periwinkle"), Error("Robin")))->toEqual(Error("Robin")) 73 | ) 74 | 75 | describe("unwrapLazy", () => 76 | test( 77 | "returns forced default arg if error", 78 | () => expect(Result.unwrapLazy(Error("Periwinkle"), ~default=lazy "Gecko"))->toEqual("Gecko"), 79 | ) 80 | ) 81 | }) 82 | -------------------------------------------------------------------------------- /test/SetTest.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | open JestStubs 3 | 4 | open Jest 5 | open Expect 6 | 7 | module Coordinate = { 8 | include Coordinate 9 | 10 | include Comparator.Make({ 11 | type t = t 12 | 13 | let compare = compare 14 | }) 15 | } 16 | 17 | test("creates a set from a list", () => { 18 | let set = Set.fromList(list{1, 2}, module(Int)) 19 | expect(Set.includes(set, 1))->toEqual(true) 20 | }) 21 | test("fromArray", () => { 22 | let set = Set.fromArray([(0, 0), (0, 1)], module(Coordinate)) 23 | expect(Set.includes(set, (0, 1)))->toEqual(true) 24 | }) 25 | test("union", () => { 26 | let xAxis = Set.fromList(list{(0, 0), (0, 1)}, module(Coordinate)) 27 | let yAxis = Set.fromList(list{(0, 0), (1, 0)}, module(Coordinate)) 28 | let union = Set.union(xAxis, yAxis) 29 | expect(Set.toArray(union))->toEqual([(0, 0), (0, 1), (1, 0)]) 30 | }) 31 | describe("Int", () => 32 | test("creates a set from a list", () => { 33 | let set = Set.Int.fromList(list{1, 2}) 34 | expect(Set.includes(set, 1))->toEqual(true) 35 | }) 36 | ) 37 | describe("String", () => 38 | test("creates a set from a list", () => { 39 | let set = Set.String.fromList(list{"Ant", "Bat"}) 40 | expect(Set.includes(set, "Ant"))->toEqual(true) 41 | }) 42 | ) 43 | -------------------------------------------------------------------------------- /test/StringTest.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | 3 | open Jest 4 | open Expect 5 | 6 | open String 7 | testAll("fromChar", list{('a', "a"), ('z', "z"), (' ', " "), ('\n', "\n")}, ((char, string)) => 8 | expect(fromChar(char))->toEqual(string) 9 | ) 10 | describe("fromArray", () => { 11 | test("creates an empty string from an empty array", () => expect(fromArray([]))->toEqual("")) 12 | test("creates a string of characters", () => 13 | expect(fromArray(['K', 'u', 'b', 'o']))->toEqual("Kubo") 14 | ) 15 | test("creates a string of characters", () => 16 | expect(fromArray([' ', '\n', '\t']))->toEqual(" \n\t") 17 | ) 18 | }) 19 | describe("indexOf", () => { 20 | test("returns some index of the first matching substring", () => 21 | expect(indexOf("hello", "h"))->toEqual(Some(0)) 22 | ) 23 | test("returns the first index even though multiple present", () => 24 | expect(indexOf("hellh", "h"))->toEqual(Some(0)) 25 | ) 26 | test("returns first substring that matches with multiple characters", () => 27 | expect(indexOf("hellh", "ell"))->toEqual(Some(1)) 28 | ) 29 | test("returns None when no substring matches", () => 30 | expect(indexOf("hello", "xy"))->toEqual(None) 31 | ) 32 | }) 33 | describe("indexOfRight", () => { 34 | test("returns some index of the last matching string", () => 35 | expect(indexOfRight("helloh", "oh"))->toEqual(Some(4)) 36 | ) 37 | test("returns the last index even though multiple present", () => 38 | expect(indexOfRight("ohelloh", "oh"))->toEqual(Some(5)) 39 | ) 40 | test("returns None when no character matches", () => 41 | expect(indexOfRight("hello", "x"))->toEqual(None) 42 | ) 43 | }) 44 | describe("fromList", () => { 45 | test("creates an empty string from an empty array", () => expect(fromList(list{}))->toEqual("")) 46 | test("creates a string of characters", () => 47 | expect(fromList(list{'K', 'u', 'b', 'o'}))->toEqual("Kubo") 48 | ) 49 | test("creates a string of characters", () => 50 | expect(fromList(list{' ', '\n', '\t'}))->toEqual(" \n\t") 51 | ) 52 | }) 53 | describe("repeat", () => { 54 | test("returns an empty string for count zero", () => expect(repeat("bun", ~count=0))->toEqual("")) 55 | test("raises for negative count", () => toThrow(expect(() => repeat("bun", ~count=-1)))) 56 | test("returns the input string repeated count times", () => 57 | expect(repeat("bun", ~count=3))->toEqual("bunbunbun") 58 | ) 59 | }) 60 | describe("initialize", () => { 61 | test("returns an empty string for count zero", () => 62 | expect(initialize(0, ~f=i => Fun.constant('A', i)))->toEqual("") 63 | ) 64 | test("raises for negative count", () => 65 | toThrow(expect(() => initialize(-1, ~f=i => Fun.constant('A', i)))) 66 | ) 67 | test("returns the input string repeated count times", () => 68 | expect(initialize(3, ~f=i => Fun.constant('A', i)))->toEqual("AAA") 69 | ) 70 | }) 71 | describe("isEmpty", () => { 72 | test("true for zero length string", () => expect(isEmpty(""))->toEqual(true)) 73 | testAll("false for length > 0 strings", list{"abc", " ", "\n"}, string => 74 | expect(isEmpty(string))->toEqual(false) 75 | ) 76 | }) 77 | test("length empty string", () => expect(String.length(""))->toEqual(0)) 78 | test("length", () => expect(String.length("123"))->toEqual(3)) 79 | test("reverse empty string", () => expect(String.reverse(""))->toEqual("")) 80 | test("reverse", () => expect(String.reverse("stressed"))->toEqual("desserts")) 81 | describe("split", () => { 82 | test("middle", () => expect(String.split("abc", ~on="b"))->toEqual(list{"a", "c"})) 83 | test("start", () => expect(String.split("ab", ~on="a"))->toEqual(list{"", "b"})) 84 | test("end", () => expect(String.split("ab", ~on="b"))->toEqual(list{"a", ""})) 85 | }) 86 | describe("insertAt", () => { 87 | test("middle", () => expect(String.insertAt("abcde", ~value="**", ~index=2))->toEqual("ab**cde")) 88 | test("start", () => expect(String.insertAt("abcde", ~value="**", ~index=0))->toEqual("**abcde")) 89 | test("end", () => expect(String.insertAt("abcde", ~value="**", ~index=5))->toEqual("abcde**")) 90 | test("negative", () => 91 | expect(String.insertAt("abcde", ~value="**", ~index=-2))->toEqual("abc**de") 92 | ) 93 | test("negative overflow", () => 94 | expect(String.insertAt("abcde", ~value="**", ~index=-9))->toEqual("**abcde") 95 | ) 96 | test("overflow", () => 97 | expect(String.insertAt("abcde", ~value="**", ~index=9))->toEqual("abcde**") 98 | ) 99 | }) 100 | test("toArray", () => 101 | expect(String.toArray("Standard"))->toEqual(['S', 't', 'a', 'n', 'd', 'a', 'r', 'd']) 102 | ) 103 | test("toList", () => 104 | expect(String.toList("Standard"))->toEqual(list{'S', 't', 'a', 'n', 'd', 'a', 'r', 'd'}) 105 | ) 106 | -------------------------------------------------------------------------------- /test/Tuple2Test.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | 3 | open Jest 4 | open Expect 5 | 6 | test("make", () => expect(Tuple2.make(3, 4))->toEqual((3, 4))) 7 | test("first", () => expect(Tuple2.first((3, 4)))->toEqual(3)) 8 | test("second", () => expect(Tuple2.second((3, 4)))->toEqual(4)) 9 | test("mapFirst", () => 10 | expect(Tuple2.mapFirst(~f=String.reverse, ("stressed", 16)))->toEqual(("desserts", 16)) 11 | ) 12 | test("mapSecond", () => 13 | expect(Tuple2.mapSecond(~f=Float.squareRoot, ("stressed", 16.)))->toEqual(("stressed", 4.)) 14 | ) 15 | test("mapEach", () => 16 | expect(Tuple2.mapEach(~f=String.reverse, ~g=Float.squareRoot, ("stressed", 16.)))->toEqual(( 17 | "desserts", 18 | 4., 19 | )) 20 | ) 21 | test("mapAll", () => 22 | expect(Tuple2.mapAll(~f=String.reverse, ("was", "stressed")))->toEqual(("saw", "desserts")) 23 | ) 24 | test("swap", () => expect(Tuple2.swap((3, 4)))->toEqual((4, 3))) 25 | test("toArray", () => expect(Tuple2.toArray((3, 4)))->toEqual([3, 4])) 26 | test("toList", () => expect(Tuple2.toList((3, 4)))->toEqual(list{3, 4})) 27 | -------------------------------------------------------------------------------- /test/Tuple3Test.res: -------------------------------------------------------------------------------- 1 | open Tablecloth 2 | open Jest 3 | open Expect 4 | 5 | open Tuple3 6 | test("make", () => expect(make(3, 4, 5))->toEqual((3, 4, 5))) 7 | test("first", () => expect(first((3, 4, 5)))->toEqual(3)) 8 | test("second", () => expect(second((3, 4, 5)))->toEqual(4)) 9 | test("third", () => expect(third((3, 4, 5)))->toEqual(5)) 10 | test("initial", () => expect(initial((3, 4, 5)))->toEqual((3, 4))) 11 | test("tail", () => expect(tail((3, 4, 5)))->toEqual((4, 5))) 12 | test("mapFirst", () => 13 | expect(mapFirst(~f=String.reverse, ("stressed", 16, false)))->toEqual(("desserts", 16, false)) 14 | ) 15 | test("mapSecond", () => 16 | expect(mapSecond(~f=Float.squareRoot, ("stressed", 16., false)))->toEqual(("stressed", 4., false)) 17 | ) 18 | test("mapThird", () => 19 | expect(mapThird(~f=not, ("stressed", 16, false)))->toEqual(("stressed", 16, true)) 20 | ) 21 | test("mapEach", () => 22 | expect( 23 | mapEach(~f=String.reverse, ~g=Float.squareRoot, ~h=not, ("stressed", 16., false)), 24 | )->toEqual(("desserts", 4., true)) 25 | ) 26 | test("mapAll", () => 27 | expect(mapAll(~f=String.reverse, ("was", "stressed", "now")))->toEqual(("saw", "desserts", "won")) 28 | ) 29 | test("rotateLeft", () => expect(rotateLeft((3, 4, 5)))->toEqual((4, 5, 3))) 30 | test("rotateRight", () => expect(rotateRight((3, 4, 5)))->toEqual((5, 3, 4))) 31 | test("toArray", () => expect(toArray((3, 4, 5)))->toEqual([3, 4, 5])) 32 | test("toList", () => expect(toList((3, 4, 5)))->toEqual(list{3, 4, 5})) 33 | --------------------------------------------------------------------------------