diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3f0c45b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "1.5.0" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/README.md b/README.md new file mode 100644 index 0000000..921f348 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# lenient_parse + +[![Package Version](https://img.shields.io/hexpm/v/lenient_parse)](https://hex.pm/packages/lenient_parse) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/lenient_parse/) + +```sh +gleam add lenient_parse@1 +``` +```gleam +import lenient_parse + +pub fn main() { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..29a59f1 --- /dev/null +++ b/gleam.toml @@ -0,0 +1,19 @@ +name = "lenient_parse" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.34.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..e851620 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,11 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/lenient_parse.gleam b/src/lenient_parse.gleam new file mode 100644 index 0000000..fa6b38c --- /dev/null +++ b/src/lenient_parse.gleam @@ -0,0 +1,36 @@ +import gleam/bool +import gleam/float +import gleam/int +import gleam/result +import gleam/string + +/// Converts a string to a float using a more lenient parsing method than gleam's `float.parse()`. It behaves similarly to Python's `float()` built-in function. +/// +/// It attempts to parse the input string in the following order: +/// 1. As a standard float +/// 2. As an integer (which is then converted to a float) +/// 3. As a float with leading or trailing decimal point +/// +/// ## Examples +/// +/// ```gleam +/// lenient_parse.to_float("3.14") // -> Ok(3.14) +/// lenient_parse.to_float("42") // -> Ok(42.0) +/// lenient_parse.to_float(".5") // -> Ok(0.5) +/// lenient_parse.to_float("2.") // -> Ok(2.0) +/// lenient_parse.to_float("dog") // -> Error(Nil) +/// ``` +pub fn to_float(text: String) -> Result(Float, Nil) { + let text = text |> string.trim + + use <- bool.guard(text |> string.is_empty, Error(Nil)) + use _ <- result.try_recover(text |> float.parse) + use _ <- result.try_recover(text |> int.parse |> result.map(int.to_float)) + + case string.first(text), string.last(text) { + Ok("."), _ -> float.parse("0" <> text) + _, Ok(".") -> float.parse(text <> "0") + _, _ -> Error(Nil) + } +} +// TODO: Update doc comment with rules and examples diff --git a/test/lenient_parse_test.gleam b/test/lenient_parse_test.gleam new file mode 100644 index 0000000..361ec68 --- /dev/null +++ b/test/lenient_parse_test.gleam @@ -0,0 +1,100 @@ +import gleeunit +import gleeunit/should +import lenient_parse + +pub fn main() { + gleeunit.main() +} + +pub fn to_float_standard_format_test() { + "1.001" + |> lenient_parse.to_float + |> should.equal(Ok(1.001)) + + "1.00" + |> lenient_parse.to_float + |> should.equal(Ok(1.0)) + + "1.0" + |> lenient_parse.to_float + |> should.equal(Ok(1.0)) + + "0.1" + |> lenient_parse.to_float + |> should.equal(Ok(0.1)) + + "+123.321" + |> lenient_parse.to_float + |> should.equal(Ok(123.321)) + + "-123.321" + |> lenient_parse.to_float + |> should.equal(Ok(-123.321)) +} + +pub fn to_float_int_test() { + "1" + |> lenient_parse.to_float + |> should.equal(Ok(1.0)) +} + +pub fn to_float_leading_trailing_dot_test() { + "1." + |> lenient_parse.to_float + |> should.equal(Ok(1.0)) + + ".1" + |> lenient_parse.to_float + |> should.equal(Ok(0.1)) +} + +pub fn to_float_whitespace_test() { + " 1 " + |> lenient_parse.to_float + |> should.equal(Ok(1.0)) + + " 1.0 " + |> lenient_parse.to_float + |> should.equal(Ok(1.0)) +} + +// TODO: scientific notation +// pub fn to_float_scientific_notation_test() { +// // "420e3" +// // |> lenient_parse.to_float +// // |> should.equal(Ok(1e10)) + +// // "420e-3" +// // |> lenient_parse.to_float +// // |> should.equal(Ok(-2.5e-3)) +// } + +pub fn to_float_shouldnt_parse_test() { + "..1" + |> lenient_parse.to_float + |> should.equal(Error(Nil)) + + "1.." + |> lenient_parse.to_float + |> should.equal(Error(Nil)) + + ".1." + |> lenient_parse.to_float + |> should.equal(Error(Nil)) + + "." + |> lenient_parse.to_float + |> should.equal(Error(Nil)) + + " " + |> lenient_parse.to_float + |> should.equal(Error(Nil)) + + "" + |> lenient_parse.to_float + |> should.equal(Error(Nil)) + + "abc" + |> lenient_parse.to_float + |> should.equal(Error(Nil)) +}