commit a6272848f91d484c1c78949142ddc77056a026ef Author: Hugo Mårdbrink Date: Sun Nov 30 15:44:22 2025 +0100 Initial commit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2b02193 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: watch serve play + +watch: + watchexec -e gleam gleam build + +serve: + python3 dev_server.py + +play: + gleam build + python3 -m http.server 3000 + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e63a703 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Stellar prune +A clicking game where you prune stars and diamonds from the cosmos. Built with Gleam for the gleam game jam of 2025. + +## How to Play +- Click on stars and diamonds as jump up +- Avoid the volatile rockets! + +### Running the Game +```bash +make play +``` +And then play the game on: `http://localhost:3000/`! diff --git a/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour.cache b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour.cache new file mode 100644 index 0000000..1c43dda Binary files /dev/null and b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour.cache differ diff --git a/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour.cache_inline b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour.cache_inline differ diff --git a/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour.cache_meta b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour.cache_meta new file mode 100644 index 0000000..963c8c5 Binary files /dev/null and b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour.cache_meta differ diff --git a/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour@accessibility.cache b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour@accessibility.cache new file mode 100644 index 0000000..5a927a8 Binary files /dev/null and b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour@accessibility.cache differ diff --git a/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour@accessibility.cache_inline b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour@accessibility.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour@accessibility.cache_inline differ diff --git a/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour@accessibility.cache_meta b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour@accessibility.cache_meta new file mode 100644 index 0000000..b367370 Binary files /dev/null and b/build/dev/javascript/gleam_community_colour/_gleam_artefacts/gleam_community@colour@accessibility.cache_meta differ diff --git a/build/dev/javascript/gleam_community_colour/gleam.mjs b/build/dev/javascript/gleam_community_colour/gleam.mjs new file mode 100644 index 0000000..197cbbc --- /dev/null +++ b/build/dev/javascript/gleam_community_colour/gleam.mjs @@ -0,0 +1 @@ +export * from "../prelude.mjs"; diff --git a/build/dev/javascript/gleam_community_colour/gleam_community/colour.mjs b/build/dev/javascript/gleam_community_colour/gleam_community/colour.mjs new file mode 100644 index 0000000..164bd50 --- /dev/null +++ b/build/dev/javascript/gleam_community_colour/gleam_community/colour.mjs @@ -0,0 +1,1444 @@ +import * as $json from "../../gleam_json/gleam/json.mjs"; +import * as $decode from "../../gleam_stdlib/gleam/dynamic/decode.mjs"; +import * as $float from "../../gleam_stdlib/gleam/float.mjs"; +import * as $int from "../../gleam_stdlib/gleam/int.mjs"; +import * as $list from "../../gleam_stdlib/gleam/list.mjs"; +import * as $result from "../../gleam_stdlib/gleam/result.mjs"; +import * as $string from "../../gleam_stdlib/gleam/string.mjs"; +import { Ok, Error, toList, CustomType as $CustomType, makeError, divideFloat } from "../gleam.mjs"; + +const FILEPATH = "src/gleam_community/colour.gleam"; + +class Rgba extends $CustomType { + constructor(r, g, b, a) { + super(); + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } +} + +class Hsla extends $CustomType { + constructor(h, s, l, a) { + super(); + this.h = h; + this.s = s; + this.l = l; + this.a = a; + } +} + +function valid_colour_value(c) { + let $ = (c > 1.0) || (c < 0.0); + if ($) { + return new Error(undefined); + } else { + return new Ok(c); + } +} + +function hue_to_rgb(hue, m1, m2) { + let _block; + if (hue < 0.0) { + _block = hue + 1.0; + } else if (hue > 1.0) { + _block = hue - 1.0; + } else { + _block = hue; + } + let h = _block; + let h_t_6 = h * 6.0; + let h_t_2 = h * 2.0; + let h_t_3 = h * 3.0; + if (h_t_6 < 1.0) { + return m1 + (((m2 - m1) * h) * 6.0); + } else if (h_t_2 < 1.0) { + return m2; + } else if (h_t_3 < 2.0) { + return m1 + (((m2 - m1) * ((2.0 / 3.0) - h)) * 6.0); + } else { + return m1; + } +} + +function hex_string_to_int(hex_string) { + let _block; + if (hex_string.startsWith("#")) { + let hex_number = hex_string.slice(1); + _block = hex_number; + } else if (hex_string.startsWith("0x")) { + let hex_number = hex_string.slice(2); + _block = hex_number; + } else { + _block = hex_string; + } + let hex = _block; + let _pipe = hex; + let _pipe$1 = $string.lowercase(_pipe); + let _pipe$2 = $string.to_graphemes(_pipe$1); + let _pipe$3 = $list.reverse(_pipe$2); + return $list.index_fold( + _pipe$3, + new Ok(0), + (total, char, index) => { + if (total instanceof Ok) { + let v = total[0]; + return $result.try$( + (() => { + if (char === "a") { + return new Ok(10); + } else if (char === "b") { + return new Ok(11); + } else if (char === "c") { + return new Ok(12); + } else if (char === "d") { + return new Ok(13); + } else if (char === "e") { + return new Ok(14); + } else if (char === "f") { + return new Ok(15); + } else { + return $int.parse(char); + } + })(), + (num) => { + return $result.try$( + $int.power(16, $int.to_float(index)), + (base) => { + return new Ok(v + $float.round($int.to_float(num) * base)); + }, + ); + }, + ); + } else { + return total; + } + }, + ); +} + +function hsla_to_rgba(h, s, l, a) { + let _block; + let $ = l <= 0.5; + if ($) { + _block = l * (s + 1.0); + } else { + _block = (l + s) - (l * s); + } + let m2 = _block; + let m1 = (l * 2.0) - m2; + let r = hue_to_rgb(h + (1.0 / 3.0), m1, m2); + let g = hue_to_rgb(h, m1, m2); + let b = hue_to_rgb(h - (1.0 / 3.0), m1, m2); + return [r, g, b, a]; +} + +function rgba_to_hsla(r, g, b, a) { + let min_colour = $float.min(r, $float.min(g, b)); + let max_colour = $float.max(r, $float.max(g, b)); + let _block; + let $ = true; + if (max_colour === r) { + _block = $float.divide(g - b, max_colour - min_colour); + } else if (max_colour === g) { + let _pipe = $float.divide(b - r, max_colour - min_colour); + _block = $result.try$(_pipe, (d) => { return new Ok(2.0 + d); }); + } else { + let _pipe = $float.divide(r - g, max_colour - min_colour); + _block = $result.try$(_pipe, (d) => { return new Ok(4.0 + d); }); + } + let h1 = _block; + let _block$1; + if (h1 instanceof Ok) { + let v = h1[0]; + _block$1 = new Ok(v * (1.0 / 6.0)); + } else { + _block$1 = h1; + } + let h2 = _block$1; + let _block$2; + if (h2 instanceof Ok) { + let v = h2[0]; + if (v < 0.0) { + _block$2 = v + 1.0; + } else { + let v$1 = h2[0]; + _block$2 = v$1; + } + } else { + _block$2 = 0.0; + } + let h3 = _block$2; + let l = (min_colour + max_colour) / 2.0; + let _block$3; + let $1 = true; + if (min_colour === max_colour) { + _block$3 = 0.0; + } else if (l < 0.5) { + _block$3 = divideFloat((max_colour - min_colour), (max_colour + min_colour)); + } else { + _block$3 = divideFloat( + (max_colour - min_colour), + ((2.0 - max_colour) - min_colour) + ); + } + let s = _block$3; + return [h3, s, l, a]; +} + +/** + * Returns a `Result(Colour)` created from the given 8 bit RGB values. + * + * Returns `Error(Nil)` if the supplied RGB values are greater than 255 or less than 0. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_rgb255(255, 0, 0) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function from_rgb255(red, green, blue) { + return $result.try$( + (() => { + let _pipe = red; + let _pipe$1 = $int.to_float(_pipe); + let _pipe$2 = $float.divide(_pipe$1, 255.0); + return $result.try$(_pipe$2, valid_colour_value); + })(), + (r) => { + return $result.try$( + (() => { + let _pipe = green; + let _pipe$1 = $int.to_float(_pipe); + let _pipe$2 = $float.divide(_pipe$1, 255.0); + return $result.try$(_pipe$2, valid_colour_value); + })(), + (g) => { + return $result.try$( + (() => { + let _pipe = blue; + let _pipe$1 = $int.to_float(_pipe); + let _pipe$2 = $float.divide(_pipe$1, 255.0); + return $result.try$(_pipe$2, valid_colour_value); + })(), + (b) => { return new Ok(new Rgba(r, g, b, 1.0)); }, + ); + }, + ); + }, + ); +} + +/** + * Returns `Result(Colour)` created from the given RGB values. + * + * If the supplied RGB values are greater than 1.0 or less than 0.0 returns `Error(Nil)` + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_rgb(1.0, 0.0, 0.0) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function from_rgb(red, green, blue) { + return $result.try$( + valid_colour_value(red), + (r) => { + return $result.try$( + valid_colour_value(green), + (g) => { + return $result.try$( + valid_colour_value(blue), + (b) => { return new Ok(new Rgba(r, g, b, 1.0)); }, + ); + }, + ); + }, + ); +} + +/** + * Returns `Result(Colour)` created from the given RGBA values. + * + * Returns `Error(Nil)` if the supplied RGBA values are greater than 1.0 or less than 0.0. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red_half_opacity) = from_rbga(1.0, 0.0, 0.0, 0.5) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function from_rgba(red, green, blue, alpha) { + return $result.try$( + valid_colour_value(red), + (r) => { + return $result.try$( + valid_colour_value(green), + (g) => { + return $result.try$( + valid_colour_value(blue), + (b) => { + return $result.try$( + valid_colour_value(alpha), + (a) => { return new Ok(new Rgba(r, g, b, a)); }, + ); + }, + ); + }, + ); + }, + ); +} + +/** + * Returns `Result(Colour)` created from the given HSLA values. + * + * Returns `Error(Nil)`f the supplied HSLA values are greater than 1.0 or less than 0.0. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red_half_opacity) = from_hsla(0.0, 1.0, 0.5, 0.5) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function from_hsla(hue, saturation, lightness, alpha) { + return $result.try$( + valid_colour_value(hue), + (h) => { + return $result.try$( + valid_colour_value(saturation), + (s) => { + return $result.try$( + valid_colour_value(lightness), + (l) => { + return $result.try$( + valid_colour_value(alpha), + (a) => { return new Ok(new Hsla(h, s, l, a)); }, + ); + }, + ); + }, + ); + }, + ); +} + +/** + * Returns `Result(Colour)` created from the given HSL values. + * + * Returns `Error(Nil)` if the supplied HSL values are greater than 1.0 or less than 0.0. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_hsla(0.0, 1.0, 0.5) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function from_hsl(hue, saturation, lightness) { + return from_hsla(hue, saturation, lightness, 1.0); +} + +/** + * Returns a `Result(Colour)` created from the given hex `Int`. + * + * Returns `Error(Nil)` if the supplied hex `Int is greater than 0xffffff or less than 0x0. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_rgb_hex(0xff0000) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function from_rgb_hex(hex) { + let $ = (hex > 0xffffff) || (hex < 0); + if ($) { + return new Error(undefined); + } else { + let _block; + let _pipe = $int.bitwise_shift_right(hex, 16); + _block = $int.bitwise_and(_pipe, 0xff); + let r = _block; + let _block$1; + let _pipe$1 = $int.bitwise_shift_right(hex, 8); + _block$1 = $int.bitwise_and(_pipe$1, 0xff); + let g = _block$1; + let b = $int.bitwise_and(hex, 0xff); + return from_rgb255(r, g, b); + } +} + +/** + * Returns a `Result(Colour)` created from the given RGB hex `String`. + * + * Returns `Error(Nil)` if the supplied hex `String` is invalid, or greater than `"#ffffff" or less than `"#0"` + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_rgb_hex_string("#ff0000") + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function from_rgb_hex_string(hex_string) { + return $result.try$( + hex_string_to_int(hex_string), + (hex_int) => { return from_rgb_hex(hex_int); }, + ); +} + +/** + * Returns a `Result(Colour)` created from the given hex `Int`. + * + * Returns `Error(Nil)` if the supplied hex `Int is greater than 0xffffffff or less than 0x0. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red_half_opacity) = from_rgba_hex(0xff00007f) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function from_rgba_hex(hex) { + let $ = (hex > 0xffffffff) || (hex < 0); + if ($) { + return new Error(undefined); + } else { + let _block; + let _pipe = $int.bitwise_shift_right(hex, 24); + let _pipe$1 = $int.bitwise_and(_pipe, 0xff); + let _pipe$2 = $int.to_float(_pipe$1); + _block = $float.divide(_pipe$2, 255.0); + let $1 = _block; + let r; + if ($1 instanceof Ok) { + r = $1[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "gleam_community/colour", + 590, + "from_rgba_hex", + "Pattern match failed, no pattern matched the value.", + { + value: $1, + start: 17111, + end: 17260, + pattern_start: 17122, + pattern_end: 17127 + } + ) + } + let _block$1; + let _pipe$3 = $int.bitwise_shift_right(hex, 16); + let _pipe$4 = $int.bitwise_and(_pipe$3, 0xff); + let _pipe$5 = $int.to_float(_pipe$4); + _block$1 = $float.divide(_pipe$5, 255.0); + let $2 = _block$1; + let g; + if ($2 instanceof Ok) { + g = $2[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "gleam_community/colour", + 596, + "from_rgba_hex", + "Pattern match failed, no pattern matched the value.", + { + value: $2, + start: 17332, + end: 17481, + pattern_start: 17343, + pattern_end: 17348 + } + ) + } + let _block$2; + let _pipe$6 = $int.bitwise_shift_right(hex, 8); + let _pipe$7 = $int.bitwise_and(_pipe$6, 0xff); + let _pipe$8 = $int.to_float(_pipe$7); + _block$2 = $float.divide(_pipe$8, 255.0); + let $3 = _block$2; + let b; + if ($3 instanceof Ok) { + b = $3[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "gleam_community/colour", + 602, + "from_rgba_hex", + "Pattern match failed, no pattern matched the value.", + { + value: $3, + start: 17553, + end: 17701, + pattern_start: 17564, + pattern_end: 17569 + } + ) + } + let _block$3; + let _pipe$9 = $int.bitwise_and(hex, 0xff); + let _pipe$10 = $int.to_float(_pipe$9); + _block$3 = $float.divide(_pipe$10, 255.0); + let $4 = _block$3; + let a; + if ($4 instanceof Ok) { + a = $4[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "gleam_community/colour", + 608, + "from_rgba_hex", + "Pattern match failed, no pattern matched the value.", + { + value: $4, + start: 17773, + end: 17883, + pattern_start: 17784, + pattern_end: 17789 + } + ) + } + return from_rgba(r, g, b, a); + } +} + +/** + * Returns a `Result(Colour)` created from the given RGBA hex `String`. + * + * Returns `Error(Nil)` if the supplied hex `String` is invalid, or greater than `"#ffffffff" or less than `"#0"` + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red_half_opacity) = from_rgba_hex_string("#ff00007f") + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function from_rgba_hex_string(hex_string) { + return $result.try$( + hex_string_to_int(hex_string), + (hex_int) => { return from_rgba_hex(hex_int); }, + ); +} + +/** + * Returns `#(Float, Float, Float, Float)` representing the given `Colour`'s + * R, G, B, and A values respectively. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_rgb255(255, 0, 0) + * let #(r, g, b, a) = to_rgba(red) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function to_rgba(colour) { + if (colour instanceof Rgba) { + let r = colour.r; + let g = colour.g; + let b = colour.b; + let a = colour.a; + return [r, g, b, a]; + } else { + let h = colour.h; + let s = colour.s; + let l = colour.l; + let a = colour.a; + return hsla_to_rgba(h, s, l, a); + } +} + +/** + * Returns `#(Float, Float, Float, Float)` representing the given `Colour`'s + * H, S, L, and A values respectively. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_rgb255(255, 0, 0) + * let #(h, s, l, a) = to_hsla(red) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function to_hsla(colour) { + if (colour instanceof Rgba) { + let r = colour.r; + let g = colour.g; + let b = colour.b; + let a = colour.a; + return rgba_to_hsla(r, g, b, a); + } else { + let h = colour.h; + let s = colour.s; + let l = colour.l; + let a = colour.a; + return [h, s, l, a]; + } +} + +/** + * Returns an rgba formatted CSS `String` created from the given `Colour`. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_rgb255(255, 0, 0) + * let css_red = to_css_rgba_string(red) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function to_css_rgba_string(colour) { + let $ = to_rgba(colour); + let r; + let g; + let b; + let a; + r = $[0]; + g = $[1]; + b = $[2]; + a = $[3]; + let percent = (x) => { + let _block; + let _pipe = x; + let _pipe$1 = $float.multiply(_pipe, 10_000.0); + let _pipe$2 = $float.round(_pipe$1); + let _pipe$3 = $int.to_float(_pipe$2); + _block = $float.divide(_pipe$3, 100.0); + let $1 = _block; + let p; + if ($1 instanceof Ok) { + p = $1[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "gleam_community/colour", + 706, + "to_css_rgba_string", + "Pattern match failed, no pattern matched the value.", + { + value: $1, + start: 20510, + end: 20646, + pattern_start: 20521, + pattern_end: 20526 + } + ) + } + return p; + }; + let round_to = (x) => { + let _block; + let _pipe = x; + let _pipe$1 = $float.multiply(_pipe, 1000.0); + let _pipe$2 = $float.round(_pipe$1); + let _pipe$3 = $int.to_float(_pipe$2); + _block = $float.divide(_pipe$3, 1000.0); + let $1 = _block; + let r$1; + if ($1 instanceof Ok) { + r$1 = $1[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "gleam_community/colour", + 718, + "to_css_rgba_string", + "Pattern match failed, no pattern matched the value.", + { + value: $1, + start: 20768, + end: 20903, + pattern_start: 20779, + pattern_end: 20784 + } + ) + } + return r$1; + }; + return $string.join( + toList([ + "rgba(", + $float.to_string(percent(r)) + "%,", + $float.to_string(percent(g)) + "%,", + $float.to_string(percent(b)) + "%,", + $float.to_string(round_to(a)), + ")", + ]), + "", + ); +} + +/** + * Returns an hex `Int` created from the given `Colour`. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_rgba(1.0, 0.0, 0.0, 1.0) + * let red_hex_int = to_rgba_hex(red) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function to_rgba_hex(colour) { + let $ = to_rgba(colour); + let r; + let g; + let b; + let a; + r = $[0]; + g = $[1]; + b = $[2]; + a = $[3]; + let _block; + let _pipe = r * 255.0; + let _pipe$1 = $float.round(_pipe); + _block = $int.bitwise_shift_left(_pipe$1, 24); + let red$1 = _block; + let _block$1; + let _pipe$2 = g * 255.0; + let _pipe$3 = $float.round(_pipe$2); + _block$1 = $int.bitwise_shift_left(_pipe$3, 16); + let green$1 = _block$1; + let _block$2; + let _pipe$4 = b * 255.0; + let _pipe$5 = $float.round(_pipe$4); + _block$2 = $int.bitwise_shift_left(_pipe$5, 8); + let blue$1 = _block$2; + let _block$3; + let _pipe$6 = a * 255.0; + _block$3 = $float.round(_pipe$6); + let alpha = _block$3; + return ((red$1 + green$1) + blue$1) + alpha; +} + +/** + * Returns an rgba hex formatted `String` created from the given `Colour`. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_rgba(1.0, 0.0, 0.0, 1.0) + * let red_hex = to_rgba_hex_string(red) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function to_rgba_hex_string(colour) { + let _block; + let _pipe = to_rgba_hex(colour); + _block = $int.to_base16(_pipe); + let hex_string = _block; + let $ = $string.length(hex_string); + if ($ === 8) { + return hex_string; + } else { + let l = $; + return $string.repeat("0", 8 - l) + hex_string; + } +} + +/** + * Returns a rgb hex `Int` created from the given `Colour`. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_rgba(255, 0, 0) + * let red_hex_int = to_rgb_hex(red) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function to_rgb_hex(colour) { + let $ = to_rgba(colour); + let r; + let g; + let b; + r = $[0]; + g = $[1]; + b = $[2]; + let _block; + let _pipe = r * 255.0; + let _pipe$1 = $float.round(_pipe); + _block = $int.bitwise_shift_left(_pipe$1, 16); + let red$1 = _block; + let _block$1; + let _pipe$2 = g * 255.0; + let _pipe$3 = $float.round(_pipe$2); + _block$1 = $int.bitwise_shift_left(_pipe$3, 8); + let green$1 = _block$1; + let _block$2; + let _pipe$4 = b * 255.0; + _block$2 = $float.round(_pipe$4); + let blue$1 = _block$2; + return (red$1 + green$1) + blue$1; +} + +/** + * Returns an rgb hex formatted `String` created from the given `Colour`. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * assert Ok(red) = from_rgba(255, 0, 0) + * let red_hex = to_rgb_hex_string(red) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function to_rgb_hex_string(colour) { + let _block; + let _pipe = to_rgb_hex(colour); + _block = $int.to_base16(_pipe); + let hex_string = _block; + let $ = $string.length(hex_string); + if ($ === 6) { + return hex_string; + } else { + let l = $; + return $string.repeat("0", 6 - l) + hex_string; + } +} + +function encode_rgba(r, g, b, a) { + return $json.object( + toList([ + ["r", $json.float(r)], + ["g", $json.float(g)], + ["b", $json.float(b)], + ["a", $json.float(a)], + ]), + ); +} + +function encode_hsla(h, s, l, a) { + return $json.object( + toList([ + ["h", $json.float(h)], + ["s", $json.float(s)], + ["l", $json.float(l)], + ["a", $json.float(a)], + ]), + ); +} + +/** + * Encodes a `Colour` value as a Gleam [`Json`](https://hexdocs.pm/gleam_json/gleam/json.html#Json) + * value. You'll need this if you want to send a `Colour` value over the network + * in a HTTP request or response, for example. + * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function encode(colour) { + if (colour instanceof Rgba) { + let r = colour.r; + let g = colour.g; + let b = colour.b; + let a = colour.a; + return encode_rgba(r, g, b, a); + } else { + let h = colour.h; + let s = colour.s; + let l = colour.l; + let a = colour.a; + return encode_hsla(h, s, l, a); + } +} + +function rgba_decoder() { + return $decode.field( + "r", + $decode.float, + (r) => { + return $decode.field( + "g", + $decode.float, + (g) => { + return $decode.field( + "b", + $decode.float, + (b) => { + return $decode.field( + "a", + $decode.float, + (a) => { return $decode.success(new Rgba(r, g, b, a)); }, + ); + }, + ); + }, + ); + }, + ); +} + +function hsla_decoder() { + return $decode.field( + "h", + $decode.float, + (h) => { + return $decode.field( + "s", + $decode.float, + (s) => { + return $decode.field( + "l", + $decode.float, + (l) => { + return $decode.field( + "a", + $decode.float, + (a) => { return $decode.success(new Hsla(h, s, l, a)); }, + ); + }, + ); + }, + ); + }, + ); +} + +/** + * Attempt to decode some [`Dynamic`](https://hexdocs.pm/gleam_stdlib/gleam/dynamic.html#Dynamic) + * value into a `Colour`. Most often you'll use this to decode some JSON. + * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function decoder() { + return $decode.one_of(rgba_decoder(), toList([hsla_decoder()])); +} + +/** + * A `Colour` reprsenting the colour RGBA(239, 41, 41, 1.0) + */ +export const light_red = /* @__PURE__ */ new Rgba( + 0.9372549019607843, + 0.1607843137254902, + 0.1607843137254902, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(204, 0, 0, 1.0) + */ +export const red = /* @__PURE__ */ new Rgba(0.8, 0.0, 0.0, 1.0); + +/** + * A `Colour` reprsenting the colour RGBA(164, 0, 0, 1.0) + */ +export const dark_red = /* @__PURE__ */ new Rgba( + 0.6431372549019608, + 0.0, + 0.0, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(252, 175, 62, 1.0) + */ +export const light_orange = /* @__PURE__ */ new Rgba( + 0.9882352941176471, + 0.6862745098039216, + 0.24313725490196078, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(245, 121, 0, 1.0) + */ +export const orange = /* @__PURE__ */ new Rgba( + 0.9607843137254902, + 0.4745098039215686, + 0.0, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(206, 92, 0, 1.0) + */ +export const dark_orange = /* @__PURE__ */ new Rgba( + 0.807843137254902, + 0.3607843137254902, + 0.0, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(255, 233, 79, 1.0) + */ +export const light_yellow = /* @__PURE__ */ new Rgba( + 1.0, + 0.9137254901960784, + 0.30980392156862746, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(237, 212, 0, 1.0) + */ +export const yellow = /* @__PURE__ */ new Rgba( + 0.9294117647058824, + 0.8313725490196079, + 0.0, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(196, 160, 0, 1.0) + */ +export const dark_yellow = /* @__PURE__ */ new Rgba( + 0.7686274509803922, + 0.6274509803921569, + 0.0, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(138, 226, 52, 1.0) + */ +export const light_green = /* @__PURE__ */ new Rgba( + 0.5411764705882353, + 0.8862745098039215, + 0.20392156862745098, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(115, 210, 22, 1.0) + */ +export const green = /* @__PURE__ */ new Rgba( + 0.45098039215686275, + 0.8235294117647058, + 0.08627450980392157, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(78, 154, 6, 1.0) + */ +export const dark_green = /* @__PURE__ */ new Rgba( + 0.3058823529411765, + 0.6039215686274509, + 0.023529411764705882, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(114, 159, 207, 1.0) + */ +export const light_blue = /* @__PURE__ */ new Rgba( + 0.4470588235294118, + 0.6235294117647059, + 0.8117647058823529, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(52, 101, 164, 1.0) + */ +export const blue = /* @__PURE__ */ new Rgba( + 0.20392156862745098, + 0.396078431372549, + 0.6431372549019608, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(32, 74, 135, 1.0) + */ +export const dark_blue = /* @__PURE__ */ new Rgba( + 0.12549019607843137, + 0.2901960784313726, + 0.5294117647058824, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(173, 127, 168, 1.0) + */ +export const light_purple = /* @__PURE__ */ new Rgba( + 0.6784313725490196, + 0.4980392156862745, + 0.6588235294117647, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(117, 80, 123, 1.0) + */ +export const purple = /* @__PURE__ */ new Rgba( + 0.4588235294117647, + 0.3137254901960784, + 0.4823529411764706, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(92, 53, 102, 1.0) + */ +export const dark_purple = /* @__PURE__ */ new Rgba( + 0.3607843137254902, + 0.20784313725490197, + 0.4, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(233, 185, 110, 1.0) + */ +export const light_brown = /* @__PURE__ */ new Rgba( + 0.9137254901960784, + 0.7254901960784313, + 0.43137254901960786, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(193, 125, 17, 1.0) + */ +export const brown = /* @__PURE__ */ new Rgba( + 0.7568627450980392, + 0.49019607843137253, + 0.06666666666666667, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(143, 89, 2, 1.0) + */ +export const dark_brown = /* @__PURE__ */ new Rgba( + 0.5607843137254902, + 0.34901960784313724, + 0.00784313725490196, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(0, 0, 0, 1.0) + */ +export const black = /* @__PURE__ */ new Rgba(0.0, 0.0, 0.0, 1.0); + +/** + * A `Colour` reprsenting the colour RGBA(255, 255, 255, 1.0) + */ +export const white = /* @__PURE__ */ new Rgba(1.0, 1.0, 1.0, 1.0); + +/** + * A `Colour` reprsenting the colour RGBA(238, 238, 236, 1.0) + */ +export const light_grey = /* @__PURE__ */ new Rgba( + 0.9333333333333333, + 0.9333333333333333, + 0.9254901960784314, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(211, 215, 207, 1.0) + */ +export const grey = /* @__PURE__ */ new Rgba( + 0.8274509803921568, + 0.8431372549019608, + 0.8117647058823529, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(186, 189, 182, 1.0) + */ +export const dark_grey = /* @__PURE__ */ new Rgba( + 0.7294117647058823, + 0.7411764705882353, + 0.7137254901960784, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(238, 238, 236, 1.0) + */ +export const light_gray = /* @__PURE__ */ new Rgba( + 0.9333333333333333, + 0.9333333333333333, + 0.9254901960784314, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(211, 215, 207, 1.0) + */ +export const gray = /* @__PURE__ */ new Rgba( + 0.8274509803921568, + 0.8431372549019608, + 0.8117647058823529, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(186, 189, 182, 1.0) + */ +export const dark_gray = /* @__PURE__ */ new Rgba( + 0.7294117647058823, + 0.7411764705882353, + 0.7137254901960784, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(136, 138, 133, 1.0) + */ +export const light_charcoal = /* @__PURE__ */ new Rgba( + 0.5333333333333333, + 0.5411764705882353, + 0.5215686274509804, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(85, 87, 83, 1.0) + */ +export const charcoal = /* @__PURE__ */ new Rgba( + 0.3333333333333333, + 0.3411764705882353, + 0.3254901960784314, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(46, 52, 54, 1.0) + */ +export const dark_charcoal = /* @__PURE__ */ new Rgba( + 0.1803921568627451, + 0.20392156862745098, + 0.21176470588235294, + 1.0, +); + +/** + * A `Colour` reprsenting the colour RGBA(255, 175, 243, 1.0) + */ +export const pink = /* @__PURE__ */ new Rgba( + 1.0, + 0.6862745098039216, + 0.9529411764705882, + 1.0, +); diff --git a/build/dev/javascript/gleam_community_colour/gleam_community/colour/accessibility.mjs b/build/dev/javascript/gleam_community_colour/gleam_community/colour/accessibility.mjs new file mode 100644 index 0000000..0c56e21 --- /dev/null +++ b/build/dev/javascript/gleam_community_colour/gleam_community/colour/accessibility.mjs @@ -0,0 +1,146 @@ +import * as $float from "../../../gleam_stdlib/gleam/float.mjs"; +import * as $list from "../../../gleam_stdlib/gleam/list.mjs"; +import { Ok, makeError, divideFloat } from "../../gleam.mjs"; +import * as $colour from "../../gleam_community/colour.mjs"; + +const FILEPATH = "src/gleam_community/colour/accessibility.gleam"; + +function intensity(colour_value) { + let $ = true; + if (colour_value <= 0.03928) { + return colour_value / 12.92; + } else { + let $1 = $float.power((colour_value + 0.055) / 1.055, 2.4); + let i; + if ($1 instanceof Ok) { + i = $1[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "gleam_community/colour/accessibility", + 62, + "intensity", + "Pattern match failed, no pattern matched the value.", + { + value: $1, + start: 2399, + end: 2470, + pattern_start: 2410, + pattern_end: 2415 + } + ) + } + return i; + } +} + +/** + * Returns the relative brightness of the given `Colour` as a `Float` between + * 0.0, and 1.0 with 0.0 being the darkest possible colour and 1.0 being the lightest. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * luminance(colour.white) // 1.0 + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function luminance(colour) { + let $ = $colour.to_rgba(colour); + let r; + let g; + let b; + r = $[0]; + g = $[1]; + b = $[2]; + let r_intensity = intensity(r); + let g_intensity = intensity(g); + let b_intensity = intensity(b); + return ((0.2126 * r_intensity) + (0.7152 * g_intensity)) + (0.0722 * b_intensity); +} + +/** + * Returns the contrast between two `Colour` values as a `Float` between 1.0, + * and 21.0 with 1.0 being no contrast and, 21.0 being the highest possible contrast. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * contrast_ratio(between: colour.white, and: colour.black) // 21.0 + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function contrast_ratio(colour_a, colour_b) { + let luminance_a = luminance(colour_a) + 0.05; + let luminance_b = luminance(colour_b) + 0.05; + let $ = luminance_a > luminance_b; + if ($) { + return divideFloat(luminance_a, luminance_b); + } else { + return divideFloat(luminance_b, luminance_a); + } +} + +/** + * Returns the `Colour` with the highest contrast between the base `Colour`, + * and and the other provided `Colour` values. + * + *
+ * Example: + * + * ```gleam + * fn example() { + * maximum_contrast( + * colour.yellow, + * [colour.white, colour.dark_blue, colour.green], + * ) + * } + * ``` + *
+ * + *
+ * + * Spot a typo? Open an issue! + * + * + * Back to top ↑ + * + *
+ */ +export function maximum_contrast(base, colours) { + let _pipe = colours; + let _pipe$1 = $list.sort( + _pipe, + (colour_a, colour_b) => { + let contrast_a = contrast_ratio(base, colour_a); + let contrast_b = contrast_ratio(base, colour_b); + return $float.compare(contrast_b, contrast_a); + }, + ); + return $list.first(_pipe$1); +} diff --git a/build/dev/javascript/gleam_community_colour/gleam_community@colour.erl b/build/dev/javascript/gleam_community_colour/gleam_community@colour.erl new file mode 100644 index 0000000..faee070 --- /dev/null +++ b/build/dev/javascript/gleam_community_colour/gleam_community@colour.erl @@ -0,0 +1,1199 @@ +-module(gleam_community@colour). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). +-define(FILEPATH, "src/gleam_community/colour.gleam"). +-export([from_rgb255/3, from_rgb/3, from_rgba/4, from_hsla/4, from_hsl/3, from_rgb_hex/1, from_rgb_hex_string/1, from_rgba_hex/1, from_rgba_hex_string/1, to_rgba/1, to_hsla/1, to_css_rgba_string/1, to_rgba_hex/1, to_rgba_hex_string/1, to_rgb_hex/1, to_rgb_hex_string/1, encode/1, decoder/0]). +-export_type([colour/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + "\n" + " - **Types**\n" + " - [`Colour`](#Colour)\n" + " - [`Color`](#Color)\n" + " - **Constructors**\n" + " - [`from_rgb255`](#from_rgb255)\n" + " - [`from_rgb`](#from_rgb)\n" + " - [`from_rgba`](#from_rgba)\n" + " - [`from_hsl`](#from_hsl)\n" + " - [`from_hsla`](#from_hsla)\n" + " - [`from_rgb_hex`](#from_rgb_hex)\n" + " - [`from_rgba_hex`](#from_rgba_hex)\n" + " - [`from_rgb_hex_string`](#from_rgb_hex_string)\n" + " - [`from_rgba_hex_string`](#from_rgba_hex_string)\n" + " - **Conversions**\n" + " - [`to_rgba`](#to_rgba)\n" + " - [`to_hsla`](#hsla)\n" + " - [`to_css_rgba_string`](#to_css_rgba_string)\n" + " - [`to_rgba_hex_string`](#to_rgba_hex_string)\n" + " - [`to_rgb_hex_string`](#to_rgb_hex_string)\n" + " - [`to_rgba_hex`](#to_rgba_hex)\n" + " - [`to_rgb_hex`](#to_rgb_hex)\n" + " - **JSON**\n" + " - [`encode`](#encode)\n" + " - [`decoder`](#decoder)\n" + " - **Colours**\n" + " - [`light_red`](#light_red)\n" + " - [`red`](#red)\n" + " - [`dark_red`](#dark_red)\n" + " - [`light_orange`](#light_orange)\n" + " - [`orange`](#orange)\n" + " - [`dark_orange`](#dark_orange)\n" + " - [`light_yellow`](#light_yellow)\n" + " - [`yellow`](#yellow)\n" + " - [`dark_yellow`](#dark_yellow)\n" + " - [`light_green`](#light_green)\n" + " - [`green`](#green)\n" + " - [`dark_green`](#dark_green)\n" + " - [`light_blue`](#light_blue)\n" + " - [`blue`](#blue)\n" + " - [`dark_blue`](#dark_blue)\n" + " - [`light_purple`](#light_purple)\n" + " - [`purple`](#purple)\n" + " - [`dark_purple`](#dark_purple)\n" + " - [`light_brown`](#light_brown)\n" + " - [`brown`](#brown)\n" + " - [`dark_brown`](#dark_brown)\n" + " - [`black`](#black)\n" + " - [`white`](#white)\n" + " - [`light_grey`](#light_grey)\n" + " - [`grey`](#grey)\n" + " - [`dark_grey`](#dark_grey)\n" + " - [`light_gray`](#light_gray)\n" + " - [`gray`](#gray)\n" + " - [`dark_gray`](#dark_gray)\n" + " - [`light_charcoal`](#light_charcoal)\n" + " - [`charcoal`](#charcoal)\n" + " - [`dark_charcoal`](#dark_charcoal)\n" + " - [`pink`](#pink)\n" + "\n" + " ---\n" + "\n" + " This package was heavily inspired by the `elm-color` module.\n" + " The original source code can be found\n" + " here.\n" + "\n" + "
\n" + " The license of that package is produced below:\n" + "\n" + "\n" + " > MIT License\n" + "\n" + " > Copyright 2018 Aaron VonderHaar\n" + "\n" + " > Redistribution and use in source and binary forms, with or without modification,\n" + " are permitted provided that the following conditions are met:\n" + "\n" + " 1. Redistributions of source code must retain the above copyright notice,\n" + " this list of conditions and the following disclaimer.\n" + "\n" + " 2. Redistributions in binary form must reproduce the above copyright notice,\n" + " this list of conditions and the following disclaimer in the documentation\n" + " and/or other materials provided with the distribution.\n" + "\n" + " 3. Neither the name of the copyright holder nor the names of its contributors\n" + " may be used to endorse or promote products derived from this software without\n" + " specific prior written permission.\n" + "\n" + " > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n" + " ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n" + " OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL\n" + " THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n" + " EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\n" + " OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n" + " ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + "\n" + " > The above copyright notice and this permission notice shall be included in all\n" + " copies or substantial portions of the Software.\n" + "
\n" + "\n" +). + +-opaque colour() :: {rgba, float(), float(), float(), float()} | + {hsla, float(), float(), float(), float()}. + +-file("src/gleam_community/colour.gleam", 155). +-spec valid_colour_value(float()) -> {ok, float()} | {error, nil}. +valid_colour_value(C) -> + case (C > 1.0) orelse (C < +0.0) of + true -> + {error, nil}; + + false -> + {ok, C} + end. + +-file("src/gleam_community/colour.gleam", 162). +-spec hue_to_rgb(float(), float(), float()) -> float(). +hue_to_rgb(Hue, M1, M2) -> + H = case Hue of + _ when Hue < +0.0 -> + Hue + 1.0; + + _ when Hue > 1.0 -> + Hue - 1.0; + + _ -> + Hue + end, + H_t_6 = H * 6.0, + H_t_2 = H * 2.0, + H_t_3 = H * 3.0, + case H of + _ when H_t_6 < 1.0 -> + M1 + (((M2 - M1) * H) * 6.0); + + _ when H_t_2 < 1.0 -> + M2; + + _ when H_t_3 < 2.0 -> + M1 + (((M2 - M1) * ((2.0 / 3.0) - H)) * 6.0); + + _ -> + M1 + end. + +-file("src/gleam_community/colour.gleam", 181). +-spec hex_string_to_int(binary()) -> {ok, integer()} | {error, nil}. +hex_string_to_int(Hex_string) -> + Hex = case Hex_string of + <<"#"/utf8, Hex_number/binary>> -> + Hex_number; + + <<"0x"/utf8, Hex_number@1/binary>> -> + Hex_number@1; + + _ -> + Hex_string + end, + _pipe = Hex, + _pipe@1 = string:lowercase(_pipe), + _pipe@2 = gleam@string:to_graphemes(_pipe@1), + _pipe@3 = lists:reverse(_pipe@2), + gleam@list:index_fold( + _pipe@3, + {ok, 0}, + fun(Total, Char, Index) -> case Total of + {error, nil} -> + {error, nil}; + + {ok, V} -> + gleam@result:'try'(case Char of + <<"a"/utf8>> -> + {ok, 10}; + + <<"b"/utf8>> -> + {ok, 11}; + + <<"c"/utf8>> -> + {ok, 12}; + + <<"d"/utf8>> -> + {ok, 13}; + + <<"e"/utf8>> -> + {ok, 14}; + + <<"f"/utf8>> -> + {ok, 15}; + + _ -> + gleam_stdlib:parse_int(Char) + end, fun(Num) -> + gleam@result:'try'( + gleam@int:power(16, erlang:float(Index)), + fun(Base) -> + {ok, + V + erlang:round( + erlang:float(Num) * Base + )} + end + ) + end) + end end + ). + +-file("src/gleam_community/colour.gleam", 212). +-spec hsla_to_rgba(float(), float(), float(), float()) -> {float(), + float(), + float(), + float()}. +hsla_to_rgba(H, S, L, A) -> + M2 = case L =< 0.5 of + true -> + L * (S + 1.0); + + false -> + (L + S) - (L * S) + end, + M1 = (L * 2.0) - M2, + R = hue_to_rgb(H + (1.0 / 3.0), M1, M2), + G = hue_to_rgb(H, M1, M2), + B = hue_to_rgb(H - (1.0 / 3.0), M1, M2), + {R, G, B, A}. + +-file("src/gleam_community/colour.gleam", 232). +-spec rgba_to_hsla(float(), float(), float(), float()) -> {float(), + float(), + float(), + float()}. +rgba_to_hsla(R, G, B, A) -> + Min_colour = gleam@float:min(R, gleam@float:min(G, B)), + Max_colour = gleam@float:max(R, gleam@float:max(G, B)), + H1 = case true of + _ when Max_colour =:= R -> + gleam@float:divide(G - B, Max_colour - Min_colour); + + _ when Max_colour =:= G -> + _pipe = gleam@float:divide(B - R, Max_colour - Min_colour), + gleam@result:'try'(_pipe, fun(D) -> {ok, 2.0 + D} end); + + _ -> + _pipe@1 = gleam@float:divide(R - G, Max_colour - Min_colour), + gleam@result:'try'(_pipe@1, fun(D@1) -> {ok, 4.0 + D@1} end) + end, + H2 = case H1 of + {ok, V} -> + {ok, V * (1.0 / 6.0)}; + + _ -> + H1 + end, + H3 = case H2 of + {ok, V@1} when V@1 < +0.0 -> + V@1 + 1.0; + + {ok, V@2} -> + V@2; + + _ -> + +0.0 + end, + L = (Min_colour + Max_colour) / 2.0, + S = case true of + _ when Min_colour =:= Max_colour -> + +0.0; + + _ when L < 0.5 -> + case (Max_colour + Min_colour) of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> (Max_colour - Min_colour) / Gleam@denominator + end; + + _ -> + case ((2.0 - Max_colour) - Min_colour) of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator@1 -> (Max_colour - Min_colour) / Gleam@denominator@1 + end + end, + {H3, S, L, A}. + +-file("src/gleam_community/colour.gleam", 300). +?DOC( + " Returns a `Result(Colour)` created from the given 8 bit RGB values.\n" + "\n" + " Returns `Error(Nil)` if the supplied RGB values are greater than 255 or less than 0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb255(255, 0, 0)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec from_rgb255(integer(), integer(), integer()) -> {ok, colour()} | + {error, nil}. +from_rgb255(Red, Green, Blue) -> + gleam@result:'try'( + begin + _pipe = Red, + _pipe@1 = erlang:float(_pipe), + _pipe@2 = gleam@float:divide(_pipe@1, 255.0), + gleam@result:'try'(_pipe@2, fun valid_colour_value/1) + end, + fun(R) -> + gleam@result:'try'( + begin + _pipe@3 = Green, + _pipe@4 = erlang:float(_pipe@3), + _pipe@5 = gleam@float:divide(_pipe@4, 255.0), + gleam@result:'try'(_pipe@5, fun valid_colour_value/1) + end, + fun(G) -> + gleam@result:'try'( + begin + _pipe@6 = Blue, + _pipe@7 = erlang:float(_pipe@6), + _pipe@8 = gleam@float:divide(_pipe@7, 255.0), + gleam@result:'try'( + _pipe@8, + fun valid_colour_value/1 + ) + end, + fun(B) -> {ok, {rgba, R, G, B, 1.0}} end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 348). +?DOC( + " Returns `Result(Colour)` created from the given RGB values.\n" + "\n" + " If the supplied RGB values are greater than 1.0 or less than 0.0 returns `Error(Nil)`\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb(1.0, 0.0, 0.0)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec from_rgb(float(), float(), float()) -> {ok, colour()} | {error, nil}. +from_rgb(Red, Green, Blue) -> + gleam@result:'try'( + valid_colour_value(Red), + fun(R) -> + gleam@result:'try'( + valid_colour_value(Green), + fun(G) -> + gleam@result:'try'( + valid_colour_value(Blue), + fun(B) -> {ok, {rgba, R, G, B, 1.0}} end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 383). +?DOC( + " Returns `Result(Colour)` created from the given RGBA values.\n" + "\n" + " Returns `Error(Nil)` if the supplied RGBA values are greater than 1.0 or less than 0.0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red_half_opacity) = from_rbga(1.0, 0.0, 0.0, 0.5)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec from_rgba(float(), float(), float(), float()) -> {ok, colour()} | + {error, nil}. +from_rgba(Red, Green, Blue, Alpha) -> + gleam@result:'try'( + valid_colour_value(Red), + fun(R) -> + gleam@result:'try'( + valid_colour_value(Green), + fun(G) -> + gleam@result:'try'( + valid_colour_value(Blue), + fun(B) -> + gleam@result:'try'( + valid_colour_value(Alpha), + fun(A) -> {ok, {rgba, R, G, B, A}} end + ) + end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 420). +?DOC( + " Returns `Result(Colour)` created from the given HSLA values.\n" + "\n" + " Returns `Error(Nil)`f the supplied HSLA values are greater than 1.0 or less than 0.0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red_half_opacity) = from_hsla(0.0, 1.0, 0.5, 0.5)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec from_hsla(float(), float(), float(), float()) -> {ok, colour()} | + {error, nil}. +from_hsla(Hue, Saturation, Lightness, Alpha) -> + gleam@result:'try'( + valid_colour_value(Hue), + fun(H) -> + gleam@result:'try'( + valid_colour_value(Saturation), + fun(S) -> + gleam@result:'try'( + valid_colour_value(Lightness), + fun(L) -> + gleam@result:'try'( + valid_colour_value(Alpha), + fun(A) -> {ok, {hsla, H, S, L, A}} end + ) + end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 457). +?DOC( + " Returns `Result(Colour)` created from the given HSL values.\n" + "\n" + " Returns `Error(Nil)` if the supplied HSL values are greater than 1.0 or less than 0.0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_hsla(0.0, 1.0, 0.5)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec from_hsl(float(), float(), float()) -> {ok, colour()} | {error, nil}. +from_hsl(Hue, Saturation, Lightness) -> + from_hsla(Hue, Saturation, Lightness, 1.0). + +-file("src/gleam_community/colour.gleam", 488). +?DOC( + " Returns a `Result(Colour)` created from the given hex `Int`.\n" + "\n" + " Returns `Error(Nil)` if the supplied hex `Int is greater than 0xffffff or less than 0x0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb_hex(0xff0000)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec from_rgb_hex(integer()) -> {ok, colour()} | {error, nil}. +from_rgb_hex(Hex) -> + case (Hex > 16#ffffff) orelse (Hex < 0) of + true -> + {error, nil}; + + false -> + R = begin + _pipe = erlang:'bsr'(Hex, 16), + erlang:'band'(_pipe, 16#ff) + end, + G = begin + _pipe@1 = erlang:'bsr'(Hex, 8), + erlang:'band'(_pipe@1, 16#ff) + end, + B = erlang:'band'(Hex, 16#ff), + from_rgb255(R, G, B) + end. + +-file("src/gleam_community/colour.gleam", 527). +?DOC( + " Returns a `Result(Colour)` created from the given RGB hex `String`.\n" + "\n" + " Returns `Error(Nil)` if the supplied hex `String` is invalid, or greater than `\"#ffffff\" or less than `\"#0\"`\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb_hex_string(\"#ff0000\")\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec from_rgb_hex_string(binary()) -> {ok, colour()} | {error, nil}. +from_rgb_hex_string(Hex_string) -> + gleam@result:'try'( + hex_string_to_int(Hex_string), + fun(Hex_int) -> from_rgb_hex(Hex_int) end + ). + +-file("src/gleam_community/colour.gleam", 585). +?DOC( + " Returns a `Result(Colour)` created from the given hex `Int`.\n" + "\n" + " Returns `Error(Nil)` if the supplied hex `Int is greater than 0xffffffff or less than 0x0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red_half_opacity) = from_rgba_hex(0xff00007f)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec from_rgba_hex(integer()) -> {ok, colour()} | {error, nil}. +from_rgba_hex(Hex) -> + case (Hex > 16#ffffffff) orelse (Hex < 0) of + true -> + {error, nil}; + + false -> + R@1 = case begin + _pipe = erlang:'bsr'(Hex, 24), + _pipe@1 = erlang:'band'(_pipe, 16#ff), + _pipe@2 = erlang:float(_pipe@1), + gleam@float:divide(_pipe@2, 255.0) + end of + {ok, R} -> R; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"from_rgba_hex"/utf8>>, + line => 590, + value => _assert_fail, + start => 17111, + 'end' => 17260, + pattern_start => 17122, + pattern_end => 17127}) + end, + G@1 = case begin + _pipe@3 = erlang:'bsr'(Hex, 16), + _pipe@4 = erlang:'band'(_pipe@3, 16#ff), + _pipe@5 = erlang:float(_pipe@4), + gleam@float:divide(_pipe@5, 255.0) + end of + {ok, G} -> G; + _assert_fail@1 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"from_rgba_hex"/utf8>>, + line => 596, + value => _assert_fail@1, + start => 17332, + 'end' => 17481, + pattern_start => 17343, + pattern_end => 17348}) + end, + B@1 = case begin + _pipe@6 = erlang:'bsr'(Hex, 8), + _pipe@7 = erlang:'band'(_pipe@6, 16#ff), + _pipe@8 = erlang:float(_pipe@7), + gleam@float:divide(_pipe@8, 255.0) + end of + {ok, B} -> B; + _assert_fail@2 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"from_rgba_hex"/utf8>>, + line => 602, + value => _assert_fail@2, + start => 17553, + 'end' => 17701, + pattern_start => 17564, + pattern_end => 17569}) + end, + A@1 = case begin + _pipe@9 = erlang:'band'(Hex, 16#ff), + _pipe@10 = erlang:float(_pipe@9), + gleam@float:divide(_pipe@10, 255.0) + end of + {ok, A} -> A; + _assert_fail@3 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"from_rgba_hex"/utf8>>, + line => 608, + value => _assert_fail@3, + start => 17773, + 'end' => 17883, + pattern_start => 17784, + pattern_end => 17789}) + end, + from_rgba(R@1, G@1, B@1, A@1) + end. + +-file("src/gleam_community/colour.gleam", 556). +?DOC( + " Returns a `Result(Colour)` created from the given RGBA hex `String`.\n" + "\n" + " Returns `Error(Nil)` if the supplied hex `String` is invalid, or greater than `\"#ffffffff\" or less than `\"#0\"`\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red_half_opacity) = from_rgba_hex_string(\"#ff00007f\")\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec from_rgba_hex_string(binary()) -> {ok, colour()} | {error, nil}. +from_rgba_hex_string(Hex_string) -> + gleam@result:'try'( + hex_string_to_int(Hex_string), + fun(Hex_int) -> from_rgba_hex(Hex_int) end + ). + +-file("src/gleam_community/colour.gleam", 642). +?DOC( + " Returns `#(Float, Float, Float, Float)` representing the given `Colour`'s\n" + " R, G, B, and A values respectively.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb255(255, 0, 0)\n" + " let #(r, g, b, a) = to_rgba(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec to_rgba(colour()) -> {float(), float(), float(), float()}. +to_rgba(Colour) -> + case Colour of + {rgba, R, G, B, A} -> + {R, G, B, A}; + + {hsla, H, S, L, A@1} -> + hsla_to_rgba(H, S, L, A@1) + end. + +-file("src/gleam_community/colour.gleam", 672). +?DOC( + " Returns `#(Float, Float, Float, Float)` representing the given `Colour`'s\n" + " H, S, L, and A values respectively.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb255(255, 0, 0)\n" + " let #(h, s, l, a) = to_hsla(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec to_hsla(colour()) -> {float(), float(), float(), float()}. +to_hsla(Colour) -> + case Colour of + {hsla, H, S, L, A} -> + {H, S, L, A}; + + {rgba, R, G, B, A@1} -> + rgba_to_hsla(R, G, B, A@1) + end. + +-file("src/gleam_community/colour.gleam", 701). +?DOC( + " Returns an rgba formatted CSS `String` created from the given `Colour`.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb255(255, 0, 0)\n" + " let css_red = to_css_rgba_string(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec to_css_rgba_string(colour()) -> binary(). +to_css_rgba_string(Colour) -> + {R, G, B, A} = to_rgba(Colour), + Percent = fun(X) -> + P@1 = case begin + _pipe = X, + _pipe@1 = gleam@float:multiply(_pipe, 10000.0), + _pipe@2 = erlang:round(_pipe@1), + _pipe@3 = erlang:float(_pipe@2), + gleam@float:divide(_pipe@3, 100.0) + end of + {ok, P} -> P; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"to_css_rgba_string"/utf8>>, + line => 706, + value => _assert_fail, + start => 20510, + 'end' => 20646, + pattern_start => 20521, + pattern_end => 20526}) + end, + P@1 + end, + Round_to = fun(X@1) -> + R@2 = case begin + _pipe@4 = X@1, + _pipe@5 = gleam@float:multiply(_pipe@4, 1000.0), + _pipe@6 = erlang:round(_pipe@5), + _pipe@7 = erlang:float(_pipe@6), + gleam@float:divide(_pipe@7, 1000.0) + end of + {ok, R@1} -> R@1; + _assert_fail@1 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"to_css_rgba_string"/utf8>>, + line => 718, + value => _assert_fail@1, + start => 20768, + 'end' => 20903, + pattern_start => 20779, + pattern_end => 20784}) + end, + R@2 + end, + gleam@string:join( + [<<"rgba("/utf8>>, + <<(gleam_stdlib:float_to_string(Percent(R)))/binary, "%,"/utf8>>, + <<(gleam_stdlib:float_to_string(Percent(G)))/binary, "%,"/utf8>>, + <<(gleam_stdlib:float_to_string(Percent(B)))/binary, "%,"/utf8>>, + gleam_stdlib:float_to_string(Round_to(A)), + <<")"/utf8>>], + <<""/utf8>> + ). + +-file("src/gleam_community/colour.gleam", 829). +?DOC( + " Returns an hex `Int` created from the given `Colour`.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgba(1.0, 0.0, 0.0, 1.0)\n" + " let red_hex_int = to_rgba_hex(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec to_rgba_hex(colour()) -> integer(). +to_rgba_hex(Colour) -> + {R, G, B, A} = to_rgba(Colour), + Red = begin + _pipe = R * 255.0, + _pipe@1 = erlang:round(_pipe), + erlang:'bsl'(_pipe@1, 24) + end, + Green = begin + _pipe@2 = G * 255.0, + _pipe@3 = erlang:round(_pipe@2), + erlang:'bsl'(_pipe@3, 16) + end, + Blue = begin + _pipe@4 = B * 255.0, + _pipe@5 = erlang:round(_pipe@4), + erlang:'bsl'(_pipe@5, 8) + end, + Alpha = begin + _pipe@6 = A * 255.0, + erlang:round(_pipe@6) + end, + ((Red + Green) + Blue) + Alpha. + +-file("src/gleam_community/colour.gleam", 763). +?DOC( + " Returns an rgba hex formatted `String` created from the given `Colour`.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgba(1.0, 0.0, 0.0, 1.0)\n" + " let red_hex = to_rgba_hex_string(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec to_rgba_hex_string(colour()) -> binary(). +to_rgba_hex_string(Colour) -> + Hex_string = begin + _pipe = to_rgba_hex(Colour), + gleam@int:to_base16(_pipe) + end, + case string:length(Hex_string) of + 8 -> + Hex_string; + + L -> + <<(gleam@string:repeat(<<"0"/utf8>>, 8 - L))/binary, + Hex_string/binary>> + end. + +-file("src/gleam_community/colour.gleam", 876). +?DOC( + " Returns a rgb hex `Int` created from the given `Colour`.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgba(255, 0, 0)\n" + " let red_hex_int = to_rgb_hex(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec to_rgb_hex(colour()) -> integer(). +to_rgb_hex(Colour) -> + {R, G, B, _} = to_rgba(Colour), + Red = begin + _pipe = R * 255.0, + _pipe@1 = erlang:round(_pipe), + erlang:'bsl'(_pipe@1, 16) + end, + Green = begin + _pipe@2 = G * 255.0, + _pipe@3 = erlang:round(_pipe@2), + erlang:'bsl'(_pipe@3, 8) + end, + Blue = begin + _pipe@4 = B * 255.0, + erlang:round(_pipe@4) + end, + (Red + Green) + Blue. + +-file("src/gleam_community/colour.gleam", 796). +?DOC( + " Returns an rgb hex formatted `String` created from the given `Colour`.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgba(255, 0, 0)\n" + " let red_hex = to_rgb_hex_string(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec to_rgb_hex_string(colour()) -> binary(). +to_rgb_hex_string(Colour) -> + Hex_string = begin + _pipe = to_rgb_hex(Colour), + gleam@int:to_base16(_pipe) + end, + case string:length(Hex_string) of + 6 -> + Hex_string; + + L -> + <<(gleam@string:repeat(<<"0"/utf8>>, 6 - L))/binary, + Hex_string/binary>> + end. + +-file("src/gleam_community/colour.gleam", 918). +-spec encode_rgba(float(), float(), float(), float()) -> gleam@json:json(). +encode_rgba(R, G, B, A) -> + gleam@json:object( + [{<<"r"/utf8>>, gleam@json:float(R)}, + {<<"g"/utf8>>, gleam@json:float(G)}, + {<<"b"/utf8>>, gleam@json:float(B)}, + {<<"a"/utf8>>, gleam@json:float(A)}] + ). + +-file("src/gleam_community/colour.gleam", 927). +-spec encode_hsla(float(), float(), float(), float()) -> gleam@json:json(). +encode_hsla(H, S, L, A) -> + gleam@json:object( + [{<<"h"/utf8>>, gleam@json:float(H)}, + {<<"s"/utf8>>, gleam@json:float(S)}, + {<<"l"/utf8>>, gleam@json:float(L)}, + {<<"a"/utf8>>, gleam@json:float(A)}] + ). + +-file("src/gleam_community/colour.gleam", 911). +?DOC( + " Encodes a `Colour` value as a Gleam [`Json`](https://hexdocs.pm/gleam_json/gleam/json.html#Json)\n" + " value. You'll need this if you want to send a `Colour` value over the network\n" + " in a HTTP request or response, for example.\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec encode(colour()) -> gleam@json:json(). +encode(Colour) -> + case Colour of + {rgba, R, G, B, A} -> + encode_rgba(R, G, B, A); + + {hsla, H, S, L, A@1} -> + encode_hsla(H, S, L, A@1) + end. + +-file("src/gleam_community/colour.gleam", 952). +-spec rgba_decoder() -> gleam@dynamic@decode:decoder(colour()). +rgba_decoder() -> + gleam@dynamic@decode:field( + <<"r"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(R) -> + gleam@dynamic@decode:field( + <<"g"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(G) -> + gleam@dynamic@decode:field( + <<"b"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(B) -> + gleam@dynamic@decode:field( + <<"a"/utf8>>, + {decoder, + fun gleam@dynamic@decode:decode_float/1}, + fun(A) -> + gleam@dynamic@decode:success( + {rgba, R, G, B, A} + ) + end + ) + end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 961). +-spec hsla_decoder() -> gleam@dynamic@decode:decoder(colour()). +hsla_decoder() -> + gleam@dynamic@decode:field( + <<"h"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(H) -> + gleam@dynamic@decode:field( + <<"s"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(S) -> + gleam@dynamic@decode:field( + <<"l"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(L) -> + gleam@dynamic@decode:field( + <<"a"/utf8>>, + {decoder, + fun gleam@dynamic@decode:decode_float/1}, + fun(A) -> + gleam@dynamic@decode:success( + {hsla, H, S, L, A} + ) + end + ) + end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 948). +?DOC( + " Attempt to decode some [`Dynamic`](https://hexdocs.pm/gleam_stdlib/gleam/dynamic.html#Dynamic)\n" + " value into a `Colour`. Most often you'll use this to decode some JSON.\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec decoder() -> gleam@dynamic@decode:decoder(colour()). +decoder() -> + gleam@dynamic@decode:one_of(rgba_decoder(), [hsla_decoder()]). diff --git a/build/dev/javascript/gleam_community_colour/gleam_community@colour@accessibility.erl b/build/dev/javascript/gleam_community_colour/gleam_community@colour@accessibility.erl new file mode 100644 index 0000000..35c1d67 --- /dev/null +++ b/build/dev/javascript/gleam_community_colour/gleam_community@colour@accessibility.erl @@ -0,0 +1,203 @@ +-module(gleam_community@colour@accessibility). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). +-define(FILEPATH, "src/gleam_community/colour/accessibility.gleam"). +-export([luminance/1, contrast_ratio/2, maximum_contrast/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " \n" + " - **Accessibility**\n" + " - [`luminance`](#luminance)\n" + " - [`contrast_ratio`](#contrast_ratio)\n" + " - [`maximum_contrast`](#maximum_contrast)\n" + "\n" + " ---\n" + "\n" + " This package was heavily inspired by the `elm-color-extra` module.\n" + " The original source code can be found\n" + " here.\n" + "\n" + "
\n" + " The license of that package is produced below:\n" + " \n" + " \n" + " > MIT License\n" + "\n" + " > Copyright (c) 2016 Andreas Köberle\n" + "\n" + " > Permission is hereby granted, free of charge, to any person obtaining a copy\n" + " of this software and associated documentation files (the \"Software\"), to deal\n" + " in the Software without restriction, including without limitation the rights\n" + " to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n" + " copies of the Software, and to permit persons to whom the Software is\n" + " furnished to do so, subject to the following conditions:\n" + "\n" + " > The above copyright notice and this permission notice shall be included in all\n" + " copies or substantial portions of the Software.\n" + "\n" + " > THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n" + " IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n" + " FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n" + " AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n" + " LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n" + " OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n" + " SOFTWARE.\n" + "\n" + "
\n" + "\n" +). + +-file("src/gleam_community/colour/accessibility.gleam", 56). +-spec intensity(float()) -> float(). +intensity(Colour_value) -> + case true of + _ when Colour_value =< 0.03928 -> + Colour_value / 12.92; + + _ -> + I@1 = case gleam@float:power((Colour_value + 0.055) / 1.055, 2.4) of + {ok, I} -> I; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour/accessibility"/utf8>>, + function => <<"intensity"/utf8>>, + line => 62, + value => _assert_fail, + start => 2399, + 'end' => 2470, + pattern_start => 2410, + pattern_end => 2415}) + end, + I@1 + end. + +-file("src/gleam_community/colour/accessibility.gleam", 92). +?DOC( + " Returns the relative brightness of the given `Colour` as a `Float` between\n" + " 0.0, and 1.0 with 0.0 being the darkest possible colour and 1.0 being the lightest.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " luminance(colour.white) // 1.0\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec luminance(gleam_community@colour:colour()) -> float(). +luminance(Colour) -> + {R, G, B, _} = gleam_community@colour:to_rgba(Colour), + R_intensity = intensity(R), + G_intensity = intensity(G), + B_intensity = intensity(B), + ((0.2126 * R_intensity) + (0.7152 * G_intensity)) + (0.0722 * B_intensity). + +-file("src/gleam_community/colour/accessibility.gleam", 125). +?DOC( + " Returns the contrast between two `Colour` values as a `Float` between 1.0,\n" + " and 21.0 with 1.0 being no contrast and, 21.0 being the highest possible contrast.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " contrast_ratio(between: colour.white, and: colour.black) // 21.0\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec contrast_ratio( + gleam_community@colour:colour(), + gleam_community@colour:colour() +) -> float(). +contrast_ratio(Colour_a, Colour_b) -> + Luminance_a = luminance(Colour_a) + 0.05, + Luminance_b = luminance(Colour_b) + 0.05, + case Luminance_a > Luminance_b of + true -> + case Luminance_b of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> Luminance_a / Gleam@denominator + end; + + false -> + case Luminance_a of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator@1 -> Luminance_b / Gleam@denominator@1 + end + end. + +-file("src/gleam_community/colour/accessibility.gleam", 161). +?DOC( + " Returns the `Colour` with the highest contrast between the base `Colour`,\n" + " and and the other provided `Colour` values.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " maximum_contrast(\n" + " colour.yellow,\n" + " [colour.white, colour.dark_blue, colour.green],\n" + " )\n" + " }\n" + " ```\n" + "
\n" + "\n" + "
\n" + " \n" + " Spot a typo? Open an issue!\n" + " \n" + " \n" + " Back to top ↑\n" + " \n" + "
\n" +). +-spec maximum_contrast( + gleam_community@colour:colour(), + list(gleam_community@colour:colour()) +) -> {ok, gleam_community@colour:colour()} | {error, nil}. +maximum_contrast(Base, Colours) -> + _pipe = Colours, + _pipe@1 = gleam@list:sort( + _pipe, + fun(Colour_a, Colour_b) -> + Contrast_a = contrast_ratio(Base, Colour_a), + Contrast_b = contrast_ratio(Base, Colour_b), + gleam@float:compare(Contrast_b, Contrast_a) + end + ), + gleam@list:first(_pipe@1). diff --git a/build/dev/javascript/gleam_json/_gleam_artefacts/gleam@json.cache b/build/dev/javascript/gleam_json/_gleam_artefacts/gleam@json.cache new file mode 100644 index 0000000..a74dd80 Binary files /dev/null and b/build/dev/javascript/gleam_json/_gleam_artefacts/gleam@json.cache differ diff --git a/build/dev/javascript/gleam_json/_gleam_artefacts/gleam@json.cache_inline b/build/dev/javascript/gleam_json/_gleam_artefacts/gleam@json.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_json/_gleam_artefacts/gleam@json.cache_inline differ diff --git a/build/dev/javascript/gleam_json/_gleam_artefacts/gleam@json.cache_meta b/build/dev/javascript/gleam_json/_gleam_artefacts/gleam@json.cache_meta new file mode 100644 index 0000000..00dd819 Binary files /dev/null and b/build/dev/javascript/gleam_json/_gleam_artefacts/gleam@json.cache_meta differ diff --git a/build/dev/javascript/gleam_json/gleam.mjs b/build/dev/javascript/gleam_json/gleam.mjs new file mode 100644 index 0000000..197cbbc --- /dev/null +++ b/build/dev/javascript/gleam_json/gleam.mjs @@ -0,0 +1 @@ +export * from "../prelude.mjs"; diff --git a/build/dev/javascript/gleam_json/gleam/json.mjs b/build/dev/javascript/gleam_json/gleam/json.mjs new file mode 100644 index 0000000..3111ddc --- /dev/null +++ b/build/dev/javascript/gleam_json/gleam/json.mjs @@ -0,0 +1,325 @@ +import * as $bit_array from "../../gleam_stdlib/gleam/bit_array.mjs"; +import * as $dict from "../../gleam_stdlib/gleam/dict.mjs"; +import * as $dynamic from "../../gleam_stdlib/gleam/dynamic.mjs"; +import * as $decode from "../../gleam_stdlib/gleam/dynamic/decode.mjs"; +import * as $list from "../../gleam_stdlib/gleam/list.mjs"; +import * as $option from "../../gleam_stdlib/gleam/option.mjs"; +import { None, Some } from "../../gleam_stdlib/gleam/option.mjs"; +import * as $result from "../../gleam_stdlib/gleam/result.mjs"; +import * as $string_tree from "../../gleam_stdlib/gleam/string_tree.mjs"; +import { Ok, Error, toList, prepend as listPrepend, CustomType as $CustomType } from "../gleam.mjs"; +import { + decode as decode_string, + json_to_string as do_to_string, + json_to_string as to_string_tree, + identity as do_string, + identity as do_bool, + identity as do_int, + identity as do_float, + do_null, + object as do_object, + array as do_preprocessed_array, +} from "../gleam_json_ffi.mjs"; + +export { to_string_tree }; + +export class UnexpectedEndOfInput extends $CustomType {} +export const DecodeError$UnexpectedEndOfInput = () => + new UnexpectedEndOfInput(); +export const DecodeError$isUnexpectedEndOfInput = (value) => + value instanceof UnexpectedEndOfInput; + +export class UnexpectedByte extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const DecodeError$UnexpectedByte = ($0) => new UnexpectedByte($0); +export const DecodeError$isUnexpectedByte = (value) => + value instanceof UnexpectedByte; +export const DecodeError$UnexpectedByte$0 = (value) => value[0]; + +export class UnexpectedSequence extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const DecodeError$UnexpectedSequence = ($0) => + new UnexpectedSequence($0); +export const DecodeError$isUnexpectedSequence = (value) => + value instanceof UnexpectedSequence; +export const DecodeError$UnexpectedSequence$0 = (value) => value[0]; + +export class UnableToDecode extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const DecodeError$UnableToDecode = ($0) => new UnableToDecode($0); +export const DecodeError$isUnableToDecode = (value) => + value instanceof UnableToDecode; +export const DecodeError$UnableToDecode$0 = (value) => value[0]; + +function do_parse(json, decoder) { + return $result.try$( + decode_string(json), + (dynamic_value) => { + let _pipe = $decode.run(dynamic_value, decoder); + return $result.map_error( + _pipe, + (var0) => { return new UnableToDecode(var0); }, + ); + }, + ); +} + +/** + * Decode a JSON string into dynamically typed data which can be decoded into + * typed data with the `gleam/dynamic` module. + * + * ## Examples + * + * ```gleam + * > parse("[1,2,3]", decode.list(of: decode.int)) + * Ok([1, 2, 3]) + * ``` + * + * ```gleam + * > parse("[", decode.list(of: decode.int)) + * Error(UnexpectedEndOfInput) + * ``` + * + * ```gleam + * > parse("1", decode.string) + * Error(UnableToDecode([decode.DecodeError("String", "Int", [])])) + * ``` + */ +export function parse(json, decoder) { + return do_parse(json, decoder); +} + +function decode_to_dynamic(json) { + let $ = $bit_array.to_string(json); + if ($ instanceof Ok) { + let string$1 = $[0]; + return decode_string(string$1); + } else { + return new Error(new UnexpectedByte("")); + } +} + +/** + * Decode a JSON bit string into dynamically typed data which can be decoded + * into typed data with the `gleam/dynamic` module. + * + * ## Examples + * + * ```gleam + * > parse_bits(<<"[1,2,3]">>, decode.list(of: decode.int)) + * Ok([1, 2, 3]) + * ``` + * + * ```gleam + * > parse_bits(<<"[">>, decode.list(of: decode.int)) + * Error(UnexpectedEndOfInput) + * ``` + * + * ```gleam + * > parse_bits(<<"1">>, decode.string) + * Error(UnableToDecode([decode.DecodeError("String", "Int", [])])), + * ``` + */ +export function parse_bits(json, decoder) { + return $result.try$( + decode_to_dynamic(json), + (dynamic_value) => { + let _pipe = $decode.run(dynamic_value, decoder); + return $result.map_error( + _pipe, + (var0) => { return new UnableToDecode(var0); }, + ); + }, + ); +} + +/** + * Convert a JSON value into a string. + * + * Where possible prefer the `to_string_tree` function as it is faster than + * this function, and BEAM VM IO is optimised for sending `StringTree` data. + * + * ## Examples + * + * ```gleam + * > to_string(array([1, 2, 3], of: int)) + * "[1,2,3]" + * ``` + */ +export function to_string(json) { + return do_to_string(json); +} + +/** + * Encode a string into JSON, using normal JSON escaping. + * + * ## Examples + * + * ```gleam + * > to_string(string("Hello!")) + * "\"Hello!\"" + * ``` + */ +export function string(input) { + return do_string(input); +} + +/** + * Encode a bool into JSON. + * + * ## Examples + * + * ```gleam + * > to_string(bool(False)) + * "false" + * ``` + */ +export function bool(input) { + return do_bool(input); +} + +/** + * Encode an int into JSON. + * + * ## Examples + * + * ```gleam + * > to_string(int(50)) + * "50" + * ``` + */ +export function int(input) { + return do_int(input); +} + +/** + * Encode a float into JSON. + * + * ## Examples + * + * ```gleam + * > to_string(float(4.7)) + * "4.7" + * ``` + */ +export function float(input) { + return do_float(input); +} + +/** + * The JSON value null. + * + * ## Examples + * + * ```gleam + * > to_string(null()) + * "null" + * ``` + */ +export function null$() { + return do_null(); +} + +/** + * Encode an optional value into JSON, using null if it is the `None` variant. + * + * ## Examples + * + * ```gleam + * > to_string(nullable(Some(50), of: int)) + * "50" + * ``` + * + * ```gleam + * > to_string(nullable(None, of: int)) + * "null" + * ``` + */ +export function nullable(input, inner_type) { + if (input instanceof Some) { + let value = input[0]; + return inner_type(value); + } else { + return null$(); + } +} + +/** + * Encode a list of key-value pairs into a JSON object. + * + * ## Examples + * + * ```gleam + * > to_string(object([ + * #("game", string("Pac-Man")), + * #("score", int(3333360)), + * ])) + * "{\"game\":\"Pac-Mac\",\"score\":3333360}" + * ``` + */ +export function object(entries) { + return do_object(entries); +} + +/** + * Encode a list of JSON values into a JSON array. + * + * ## Examples + * + * ```gleam + * > to_string(preprocessed_array([int(1), float(2.0), string("3")])) + * "[1, 2.0, \"3\"]" + * ``` + */ +export function preprocessed_array(from) { + return do_preprocessed_array(from); +} + +/** + * Encode a list into a JSON array. + * + * ## Examples + * + * ```gleam + * > to_string(array([1, 2, 3], of: int)) + * "[1, 2, 3]" + * ``` + */ +export function array(entries, inner_type) { + let _pipe = entries; + let _pipe$1 = $list.map(_pipe, inner_type); + return preprocessed_array(_pipe$1); +} + +/** + * Encode a Dict into a JSON object using the supplied functions to encode + * the keys and the values respectively. + * + * ## Examples + * + * ```gleam + * > to_string(dict(dict.from_list([ #(3, 3.0), #(4, 4.0)]), int.to_string, float) + * "{\"3\": 3.0, \"4\": 4.0}" + * ``` + */ +export function dict(dict, keys, values) { + return object( + $dict.fold( + dict, + toList([]), + (acc, k, v) => { return listPrepend([keys(k), values(v)], acc); }, + ), + ); +} diff --git a/build/dev/javascript/gleam_json/gleam@json.erl b/build/dev/javascript/gleam_json/gleam@json.erl new file mode 100644 index 0000000..8a33e49 --- /dev/null +++ b/build/dev/javascript/gleam_json/gleam@json.erl @@ -0,0 +1,304 @@ +-module(gleam@json). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/json.gleam"). +-export([parse_bits/2, parse/2, to_string/1, to_string_tree/1, string/1, bool/1, int/1, float/1, null/0, nullable/2, object/1, preprocessed_array/1, array/2, dict/3]). +-export_type([json/0, decode_error/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type json() :: any(). + +-type decode_error() :: unexpected_end_of_input | + {unexpected_byte, binary()} | + {unexpected_sequence, binary()} | + {unable_to_decode, list(gleam@dynamic@decode:decode_error())}. + +-file("src/gleam/json.gleam", 88). +?DOC( + " Decode a JSON bit string into dynamically typed data which can be decoded\n" + " into typed data with the `gleam/dynamic` module.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > parse_bits(<<\"[1,2,3]\">>, decode.list(of: decode.int))\n" + " Ok([1, 2, 3])\n" + " ```\n" + "\n" + " ```gleam\n" + " > parse_bits(<<\"[\">>, decode.list(of: decode.int))\n" + " Error(UnexpectedEndOfInput)\n" + " ```\n" + "\n" + " ```gleam\n" + " > parse_bits(<<\"1\">>, decode.string)\n" + " Error(UnableToDecode([decode.DecodeError(\"String\", \"Int\", [])])),\n" + " ```\n" +). +-spec parse_bits(bitstring(), gleam@dynamic@decode:decoder(DNO)) -> {ok, DNO} | + {error, decode_error()}. +parse_bits(Json, Decoder) -> + gleam@result:'try'( + gleam_json_ffi:decode(Json), + fun(Dynamic_value) -> + _pipe = gleam@dynamic@decode:run(Dynamic_value, Decoder), + gleam@result:map_error( + _pipe, + fun(Field@0) -> {unable_to_decode, Field@0} end + ) + end + ). + +-file("src/gleam/json.gleam", 47). +-spec do_parse(binary(), gleam@dynamic@decode:decoder(DNI)) -> {ok, DNI} | + {error, decode_error()}. +do_parse(Json, Decoder) -> + Bits = gleam_stdlib:identity(Json), + parse_bits(Bits, Decoder). + +-file("src/gleam/json.gleam", 39). +?DOC( + " Decode a JSON string into dynamically typed data which can be decoded into\n" + " typed data with the `gleam/dynamic` module.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > parse(\"[1,2,3]\", decode.list(of: decode.int))\n" + " Ok([1, 2, 3])\n" + " ```\n" + "\n" + " ```gleam\n" + " > parse(\"[\", decode.list(of: decode.int))\n" + " Error(UnexpectedEndOfInput)\n" + " ```\n" + "\n" + " ```gleam\n" + " > parse(\"1\", decode.string)\n" + " Error(UnableToDecode([decode.DecodeError(\"String\", \"Int\", [])]))\n" + " ```\n" +). +-spec parse(binary(), gleam@dynamic@decode:decoder(DNE)) -> {ok, DNE} | + {error, decode_error()}. +parse(Json, Decoder) -> + do_parse(Json, Decoder). + +-file("src/gleam/json.gleam", 117). +?DOC( + " Convert a JSON value into a string.\n" + "\n" + " Where possible prefer the `to_string_tree` function as it is faster than\n" + " this function, and BEAM VM IO is optimised for sending `StringTree` data.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(array([1, 2, 3], of: int))\n" + " \"[1,2,3]\"\n" + " ```\n" +). +-spec to_string(json()) -> binary(). +to_string(Json) -> + gleam_json_ffi:json_to_string(Json). + +-file("src/gleam/json.gleam", 140). +?DOC( + " Convert a JSON value into a string tree.\n" + "\n" + " Where possible prefer this function to the `to_string` function as it is\n" + " slower than this function, and BEAM VM IO is optimised for sending\n" + " `StringTree` data.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string_tree(array([1, 2, 3], of: int))\n" + " string_tree.from_string(\"[1,2,3]\")\n" + " ```\n" +). +-spec to_string_tree(json()) -> gleam@string_tree:string_tree(). +to_string_tree(Json) -> + gleam_json_ffi:json_to_iodata(Json). + +-file("src/gleam/json.gleam", 151). +?DOC( + " Encode a string into JSON, using normal JSON escaping.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(string(\"Hello!\"))\n" + " \"\\\"Hello!\\\"\"\n" + " ```\n" +). +-spec string(binary()) -> json(). +string(Input) -> + gleam_json_ffi:string(Input). + +-file("src/gleam/json.gleam", 168). +?DOC( + " Encode a bool into JSON.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(bool(False))\n" + " \"false\"\n" + " ```\n" +). +-spec bool(boolean()) -> json(). +bool(Input) -> + gleam_json_ffi:bool(Input). + +-file("src/gleam/json.gleam", 185). +?DOC( + " Encode an int into JSON.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(int(50))\n" + " \"50\"\n" + " ```\n" +). +-spec int(integer()) -> json(). +int(Input) -> + gleam_json_ffi:int(Input). + +-file("src/gleam/json.gleam", 202). +?DOC( + " Encode a float into JSON.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(float(4.7))\n" + " \"4.7\"\n" + " ```\n" +). +-spec float(float()) -> json(). +float(Input) -> + gleam_json_ffi:float(Input). + +-file("src/gleam/json.gleam", 219). +?DOC( + " The JSON value null.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(null())\n" + " \"null\"\n" + " ```\n" +). +-spec null() -> json(). +null() -> + gleam_json_ffi:null(). + +-file("src/gleam/json.gleam", 241). +?DOC( + " Encode an optional value into JSON, using null if it is the `None` variant.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(nullable(Some(50), of: int))\n" + " \"50\"\n" + " ```\n" + "\n" + " ```gleam\n" + " > to_string(nullable(None, of: int))\n" + " \"null\"\n" + " ```\n" +). +-spec nullable(gleam@option:option(DNU), fun((DNU) -> json())) -> json(). +nullable(Input, Inner_type) -> + case Input of + {some, Value} -> + Inner_type(Value); + + none -> + null() + end. + +-file("src/gleam/json.gleam", 260). +?DOC( + " Encode a list of key-value pairs into a JSON object.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(object([\n" + " #(\"game\", string(\"Pac-Man\")),\n" + " #(\"score\", int(3333360)),\n" + " ]))\n" + " \"{\\\"game\\\":\\\"Pac-Mac\\\",\\\"score\\\":3333360}\"\n" + " ```\n" +). +-spec object(list({binary(), json()})) -> json(). +object(Entries) -> + gleam_json_ffi:object(Entries). + +-file("src/gleam/json.gleam", 292). +?DOC( + " Encode a list of JSON values into a JSON array.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(preprocessed_array([int(1), float(2.0), string(\"3\")]))\n" + " \"[1, 2.0, \\\"3\\\"]\"\n" + " ```\n" +). +-spec preprocessed_array(list(json())) -> json(). +preprocessed_array(From) -> + gleam_json_ffi:array(From). + +-file("src/gleam/json.gleam", 277). +?DOC( + " Encode a list into a JSON array.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(array([1, 2, 3], of: int))\n" + " \"[1, 2, 3]\"\n" + " ```\n" +). +-spec array(list(DNY), fun((DNY) -> json())) -> json(). +array(Entries, Inner_type) -> + _pipe = Entries, + _pipe@1 = gleam@list:map(_pipe, Inner_type), + preprocessed_array(_pipe@1). + +-file("src/gleam/json.gleam", 310). +?DOC( + " Encode a Dict into a JSON object using the supplied functions to encode\n" + " the keys and the values respectively.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(dict(dict.from_list([ #(3, 3.0), #(4, 4.0)]), int.to_string, float)\n" + " \"{\\\"3\\\": 3.0, \\\"4\\\": 4.0}\"\n" + " ```\n" +). +-spec dict( + gleam@dict:dict(DOC, DOD), + fun((DOC) -> binary()), + fun((DOD) -> json()) +) -> json(). +dict(Dict, Keys, Values) -> + object( + gleam@dict:fold( + Dict, + [], + fun(Acc, K, V) -> [{Keys(K), Values(V)} | Acc] end + ) + ). diff --git a/build/dev/javascript/gleam_json/gleam_json_ffi.erl b/build/dev/javascript/gleam_json/gleam_json_ffi.erl new file mode 100644 index 0000000..06a26a0 --- /dev/null +++ b/build/dev/javascript/gleam_json/gleam_json_ffi.erl @@ -0,0 +1,66 @@ +-module(gleam_json_ffi). + +-export([ + decode/1, json_to_iodata/1, json_to_string/1, int/1, float/1, string/1, + bool/1, null/0, array/1, object/1 +]). + +-if(?OTP_RELEASE < 27). +-define(bad_version, + error({erlang_otp_27_required, << "Insufficient Erlang/OTP version. + +`gleam_json` uses the Erlang `json` module introduced in Erlang/OTP 27. +You are using Erlang/OTP "/utf8, (integer_to_binary(?OTP_RELEASE))/binary, " +Please upgrade your Erlang install or downgrade to `gleam_json` v1.0.1. +"/utf8>>})). + +decode(_) -> ?bad_version. +json_to_iodata(_) -> ?bad_version. +json_to_string(_) -> ?bad_version. +int(_) -> ?bad_version. +float(_) -> ?bad_version. +string(_) -> ?bad_version. +bool(_) -> ?bad_version. +array(_) -> ?bad_version. +object(_) -> ?bad_version. +null() -> ?bad_version. +-else. + +decode(Json) -> + try + {ok, json:decode(Json)} + catch + error:unexpected_end -> {error, unexpected_end_of_input}; + error:{invalid_byte, Byte} -> {error, {unexpected_byte, hex(Byte)}}; + error:{unexpected_sequence, Byte} -> {error, {unexpected_sequence, Byte}} + end. + +hex(I) -> + H = list_to_binary(integer_to_list(I, 16)), + <<"0x"/utf8, H/binary>>. + +json_to_iodata(Json) -> + Json. + +json_to_string(Json) when is_binary(Json) -> + Json; +json_to_string(Json) when is_list(Json) -> + list_to_binary(Json). + +null() -> <<"null">>. +bool(true) -> <<"true">>; +bool(false) -> <<"false">>. +int(X) -> json:encode_integer(X). +float(X) -> json:encode_float(X). +string(X) -> json:encode_binary(X). + +array([]) -> <<"[]">>; +array([First | Rest]) -> [$[, First | array_loop(Rest)]. +array_loop([]) -> "]"; +array_loop([Elem | Rest]) -> [$,, Elem | array_loop(Rest)]. + +object(List) -> encode_object([[$,, string(Key), $: | Value] || {Key, Value} <- List]). +encode_object([]) -> <<"{}">>; +encode_object([[_Comma | Entry] | Rest]) -> ["{", Entry, Rest, "}"]. + +-endif. diff --git a/build/dev/javascript/gleam_json/gleam_json_ffi.mjs b/build/dev/javascript/gleam_json/gleam_json_ffi.mjs new file mode 100644 index 0000000..1d8d3ff --- /dev/null +++ b/build/dev/javascript/gleam_json/gleam_json_ffi.mjs @@ -0,0 +1,201 @@ +import { + Result$Ok, + Result$Error, + List$isNonEmpty, + List$NonEmpty$first, + List$NonEmpty$rest, +} from "./gleam.mjs"; +import { + DecodeError$UnexpectedByte, + DecodeError$UnexpectedEndOfInput, +} from "./gleam/json.mjs"; + +export function json_to_string(json) { + return JSON.stringify(json); +} + +export function object(entries) { + return Object.fromEntries(entries); +} + +export function identity(x) { + return x; +} + +export function array(list) { + const array = []; + while (List$isNonEmpty(list)) { + array.push(List$NonEmpty$first(list)); + list = List$NonEmpty$rest(list); + } + return array; +} + +export function do_null() { + return null; +} + +export function decode(string) { + try { + const result = JSON.parse(string); + return Result$Ok(result); + } catch (err) { + return Result$Error(getJsonDecodeError(err, string)); + } +} + +export function getJsonDecodeError(stdErr, json) { + if (isUnexpectedEndOfInput(stdErr)) return DecodeError$UnexpectedEndOfInput(); + return toUnexpectedByteError(stdErr, json); +} + +/** + * Matches unexpected end of input messages in: + * - Chromium (edge, chrome, node) + * - Spidermonkey (firefox) + * - JavascriptCore (safari) + * + * Note that Spidermonkey and JavascriptCore will both incorrectly report some + * UnexpectedByte errors as UnexpectedEndOfInput errors. For example: + * + * @example + * // in JavascriptCore + * JSON.parse('{"a"]: "b"}) + * // => JSON Parse error: Expected ':' before value + * + * JSON.parse('{"a"') + * // => JSON Parse error: Expected ':' before value + * + * // in Chromium (correct) + * JSON.parse('{"a"]: "b"}) + * // => Unexpected token ] in JSON at position 4 + * + * JSON.parse('{"a"') + * // => Unexpected end of JSON input + */ +function isUnexpectedEndOfInput(err) { + const unexpectedEndOfInputRegex = + /((unexpected (end|eof))|(end of data)|(unterminated string)|(json( parse error|\.parse)\: expected '(\:|\}|\])'))/i; + return unexpectedEndOfInputRegex.test(err.message); +} + +/** + * Converts a SyntaxError to an UnexpectedByte error based on the JS runtime. + * + * For Chromium, the unexpected byte and position are reported by the runtime. + * + * For JavascriptCore, only the unexpected byte is reported by the runtime, so + * there is no way to know which position that character is in unless we then + * parse the string again ourselves. So instead, the position is reported as 0. + * + * For Spidermonkey, the position is reported by the runtime as a line and column number + * and the unexpected byte is found using those coordinates. + */ +function toUnexpectedByteError(err, json) { + let converters = [ + v8UnexpectedByteError, + oldV8UnexpectedByteError, + jsCoreUnexpectedByteError, + spidermonkeyUnexpectedByteError, + ]; + + for (let converter of converters) { + let result = converter(err, json); + if (result) return result; + } + + return DecodeError$UnexpectedByte(""); +} + +/** + * Matches unexpected byte messages in: + * - V8 (edge, chrome, node) + * + * Matches the character but not the position as this is no longer reported by + * V8. Boo! + */ +function v8UnexpectedByteError(err) { + const regex = /unexpected token '(.)', ".+" is not valid JSON/i; + const match = regex.exec(err.message); + if (!match) return null; + const byte = toHex(match[1]); + return DecodeError$UnexpectedByte(byte); +} + +/** + * Matches unexpected byte messages in: + * - V8 (edge, chrome, node) + * + * No longer works in current versions of V8. + * + * Matches the character and its position. + */ +function oldV8UnexpectedByteError(err) { + const regex = /unexpected token (.) in JSON at position (\d+)/i; + const match = regex.exec(err.message); + if (!match) return null; + const byte = toHex(match[1]); + return DecodeError$UnexpectedByte(byte); +} + +/** + * Matches unexpected byte messages in: + * - Spidermonkey (firefox) + * + * Matches the position in a 2d grid only and not the character. + */ +function spidermonkeyUnexpectedByteError(err, json) { + const regex = + /(unexpected character|expected .*) at line (\d+) column (\d+)/i; + const match = regex.exec(err.message); + if (!match) return null; + const line = Number(match[2]); + const column = Number(match[3]); + const position = getPositionFromMultiline(line, column, json); + const byte = toHex(json[position]); + return DecodeError$UnexpectedByte(byte); +} + +/** + * Matches unexpected byte messages in: + * - JavascriptCore (safari) + * + * JavascriptCore only reports what the character is and not its position. + */ +function jsCoreUnexpectedByteError(err) { + const regex = /unexpected (identifier|token) "(.)"/i; + const match = regex.exec(err.message); + if (!match) return null; + const byte = toHex(match[2]); + return DecodeError$UnexpectedByte(byte); +} + +function toHex(char) { + return "0x" + char.charCodeAt(0).toString(16).toUpperCase(); +} + +/** + * Gets the position of a character in a flattened (i.e. single line) string + * from a line and column number. Note that the position is 0-indexed and + * the line and column numbers are 1-indexed. + * + * @param {number} line + * @param {number} column + * @param {string} string + */ +function getPositionFromMultiline(line, column, string) { + if (line === 1) return column - 1; + + let currentLn = 1; + let position = 0; + string.split("").find((char, idx) => { + if (char === "\n") currentLn += 1; + if (currentLn === line) { + position = idx + column; + return true; + } + return false; + }); + + return position; +} diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bit_array.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bit_array.cache new file mode 100644 index 0000000..8c670b6 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bit_array.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bit_array.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bit_array.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bit_array.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bit_array.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bit_array.cache_meta new file mode 100644 index 0000000..35d8d57 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bit_array.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bool.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bool.cache new file mode 100644 index 0000000..23c6ee1 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bool.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bool.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bool.cache_inline new file mode 100644 index 0000000..52a93fa Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bool.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bool.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bool.cache_meta new file mode 100644 index 0000000..c6b30c1 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bool.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bytes_tree.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bytes_tree.cache new file mode 100644 index 0000000..e7446ce Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bytes_tree.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bytes_tree.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bytes_tree.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bytes_tree.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bytes_tree.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bytes_tree.cache_meta new file mode 100644 index 0000000..b00ed33 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@bytes_tree.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dict.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dict.cache new file mode 100644 index 0000000..8c73d9f Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dict.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dict.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dict.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dict.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dict.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dict.cache_meta new file mode 100644 index 0000000..db29c27 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dict.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic.cache new file mode 100644 index 0000000..bbe4a32 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic.cache_meta new file mode 100644 index 0000000..28bcb8b Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic@decode.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic@decode.cache new file mode 100644 index 0000000..3035efb Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic@decode.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic@decode.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic@decode.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic@decode.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic@decode.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic@decode.cache_meta new file mode 100644 index 0000000..cf83b53 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@dynamic@decode.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@float.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@float.cache new file mode 100644 index 0000000..ec9449c Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@float.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@float.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@float.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@float.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@float.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@float.cache_meta new file mode 100644 index 0000000..604414b Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@float.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@function.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@function.cache new file mode 100644 index 0000000..4664383 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@function.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@function.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@function.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@function.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@function.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@function.cache_meta new file mode 100644 index 0000000..1f87f09 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@function.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@int.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@int.cache new file mode 100644 index 0000000..4224cad Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@int.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@int.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@int.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@int.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@int.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@int.cache_meta new file mode 100644 index 0000000..8730828 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@int.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@io.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@io.cache new file mode 100644 index 0000000..fb831f1 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@io.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@io.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@io.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@io.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@io.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@io.cache_meta new file mode 100644 index 0000000..b6b0435 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@io.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@list.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@list.cache new file mode 100644 index 0000000..7513b89 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@list.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@list.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@list.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@list.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@list.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@list.cache_meta new file mode 100644 index 0000000..78ca370 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@list.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@option.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@option.cache new file mode 100644 index 0000000..76a7836 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@option.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@option.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@option.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@option.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@option.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@option.cache_meta new file mode 100644 index 0000000..8656c26 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@option.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@order.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@order.cache new file mode 100644 index 0000000..ad64dbc Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@order.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@order.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@order.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@order.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@order.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@order.cache_meta new file mode 100644 index 0000000..4404517 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@order.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@pair.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@pair.cache new file mode 100644 index 0000000..7032bdd Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@pair.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@pair.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@pair.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@pair.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@pair.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@pair.cache_meta new file mode 100644 index 0000000..daa77fc Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@pair.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@result.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@result.cache new file mode 100644 index 0000000..3e0c211 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@result.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@result.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@result.cache_inline new file mode 100644 index 0000000..3d06590 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@result.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@result.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@result.cache_meta new file mode 100644 index 0000000..b023748 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@result.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@set.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@set.cache new file mode 100644 index 0000000..12a446a Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@set.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@set.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@set.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@set.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@set.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@set.cache_meta new file mode 100644 index 0000000..e5bd75e Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@set.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string.cache new file mode 100644 index 0000000..f316fb5 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string.cache_meta new file mode 100644 index 0000000..09ee85c Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string_tree.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string_tree.cache new file mode 100644 index 0000000..93d3cc1 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string_tree.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string_tree.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string_tree.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string_tree.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string_tree.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string_tree.cache_meta new file mode 100644 index 0000000..abfcf6a Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@string_tree.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@uri.cache b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@uri.cache new file mode 100644 index 0000000..f51ee88 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@uri.cache differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@uri.cache_inline b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@uri.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@uri.cache_inline differ diff --git a/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@uri.cache_meta b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@uri.cache_meta new file mode 100644 index 0000000..a2b0e30 Binary files /dev/null and b/build/dev/javascript/gleam_stdlib/_gleam_artefacts/gleam@uri.cache_meta differ diff --git a/build/dev/javascript/gleam_stdlib/dict.mjs b/build/dev/javascript/gleam_stdlib/dict.mjs new file mode 100644 index 0000000..f39cd54 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/dict.mjs @@ -0,0 +1,993 @@ +/** + * This file uses jsdoc to annotate types. + * These types can be checked using the typescript compiler with "checkjs" option. + */ + +import { isEqual } from "./gleam.mjs"; + +const referenceMap = /* @__PURE__ */ new WeakMap(); +const tempDataView = /* @__PURE__ */ new DataView( + /* @__PURE__ */ new ArrayBuffer(8), +); +let referenceUID = 0; +/** + * hash the object by reference using a weak map and incrementing uid + * @param {any} o + * @returns {number} + */ +function hashByReference(o) { + const known = referenceMap.get(o); + if (known !== undefined) { + return known; + } + const hash = referenceUID++; + if (referenceUID === 0x7fffffff) { + referenceUID = 0; + } + referenceMap.set(o, hash); + return hash; +} + +/** + * merge two hashes in an order sensitive way + * @param {number} a + * @param {number} b + * @returns {number} + */ +function hashMerge(a, b) { + return (a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2))) | 0; +} + +/** + * standard string hash popularised by java + * @param {string} s + * @returns {number} + */ +function hashString(s) { + let hash = 0; + const len = s.length; + for (let i = 0; i < len; i++) { + hash = (Math.imul(31, hash) + s.charCodeAt(i)) | 0; + } + return hash; +} + +/** + * hash a number by converting to two integers and do some jumbling + * @param {number} n + * @returns {number} + */ +function hashNumber(n) { + tempDataView.setFloat64(0, n); + const i = tempDataView.getInt32(0); + const j = tempDataView.getInt32(4); + return Math.imul(0x45d9f3b, (i >> 16) ^ i) ^ j; +} + +/** + * hash a BigInt by converting it to a string and hashing that + * @param {BigInt} n + * @returns {number} + */ +function hashBigInt(n) { + return hashString(n.toString()); +} + +/** + * hash any js object + * @param {any} o + * @returns {number} + */ +function hashObject(o) { + const proto = Object.getPrototypeOf(o); + if (proto !== null && typeof proto.hashCode === "function") { + try { + const code = o.hashCode(o); + if (typeof code === "number") { + return code; + } + } catch {} + } + if (o instanceof Promise || o instanceof WeakSet || o instanceof WeakMap) { + return hashByReference(o); + } + if (o instanceof Date) { + return hashNumber(o.getTime()); + } + let h = 0; + if (o instanceof ArrayBuffer) { + o = new Uint8Array(o); + } + if (Array.isArray(o) || o instanceof Uint8Array) { + for (let i = 0; i < o.length; i++) { + h = (Math.imul(31, h) + getHash(o[i])) | 0; + } + } else if (o instanceof Set) { + o.forEach((v) => { + h = (h + getHash(v)) | 0; + }); + } else if (o instanceof Map) { + o.forEach((v, k) => { + h = (h + hashMerge(getHash(v), getHash(k))) | 0; + }); + } else { + const keys = Object.keys(o); + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + const v = o[k]; + h = (h + hashMerge(getHash(v), hashString(k))) | 0; + } + } + return h; +} + +/** + * hash any js value + * @param {any} u + * @returns {number} + */ +export function getHash(u) { + if (u === null) return 0x42108422; + if (u === undefined) return 0x42108423; + if (u === true) return 0x42108421; + if (u === false) return 0x42108420; + switch (typeof u) { + case "number": + return hashNumber(u); + case "string": + return hashString(u); + case "bigint": + return hashBigInt(u); + case "object": + return hashObject(u); + case "symbol": + return hashByReference(u); + case "function": + return hashByReference(u); + default: + return 0; // should be unreachable + } +} + +/** + * @template K,V + * @typedef {ArrayNode | IndexNode | CollisionNode} Node + */ +/** + * @template K,V + * @typedef {{ type: typeof ENTRY, k: K, v: V }} Entry + */ +/** + * @template K,V + * @typedef {{ type: typeof ARRAY_NODE, size: number, array: (undefined | Entry | Node)[] }} ArrayNode + */ +/** + * @template K,V + * @typedef {{ type: typeof INDEX_NODE, bitmap: number, array: (Entry | Node)[] }} IndexNode + */ +/** + * @template K,V + * @typedef {{ type: typeof COLLISION_NODE, hash: number, array: Entry[] }} CollisionNode + */ +/** + * @typedef {{ val: boolean }} Flag + */ +const SHIFT = 5; // number of bits you need to shift by to get the next bucket +const BUCKET_SIZE = Math.pow(2, SHIFT); +const MASK = BUCKET_SIZE - 1; // used to zero out all bits not in the bucket +const MAX_INDEX_NODE = BUCKET_SIZE / 2; // when does index node grow into array node +const MIN_ARRAY_NODE = BUCKET_SIZE / 4; // when does array node shrink to index node +const ENTRY = 0; +const ARRAY_NODE = 1; +const INDEX_NODE = 2; +const COLLISION_NODE = 3; + +/** @type {IndexNode} */ +const EMPTY = { + type: INDEX_NODE, + bitmap: 0, + array: [], +}; +/** + * Mask the hash to get only the bucket corresponding to shift + * @param {number} hash + * @param {number} shift + * @returns {number} + */ +function mask(hash, shift) { + return (hash >>> shift) & MASK; +} + +/** + * Set only the Nth bit where N is the masked hash + * @param {number} hash + * @param {number} shift + * @returns {number} + */ +function bitpos(hash, shift) { + return 1 << mask(hash, shift); +} + +/** + * Count the number of 1 bits in a number + * @param {number} x + * @returns {number} + */ +function bitcount(x) { + x -= (x >> 1) & 0x55555555; + x = (x & 0x33333333) + ((x >> 2) & 0x33333333); + x = (x + (x >> 4)) & 0x0f0f0f0f; + x += x >> 8; + x += x >> 16; + return x & 0x7f; +} + +/** + * Calculate the array index of an item in a bitmap index node + * @param {number} bitmap + * @param {number} bit + * @returns {number} + */ +function index(bitmap, bit) { + return bitcount(bitmap & (bit - 1)); +} + +/** + * Efficiently copy an array and set one value at an index + * @template T + * @param {T[]} arr + * @param {number} at + * @param {T} val + * @returns {T[]} + */ +function cloneAndSet(arr, at, val) { + const len = arr.length; + const out = new Array(len); + for (let i = 0; i < len; ++i) { + out[i] = arr[i]; + } + out[at] = val; + return out; +} + +/** + * Efficiently copy an array and insert one value at an index + * @template T + * @param {T[]} arr + * @param {number} at + * @param {T} val + * @returns {T[]} + */ +function spliceIn(arr, at, val) { + const len = arr.length; + const out = new Array(len + 1); + let i = 0; + let g = 0; + while (i < at) { + out[g++] = arr[i++]; + } + out[g++] = val; + while (i < len) { + out[g++] = arr[i++]; + } + return out; +} + +/** + * Efficiently copy an array and remove one value at an index + * @template T + * @param {T[]} arr + * @param {number} at + * @returns {T[]} + */ +function spliceOut(arr, at) { + const len = arr.length; + const out = new Array(len - 1); + let i = 0; + let g = 0; + while (i < at) { + out[g++] = arr[i++]; + } + ++i; + while (i < len) { + out[g++] = arr[i++]; + } + return out; +} + +/** + * Create a new node containing two entries + * @template K,V + * @param {number} shift + * @param {K} key1 + * @param {V} val1 + * @param {number} key2hash + * @param {K} key2 + * @param {V} val2 + * @returns {Node} + */ +function createNode(shift, key1, val1, key2hash, key2, val2) { + const key1hash = getHash(key1); + if (key1hash === key2hash) { + return { + type: COLLISION_NODE, + hash: key1hash, + array: [ + { type: ENTRY, k: key1, v: val1 }, + { type: ENTRY, k: key2, v: val2 }, + ], + }; + } + const addedLeaf = { val: false }; + return assoc( + assocIndex(EMPTY, shift, key1hash, key1, val1, addedLeaf), + shift, + key2hash, + key2, + val2, + addedLeaf, + ); +} + +/** + * @template T,K,V + * @callback AssocFunction + * @param {T} root + * @param {number} shift + * @param {number} hash + * @param {K} key + * @param {V} val + * @param {Flag} addedLeaf + * @returns {Node} + */ +/** + * Associate a node with a new entry, creating a new node + * @template T,K,V + * @type {AssocFunction,K,V>} + */ +function assoc(root, shift, hash, key, val, addedLeaf) { + switch (root.type) { + case ARRAY_NODE: + return assocArray(root, shift, hash, key, val, addedLeaf); + case INDEX_NODE: + return assocIndex(root, shift, hash, key, val, addedLeaf); + case COLLISION_NODE: + return assocCollision(root, shift, hash, key, val, addedLeaf); + } +} +/** + * @template T,K,V + * @type {AssocFunction,K,V>} + */ +function assocArray(root, shift, hash, key, val, addedLeaf) { + const idx = mask(hash, shift); + const node = root.array[idx]; + // if the corresponding index is empty set the index to a newly created node + if (node === undefined) { + addedLeaf.val = true; + return { + type: ARRAY_NODE, + size: root.size + 1, + array: cloneAndSet(root.array, idx, { type: ENTRY, k: key, v: val }), + }; + } + if (node.type === ENTRY) { + // if keys are equal replace the entry + if (isEqual(key, node.k)) { + if (val === node.v) { + return root; + } + return { + type: ARRAY_NODE, + size: root.size, + array: cloneAndSet(root.array, idx, { + type: ENTRY, + k: key, + v: val, + }), + }; + } + // otherwise upgrade the entry to a node and insert + addedLeaf.val = true; + return { + type: ARRAY_NODE, + size: root.size, + array: cloneAndSet( + root.array, + idx, + createNode(shift + SHIFT, node.k, node.v, hash, key, val), + ), + }; + } + // otherwise call assoc on the child node + const n = assoc(node, shift + SHIFT, hash, key, val, addedLeaf); + // if the child node hasn't changed just return the old root + if (n === node) { + return root; + } + // otherwise set the index to the new node + return { + type: ARRAY_NODE, + size: root.size, + array: cloneAndSet(root.array, idx, n), + }; +} +/** + * @template T,K,V + * @type {AssocFunction,K,V>} + */ +function assocIndex(root, shift, hash, key, val, addedLeaf) { + const bit = bitpos(hash, shift); + const idx = index(root.bitmap, bit); + // if there is already a item at this hash index.. + if ((root.bitmap & bit) !== 0) { + // if there is a node at the index (not an entry), call assoc on the child node + const node = root.array[idx]; + if (node.type !== ENTRY) { + const n = assoc(node, shift + SHIFT, hash, key, val, addedLeaf); + if (n === node) { + return root; + } + return { + type: INDEX_NODE, + bitmap: root.bitmap, + array: cloneAndSet(root.array, idx, n), + }; + } + // otherwise there is an entry at the index + // if the keys are equal replace the entry with the updated value + const nodeKey = node.k; + if (isEqual(key, nodeKey)) { + if (val === node.v) { + return root; + } + return { + type: INDEX_NODE, + bitmap: root.bitmap, + array: cloneAndSet(root.array, idx, { + type: ENTRY, + k: key, + v: val, + }), + }; + } + // if the keys are not equal, replace the entry with a new child node + addedLeaf.val = true; + return { + type: INDEX_NODE, + bitmap: root.bitmap, + array: cloneAndSet( + root.array, + idx, + createNode(shift + SHIFT, nodeKey, node.v, hash, key, val), + ), + }; + } else { + // else there is currently no item at the hash index + const n = root.array.length; + // if the number of nodes is at the maximum, expand this node into an array node + if (n >= MAX_INDEX_NODE) { + // create a 32 length array for the new array node (one for each bit in the hash) + const nodes = new Array(32); + // create and insert a node for the new entry + const jdx = mask(hash, shift); + nodes[jdx] = assocIndex(EMPTY, shift + SHIFT, hash, key, val, addedLeaf); + let j = 0; + let bitmap = root.bitmap; + // place each item in the index node into the correct spot in the array node + // loop through all 32 bits / array positions + for (let i = 0; i < 32; i++) { + if ((bitmap & 1) !== 0) { + const node = root.array[j++]; + nodes[i] = node; + } + // shift the bitmap to process the next bit + bitmap = bitmap >>> 1; + } + return { + type: ARRAY_NODE, + size: n + 1, + array: nodes, + }; + } else { + // else there is still space in this index node + // simply insert a new entry at the hash index + const newArray = spliceIn(root.array, idx, { + type: ENTRY, + k: key, + v: val, + }); + addedLeaf.val = true; + return { + type: INDEX_NODE, + bitmap: root.bitmap | bit, + array: newArray, + }; + } + } +} +/** + * @template T,K,V + * @type {AssocFunction,K,V>} + */ +function assocCollision(root, shift, hash, key, val, addedLeaf) { + // if there is a hash collision + if (hash === root.hash) { + const idx = collisionIndexOf(root, key); + // if this key already exists replace the entry with the new value + if (idx !== -1) { + const entry = root.array[idx]; + if (entry.v === val) { + return root; + } + return { + type: COLLISION_NODE, + hash: hash, + array: cloneAndSet(root.array, idx, { type: ENTRY, k: key, v: val }), + }; + } + // otherwise insert the entry at the end of the array + const size = root.array.length; + addedLeaf.val = true; + return { + type: COLLISION_NODE, + hash: hash, + array: cloneAndSet(root.array, size, { type: ENTRY, k: key, v: val }), + }; + } + // if there is no hash collision, upgrade to an index node + return assoc( + { + type: INDEX_NODE, + bitmap: bitpos(root.hash, shift), + array: [root], + }, + shift, + hash, + key, + val, + addedLeaf, + ); +} +/** + * Find the index of a key in the collision node's array + * @template K,V + * @param {CollisionNode} root + * @param {K} key + * @returns {number} + */ +function collisionIndexOf(root, key) { + const size = root.array.length; + for (let i = 0; i < size; i++) { + if (isEqual(key, root.array[i].k)) { + return i; + } + } + return -1; +} +/** + * @template T,K,V + * @callback FindFunction + * @param {T} root + * @param {number} shift + * @param {number} hash + * @param {K} key + * @returns {undefined | Entry} + */ +/** + * Return the found entry or undefined if not present in the root + * @template K,V + * @type {FindFunction,K,V>} + */ +function find(root, shift, hash, key) { + switch (root.type) { + case ARRAY_NODE: + return findArray(root, shift, hash, key); + case INDEX_NODE: + return findIndex(root, shift, hash, key); + case COLLISION_NODE: + return findCollision(root, key); + } +} +/** + * @template K,V + * @type {FindFunction,K,V>} + */ +function findArray(root, shift, hash, key) { + const idx = mask(hash, shift); + const node = root.array[idx]; + if (node === undefined) { + return undefined; + } + if (node.type !== ENTRY) { + return find(node, shift + SHIFT, hash, key); + } + if (isEqual(key, node.k)) { + return node; + } + return undefined; +} +/** + * @template K,V + * @type {FindFunction,K,V>} + */ +function findIndex(root, shift, hash, key) { + const bit = bitpos(hash, shift); + if ((root.bitmap & bit) === 0) { + return undefined; + } + const idx = index(root.bitmap, bit); + const node = root.array[idx]; + if (node.type !== ENTRY) { + return find(node, shift + SHIFT, hash, key); + } + if (isEqual(key, node.k)) { + return node; + } + return undefined; +} +/** + * @template K,V + * @param {CollisionNode} root + * @param {K} key + * @returns {undefined | Entry} + */ +function findCollision(root, key) { + const idx = collisionIndexOf(root, key); + if (idx < 0) { + return undefined; + } + return root.array[idx]; +} +/** + * @template T,K,V + * @callback WithoutFunction + * @param {T} root + * @param {number} shift + * @param {number} hash + * @param {K} key + * @returns {undefined | Node} + */ +/** + * Remove an entry from the root, returning the updated root. + * Returns undefined if the node should be removed from the parent. + * @template K,V + * @type {WithoutFunction,K,V>} + * */ +function without(root, shift, hash, key) { + switch (root.type) { + case ARRAY_NODE: + return withoutArray(root, shift, hash, key); + case INDEX_NODE: + return withoutIndex(root, shift, hash, key); + case COLLISION_NODE: + return withoutCollision(root, key); + } +} +/** + * @template K,V + * @type {WithoutFunction,K,V>} + */ +function withoutArray(root, shift, hash, key) { + const idx = mask(hash, shift); + const node = root.array[idx]; + if (node === undefined) { + return root; // already empty + } + let n = undefined; + // if node is an entry and the keys are not equal there is nothing to remove + // if node is not an entry do a recursive call + if (node.type === ENTRY) { + if (!isEqual(node.k, key)) { + return root; // no changes + } + } else { + n = without(node, shift + SHIFT, hash, key); + if (n === node) { + return root; // no changes + } + } + // if the recursive call returned undefined the node should be removed + if (n === undefined) { + // if the number of child nodes is at the minimum, pack into an index node + if (root.size <= MIN_ARRAY_NODE) { + const arr = root.array; + const out = new Array(root.size - 1); + let i = 0; + let j = 0; + let bitmap = 0; + while (i < idx) { + const nv = arr[i]; + if (nv !== undefined) { + out[j] = nv; + bitmap |= 1 << i; + ++j; + } + ++i; + } + ++i; // skip copying the removed node + while (i < arr.length) { + const nv = arr[i]; + if (nv !== undefined) { + out[j] = nv; + bitmap |= 1 << i; + ++j; + } + ++i; + } + return { + type: INDEX_NODE, + bitmap: bitmap, + array: out, + }; + } + return { + type: ARRAY_NODE, + size: root.size - 1, + array: cloneAndSet(root.array, idx, n), + }; + } + return { + type: ARRAY_NODE, + size: root.size, + array: cloneAndSet(root.array, idx, n), + }; +} +/** + * @template K,V + * @type {WithoutFunction,K,V>} + */ +function withoutIndex(root, shift, hash, key) { + const bit = bitpos(hash, shift); + if ((root.bitmap & bit) === 0) { + return root; // already empty + } + const idx = index(root.bitmap, bit); + const node = root.array[idx]; + // if the item is not an entry + if (node.type !== ENTRY) { + const n = without(node, shift + SHIFT, hash, key); + if (n === node) { + return root; // no changes + } + // if not undefined, the child node still has items, so update it + if (n !== undefined) { + return { + type: INDEX_NODE, + bitmap: root.bitmap, + array: cloneAndSet(root.array, idx, n), + }; + } + // otherwise the child node should be removed + // if it was the only child node, remove this node from the parent + if (root.bitmap === bit) { + return undefined; + } + // otherwise just remove the child node + return { + type: INDEX_NODE, + bitmap: root.bitmap ^ bit, + array: spliceOut(root.array, idx), + }; + } + // otherwise the item is an entry, remove it if the key matches + if (isEqual(key, node.k)) { + if (root.bitmap === bit) { + return undefined; + } + return { + type: INDEX_NODE, + bitmap: root.bitmap ^ bit, + array: spliceOut(root.array, idx), + }; + } + return root; +} +/** + * @template K,V + * @param {CollisionNode} root + * @param {K} key + * @returns {undefined | Node} + */ +function withoutCollision(root, key) { + const idx = collisionIndexOf(root, key); + // if the key not found, no changes + if (idx < 0) { + return root; + } + // otherwise the entry was found, remove it + // if it was the only entry in this node, remove the whole node + if (root.array.length === 1) { + return undefined; + } + // otherwise just remove the entry + return { + type: COLLISION_NODE, + hash: root.hash, + array: spliceOut(root.array, idx), + }; +} +/** + * @template K,V + * @param {undefined | Node} root + * @param {(value:V,key:K)=>void} fn + * @returns {void} + */ +function forEach(root, fn) { + if (root === undefined) { + return; + } + const items = root.array; + const size = items.length; + for (let i = 0; i < size; i++) { + const item = items[i]; + if (item === undefined) { + continue; + } + if (item.type === ENTRY) { + fn(item.v, item.k); + continue; + } + forEach(item, fn); + } +} + +/** + * Extra wrapper to keep track of Dict size and clean up the API + * @template K,V + */ +export default class Dict { + /** + * @template V + * @param {Record} o + * @returns {Dict} + */ + static fromObject(o) { + const keys = Object.keys(o); + /** @type Dict */ + let m = Dict.new(); + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + m = m.set(k, o[k]); + } + return m; + } + + /** + * @template K,V + * @param {Map} o + * @returns {Dict} + */ + static fromMap(o) { + /** @type Dict */ + let m = Dict.new(); + o.forEach((v, k) => { + m = m.set(k, v); + }); + return m; + } + + static new() { + return new Dict(undefined, 0); + } + + /** + * @param {undefined | Node} root + * @param {number} size + */ + constructor(root, size) { + this.root = root; + this.size = size; + } + /** + * @template NotFound + * @param {K} key + * @param {NotFound} notFound + * @returns {NotFound | V} + */ + get(key, notFound) { + if (this.root === undefined) { + return notFound; + } + const found = find(this.root, 0, getHash(key), key); + if (found === undefined) { + return notFound; + } + return found.v; + } + /** + * @param {K} key + * @param {V} val + * @returns {Dict} + */ + set(key, val) { + const addedLeaf = { val: false }; + const root = this.root === undefined ? EMPTY : this.root; + const newRoot = assoc(root, 0, getHash(key), key, val, addedLeaf); + if (newRoot === this.root) { + return this; + } + return new Dict(newRoot, addedLeaf.val ? this.size + 1 : this.size); + } + /** + * @param {K} key + * @returns {Dict} + */ + delete(key) { + if (this.root === undefined) { + return this; + } + const newRoot = without(this.root, 0, getHash(key), key); + if (newRoot === this.root) { + return this; + } + if (newRoot === undefined) { + return Dict.new(); + } + return new Dict(newRoot, this.size - 1); + } + /** + * @param {K} key + * @returns {boolean} + */ + has(key) { + if (this.root === undefined) { + return false; + } + return find(this.root, 0, getHash(key), key) !== undefined; + } + /** + * @returns {[K,V][]} + */ + entries() { + if (this.root === undefined) { + return []; + } + /** @type [K,V][] */ + const result = []; + this.forEach((v, k) => result.push([k, v])); + return result; + } + /** + * + * @param {(val:V,key:K)=>void} fn + */ + forEach(fn) { + forEach(this.root, fn); + } + hashCode() { + let h = 0; + this.forEach((v, k) => { + h = (h + hashMerge(getHash(v), getHash(k))) | 0; + }); + return h; + } + /** + * @param {unknown} o + * @returns {boolean} + */ + equals(o) { + if (!(o instanceof Dict) || this.size !== o.size) { + return false; + } + + try { + this.forEach((v, k) => { + if (!isEqual(o.get(k, !v), v)) { + throw unequalDictSymbol; + } + }); + return true; + } catch (e) { + if (e === unequalDictSymbol) { + return false; + } + + throw e; + } + } +} + +// This is thrown internally in Dict.equals() so that it returns false as soon +// as a non-matching key is found +const unequalDictSymbol = /* @__PURE__ */ Symbol(); diff --git a/build/dev/javascript/gleam_stdlib/gleam.mjs b/build/dev/javascript/gleam_stdlib/gleam.mjs new file mode 100644 index 0000000..197cbbc --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam.mjs @@ -0,0 +1 @@ +export * from "../prelude.mjs"; diff --git a/build/dev/javascript/gleam_stdlib/gleam/bit_array.mjs b/build/dev/javascript/gleam_stdlib/gleam/bit_array.mjs new file mode 100644 index 0000000..e6c1a3a --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/bit_array.mjs @@ -0,0 +1,285 @@ +import { Ok, toList, bitArraySlice, bitArraySliceToInt } from "../gleam.mjs"; +import * as $int from "../gleam/int.mjs"; +import * as $order from "../gleam/order.mjs"; +import * as $string from "../gleam/string.mjs"; +import { + bit_array_from_string as from_string, + bit_array_bit_size as bit_size, + bit_array_byte_size as byte_size, + bit_array_pad_to_bytes as pad_to_bytes, + bit_array_slice as slice, + bit_array_concat as concat, + base64_encode, + base64_decode as decode64, + base16_encode, + base16_decode, + bit_array_to_int_and_size, + bit_array_starts_with as starts_with, + bit_array_to_string as to_string, +} from "../gleam_stdlib.mjs"; + +export { + base16_decode, + base16_encode, + base64_encode, + bit_size, + byte_size, + concat, + from_string, + pad_to_bytes, + slice, + starts_with, + to_string, +}; + +/** + * Creates a new bit array by joining two bit arrays. + * + * ## Examples + * + * ```gleam + * append(to: from_string("butter"), suffix: from_string("fly")) + * // -> from_string("butterfly") + * ``` + */ +export function append(first, second) { + return concat(toList([first, second])); +} + +/** + * Decodes a base 64 encoded string into a `BitArray`. + */ +export function base64_decode(encoded) { + let _block; + let $ = byte_size(from_string(encoded)) % 4; + if ($ === 0) { + _block = encoded; + } else { + let n = $; + _block = $string.append(encoded, $string.repeat("=", 4 - n)); + } + let padded = _block; + return decode64(padded); +} + +/** + * Encodes a `BitArray` into a base 64 encoded string with URL and filename + * safe alphabet. + * + * If the bit array does not contain a whole number of bytes then it is padded + * with zero bits prior to being encoded. + */ +export function base64_url_encode(input, padding) { + let _pipe = input; + let _pipe$1 = base64_encode(_pipe, padding); + let _pipe$2 = $string.replace(_pipe$1, "+", "-"); + return $string.replace(_pipe$2, "/", "_"); +} + +/** + * Decodes a base 64 encoded string with URL and filename safe alphabet into a + * `BitArray`. + */ +export function base64_url_decode(encoded) { + let _pipe = encoded; + let _pipe$1 = $string.replace(_pipe, "-", "+"); + let _pipe$2 = $string.replace(_pipe$1, "_", "/"); + return base64_decode(_pipe$2); +} + +function inspect_loop(loop$input, loop$accumulator) { + while (true) { + let input = loop$input; + let accumulator = loop$accumulator; + if (input.bitSize === 0) { + return accumulator; + } else if (input.bitSize === 1) { + let x = bitArraySliceToInt(input, 0, 1, true, false); + return (accumulator + $int.to_string(x)) + ":size(1)"; + } else if (input.bitSize === 2) { + let x = bitArraySliceToInt(input, 0, 2, true, false); + return (accumulator + $int.to_string(x)) + ":size(2)"; + } else if (input.bitSize === 3) { + let x = bitArraySliceToInt(input, 0, 3, true, false); + return (accumulator + $int.to_string(x)) + ":size(3)"; + } else if (input.bitSize === 4) { + let x = bitArraySliceToInt(input, 0, 4, true, false); + return (accumulator + $int.to_string(x)) + ":size(4)"; + } else if (input.bitSize === 5) { + let x = bitArraySliceToInt(input, 0, 5, true, false); + return (accumulator + $int.to_string(x)) + ":size(5)"; + } else if (input.bitSize === 6) { + let x = bitArraySliceToInt(input, 0, 6, true, false); + return (accumulator + $int.to_string(x)) + ":size(6)"; + } else if (input.bitSize === 7) { + let x = bitArraySliceToInt(input, 0, 7, true, false); + return (accumulator + $int.to_string(x)) + ":size(7)"; + } else if (input.bitSize >= 8) { + let x = input.byteAt(0); + let rest = bitArraySlice(input, 8); + let _block; + if (rest.bitSize === 0) { + _block = ""; + } else { + _block = ", "; + } + let suffix = _block; + let accumulator$1 = (accumulator + $int.to_string(x)) + suffix; + loop$input = rest; + loop$accumulator = accumulator$1; + } else { + return accumulator; + } + } +} + +/** + * Converts a bit array to a string containing the decimal value of each byte. + * + * Use this over `string.inspect` when you have a bit array you want printed + * in the array syntax even if it is valid UTF-8. + * + * ## Examples + * + * ```gleam + * inspect(<<0, 20, 0x20, 255>>) + * // -> "<<0, 20, 32, 255>>" + * + * inspect(<<100, 5:3>>) + * // -> "<<100, 5:size(3)>>" + * ``` + */ +export function inspect(input) { + return inspect_loop(input, "<<") + ">>"; +} + +/** + * Compare two bit arrays as sequences of bytes. + * + * ## Examples + * + * ```gleam + * compare(<<1>>, <<2>>) + * // -> Lt + * + * compare(<<"AB":utf8>>, <<"AA":utf8>>) + * // -> Gt + * + * compare(<<1, 2:size(2)>>, with: <<1, 2:size(2)>>) + * // -> Eq + * ``` + */ +export function compare(loop$a, loop$b) { + while (true) { + let a = loop$a; + let b = loop$b; + if (a.bitSize >= 8) { + if (b.bitSize >= 8) { + let first_byte = a.byteAt(0); + let first_rest = bitArraySlice(a, 8); + let second_byte = b.byteAt(0); + let second_rest = bitArraySlice(b, 8); + let f = first_byte; + let s = second_byte; + if (f > s) { + return new $order.Gt(); + } else { + let f$1 = first_byte; + let s$1 = second_byte; + if (f$1 < s$1) { + return new $order.Lt(); + } else { + loop$a = first_rest; + loop$b = second_rest; + } + } + } else if (b.bitSize === 0) { + return new $order.Gt(); + } else { + let first = a; + let second = b; + let $ = bit_array_to_int_and_size(first); + let $1 = bit_array_to_int_and_size(second); + let a$1 = $[0]; + let b$1 = $1[0]; + if (a$1 > b$1) { + return new $order.Gt(); + } else { + let a$2 = $[0]; + let b$2 = $1[0]; + if (a$2 < b$2) { + return new $order.Lt(); + } else { + let size_a = $[1]; + let size_b = $1[1]; + if (size_a > size_b) { + return new $order.Gt(); + } else { + let size_a$1 = $[1]; + let size_b$1 = $1[1]; + if (size_a$1 < size_b$1) { + return new $order.Lt(); + } else { + return new $order.Eq(); + } + } + } + } + } + } else if (b.bitSize === 0) { + if (a.bitSize === 0) { + return new $order.Eq(); + } else { + return new $order.Gt(); + } + } else if (a.bitSize === 0) { + return new $order.Lt(); + } else { + let first = a; + let second = b; + let $ = bit_array_to_int_and_size(first); + let $1 = bit_array_to_int_and_size(second); + let a$1 = $[0]; + let b$1 = $1[0]; + if (a$1 > b$1) { + return new $order.Gt(); + } else { + let a$2 = $[0]; + let b$2 = $1[0]; + if (a$2 < b$2) { + return new $order.Lt(); + } else { + let size_a = $[1]; + let size_b = $1[1]; + if (size_a > size_b) { + return new $order.Gt(); + } else { + let size_a$1 = $[1]; + let size_b$1 = $1[1]; + if (size_a$1 < size_b$1) { + return new $order.Lt(); + } else { + return new $order.Eq(); + } + } + } + } + } + } +} + +/** + * Tests to see whether a bit array is valid UTF-8. + */ +export function is_utf8(bits) { + return is_utf8_loop(bits); +} + +function is_utf8_loop(bits) { + let $ = to_string(bits); + if ($ instanceof Ok) { + return true; + } else { + return false; + } +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/bool.mjs b/build/dev/javascript/gleam_stdlib/gleam/bool.mjs new file mode 100644 index 0000000..1a761a2 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/bool.mjs @@ -0,0 +1,313 @@ +/** + * Returns the and of two bools, but it evaluates both arguments. + * + * It's the function equivalent of the `&&` operator. + * This function is useful in higher order functions or pipes. + * + * ## Examples + * + * ```gleam + * and(True, True) + * // -> True + * ``` + * + * ```gleam + * and(False, True) + * // -> False + * ``` + * + * ```gleam + * False |> and(True) + * // -> False + * ``` + */ +export function and(a, b) { + return a && b; +} + +/** + * Returns the or of two bools, but it evaluates both arguments. + * + * It's the function equivalent of the `||` operator. + * This function is useful in higher order functions or pipes. + * + * ## Examples + * + * ```gleam + * or(True, True) + * // -> True + * ``` + * + * ```gleam + * or(False, True) + * // -> True + * ``` + * + * ```gleam + * False |> or(True) + * // -> True + * ``` + */ +export function or(a, b) { + return a || b; +} + +/** + * Returns the opposite bool value. + * + * This is the same as the `!` or `not` operators in some other languages. + * + * ## Examples + * + * ```gleam + * negate(True) + * // -> False + * ``` + * + * ```gleam + * negate(False) + * // -> True + * ``` + */ +export function negate(bool) { + return !bool; +} + +/** + * Returns the nor of two bools. + * + * ## Examples + * + * ```gleam + * nor(False, False) + * // -> True + * ``` + * + * ```gleam + * nor(False, True) + * // -> False + * ``` + * + * ```gleam + * nor(True, False) + * // -> False + * ``` + * + * ```gleam + * nor(True, True) + * // -> False + * ``` + */ +export function nor(a, b) { + return !(a || b); +} + +/** + * Returns the nand of two bools. + * + * ## Examples + * + * ```gleam + * nand(False, False) + * // -> True + * ``` + * + * ```gleam + * nand(False, True) + * // -> True + * ``` + * + * ```gleam + * nand(True, False) + * // -> True + * ``` + * + * ```gleam + * nand(True, True) + * // -> False + * ``` + */ +export function nand(a, b) { + return !(a && b); +} + +/** + * Returns the exclusive or of two bools. + * + * ## Examples + * + * ```gleam + * exclusive_or(False, False) + * // -> False + * ``` + * + * ```gleam + * exclusive_or(False, True) + * // -> True + * ``` + * + * ```gleam + * exclusive_or(True, False) + * // -> True + * ``` + * + * ```gleam + * exclusive_or(True, True) + * // -> False + * ``` + */ +export function exclusive_or(a, b) { + return a !== b; +} + +/** + * Returns the exclusive nor of two bools. + * + * ## Examples + * + * ```gleam + * exclusive_nor(False, False) + * // -> True + * ``` + * + * ```gleam + * exclusive_nor(False, True) + * // -> False + * ``` + * + * ```gleam + * exclusive_nor(True, False) + * // -> False + * ``` + * + * ```gleam + * exclusive_nor(True, True) + * // -> True + * ``` + */ +export function exclusive_nor(a, b) { + return a === b; +} + +/** + * Returns a string representation of the given bool. + * + * ## Examples + * + * ```gleam + * to_string(True) + * // -> "True" + * ``` + * + * ```gleam + * to_string(False) + * // -> "False" + * ``` + */ +export function to_string(bool) { + if (bool) { + return "True"; + } else { + return "False"; + } +} + +/** + * Run a callback function if the given bool is `False`, otherwise return a + * default value. + * + * With a `use` expression this function can simulate the early-return pattern + * found in some other programming languages. + * + * In a procedural language: + * + * ```js + * if (predicate) return value; + * // ... + * ``` + * + * In Gleam with a `use` expression: + * + * ```gleam + * use <- guard(when: predicate, return: value) + * // ... + * ``` + * + * Like everything in Gleam `use` is an expression, so it short circuits the + * current block, not the entire function. As a result you can assign the value + * to a variable: + * + * ```gleam + * let x = { + * use <- guard(when: predicate, return: value) + * // ... + * } + * ``` + * + * Note that unlike in procedural languages the `return` value is evaluated + * even when the predicate is `False`, so it is advisable not to perform + * expensive computation nor side-effects there. + * + * + * ## Examples + * + * ```gleam + * let name = "" + * use <- guard(when: name == "", return: "Welcome!") + * "Hello, " <> name + * // -> "Welcome!" + * ``` + * + * ```gleam + * let name = "Kamaka" + * use <- guard(when: name == "", return: "Welcome!") + * "Hello, " <> name + * // -> "Hello, Kamaka" + * ``` + */ +export function guard(requirement, consequence, alternative) { + if (requirement) { + return consequence; + } else { + return alternative(); + } +} + +/** + * Runs a callback function if the given bool is `True`, otherwise runs an + * alternative callback function. + * + * Useful when further computation should be delayed regardless of the given + * bool's value. + * + * See [`guard`](#guard) for more info. + * + * ## Examples + * + * ```gleam + * let name = "Kamaka" + * let inquiry = fn() { "How may we address you?" } + * use <- lazy_guard(when: name == "", return: inquiry) + * "Hello, " <> name + * // -> "Hello, Kamaka" + * ``` + * + * ```gleam + * import gleam/int + * + * let name = "" + * let greeting = fn() { "Hello, " <> name } + * use <- lazy_guard(when: name == "", otherwise: greeting) + * let number = int.random(99) + * let name = "User " <> int.to_string(number) + * "Welcome, " <> name + * // -> "Welcome, User 54" + * ``` + */ +export function lazy_guard(requirement, consequence, alternative) { + if (requirement) { + return consequence(); + } else { + return alternative(); + } +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/bytes_tree.mjs b/build/dev/javascript/gleam_stdlib/gleam/bytes_tree.mjs new file mode 100644 index 0000000..517efd0 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/bytes_tree.mjs @@ -0,0 +1,225 @@ +import { + toList, + Empty as $Empty, + prepend as listPrepend, + CustomType as $CustomType, +} from "../gleam.mjs"; +import * as $bit_array from "../gleam/bit_array.mjs"; +import * as $list from "../gleam/list.mjs"; +import * as $string_tree from "../gleam/string_tree.mjs"; + +class Bytes extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} + +class Text extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} + +class Many extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} + +/** + * Appends a bytes tree onto the end of another. + * + * Runs in constant time. + */ +export function append_tree(first, second) { + if (second instanceof Bytes) { + return new Many(toList([first, second])); + } else if (second instanceof Text) { + return new Many(toList([first, second])); + } else { + let trees = second[0]; + return new Many(listPrepend(first, trees)); + } +} + +/** + * Prepends a bytes tree onto the start of another. + * + * Runs in constant time. + */ +export function prepend_tree(second, first) { + return append_tree(first, second); +} + +/** + * Joins a list of bytes trees into a single one. + * + * Runs in constant time. + */ +export function concat(trees) { + return new Many(trees); +} + +/** + * Create an empty `BytesTree`. Useful as the start of a pipe chaining many + * trees together. + */ +export function new$() { + return concat(toList([])); +} + +/** + * Creates a new bytes tree from a string. + * + * Runs in constant time when running on Erlang. + * Runs in linear time otherwise. + */ +export function from_string(string) { + return new Text($string_tree.from_string(string)); +} + +/** + * Prepends a string onto the start of a bytes tree. + * + * Runs in constant time when running on Erlang. + * Runs in linear time with the length of the string otherwise. + */ +export function prepend_string(second, first) { + return append_tree(from_string(first), second); +} + +/** + * Appends a string onto the end of a bytes tree. + * + * Runs in constant time when running on Erlang. + * Runs in linear time with the length of the string otherwise. + */ +export function append_string(first, second) { + return append_tree(first, from_string(second)); +} + +/** + * Creates a new bytes tree from a string tree. + * + * Runs in constant time when running on Erlang. + * Runs in linear time otherwise. + */ +export function from_string_tree(tree) { + return new Text(tree); +} + +function wrap_list(bits) { + return new Bytes(bits); +} + +/** + * Creates a new bytes tree from a bit array. + * + * Runs in constant time. + */ +export function from_bit_array(bits) { + let _pipe = bits; + let _pipe$1 = $bit_array.pad_to_bytes(_pipe); + return wrap_list(_pipe$1); +} + +/** + * Prepends a bit array to the start of a bytes tree. + * + * Runs in constant time. + */ +export function prepend(second, first) { + return append_tree(from_bit_array(first), second); +} + +/** + * Appends a bit array to the end of a bytes tree. + * + * Runs in constant time. + */ +export function append(first, second) { + return append_tree(first, from_bit_array(second)); +} + +/** + * Joins a list of bit arrays into a single bytes tree. + * + * Runs in constant time. + */ +export function concat_bit_arrays(bits) { + let _pipe = bits; + let _pipe$1 = $list.map(_pipe, from_bit_array); + return concat(_pipe$1); +} + +function to_list(loop$stack, loop$acc) { + while (true) { + let stack = loop$stack; + let acc = loop$acc; + if (stack instanceof $Empty) { + return acc; + } else { + let $ = stack.head; + if ($ instanceof $Empty) { + let remaining_stack = stack.tail; + loop$stack = remaining_stack; + loop$acc = acc; + } else { + let $1 = $.head; + if ($1 instanceof Bytes) { + let remaining_stack = stack.tail; + let rest = $.tail; + let bits = $1[0]; + loop$stack = listPrepend(rest, remaining_stack); + loop$acc = listPrepend(bits, acc); + } else if ($1 instanceof Text) { + let remaining_stack = stack.tail; + let rest = $.tail; + let tree = $1[0]; + let bits = $bit_array.from_string($string_tree.to_string(tree)); + loop$stack = listPrepend(rest, remaining_stack); + loop$acc = listPrepend(bits, acc); + } else { + let remaining_stack = stack.tail; + let rest = $.tail; + let trees = $1[0]; + loop$stack = listPrepend(trees, listPrepend(rest, remaining_stack)); + loop$acc = acc; + } + } + } + } +} + +/** + * Turns a bytes tree into a bit array. + * + * Runs in linear time. + * + * When running on Erlang this function is implemented natively by the + * virtual machine and is highly optimised. + */ +export function to_bit_array(tree) { + let _pipe = toList([toList([tree])]); + let _pipe$1 = to_list(_pipe, toList([])); + let _pipe$2 = $list.reverse(_pipe$1); + return $bit_array.concat(_pipe$2); +} + +/** + * Returns the size of the bytes tree's content in bytes. + * + * Runs in linear time. + */ +export function byte_size(tree) { + let _pipe = toList([toList([tree])]); + let _pipe$1 = to_list(_pipe, toList([])); + return $list.fold( + _pipe$1, + 0, + (acc, bits) => { return $bit_array.byte_size(bits) + acc; }, + ); +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/dict.mjs b/build/dev/javascript/gleam_stdlib/gleam/dict.mjs new file mode 100644 index 0000000..aa206af --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/dict.mjs @@ -0,0 +1,534 @@ +import { Ok, Error, toList, Empty as $Empty, prepend as listPrepend, isEqual } from "../gleam.mjs"; +import * as $option from "../gleam/option.mjs"; +import { + map_size as size, + map_to_list as to_list, + new_map as new$, + map_get as get, + map_insert as do_insert, + map_remove as do_delete, +} from "../gleam_stdlib.mjs"; + +export { get, new$, size, to_list }; + +/** + * Determines whether or not the dict is empty. + * + * ## Examples + * + * ```gleam + * new() |> is_empty + * // -> True + * ``` + * + * ```gleam + * new() |> insert("b", 1) |> is_empty + * // -> False + * ``` + */ +export function is_empty(dict) { + return size(dict) === 0; +} + +function do_has_key(key, dict) { + return !isEqual(get(dict, key), new Error(undefined)); +} + +/** + * Determines whether or not a value present in the dict for a given key. + * + * ## Examples + * + * ```gleam + * new() |> insert("a", 0) |> has_key("a") + * // -> True + * ``` + * + * ```gleam + * new() |> insert("a", 0) |> has_key("b") + * // -> False + * ``` + */ +export function has_key(dict, key) { + return do_has_key(key, dict); +} + +/** + * Inserts a value into the dict with the given key. + * + * If the dict already has a value for the given key then the value is + * replaced with the new value. + * + * ## Examples + * + * ```gleam + * new() |> insert("a", 0) + * // -> from_list([#("a", 0)]) + * ``` + * + * ```gleam + * new() |> insert("a", 0) |> insert("a", 5) + * // -> from_list([#("a", 5)]) + * ``` + */ +export function insert(dict, key, value) { + return do_insert(key, value, dict); +} + +function from_list_loop(loop$list, loop$initial) { + while (true) { + let list = loop$list; + let initial = loop$initial; + if (list instanceof $Empty) { + return initial; + } else { + let rest = list.tail; + let key = list.head[0]; + let value = list.head[1]; + loop$list = rest; + loop$initial = insert(initial, key, value); + } + } +} + +/** + * Converts a list of 2-element tuples `#(key, value)` to a dict. + * + * If two tuples have the same key the last one in the list will be the one + * that is present in the dict. + */ +export function from_list(list) { + return from_list_loop(list, new$()); +} + +function reverse_and_concat(loop$remaining, loop$accumulator) { + while (true) { + let remaining = loop$remaining; + let accumulator = loop$accumulator; + if (remaining instanceof $Empty) { + return accumulator; + } else { + let first = remaining.head; + let rest = remaining.tail; + loop$remaining = rest; + loop$accumulator = listPrepend(first, accumulator); + } + } +} + +function do_keys_loop(loop$list, loop$acc) { + while (true) { + let list = loop$list; + let acc = loop$acc; + if (list instanceof $Empty) { + return reverse_and_concat(acc, toList([])); + } else { + let rest = list.tail; + let key = list.head[0]; + loop$list = rest; + loop$acc = listPrepend(key, acc); + } + } +} + +/** + * Gets a list of all keys in a given dict. + * + * Dicts are not ordered so the keys are not returned in any specific order. Do + * not write code that relies on the order keys are returned by this function + * as it may change in later versions of Gleam or Erlang. + * + * ## Examples + * + * ```gleam + * from_list([#("a", 0), #("b", 1)]) |> keys + * // -> ["a", "b"] + * ``` + */ +export function keys(dict) { + return do_keys_loop(to_list(dict), toList([])); +} + +function do_values_loop(loop$list, loop$acc) { + while (true) { + let list = loop$list; + let acc = loop$acc; + if (list instanceof $Empty) { + return reverse_and_concat(acc, toList([])); + } else { + let rest = list.tail; + let value = list.head[1]; + loop$list = rest; + loop$acc = listPrepend(value, acc); + } + } +} + +/** + * Gets a list of all values in a given dict. + * + * Dicts are not ordered so the values are not returned in any specific order. Do + * not write code that relies on the order values are returned by this function + * as it may change in later versions of Gleam or Erlang. + * + * ## Examples + * + * ```gleam + * from_list([#("a", 0), #("b", 1)]) |> values + * // -> [0, 1] + * ``` + */ +export function values(dict) { + let list_of_pairs = to_list(dict); + return do_values_loop(list_of_pairs, toList([])); +} + +function do_take_loop(loop$dict, loop$desired_keys, loop$acc) { + while (true) { + let dict = loop$dict; + let desired_keys = loop$desired_keys; + let acc = loop$acc; + let insert$1 = (taken, key) => { + let $ = get(dict, key); + if ($ instanceof Ok) { + let value = $[0]; + return insert(taken, key, value); + } else { + return taken; + } + }; + if (desired_keys instanceof $Empty) { + return acc; + } else { + let first = desired_keys.head; + let rest = desired_keys.tail; + loop$dict = dict; + loop$desired_keys = rest; + loop$acc = insert$1(acc, first); + } + } +} + +function do_take(desired_keys, dict) { + return do_take_loop(dict, desired_keys, new$()); +} + +/** + * Creates a new dict from a given dict, only including any entries for which the + * keys are in a given list. + * + * ## Examples + * + * ```gleam + * from_list([#("a", 0), #("b", 1)]) + * |> take(["b"]) + * // -> from_list([#("b", 1)]) + * ``` + * + * ```gleam + * from_list([#("a", 0), #("b", 1)]) + * |> take(["a", "b", "c"]) + * // -> from_list([#("a", 0), #("b", 1)]) + * ``` + */ +export function take(dict, desired_keys) { + return do_take(desired_keys, dict); +} + +function insert_pair(dict, pair) { + return insert(dict, pair[0], pair[1]); +} + +function fold_inserts(loop$new_entries, loop$dict) { + while (true) { + let new_entries = loop$new_entries; + let dict = loop$dict; + if (new_entries instanceof $Empty) { + return dict; + } else { + let first = new_entries.head; + let rest = new_entries.tail; + loop$new_entries = rest; + loop$dict = insert_pair(dict, first); + } + } +} + +/** + * Creates a new dict from a pair of given dicts by combining their entries. + * + * If there are entries with the same keys in both dicts the entry from the + * second dict takes precedence. + * + * ## Examples + * + * ```gleam + * let a = from_list([#("a", 0), #("b", 1)]) + * let b = from_list([#("b", 2), #("c", 3)]) + * merge(a, b) + * // -> from_list([#("a", 0), #("b", 2), #("c", 3)]) + * ``` + */ +export function merge(dict, new_entries) { + let _pipe = new_entries; + let _pipe$1 = to_list(_pipe); + return fold_inserts(_pipe$1, dict); +} + +/** + * Creates a new dict from a given dict with all the same entries except for the + * one with a given key, if it exists. + * + * ## Examples + * + * ```gleam + * from_list([#("a", 0), #("b", 1)]) |> delete("a") + * // -> from_list([#("b", 1)]) + * ``` + * + * ```gleam + * from_list([#("a", 0), #("b", 1)]) |> delete("c") + * // -> from_list([#("a", 0), #("b", 1)]) + * ``` + */ +export function delete$(dict, key) { + return do_delete(key, dict); +} + +/** + * Creates a new dict from a given dict with all the same entries except any with + * keys found in a given list. + * + * ## Examples + * + * ```gleam + * from_list([#("a", 0), #("b", 1)]) |> drop(["a"]) + * // -> from_list([#("b", 1)]) + * ``` + * + * ```gleam + * from_list([#("a", 0), #("b", 1)]) |> drop(["c"]) + * // -> from_list([#("a", 0), #("b", 1)]) + * ``` + * + * ```gleam + * from_list([#("a", 0), #("b", 1)]) |> drop(["a", "b", "c"]) + * // -> from_list([]) + * ``` + */ +export function drop(loop$dict, loop$disallowed_keys) { + while (true) { + let dict = loop$dict; + let disallowed_keys = loop$disallowed_keys; + if (disallowed_keys instanceof $Empty) { + return dict; + } else { + let first = disallowed_keys.head; + let rest = disallowed_keys.tail; + loop$dict = delete$(dict, first); + loop$disallowed_keys = rest; + } + } +} + +/** + * Creates a new dict with one entry inserted or updated using a given function. + * + * If there was not an entry in the dict for the given key then the function + * gets `None` as its argument, otherwise it gets `Some(value)`. + * + * ## Example + * + * ```gleam + * let dict = from_list([#("a", 0)]) + * let increment = fn(x) { + * case x { + * Some(i) -> i + 1 + * None -> 0 + * } + * } + * + * upsert(dict, "a", increment) + * // -> from_list([#("a", 1)]) + * + * upsert(dict, "b", increment) + * // -> from_list([#("a", 0), #("b", 0)]) + * ``` + */ +export function upsert(dict, key, fun) { + let $ = get(dict, key); + if ($ instanceof Ok) { + let value = $[0]; + return insert(dict, key, fun(new $option.Some(value))); + } else { + return insert(dict, key, fun(new $option.None())); + } +} + +function fold_loop(loop$list, loop$initial, loop$fun) { + while (true) { + let list = loop$list; + let initial = loop$initial; + let fun = loop$fun; + if (list instanceof $Empty) { + return initial; + } else { + let rest = list.tail; + let k = list.head[0]; + let v = list.head[1]; + loop$list = rest; + loop$initial = fun(initial, k, v); + loop$fun = fun; + } + } +} + +/** + * Combines all entries into a single value by calling a given function on each + * one. + * + * Dicts are not ordered so the values are not returned in any specific order. Do + * not write code that relies on the order entries are used by this function + * as it may change in later versions of Gleam or Erlang. + * + * # Examples + * + * ```gleam + * let dict = from_list([#("a", 1), #("b", 3), #("c", 9)]) + * fold(dict, 0, fn(accumulator, key, value) { accumulator + value }) + * // -> 13 + * ``` + * + * ```gleam + * import gleam/string + * + * let dict = from_list([#("a", 1), #("b", 3), #("c", 9)]) + * fold(dict, "", fn(accumulator, key, value) { + * string.append(accumulator, key) + * }) + * // -> "abc" + * ``` + */ +export function fold(dict, initial, fun) { + return fold_loop(to_list(dict), initial, fun); +} + +function do_map_values(f, dict) { + let f$1 = (dict, k, v) => { return insert(dict, k, f(k, v)); }; + return fold(dict, new$(), f$1); +} + +/** + * Updates all values in a given dict by calling a given function on each key + * and value. + * + * ## Examples + * + * ```gleam + * from_list([#(3, 3), #(2, 4)]) + * |> map_values(fn(key, value) { key * value }) + * // -> from_list([#(3, 9), #(2, 8)]) + * ``` + */ +export function map_values(dict, fun) { + return do_map_values(fun, dict); +} + +function do_filter(f, dict) { + let insert$1 = (dict, k, v) => { + let $ = f(k, v); + if ($) { + return insert(dict, k, v); + } else { + return dict; + } + }; + return fold(dict, new$(), insert$1); +} + +/** + * Creates a new dict from a given dict, minus any entries that a given function + * returns `False` for. + * + * ## Examples + * + * ```gleam + * from_list([#("a", 0), #("b", 1)]) + * |> filter(fn(key, value) { value != 0 }) + * // -> from_list([#("b", 1)]) + * ``` + * + * ```gleam + * from_list([#("a", 0), #("b", 1)]) + * |> filter(fn(key, value) { True }) + * // -> from_list([#("a", 0), #("b", 1)]) + * ``` + */ +export function filter(dict, predicate) { + return do_filter(predicate, dict); +} + +/** + * Calls a function for each key and value in a dict, discarding the return + * value. + * + * Useful for producing a side effect for every item of a dict. + * + * ```gleam + * import gleam/io + * + * let dict = from_list([#("a", "apple"), #("b", "banana"), #("c", "cherry")]) + * + * each(dict, fn(k, v) { + * io.println(key <> " => " <> value) + * }) + * // -> Nil + * // a => apple + * // b => banana + * // c => cherry + * ``` + * + * The order of elements in the iteration is an implementation detail that + * should not be relied upon. + */ +export function each(dict, fun) { + return fold( + dict, + undefined, + (nil, k, v) => { + fun(k, v); + return nil; + }, + ); +} + +/** + * Creates a new dict from a pair of given dicts by combining their entries. + * + * If there are entries with the same keys in both dicts the given function is + * used to determine the new value to use in the resulting dict. + * + * ## Examples + * + * ```gleam + * let a = from_list([#("a", 0), #("b", 1)]) + * let b = from_list([#("a", 2), #("c", 3)]) + * combine(a, b, fn(one, other) { one + other }) + * // -> from_list([#("a", 2), #("b", 1), #("c", 3)]) + * ``` + */ +export function combine(dict, other, fun) { + return fold( + dict, + other, + (acc, key, value) => { + let $ = get(acc, key); + if ($ instanceof Ok) { + let other_value = $[0]; + return insert(acc, key, fun(value, other_value)); + } else { + return insert(acc, key, value); + } + }, + ); +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/dynamic.mjs b/build/dev/javascript/gleam_stdlib/gleam/dynamic.mjs new file mode 100644 index 0000000..3c67bbb --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/dynamic.mjs @@ -0,0 +1,35 @@ +import * as $dict from "../gleam/dict.mjs"; +import { + classify_dynamic as classify, + identity as bool, + identity as string, + identity as float, + identity as int, + identity as bit_array, + identity as list, + list_to_array as array, + identity as cast, +} from "../gleam_stdlib.mjs"; + +export { array, bit_array, bool, classify, float, int, list, string }; + +/** + * Create a dynamic value made an unordered series of keys and values, where + * the keys are unique. + * + * On Erlang this will be a map, on JavaScript this will be a Gleam dict + * object. + */ +export function properties(entries) { + return cast($dict.from_list(entries)); +} + +/** + * A dynamic value representing nothing. + * + * On Erlang this will be the atom `nil`, on JavaScript this will be + * `undefined`. + */ +export function nil() { + return cast(undefined); +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/dynamic/decode.mjs b/build/dev/javascript/gleam_stdlib/gleam/dynamic/decode.mjs new file mode 100644 index 0000000..25c8902 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/dynamic/decode.mjs @@ -0,0 +1,947 @@ +import { + Ok, + Error, + toList, + Empty as $Empty, + prepend as listPrepend, + CustomType as $CustomType, + isEqual, +} from "../../gleam.mjs"; +import * as $bit_array from "../../gleam/bit_array.mjs"; +import * as $dict from "../../gleam/dict.mjs"; +import * as $dynamic from "../../gleam/dynamic.mjs"; +import * as $int from "../../gleam/int.mjs"; +import * as $list from "../../gleam/list.mjs"; +import * as $option from "../../gleam/option.mjs"; +import { None, Some } from "../../gleam/option.mjs"; +import { + index as bare_index, + int as dynamic_int, + float as dynamic_float, + bit_array as dynamic_bit_array, + list as decode_list, + dict as decode_dict, + identity as cast, + is_null, + string as dynamic_string, +} from "../../gleam_stdlib.mjs"; + +export class DecodeError extends $CustomType { + constructor(expected, found, path) { + super(); + this.expected = expected; + this.found = found; + this.path = path; + } +} +export const DecodeError$DecodeError = (expected, found, path) => + new DecodeError(expected, found, path); +export const DecodeError$isDecodeError = (value) => + value instanceof DecodeError; +export const DecodeError$DecodeError$expected = (value) => value.expected; +export const DecodeError$DecodeError$0 = (value) => value.expected; +export const DecodeError$DecodeError$found = (value) => value.found; +export const DecodeError$DecodeError$1 = (value) => value.found; +export const DecodeError$DecodeError$path = (value) => value.path; +export const DecodeError$DecodeError$2 = (value) => value.path; + +class Decoder extends $CustomType { + constructor(function$) { + super(); + this.function = function$; + } +} + +/** + * Run a decoder on a `Dynamic` value, decoding the value if it is of the + * desired type, or returning errors. + * + * # Examples + * + * ```gleam + * let decoder = { + * use name <- decode.field("email", decode.string) + * use email <- decode.field("password", decode.string) + * decode.success(SignUp(name: name, email: email)) + * } + * + * decode.run(data, decoder) + * ``` + */ +export function run(data, decoder) { + let $ = decoder.function(data); + let maybe_invalid_data; + let errors; + maybe_invalid_data = $[0]; + errors = $[1]; + if (errors instanceof $Empty) { + return new Ok(maybe_invalid_data); + } else { + return new Error(errors); + } +} + +/** + * Finalise a decoder having successfully extracted a value. + * + * # Examples + * + * ```gleam + * let data = dynamic.properties([ + * #(dynamic.string("email"), dynamic.string("lucy@example.com")), + * #(dynamic.string("name"), dynamic.string("Lucy")), + * ])) + * + * let decoder = { + * use name <- decode.field("name", string) + * use email <- decode.field("email", string) + * decode.success(SignUp(name: name, email: email)) + * } + * + * let result = decode.run(data, decoder) + * assert result == Ok(SignUp(name: "Lucy", email: "lucy@example.com")) + * ``` + */ +export function success(data) { + return new Decoder((_) => { return [data, toList([])]; }); +} + +function decode_dynamic(data) { + return [data, toList([])]; +} + +/** + * Apply a transformation function to any value decoded by the decoder. + * + * # Examples + * + * ```gleam + * let decoder = decode.int |> decode.map(int.to_string) + * let result = decode.run(dynamic.int(1000), decoder) + * assert result == Ok("1000") + * ``` + */ +export function map(decoder, transformer) { + return new Decoder( + (d) => { + let $ = decoder.function(d); + let data; + let errors; + data = $[0]; + errors = $[1]; + return [transformer(data), errors]; + }, + ); +} + +/** + * Apply a transformation function to any errors returned by the decoder. + */ +export function map_errors(decoder, transformer) { + return new Decoder( + (d) => { + let $ = decoder.function(d); + let data; + let errors; + data = $[0]; + errors = $[1]; + return [data, transformer(errors)]; + }, + ); +} + +/** + * Create a new decoder based upon the value of a previous decoder. + * + * This may be useful to run one previous decoder to use in further decoding. + */ +export function then$(decoder, next) { + return new Decoder( + (dynamic_data) => { + let $ = decoder.function(dynamic_data); + let data; + let errors; + data = $[0]; + errors = $[1]; + let decoder$1 = next(data); + let $1 = decoder$1.function(dynamic_data); + let layer; + let data$1; + layer = $1; + data$1 = $1[0]; + if (errors instanceof $Empty) { + return layer; + } else { + return [data$1, errors]; + } + }, + ); +} + +function run_decoders(loop$data, loop$failure, loop$decoders) { + while (true) { + let data = loop$data; + let failure = loop$failure; + let decoders = loop$decoders; + if (decoders instanceof $Empty) { + return failure; + } else { + let decoder = decoders.head; + let decoders$1 = decoders.tail; + let $ = decoder.function(data); + let layer; + let errors; + layer = $; + errors = $[1]; + if (errors instanceof $Empty) { + return layer; + } else { + loop$data = data; + loop$failure = failure; + loop$decoders = decoders$1; + } + } + } +} + +/** + * Create a new decoder from several other decoders. Each of the inner + * decoders is run in turn, and the value from the first to succeed is used. + * + * If no decoder succeeds then the errors from the first decoder is used. + * If you wish for different errors then you may wish to use the + * `collapse_errors` or `map_errors` functions. + * + * # Examples + * + * ```gleam + * let decoder = decode.one_of(decode.string, or: [ + * decode.int |> decode.map(int.to_string), + * decode.float |> decode.map(float.to_string), + * ]) + * decode.run(dynamic.int(1000), decoder) + * // -> Ok("1000") + * ``` + */ +export function one_of(first, alternatives) { + return new Decoder( + (dynamic_data) => { + let $ = first.function(dynamic_data); + let layer; + let errors; + layer = $; + errors = $[1]; + if (errors instanceof $Empty) { + return layer; + } else { + return run_decoders(dynamic_data, layer, alternatives); + } + }, + ); +} + +/** + * Create a decoder that can refer to itself, useful for decoding deeply + * nested data. + * + * Attempting to create a recursive decoder without this function could result + * in an infinite loop. If you are using `field` or other `use`able functions + * then you may not need to use this function. + * + * ```gleam + * type Nested { + * Nested(List(Nested)) + * Value(String) + * } + * + * fn nested_decoder() -> decode.Decoder(Nested) { + * use <- decode.recursive + * decode.one_of(decode.string |> decode.map(Value), [ + * decode.list(nested_decoder()) |> decode.map(Nested), + * ]) + * } + * ``` + */ +export function recursive(inner) { + return new Decoder( + (data) => { + let decoder = inner(); + return decoder.function(data); + }, + ); +} + +/** + * A decoder that decodes nullable values of a type decoded by with a given + * decoder. + * + * This function can handle common representations of null on all runtimes, such as + * `nil`, `null`, and `undefined` on Erlang, and `undefined` and `null` on + * JavaScript. + * + * # Examples + * + * ```gleam + * let result = decode.run(dynamic.int(100), decode.optional(decode.int)) + * assert result == Ok(option.Some(100)) + * ``` + * + * ```gleam + * let result = decode.run(dynamic.nil(), decode.optional(decode.int)) + * assert result == Ok(option.None) + * ``` + */ +export function optional(inner) { + return new Decoder( + (data) => { + let $ = is_null(data); + if ($) { + return [new $option.None(), toList([])]; + } else { + let $1 = inner.function(data); + let data$1; + let errors; + data$1 = $1[0]; + errors = $1[1]; + return [new $option.Some(data$1), errors]; + } + }, + ); +} + +/** + * A decoder that decodes `Dynamic` values. This decoder never returns an error. + * + * # Examples + * + * ```gleam + * let result = decode.run(dynamic.float(3.14), decode.dynamic) + * assert result == Ok(dynamic.float(3.14)) + * ``` + */ +export const dynamic = /* @__PURE__ */ new Decoder(decode_dynamic); + +/** + * Construct a decode error for some unexpected dynamic data. + */ +export function decode_error(expected, found) { + return toList([ + new DecodeError(expected, $dynamic.classify(found), toList([])), + ]); +} + +function run_dynamic_function(data, name, f) { + let $ = f(data); + if ($ instanceof Ok) { + let data$1 = $[0]; + return [data$1, toList([])]; + } else { + let zero = $[0]; + return [ + zero, + toList([new DecodeError(name, $dynamic.classify(data), toList([]))]), + ]; + } +} + +function decode_bool(data) { + let $ = isEqual(cast(true), data); + if ($) { + return [true, toList([])]; + } else { + let $1 = isEqual(cast(false), data); + if ($1) { + return [false, toList([])]; + } else { + return [false, decode_error("Bool", data)]; + } + } +} + +function decode_int(data) { + return run_dynamic_function(data, "Int", dynamic_int); +} + +function decode_float(data) { + return run_dynamic_function(data, "Float", dynamic_float); +} + +function decode_bit_array(data) { + return run_dynamic_function(data, "BitArray", dynamic_bit_array); +} + +/** + * Replace all errors produced by a decoder with one single error for a named + * expected type. + * + * This function may be useful if you wish to simplify errors before + * presenting them to a user, particularly when using the `one_of` function. + * + * # Examples + * + * ```gleam + * let decoder = decode.string |> decode.collapse_errors("MyThing") + * let result = decode.run(dynamic.int(1000), decoder) + * assert result == Error([DecodeError("MyThing", "Int", [])]) + * ``` + */ +export function collapse_errors(decoder, name) { + return new Decoder( + (dynamic_data) => { + let $ = decoder.function(dynamic_data); + let layer; + let data; + let errors; + layer = $; + data = $[0]; + errors = $[1]; + if (errors instanceof $Empty) { + return layer; + } else { + return [data, decode_error(name, dynamic_data)]; + } + }, + ); +} + +/** + * Define a decoder that always fails. The parameter for this function is the + * name of the type that has failed to decode. + */ +export function failure(zero, expected) { + return new Decoder((d) => { return [zero, decode_error(expected, d)]; }); +} + +/** + * Create a decoder for a new data type from a decoding function. + * + * This function is used for new primitive types. For example, you might + * define a decoder for Erlang's pid type. + * + * A default "zero" value is also required to make a decoder. When this + * decoder is used as part of a larger decoder this zero value used as + * a placeholder so that the rest of the decoder can continue to run and + * collect all decoding errors. + * + * If you were to make a decoder for the `String` type (rather than using the + * build-in `string` decoder) you would define it like so: + * + * ```gleam + * pub fn string_decoder() -> decode.Decoder(String) { + * let default = "" + * decode.new_primitive_decoder("String", fn(data) { + * case dynamic.string(data) { + * Ok(x) -> Ok(x) + * Error(_) -> Error(default) + * } + * }) + * } + * ``` + */ +export function new_primitive_decoder(name, decoding_function) { + return new Decoder( + (d) => { + let $ = decoding_function(d); + if ($ instanceof Ok) { + let t = $[0]; + return [t, toList([])]; + } else { + let zero = $[0]; + return [ + zero, + toList([new DecodeError(name, $dynamic.classify(d), toList([]))]), + ]; + } + }, + ); +} + +/** + * A decoder that decodes `Bool` values. + * + * # Examples + * + * ```gleam + * let result = decode.run(dynamic.bool(True), decode.bool) + * assert result == Ok(True) + * ``` + */ +export const bool = /* @__PURE__ */ new Decoder(decode_bool); + +/** + * A decoder that decodes `Int` values. + * + * # Examples + * + * ```gleam + * let result = decode.run(dynamic.int(147), decode.int) + * assert result == Ok(147) + * ``` + */ +export const int = /* @__PURE__ */ new Decoder(decode_int); + +/** + * A decoder that decodes `Float` values. + * + * # Examples + * + * ```gleam + * let result = decode.run(dynamic.float(3.14), decode.float) + * assert result == Ok(3.14) + * ``` + */ +export const float = /* @__PURE__ */ new Decoder(decode_float); + +/** + * A decoder that decodes `BitArray` values. This decoder never returns an error. + * + * # Examples + * + * ```gleam + * let result = decode.run(dynamic.bit_array(<<5, 7>>), decode.bit_array) + * assert result == Ok(<<5, 7>>) + * ``` + */ +export const bit_array = /* @__PURE__ */ new Decoder(decode_bit_array); + +function decode_string(data) { + return run_dynamic_function(data, "String", dynamic_string); +} + +/** + * A decoder that decodes `String` values. + * + * # Examples + * + * ```gleam + * let result = decode.run(dynamic.string("Hello!"), decode.string) + * assert result == Ok("Hello!") + * ``` + */ +export const string = /* @__PURE__ */ new Decoder(decode_string); + +function fold_dict(acc, key, value, key_decoder, value_decoder) { + let $ = key_decoder(key); + let $1 = $[1]; + if ($1 instanceof $Empty) { + let key$1 = $[0]; + let $2 = value_decoder(value); + let $3 = $2[1]; + if ($3 instanceof $Empty) { + let value$1 = $2[0]; + let dict$1 = $dict.insert(acc[0], key$1, value$1); + return [dict$1, acc[1]]; + } else { + let errors = $3; + return push_path([$dict.new$(), errors], toList(["values"])); + } + } else { + let errors = $1; + return push_path([$dict.new$(), errors], toList(["keys"])); + } +} + +/** + * A decoder that decodes dicts where all keys and vales are decoded with + * given decoders. + * + * # Examples + * + * ```gleam + * let values = dynamic.properties([ + * #(dynamic.string("one"), dynamic.int(1)), + * #(dynamic.string("two"), dynamic.int(2)), + * ]) + * + * let result = + * decode.run(values, decode.dict(decode.string, decode.int)) + * assert result == Ok(values) + * ``` + */ +export function dict(key, value) { + return new Decoder( + (data) => { + let $ = decode_dict(data); + if ($ instanceof Ok) { + let dict$1 = $[0]; + return $dict.fold( + dict$1, + [$dict.new$(), toList([])], + (a, k, v) => { + let $1 = a[1]; + if ($1 instanceof $Empty) { + return fold_dict(a, k, v, key.function, value.function); + } else { + return a; + } + }, + ); + } else { + return [$dict.new$(), decode_error("Dict", data)]; + } + }, + ); +} + +/** + * A decoder that decodes lists where all elements are decoded with a given + * decoder. + * + * # Examples + * + * ```gleam + * let result = + * [1, 2, 3] + * |> list.map(dynamic.int) + * |> dynamic.list + * |> decode.run(decode.list(of: decode.int)) + * assert result == Ok([1, 2, 3]) + * ``` + */ +export function list(inner) { + return new Decoder( + (data) => { + return decode_list( + data, + inner.function, + (p, k) => { return push_path(p, toList([k])); }, + 0, + toList([]), + ); + }, + ); +} + +function push_path(layer, path) { + let decoder = one_of( + string, + toList([ + (() => { + let _pipe = int; + return map(_pipe, $int.to_string); + })(), + ]), + ); + let path$1 = $list.map( + path, + (key) => { + let key$1 = cast(key); + let $ = run(key$1, decoder); + if ($ instanceof Ok) { + let key$2 = $[0]; + return key$2; + } else { + return ("<" + $dynamic.classify(key$1)) + ">"; + } + }, + ); + let errors = $list.map( + layer[1], + (error) => { + return new DecodeError( + error.expected, + error.found, + $list.append(path$1, error.path), + ); + }, + ); + return [layer[0], errors]; +} + +function index( + loop$path, + loop$position, + loop$inner, + loop$data, + loop$handle_miss +) { + while (true) { + let path = loop$path; + let position = loop$position; + let inner = loop$inner; + let data = loop$data; + let handle_miss = loop$handle_miss; + if (path instanceof $Empty) { + let _pipe = data; + let _pipe$1 = inner(_pipe); + return push_path(_pipe$1, $list.reverse(position)); + } else { + let key = path.head; + let path$1 = path.tail; + let $ = bare_index(data, key); + if ($ instanceof Ok) { + let $1 = $[0]; + if ($1 instanceof Some) { + let data$1 = $1[0]; + loop$path = path$1; + loop$position = listPrepend(key, position); + loop$inner = inner; + loop$data = data$1; + loop$handle_miss = handle_miss; + } else { + return handle_miss(data, listPrepend(key, position)); + } + } else { + let kind = $[0]; + let $1 = inner(data); + let default$; + default$ = $1[0]; + let _pipe = [ + default$, + toList([new DecodeError(kind, $dynamic.classify(data), toList([]))]), + ]; + return push_path(_pipe, $list.reverse(position)); + } + } + } +} + +/** + * The same as [`field`](#field), except taking a path to the value rather + * than a field name. + * + * This function will index into dictionaries with any key type, and if the key is + * an int then it'll also index into Erlang tuples and JavaScript arrays, and + * the first eight elements of Gleam lists. + * + * # Examples + * + * ```gleam + * let data = dynamic.properties([ + * #(dynamic.string("data"), dynamic.properties([ + * #(dynamic.string("email"), dynamic.string("lucy@example.com")), + * #(dynamic.string("name"), dynamic.string("Lucy")), + * ]) + * ])) + * + * let decoder = { + * use name <- decode.subfield(["data", "name"], decode.string) + * use email <- decode.subfield(["data", "email"], decode.string) + * decode.success(SignUp(name: name, email: email)) + * } + * let result = decode.run(data, decoder) + * assert result == Ok(SignUp(name: "Lucy", email: "lucy@example.com")) + * ``` + */ +export function subfield(field_path, field_decoder, next) { + return new Decoder( + (data) => { + let $ = index( + field_path, + toList([]), + field_decoder.function, + data, + (data, position) => { + let $1 = field_decoder.function(data); + let default$; + default$ = $1[0]; + let _pipe = [ + default$, + toList([new DecodeError("Field", "Nothing", toList([]))]), + ]; + return push_path(_pipe, $list.reverse(position)); + }, + ); + let out; + let errors1; + out = $[0]; + errors1 = $[1]; + let $1 = next(out).function(data); + let out$1; + let errors2; + out$1 = $1[0]; + errors2 = $1[1]; + return [out$1, $list.append(errors1, errors2)]; + }, + ); +} + +/** + * A decoder that decodes a value that is nested within other values. For + * example, decoding a value that is within some deeply nested JSON objects. + * + * This function will index into dictionaries with any key type, and if the key is + * an int then it'll also index into Erlang tuples and JavaScript arrays, and + * the first eight elements of Gleam lists. + * + * # Examples + * + * ```gleam + * let decoder = decode.at(["one", "two"], decode.int) + * + * let data = dynamic.properties([ + * #(dynamic.string("one"), dynamic.properties([ + * #(dynamic.string("two"), dynamic.int(1000)), + * ])), + * ])) + * + * + * decode.run(data, decoder) + * // -> Ok(1000) + * ``` + * + * ```gleam + * dynamic.nil() + * |> decode.run(decode.optional(decode.int)) + * // -> Ok(option.None) + * ``` + */ +export function at(path, inner) { + return new Decoder( + (data) => { + return index( + path, + toList([]), + inner.function, + data, + (data, position) => { + let $ = inner.function(data); + let default$; + default$ = $[0]; + let _pipe = [ + default$, + toList([new DecodeError("Field", "Nothing", toList([]))]), + ]; + return push_path(_pipe, $list.reverse(position)); + }, + ); + }, + ); +} + +/** + * Run a decoder on a field of a `Dynamic` value, decoding the value if it is + * of the desired type, or returning errors. An error is returned if there is + * no field for the specified key. + * + * This function will index into dictionaries with any key type, and if the key is + * an int then it'll also index into Erlang tuples and JavaScript arrays, and + * the first eight elements of Gleam lists. + * + * # Examples + * + * ```gleam + * let data = dynamic.properties([ + * #(dynamic.string("email"), dynamic.string("lucy@example.com")), + * #(dynamic.string("name"), dynamic.string("Lucy")), + * ])) + * + * let decoder = { + * use name <- decode.field("name", string) + * use email <- decode.field("email", string) + * decode.success(SignUp(name: name, email: email)) + * } + * + * let result = decode.run(data, decoder) + * assert result == Ok(SignUp(name: "Lucy", email: "lucy@example.com")) + * ``` + * + * If you wish to decode a value that is more deeply nested within the dynamic + * data, see [`subfield`](#subfield) and [`at`](#at). + * + * If you wish to return a default in the event that a field is not present, + * see [`optional_field`](#optional_field) and / [`optionally_at`](#optionally_at). + */ +export function field(field_name, field_decoder, next) { + return subfield(toList([field_name]), field_decoder, next); +} + +/** + * Run a decoder on a field of a `Dynamic` value, decoding the value if it is + * of the desired type, or returning errors. The given default value is + * returned if there is no field for the specified key. + * + * This function will index into dictionaries with any key type, and if the key is + * an int then it'll also index into Erlang tuples and JavaScript arrays, and + * the first eight elements of Gleam lists. + * + * # Examples + * + * ```gleam + * let data = dynamic.properties([ + * #(dynamic.string("name"), dynamic.string("Lucy")), + * ])) + * + * let decoder = { + * use name <- decode.field("name", string) + * use email <- decode.optional_field("email", "n/a", string) + * decode.success(SignUp(name: name, email: email)) + * } + * + * let result = decode.run(data, decoder) + * assert result == Ok(SignUp(name: "Lucy", email: "n/a")) + * ``` + */ +export function optional_field(key, default$, field_decoder, next) { + return new Decoder( + (data) => { + let _block; + let _block$1; + let $1 = bare_index(data, key); + if ($1 instanceof Ok) { + let $2 = $1[0]; + if ($2 instanceof Some) { + let data$1 = $2[0]; + _block$1 = field_decoder.function(data$1); + } else { + _block$1 = [default$, toList([])]; + } + } else { + let kind = $1[0]; + _block$1 = [ + default$, + toList([new DecodeError(kind, $dynamic.classify(data), toList([]))]), + ]; + } + let _pipe = _block$1; + _block = push_path(_pipe, toList([key])); + let $ = _block; + let out; + let errors1; + out = $[0]; + errors1 = $[1]; + let $2 = next(out).function(data); + let out$1; + let errors2; + out$1 = $2[0]; + errors2 = $2[1]; + return [out$1, $list.append(errors1, errors2)]; + }, + ); +} + +/** + * A decoder that decodes a value that is nested within other values. For + * example, decoding a value that is within some deeply nested JSON objects. + * + * This function will index into dictionaries with any key type, and if the key is + * an int then it'll also index into Erlang tuples and JavaScript arrays, and + * the first eight elements of Gleam lists. + * + * # Examples + * + * ```gleam + * let decoder = decode.optionally_at(["one", "two"], 100, decode.int) + * + * let data = dynamic.properties([ + * #(dynamic.string("one"), dynamic.properties([])), + * ])) + * + * + * decode.run(data, decoder) + * // -> Ok(100) + * ``` + */ +export function optionally_at(path, default$, inner) { + return new Decoder( + (data) => { + return index( + path, + toList([]), + inner.function, + data, + (_, _1) => { return [default$, toList([])]; }, + ); + }, + ); +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/float.mjs b/build/dev/javascript/gleam_stdlib/gleam/float.mjs new file mode 100644 index 0000000..4f3aaa6 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/float.mjs @@ -0,0 +1,553 @@ +import { Ok, Error, Empty as $Empty, divideFloat } from "../gleam.mjs"; +import * as $order from "../gleam/order.mjs"; +import { + parse_float as parse, + float_to_string as to_string, + ceiling, + floor, + round as js_round, + truncate, + identity as do_to_float, + power as do_power, + random_uniform as random, + log as do_log, + exp as exponential, +} from "../gleam_stdlib.mjs"; + +export { ceiling, exponential, floor, parse, random, to_string, truncate }; + +/** + * Compares two `Float`s, returning an `Order`: + * `Lt` for lower than, `Eq` for equals, or `Gt` for greater than. + * + * ## Examples + * + * ```gleam + * compare(2.0, 2.3) + * // -> Lt + * ``` + * + * To handle + * [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems) + * you may use [`loosely_compare`](#loosely_compare) instead. + */ +export function compare(a, b) { + let $ = a === b; + if ($) { + return new $order.Eq(); + } else { + let $1 = a < b; + if ($1) { + return new $order.Lt(); + } else { + return new $order.Gt(); + } + } +} + +/** + * Compares two `Float`s, returning the smaller of the two. + * + * ## Examples + * + * ```gleam + * min(2.0, 2.3) + * // -> 2.0 + * ``` + */ +export function min(a, b) { + let $ = a < b; + if ($) { + return a; + } else { + return b; + } +} + +/** + * Compares two `Float`s, returning the larger of the two. + * + * ## Examples + * + * ```gleam + * max(2.0, 2.3) + * // -> 2.3 + * ``` + */ +export function max(a, b) { + let $ = a > b; + if ($) { + return a; + } else { + return b; + } +} + +/** + * Restricts a `Float` between a lower and upper bound. + * + * ## Examples + * + * ```gleam + * clamp(1.2, min: 1.4, max: 1.6) + * // -> 1.4 + * ``` + */ +export function clamp(x, min_bound, max_bound) { + let _pipe = x; + let _pipe$1 = min(_pipe, max_bound); + return max(_pipe$1, min_bound); +} + +/** + * Returns the absolute value of the input as a `Float`. + * + * ## Examples + * + * ```gleam + * absolute_value(-12.5) + * // -> 12.5 + * ``` + * + * ```gleam + * absolute_value(10.2) + * // -> 10.2 + * ``` + */ +export function absolute_value(x) { + let $ = x >= 0.0; + if ($) { + return x; + } else { + return 0.0 - x; + } +} + +/** + * Compares two `Float`s within a tolerance, returning an `Order`: + * `Lt` for lower than, `Eq` for equals, or `Gt` for greater than. + * + * This function allows Float comparison while handling + * [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems). + * + * Notice: For `Float`s the tolerance won't be exact: + * `5.3 - 5.0` is not exactly `0.3`. + * + * ## Examples + * + * ```gleam + * loosely_compare(5.0, with: 5.3, tolerating: 0.5) + * // -> Eq + * ``` + * + * If you want to check only for equality you may use + * [`loosely_equals`](#loosely_equals) instead. + */ +export function loosely_compare(a, b, tolerance) { + let difference = absolute_value(a - b); + let $ = difference <= tolerance; + if ($) { + return new $order.Eq(); + } else { + return compare(a, b); + } +} + +/** + * Checks for equality of two `Float`s within a tolerance, + * returning an `Bool`. + * + * This function allows Float comparison while handling + * [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems). + * + * Notice: For `Float`s the tolerance won't be exact: + * `5.3 - 5.0` is not exactly `0.3`. + * + * ## Examples + * + * ```gleam + * loosely_equals(5.0, with: 5.3, tolerating: 0.5) + * // -> True + * ``` + * + * ```gleam + * loosely_equals(5.0, with: 5.1, tolerating: 0.1) + * // -> False + * ``` + */ +export function loosely_equals(a, b, tolerance) { + let difference = absolute_value(a - b); + return difference <= tolerance; +} + +/** + * Returns the results of the base being raised to the power of the + * exponent, as a `Float`. + * + * ## Examples + * + * ```gleam + * power(2.0, -1.0) + * // -> Ok(0.5) + * ``` + * + * ```gleam + * power(2.0, 2.0) + * // -> Ok(4.0) + * ``` + * + * ```gleam + * power(8.0, 1.5) + * // -> Ok(22.627416997969522) + * ``` + * + * ```gleam + * 4.0 |> power(of: 2.0) + * // -> Ok(16.0) + * ``` + * + * ```gleam + * power(-1.0, 0.5) + * // -> Error(Nil) + * ``` + */ +export function power(base, exponent) { + let fractional = (ceiling(exponent) - exponent) > 0.0; + let $ = ((base < 0.0) && fractional) || ((base === 0.0) && (exponent < 0.0)); + if ($) { + return new Error(undefined); + } else { + return new Ok(do_power(base, exponent)); + } +} + +/** + * Returns the square root of the input as a `Float`. + * + * ## Examples + * + * ```gleam + * square_root(4.0) + * // -> Ok(2.0) + * ``` + * + * ```gleam + * square_root(-16.0) + * // -> Error(Nil) + * ``` + */ +export function square_root(x) { + return power(x, 0.5); +} + +/** + * Returns the negative of the value provided. + * + * ## Examples + * + * ```gleam + * negate(1.0) + * // -> -1.0 + * ``` + */ +export function negate(x) { + return -1.0 * x; +} + +/** + * Rounds the value to the nearest whole number as an `Int`. + * + * ## Examples + * + * ```gleam + * round(2.3) + * // -> 2 + * ``` + * + * ```gleam + * round(2.5) + * // -> 3 + * ``` + */ +export function round(x) { + let $ = x >= 0.0; + if ($) { + return js_round(x); + } else { + return 0 - js_round(negate(x)); + } +} + +/** + * Converts the value to a given precision as a `Float`. + * The precision is the number of allowed decimal places. + * Negative precisions are allowed and force rounding + * to the nearest tenth, hundredth, thousandth etc. + * + * ## Examples + * + * ```gleam + * to_precision(2.43434348473, precision: 2) + * // -> 2.43 + * ``` + * + * ```gleam + * to_precision(547890.453444, precision: -3) + * // -> 548000.0 + * ``` + */ +export function to_precision(x, precision) { + let $ = precision <= 0; + if ($) { + let factor = do_power(10.0, do_to_float(- precision)); + return do_to_float(round(divideFloat(x, factor))) * factor; + } else { + let factor = do_power(10.0, do_to_float(precision)); + return divideFloat(do_to_float(round(x * factor)), factor); + } +} + +function sum_loop(loop$numbers, loop$initial) { + while (true) { + let numbers = loop$numbers; + let initial = loop$initial; + if (numbers instanceof $Empty) { + return initial; + } else { + let first = numbers.head; + let rest = numbers.tail; + loop$numbers = rest; + loop$initial = first + initial; + } + } +} + +/** + * Sums a list of `Float`s. + * + * ## Example + * + * ```gleam + * sum([1.0, 2.2, 3.3]) + * // -> 6.5 + * ``` + */ +export function sum(numbers) { + return sum_loop(numbers, 0.0); +} + +function product_loop(loop$numbers, loop$initial) { + while (true) { + let numbers = loop$numbers; + let initial = loop$initial; + if (numbers instanceof $Empty) { + return initial; + } else { + let first = numbers.head; + let rest = numbers.tail; + loop$numbers = rest; + loop$initial = first * initial; + } + } +} + +/** + * Multiplies a list of `Float`s and returns the product. + * + * ## Example + * + * ```gleam + * product([2.5, 3.2, 4.2]) + * // -> 33.6 + * ``` + */ +export function product(numbers) { + return product_loop(numbers, 1.0); +} + +/** + * Computes the modulo of an float division of inputs as a `Result`. + * + * Returns division of the inputs as a `Result`: If the given divisor equals + * `0`, this function returns an `Error`. + * + * ## Examples + * + * ```gleam + * modulo(13.3, by: 3.3) + * // -> Ok(0.1) + * ``` + * + * ```gleam + * modulo(-13.3, by: 3.3) + * // -> Ok(3.2) + * ``` + * + * ```gleam + * modulo(13.3, by: -3.3) + * // -> Ok(-3.2) + * ``` + * + * ```gleam + * modulo(-13.3, by: -3.3) + * // -> Ok(-0.1) + * ``` + */ +export function modulo(dividend, divisor) { + if (divisor === 0.0) { + return new Error(undefined); + } else { + return new Ok(dividend - (floor(divideFloat(dividend, divisor)) * divisor)); + } +} + +/** + * Returns division of the inputs as a `Result`. + * + * ## Examples + * + * ```gleam + * divide(0.0, 1.0) + * // -> Ok(0.0) + * ``` + * + * ```gleam + * divide(1.0, 0.0) + * // -> Error(Nil) + * ``` + */ +export function divide(a, b) { + if (b === 0.0) { + return new Error(undefined); + } else { + let b$1 = b; + return new Ok(divideFloat(a, b$1)); + } +} + +/** + * Adds two floats together. + * + * It's the function equivalent of the `+.` operator. + * This function is useful in higher order functions or pipes. + * + * ## Examples + * + * ```gleam + * add(1.0, 2.0) + * // -> 3.0 + * ``` + * + * ```gleam + * import gleam/list + * + * list.fold([1.0, 2.0, 3.0], 0.0, add) + * // -> 6.0 + * ``` + * + * ```gleam + * 3.0 |> add(2.0) + * // -> 5.0 + * ``` + */ +export function add(a, b) { + return a + b; +} + +/** + * Multiplies two floats together. + * + * It's the function equivalent of the `*.` operator. + * This function is useful in higher order functions or pipes. + * + * ## Examples + * + * ```gleam + * multiply(2.0, 4.0) + * // -> 8.0 + * ``` + * + * ```gleam + * import gleam/list + * + * list.fold([2.0, 3.0, 4.0], 1.0, multiply) + * // -> 24.0 + * ``` + * + * ```gleam + * 3.0 |> multiply(2.0) + * // -> 6.0 + * ``` + */ +export function multiply(a, b) { + return a * b; +} + +/** + * Subtracts one float from another. + * + * It's the function equivalent of the `-.` operator. + * This function is useful in higher order functions or pipes. + * + * ## Examples + * + * ```gleam + * subtract(3.0, 1.0) + * // -> 2.0 + * ``` + * + * ```gleam + * import gleam/list + * + * list.fold([1.0, 2.0, 3.0], 10.0, subtract) + * // -> 4.0 + * ``` + * + * ```gleam + * 3.0 |> subtract(_, 2.0) + * // -> 1.0 + * ``` + * + * ```gleam + * 3.0 |> subtract(2.0, _) + * // -> -1.0 + * ``` + */ +export function subtract(a, b) { + return a - b; +} + +/** + * Returns the natural logarithm (base e) of the given as a `Result`. If the + * input is less than or equal to 0, returns `Error(Nil)`. + * + * ## Examples + * + * ```gleam + * logarithm(1.0) + * // -> Ok(0.0) + * ``` + * + * ```gleam + * logarithm(2.718281828459045) // e + * // -> Ok(1.0) + * ``` + * + * ```gleam + * logarithm(0.0) + * // -> Error(Nil) + * ``` + * + * ```gleam + * logarithm(-1.0) + * // -> Error(Nil) + * ``` + */ +export function logarithm(x) { + let $ = x <= 0.0; + if ($) { + return new Error(undefined); + } else { + return new Ok(do_log(x)); + } +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/function.mjs b/build/dev/javascript/gleam_stdlib/gleam/function.mjs new file mode 100644 index 0000000..2292fbc --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/function.mjs @@ -0,0 +1,17 @@ +/** + * Takes a single argument and always returns its input value. + */ +export function identity(x) { + return x; +} + +/** + * Takes an argument and a single function, calls that function with that + * argument and returns that argument instead of the function return value. + * + * Useful for running synchronous side effects in a pipeline. + */ +export function tap(arg, effect) { + effect(arg); + return arg; +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/int.mjs b/build/dev/javascript/gleam_stdlib/gleam/int.mjs new file mode 100644 index 0000000..5824eb6 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/int.mjs @@ -0,0 +1,816 @@ +import { + Ok, + Error, + toList, + Empty as $Empty, + prepend as listPrepend, + remainderInt, + divideInt, +} from "../gleam.mjs"; +import * as $float from "../gleam/float.mjs"; +import * as $order from "../gleam/order.mjs"; +import { + parse_int as parse, + int_from_base_string as do_base_parse, + to_string, + int_to_base_string as do_to_base_string, + identity as to_float, + bitwise_and, + bitwise_not, + bitwise_or, + bitwise_exclusive_or, + bitwise_shift_left, + bitwise_shift_right, +} from "../gleam_stdlib.mjs"; + +export { + bitwise_and, + bitwise_exclusive_or, + bitwise_not, + bitwise_or, + bitwise_shift_left, + bitwise_shift_right, + parse, + to_float, + to_string, +}; + +/** + * Returns the absolute value of the input. + * + * ## Examples + * + * ```gleam + * absolute_value(-12) + * // -> 12 + * ``` + * + * ```gleam + * absolute_value(10) + * // -> 10 + * ``` + */ +export function absolute_value(x) { + let $ = x >= 0; + if ($) { + return x; + } else { + return x * -1; + } +} + +/** + * Parses a given string as an int in a given base if possible. + * Supports only bases 2 to 36, for values outside of which this function returns an `Error(Nil)`. + * + * ## Examples + * + * ```gleam + * base_parse("10", 2) + * // -> Ok(2) + * ``` + * + * ```gleam + * base_parse("30", 16) + * // -> Ok(48) + * ``` + * + * ```gleam + * base_parse("1C", 36) + * // -> Ok(48) + * ``` + * + * ```gleam + * base_parse("48", 1) + * // -> Error(Nil) + * ``` + * + * ```gleam + * base_parse("48", 37) + * // -> Error(Nil) + * ``` + */ +export function base_parse(string, base) { + let $ = (base >= 2) && (base <= 36); + if ($) { + return do_base_parse(string, base); + } else { + return new Error(undefined); + } +} + +/** + * Prints a given int to a string using the base number provided. + * Supports only bases 2 to 36, for values outside of which this function returns an `Error(Nil)`. + * For common bases (2, 8, 16, 36), use the `to_baseN` functions. + * + * ## Examples + * + * ```gleam + * to_base_string(2, 2) + * // -> Ok("10") + * ``` + * + * ```gleam + * to_base_string(48, 16) + * // -> Ok("30") + * ``` + * + * ```gleam + * to_base_string(48, 36) + * // -> Ok("1C") + * ``` + * + * ```gleam + * to_base_string(48, 1) + * // -> Error(Nil) + * ``` + * + * ```gleam + * to_base_string(48, 37) + * // -> Error(Nil) + * ``` + */ +export function to_base_string(x, base) { + let $ = (base >= 2) && (base <= 36); + if ($) { + return new Ok(do_to_base_string(x, base)); + } else { + return new Error(undefined); + } +} + +/** + * Prints a given int to a string using base-2. + * + * ## Examples + * + * ```gleam + * to_base2(2) + * // -> "10" + * ``` + */ +export function to_base2(x) { + return do_to_base_string(x, 2); +} + +/** + * Prints a given int to a string using base-8. + * + * ## Examples + * + * ```gleam + * to_base8(15) + * // -> "17" + * ``` + */ +export function to_base8(x) { + return do_to_base_string(x, 8); +} + +/** + * Prints a given int to a string using base-16. + * + * ## Examples + * + * ```gleam + * to_base16(48) + * // -> "30" + * ``` + */ +export function to_base16(x) { + return do_to_base_string(x, 16); +} + +/** + * Prints a given int to a string using base-36. + * + * ## Examples + * + * ```gleam + * to_base36(48) + * // -> "1C" + * ``` + */ +export function to_base36(x) { + return do_to_base_string(x, 36); +} + +/** + * Returns the results of the base being raised to the power of the + * exponent, as a `Float`. + * + * ## Examples + * + * ```gleam + * power(2, -1.0) + * // -> Ok(0.5) + * ``` + * + * ```gleam + * power(2, 2.0) + * // -> Ok(4.0) + * ``` + * + * ```gleam + * power(8, 1.5) + * // -> Ok(22.627416997969522) + * ``` + * + * ```gleam + * 4 |> power(of: 2.0) + * // -> Ok(16.0) + * ``` + * + * ```gleam + * power(-1, 0.5) + * // -> Error(Nil) + * ``` + */ +export function power(base, exponent) { + let _pipe = base; + let _pipe$1 = to_float(_pipe); + return $float.power(_pipe$1, exponent); +} + +/** + * Returns the square root of the input as a `Float`. + * + * ## Examples + * + * ```gleam + * square_root(4) + * // -> Ok(2.0) + * ``` + * + * ```gleam + * square_root(-16) + * // -> Error(Nil) + * ``` + */ +export function square_root(x) { + let _pipe = x; + let _pipe$1 = to_float(_pipe); + return $float.square_root(_pipe$1); +} + +/** + * Compares two ints, returning an order. + * + * ## Examples + * + * ```gleam + * compare(2, 3) + * // -> Lt + * ``` + * + * ```gleam + * compare(4, 3) + * // -> Gt + * ``` + * + * ```gleam + * compare(3, 3) + * // -> Eq + * ``` + */ +export function compare(a, b) { + let $ = a === b; + if ($) { + return new $order.Eq(); + } else { + let $1 = a < b; + if ($1) { + return new $order.Lt(); + } else { + return new $order.Gt(); + } + } +} + +/** + * Compares two ints, returning the smaller of the two. + * + * ## Examples + * + * ```gleam + * min(2, 3) + * // -> 2 + * ``` + */ +export function min(a, b) { + let $ = a < b; + if ($) { + return a; + } else { + return b; + } +} + +/** + * Compares two ints, returning the larger of the two. + * + * ## Examples + * + * ```gleam + * max(2, 3) + * // -> 3 + * ``` + */ +export function max(a, b) { + let $ = a > b; + if ($) { + return a; + } else { + return b; + } +} + +/** + * Restricts an int between a lower and upper bound. + * + * ## Examples + * + * ```gleam + * clamp(40, min: 50, max: 60) + * // -> 50 + * ``` + */ +export function clamp(x, min_bound, max_bound) { + let _pipe = x; + let _pipe$1 = min(_pipe, max_bound); + return max(_pipe$1, min_bound); +} + +/** + * Returns whether the value provided is even. + * + * ## Examples + * + * ```gleam + * is_even(2) + * // -> True + * ``` + * + * ```gleam + * is_even(3) + * // -> False + * ``` + */ +export function is_even(x) { + return (x % 2) === 0; +} + +/** + * Returns whether the value provided is odd. + * + * ## Examples + * + * ```gleam + * is_odd(3) + * // -> True + * ``` + * + * ```gleam + * is_odd(2) + * // -> False + * ``` + */ +export function is_odd(x) { + return (x % 2) !== 0; +} + +/** + * Returns the negative of the value provided. + * + * ## Examples + * + * ```gleam + * negate(1) + * // -> -1 + * ``` + */ +export function negate(x) { + return -1 * x; +} + +function sum_loop(loop$numbers, loop$initial) { + while (true) { + let numbers = loop$numbers; + let initial = loop$initial; + if (numbers instanceof $Empty) { + return initial; + } else { + let first = numbers.head; + let rest = numbers.tail; + loop$numbers = rest; + loop$initial = first + initial; + } + } +} + +/** + * Sums a list of ints. + * + * ## Example + * + * ```gleam + * sum([1, 2, 3]) + * // -> 6 + * ``` + */ +export function sum(numbers) { + return sum_loop(numbers, 0); +} + +function product_loop(loop$numbers, loop$initial) { + while (true) { + let numbers = loop$numbers; + let initial = loop$initial; + if (numbers instanceof $Empty) { + return initial; + } else { + let first = numbers.head; + let rest = numbers.tail; + loop$numbers = rest; + loop$initial = first * initial; + } + } +} + +/** + * Multiplies a list of ints and returns the product. + * + * ## Example + * + * ```gleam + * product([2, 3, 4]) + * // -> 24 + * ``` + */ +export function product(numbers) { + return product_loop(numbers, 1); +} + +function digits_loop(loop$x, loop$base, loop$acc) { + while (true) { + let x = loop$x; + let base = loop$base; + let acc = loop$acc; + let $ = absolute_value(x) < base; + if ($) { + return listPrepend(x, acc); + } else { + loop$x = divideInt(x, base); + loop$base = base; + loop$acc = listPrepend(remainderInt(x, base), acc); + } + } +} + +export function digits(x, base) { + let $ = base < 2; + if ($) { + return new Error(undefined); + } else { + return new Ok(digits_loop(x, base, toList([]))); + } +} + +function undigits_loop(loop$numbers, loop$base, loop$acc) { + while (true) { + let numbers = loop$numbers; + let base = loop$base; + let acc = loop$acc; + if (numbers instanceof $Empty) { + return new Ok(acc); + } else { + let digit = numbers.head; + if (digit >= base) { + return new Error(undefined); + } else { + let digit$1 = numbers.head; + let rest = numbers.tail; + loop$numbers = rest; + loop$base = base; + loop$acc = acc * base + digit$1; + } + } + } +} + +export function undigits(numbers, base) { + let $ = base < 2; + if ($) { + return new Error(undefined); + } else { + return undigits_loop(numbers, base, 0); + } +} + +/** + * Generates a random int between zero and the given maximum. + * + * The lower number is inclusive, the upper number is exclusive. + * + * ## Examples + * + * ```gleam + * random(10) + * // -> 4 + * ``` + * + * ```gleam + * random(1) + * // -> 0 + * ``` + * + * ```gleam + * random(-1) + * // -> -1 + * ``` + */ +export function random(max) { + let _pipe = ($float.random() * to_float(max)); + let _pipe$1 = $float.floor(_pipe); + return $float.round(_pipe$1); +} + +/** + * Performs a truncated integer division. + * + * Returns division of the inputs as a `Result`: If the given divisor equals + * `0`, this function returns an `Error`. + * + * ## Examples + * + * ```gleam + * divide(0, 1) + * // -> Ok(0) + * ``` + * + * ```gleam + * divide(1, 0) + * // -> Error(Nil) + * ``` + * + * ```gleam + * divide(5, 2) + * // -> Ok(2) + * ``` + * + * ```gleam + * divide(-99, 2) + * // -> Ok(-49) + * ``` + */ +export function divide(dividend, divisor) { + if (divisor === 0) { + return new Error(undefined); + } else { + let divisor$1 = divisor; + return new Ok(divideInt(dividend, divisor$1)); + } +} + +/** + * Computes the remainder of an integer division of inputs as a `Result`. + * + * Returns division of the inputs as a `Result`: If the given divisor equals + * `0`, this function returns an `Error`. + * + * Most the time you will want to use the `%` operator instead of this + * function. + * + * ## Examples + * + * ```gleam + * remainder(3, 2) + * // -> Ok(1) + * ``` + * + * ```gleam + * remainder(1, 0) + * // -> Error(Nil) + * ``` + * + * ```gleam + * remainder(10, -1) + * // -> Ok(0) + * ``` + * + * ```gleam + * remainder(13, by: 3) + * // -> Ok(1) + * ``` + * + * ```gleam + * remainder(-13, by: 3) + * // -> Ok(-1) + * ``` + * + * ```gleam + * remainder(13, by: -3) + * // -> Ok(1) + * ``` + * + * ```gleam + * remainder(-13, by: -3) + * // -> Ok(-1) + * ``` + */ +export function remainder(dividend, divisor) { + if (divisor === 0) { + return new Error(undefined); + } else { + let divisor$1 = divisor; + return new Ok(remainderInt(dividend, divisor$1)); + } +} + +/** + * Computes the modulo of an integer division of inputs as a `Result`. + * + * Returns division of the inputs as a `Result`: If the given divisor equals + * `0`, this function returns an `Error`. + * + * Most the time you will want to use the `%` operator instead of this + * function. + * + * ## Examples + * + * ```gleam + * modulo(3, 2) + * // -> Ok(1) + * ``` + * + * ```gleam + * modulo(1, 0) + * // -> Error(Nil) + * ``` + * + * ```gleam + * modulo(10, -1) + * // -> Ok(0) + * ``` + * + * ```gleam + * modulo(13, by: 3) + * // -> Ok(1) + * ``` + * + * ```gleam + * modulo(-13, by: 3) + * // -> Ok(2) + * ``` + */ +export function modulo(dividend, divisor) { + if (divisor === 0) { + return new Error(undefined); + } else { + let remainder$1 = remainderInt(dividend, divisor); + let $ = remainder$1 * divisor < 0; + if ($) { + return new Ok(remainder$1 + divisor); + } else { + return new Ok(remainder$1); + } + } +} + +/** + * Performs a *floored* integer division, which means that the result will + * always be rounded towards negative infinity. + * + * If you want to perform truncated integer division (rounding towards zero), + * use `int.divide()` or the `/` operator instead. + * + * Returns division of the inputs as a `Result`: If the given divisor equals + * `0`, this function returns an `Error`. + * + * ## Examples + * + * ```gleam + * floor_divide(1, 0) + * // -> Error(Nil) + * ``` + * + * ```gleam + * floor_divide(5, 2) + * // -> Ok(2) + * ``` + * + * ```gleam + * floor_divide(6, -4) + * // -> Ok(-2) + * ``` + * + * ```gleam + * floor_divide(-99, 2) + * // -> Ok(-50) + * ``` + */ +export function floor_divide(dividend, divisor) { + if (divisor === 0) { + return new Error(undefined); + } else { + let divisor$1 = divisor; + let $ = (dividend * divisor$1 < 0) && ((remainderInt(dividend, divisor$1)) !== 0); + if ($) { + return new Ok((divideInt(dividend, divisor$1)) - 1); + } else { + return new Ok(divideInt(dividend, divisor$1)); + } + } +} + +/** + * Adds two integers together. + * + * It's the function equivalent of the `+` operator. + * This function is useful in higher order functions or pipes. + * + * ## Examples + * + * ```gleam + * add(1, 2) + * // -> 3 + * ``` + * + * ```gleam + * import gleam/list + * list.fold([1, 2, 3], 0, add) + * // -> 6 + * ``` + * + * ```gleam + * 3 |> add(2) + * // -> 5 + * ``` + */ +export function add(a, b) { + return a + b; +} + +/** + * Multiplies two integers together. + * + * It's the function equivalent of the `*` operator. + * This function is useful in higher order functions or pipes. + * + * ## Examples + * + * ```gleam + * multiply(2, 4) + * // -> 8 + * ``` + * + * ```gleam + * import gleam/list + * + * list.fold([2, 3, 4], 1, multiply) + * // -> 24 + * ``` + * + * ```gleam + * 3 |> multiply(2) + * // -> 6 + * ``` + */ +export function multiply(a, b) { + return a * b; +} + +/** + * Subtracts one int from another. + * + * It's the function equivalent of the `-` operator. + * This function is useful in higher order functions or pipes. + * + * ## Examples + * + * ```gleam + * subtract(3, 1) + * // -> 2 + * ``` + * + * ```gleam + * import gleam/list + * + * list.fold([1, 2, 3], 10, subtract) + * // -> 4 + * ``` + * + * ```gleam + * 3 |> subtract(2) + * // -> 1 + * ``` + * + * ```gleam + * 3 |> subtract(2, _) + * // -> -1 + * ``` + */ +export function subtract(a, b) { + return a - b; +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/io.mjs b/build/dev/javascript/gleam_stdlib/gleam/io.mjs new file mode 100644 index 0000000..ddab435 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/io.mjs @@ -0,0 +1,8 @@ +import { + print, + print_error, + console_log as println, + console_error as println_error, +} from "../gleam_stdlib.mjs"; + +export { print, print_error, println, println_error }; diff --git a/build/dev/javascript/gleam_stdlib/gleam/list.mjs b/build/dev/javascript/gleam_stdlib/gleam/list.mjs new file mode 100644 index 0000000..2ea158e --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/list.mjs @@ -0,0 +1,3209 @@ +import { + Ok, + Error, + toList, + Empty as $Empty, + prepend as listPrepend, + CustomType as $CustomType, + makeError, + divideFloat, + isEqual, +} from "../gleam.mjs"; +import * as $dict from "../gleam/dict.mjs"; +import * as $float from "../gleam/float.mjs"; +import * as $int from "../gleam/int.mjs"; +import * as $order from "../gleam/order.mjs"; + +const FILEPATH = "src/gleam/list.gleam"; + +export class Continue extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const ContinueOrStop$Continue = ($0) => new Continue($0); +export const ContinueOrStop$isContinue = (value) => value instanceof Continue; +export const ContinueOrStop$Continue$0 = (value) => value[0]; + +export class Stop extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const ContinueOrStop$Stop = ($0) => new Stop($0); +export const ContinueOrStop$isStop = (value) => value instanceof Stop; +export const ContinueOrStop$Stop$0 = (value) => value[0]; + +class Ascending extends $CustomType {} + +class Descending extends $CustomType {} + +function length_loop(loop$list, loop$count) { + while (true) { + let list = loop$list; + let count = loop$count; + if (list instanceof $Empty) { + return count; + } else { + let list$1 = list.tail; + loop$list = list$1; + loop$count = count + 1; + } + } +} + +/** + * Counts the number of elements in a given list. + * + * This function has to traverse the list to determine the number of elements, + * so it runs in linear time. + * + * This function is natively implemented by the virtual machine and is highly + * optimised. + * + * ## Examples + * + * ```gleam + * length([]) + * // -> 0 + * ``` + * + * ```gleam + * length([1]) + * // -> 1 + * ``` + * + * ```gleam + * length([1, 2]) + * // -> 2 + * ``` + */ +export function length(list) { + return length_loop(list, 0); +} + +function count_loop(loop$list, loop$predicate, loop$acc) { + while (true) { + let list = loop$list; + let predicate = loop$predicate; + let acc = loop$acc; + if (list instanceof $Empty) { + return acc; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = predicate(first$1); + if ($) { + loop$list = rest$1; + loop$predicate = predicate; + loop$acc = acc + 1; + } else { + loop$list = rest$1; + loop$predicate = predicate; + loop$acc = acc; + } + } + } +} + +/** + * Counts the number of elements in a given list satisfying a given predicate. + * + * This function has to traverse the list to determine the number of elements, + * so it runs in linear time. + * + * ## Examples + * + * ```gleam + * count([], fn(a) { a > 0 }) + * // -> 0 + * ``` + * + * ```gleam + * count([1], fn(a) { a > 0 }) + * // -> 1 + * ``` + * + * ```gleam + * count([1, 2, 3], int.is_odd) + * // -> 2 + * ``` + */ +export function count(list, predicate) { + return count_loop(list, predicate, 0); +} + +/** + * Reverses a list and prepends it to another list. + * This function runs in linear time, proportional to the length of the list + * to prepend. + * + * @ignore + */ +function reverse_and_prepend(loop$prefix, loop$suffix) { + while (true) { + let prefix = loop$prefix; + let suffix = loop$suffix; + if (prefix instanceof $Empty) { + return suffix; + } else { + let first$1 = prefix.head; + let rest$1 = prefix.tail; + loop$prefix = rest$1; + loop$suffix = listPrepend(first$1, suffix); + } + } +} + +/** + * Creates a new list from a given list containing the same elements but in the + * opposite order. + * + * This function has to traverse the list to create the new reversed list, so + * it runs in linear time. + * + * This function is natively implemented by the virtual machine and is highly + * optimised. + * + * ## Examples + * + * ```gleam + * reverse([]) + * // -> [] + * ``` + * + * ```gleam + * reverse([1]) + * // -> [1] + * ``` + * + * ```gleam + * reverse([1, 2]) + * // -> [2, 1] + * ``` + */ +export function reverse(list) { + return reverse_and_prepend(list, toList([])); +} + +/** + * Determines whether or not the list is empty. + * + * This function runs in constant time. + * + * ## Examples + * + * ```gleam + * is_empty([]) + * // -> True + * ``` + * + * ```gleam + * is_empty([1]) + * // -> False + * ``` + * + * ```gleam + * is_empty([1, 1]) + * // -> False + * ``` + */ +export function is_empty(list) { + return isEqual(list, toList([])); +} + +/** + * Determines whether or not a given element exists within a given list. + * + * This function traverses the list to find the element, so it runs in linear + * time. + * + * ## Examples + * + * ```gleam + * [] |> contains(any: 0) + * // -> False + * ``` + * + * ```gleam + * [0] |> contains(any: 0) + * // -> True + * ``` + * + * ```gleam + * [1] |> contains(any: 0) + * // -> False + * ``` + * + * ```gleam + * [1, 1] |> contains(any: 0) + * // -> False + * ``` + * + * ```gleam + * [1, 0] |> contains(any: 0) + * // -> True + * ``` + */ +export function contains(loop$list, loop$elem) { + while (true) { + let list = loop$list; + let elem = loop$elem; + if (list instanceof $Empty) { + return false; + } else { + let first$1 = list.head; + if (isEqual(first$1, elem)) { + return true; + } else { + let rest$1 = list.tail; + loop$list = rest$1; + loop$elem = elem; + } + } + } +} + +/** + * Gets the first element from the start of the list, if there is one. + * + * ## Examples + * + * ```gleam + * first([]) + * // -> Error(Nil) + * ``` + * + * ```gleam + * first([0]) + * // -> Ok(0) + * ``` + * + * ```gleam + * first([1, 2]) + * // -> Ok(1) + * ``` + */ +export function first(list) { + if (list instanceof $Empty) { + return new Error(undefined); + } else { + let first$1 = list.head; + return new Ok(first$1); + } +} + +/** + * Returns the list minus the first element. If the list is empty, `Error(Nil)` is + * returned. + * + * This function runs in constant time and does not make a copy of the list. + * + * ## Examples + * + * ```gleam + * rest([]) + * // -> Error(Nil) + * ``` + * + * ```gleam + * rest([0]) + * // -> Ok([]) + * ``` + * + * ```gleam + * rest([1, 2]) + * // -> Ok([2]) + * ``` + */ +export function rest(list) { + if (list instanceof $Empty) { + return new Error(undefined); + } else { + let rest$1 = list.tail; + return new Ok(rest$1); + } +} + +function group_loop(loop$list, loop$to_key, loop$groups) { + while (true) { + let list = loop$list; + let to_key = loop$to_key; + let groups = loop$groups; + if (list instanceof $Empty) { + return groups; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let key = to_key(first$1); + let _block; + let $ = $dict.get(groups, key); + if ($ instanceof Ok) { + let existing = $[0]; + _block = $dict.insert(groups, key, listPrepend(first$1, existing)); + } else { + _block = $dict.insert(groups, key, toList([first$1])); + } + let groups$1 = _block; + loop$list = rest$1; + loop$to_key = to_key; + loop$groups = groups$1; + } + } +} + +/** + * Groups the elements from the given list by the given key function. + * + * Does not preserve the initial value order. + * + * ## Examples + * + * ```gleam + * import gleam/dict + * + * [Ok(3), Error("Wrong"), Ok(200), Ok(73)] + * |> group(by: fn(i) { + * case i { + * Ok(_) -> "Successful" + * Error(_) -> "Failed" + * } + * }) + * |> dict.to_list + * // -> [ + * // #("Failed", [Error("Wrong")]), + * // #("Successful", [Ok(73), Ok(200), Ok(3)]) + * // ] + * ``` + * + * ```gleam + * import gleam/dict + * + * group([1,2,3,4,5], by: fn(i) { i - i / 3 * 3 }) + * |> dict.to_list + * // -> [#(0, [3]), #(1, [4, 1]), #(2, [5, 2])] + * ``` + */ +export function group(list, key) { + return group_loop(list, key, $dict.new$()); +} + +function filter_loop(loop$list, loop$fun, loop$acc) { + while (true) { + let list = loop$list; + let fun = loop$fun; + let acc = loop$acc; + if (list instanceof $Empty) { + return reverse(acc); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let _block; + let $ = fun(first$1); + if ($) { + _block = listPrepend(first$1, acc); + } else { + _block = acc; + } + let new_acc = _block; + loop$list = rest$1; + loop$fun = fun; + loop$acc = new_acc; + } + } +} + +/** + * Returns a new list containing only the elements from the first list for + * which the given functions returns `True`. + * + * ## Examples + * + * ```gleam + * filter([2, 4, 6, 1], fn(x) { x > 2 }) + * // -> [4, 6] + * ``` + * + * ```gleam + * filter([2, 4, 6, 1], fn(x) { x > 6 }) + * // -> [] + * ``` + */ +export function filter(list, predicate) { + return filter_loop(list, predicate, toList([])); +} + +function filter_map_loop(loop$list, loop$fun, loop$acc) { + while (true) { + let list = loop$list; + let fun = loop$fun; + let acc = loop$acc; + if (list instanceof $Empty) { + return reverse(acc); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let _block; + let $ = fun(first$1); + if ($ instanceof Ok) { + let first$2 = $[0]; + _block = listPrepend(first$2, acc); + } else { + _block = acc; + } + let new_acc = _block; + loop$list = rest$1; + loop$fun = fun; + loop$acc = new_acc; + } + } +} + +/** + * Returns a new list containing only the elements from the first list for + * which the given functions returns `Ok(_)`. + * + * ## Examples + * + * ```gleam + * filter_map([2, 4, 6, 1], Error) + * // -> [] + * ``` + * + * ```gleam + * filter_map([2, 4, 6, 1], fn(x) { Ok(x + 1) }) + * // -> [3, 5, 7, 2] + * ``` + */ +export function filter_map(list, fun) { + return filter_map_loop(list, fun, toList([])); +} + +function map_loop(loop$list, loop$fun, loop$acc) { + while (true) { + let list = loop$list; + let fun = loop$fun; + let acc = loop$acc; + if (list instanceof $Empty) { + return reverse(acc); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + loop$list = rest$1; + loop$fun = fun; + loop$acc = listPrepend(fun(first$1), acc); + } + } +} + +/** + * Returns a new list containing only the elements of the first list after the + * function has been applied to each one. + * + * ## Examples + * + * ```gleam + * map([2, 4, 6], fn(x) { x * 2 }) + * // -> [4, 8, 12] + * ``` + */ +export function map(list, fun) { + return map_loop(list, fun, toList([])); +} + +function map2_loop(loop$list1, loop$list2, loop$fun, loop$acc) { + while (true) { + let list1 = loop$list1; + let list2 = loop$list2; + let fun = loop$fun; + let acc = loop$acc; + if (list1 instanceof $Empty) { + return reverse(acc); + } else if (list2 instanceof $Empty) { + return reverse(acc); + } else { + let a = list1.head; + let as_ = list1.tail; + let b = list2.head; + let bs = list2.tail; + loop$list1 = as_; + loop$list2 = bs; + loop$fun = fun; + loop$acc = listPrepend(fun(a, b), acc); + } + } +} + +/** + * Combines two lists into a single list using the given function. + * + * If a list is longer than the other the extra elements are dropped. + * + * ## Examples + * + * ```gleam + * map2([1, 2, 3], [4, 5, 6], fn(x, y) { x + y }) + * // -> [5, 7, 9] + * ``` + * + * ```gleam + * map2([1, 2], ["a", "b", "c"], fn(i, x) { #(i, x) }) + * // -> [#(1, "a"), #(2, "b")] + * ``` + */ +export function map2(list1, list2, fun) { + return map2_loop(list1, list2, fun, toList([])); +} + +function map_fold_loop(loop$list, loop$fun, loop$acc, loop$list_acc) { + while (true) { + let list = loop$list; + let fun = loop$fun; + let acc = loop$acc; + let list_acc = loop$list_acc; + if (list instanceof $Empty) { + return [acc, reverse(list_acc)]; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = fun(acc, first$1); + let acc$1; + let first$2; + acc$1 = $[0]; + first$2 = $[1]; + loop$list = rest$1; + loop$fun = fun; + loop$acc = acc$1; + loop$list_acc = listPrepend(first$2, list_acc); + } + } +} + +/** + * Similar to `map` but also lets you pass around an accumulated value. + * + * ## Examples + * + * ```gleam + * map_fold( + * over: [1, 2, 3], + * from: 100, + * with: fn(memo, i) { #(memo + i, i * 2) } + * ) + * // -> #(106, [2, 4, 6]) + * ``` + */ +export function map_fold(list, initial, fun) { + return map_fold_loop(list, fun, initial, toList([])); +} + +function index_map_loop(loop$list, loop$fun, loop$index, loop$acc) { + while (true) { + let list = loop$list; + let fun = loop$fun; + let index = loop$index; + let acc = loop$acc; + if (list instanceof $Empty) { + return reverse(acc); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let acc$1 = listPrepend(fun(first$1, index), acc); + loop$list = rest$1; + loop$fun = fun; + loop$index = index + 1; + loop$acc = acc$1; + } + } +} + +/** + * Returns a new list containing only the elements of the first list after the + * function has been applied to each one and their index. + * + * The index starts at 0, so the first element is 0, the second is 1, and so + * on. + * + * ## Examples + * + * ```gleam + * index_map(["a", "b"], fn(x, i) { #(i, x) }) + * // -> [#(0, "a"), #(1, "b")] + * ``` + */ +export function index_map(list, fun) { + return index_map_loop(list, fun, 0, toList([])); +} + +function try_map_loop(loop$list, loop$fun, loop$acc) { + while (true) { + let list = loop$list; + let fun = loop$fun; + let acc = loop$acc; + if (list instanceof $Empty) { + return new Ok(reverse(acc)); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = fun(first$1); + if ($ instanceof Ok) { + let first$2 = $[0]; + loop$list = rest$1; + loop$fun = fun; + loop$acc = listPrepend(first$2, acc); + } else { + return $; + } + } + } +} + +/** + * Takes a function that returns a `Result` and applies it to each element in a + * given list in turn. + * + * If the function returns `Ok(new_value)` for all elements in the list then a + * list of the new values is returned. + * + * If the function returns `Error(reason)` for any of the elements then it is + * returned immediately. None of the elements in the list are processed after + * one returns an `Error`. + * + * ## Examples + * + * ```gleam + * try_map([1, 2, 3], fn(x) { Ok(x + 2) }) + * // -> Ok([3, 4, 5]) + * ``` + * + * ```gleam + * try_map([1, 2, 3], fn(_) { Error(0) }) + * // -> Error(0) + * ``` + * + * ```gleam + * try_map([[1], [2, 3]], first) + * // -> Ok([1, 2]) + * ``` + * + * ```gleam + * try_map([[1], [], [2]], first) + * // -> Error(Nil) + * ``` + */ +export function try_map(list, fun) { + return try_map_loop(list, fun, toList([])); +} + +/** + * Returns a list that is the given list with up to the given number of + * elements removed from the front of the list. + * + * If the element has less than the number of elements an empty list is + * returned. + * + * This function runs in linear time but does not copy the list. + * + * ## Examples + * + * ```gleam + * drop([1, 2, 3, 4], 2) + * // -> [3, 4] + * ``` + * + * ```gleam + * drop([1, 2, 3, 4], 9) + * // -> [] + * ``` + */ +export function drop(loop$list, loop$n) { + while (true) { + let list = loop$list; + let n = loop$n; + let $ = n <= 0; + if ($) { + return list; + } else { + if (list instanceof $Empty) { + return list; + } else { + let rest$1 = list.tail; + loop$list = rest$1; + loop$n = n - 1; + } + } + } +} + +function take_loop(loop$list, loop$n, loop$acc) { + while (true) { + let list = loop$list; + let n = loop$n; + let acc = loop$acc; + let $ = n <= 0; + if ($) { + return reverse(acc); + } else { + if (list instanceof $Empty) { + return reverse(acc); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + loop$list = rest$1; + loop$n = n - 1; + loop$acc = listPrepend(first$1, acc); + } + } + } +} + +/** + * Returns a list containing the first given number of elements from the given + * list. + * + * If the element has less than the number of elements then the full list is + * returned. + * + * This function runs in linear time. + * + * ## Examples + * + * ```gleam + * take([1, 2, 3, 4], 2) + * // -> [1, 2] + * ``` + * + * ```gleam + * take([1, 2, 3, 4], 9) + * // -> [1, 2, 3, 4] + * ``` + */ +export function take(list, n) { + return take_loop(list, n, toList([])); +} + +/** + * Returns a new empty list. + * + * ## Examples + * + * ```gleam + * new() + * // -> [] + * ``` + */ +export function new$() { + return toList([]); +} + +/** + * Returns the given item wrapped in a list. + * + * ## Examples + * + * ```gleam + * wrap(1) + * // -> [1] + * + * wrap(["a", "b", "c"]) + * // -> [["a", "b", "c"]] + * + * wrap([[]]) + * // -> [[[]]] + * ``` + */ +export function wrap(item) { + return toList([item]); +} + +function append_loop(loop$first, loop$second) { + while (true) { + let first = loop$first; + let second = loop$second; + if (first instanceof $Empty) { + return second; + } else { + let first$1 = first.head; + let rest$1 = first.tail; + loop$first = rest$1; + loop$second = listPrepend(first$1, second); + } + } +} + +/** + * Joins one list onto the end of another. + * + * This function runs in linear time, and it traverses and copies the first + * list. + * + * ## Examples + * + * ```gleam + * append([1, 2], [3]) + * // -> [1, 2, 3] + * ``` + */ +export function append(first, second) { + return append_loop(reverse(first), second); +} + +/** + * Prefixes an item to a list. This can also be done using the dedicated + * syntax instead + * + * ```gleam + * let existing_list = [2, 3, 4] + * + * [1, ..existing_list] + * // -> [1, 2, 3, 4] + * + * prepend(to: existing_list, this: 1) + * // -> [1, 2, 3, 4] + * ``` + */ +export function prepend(list, item) { + return listPrepend(item, list); +} + +function flatten_loop(loop$lists, loop$acc) { + while (true) { + let lists = loop$lists; + let acc = loop$acc; + if (lists instanceof $Empty) { + return reverse(acc); + } else { + let list = lists.head; + let further_lists = lists.tail; + loop$lists = further_lists; + loop$acc = reverse_and_prepend(list, acc); + } + } +} + +/** + * Joins a list of lists into a single list. + * + * This function traverses all elements twice on the JavaScript target. + * This function traverses all elements once on the Erlang target. + * + * ## Examples + * + * ```gleam + * flatten([[1], [2, 3], []]) + * // -> [1, 2, 3] + * ``` + */ +export function flatten(lists) { + return flatten_loop(lists, toList([])); +} + +/** + * Maps the list with the given function into a list of lists, and then flattens it. + * + * ## Examples + * + * ```gleam + * flat_map([2, 4, 6], fn(x) { [x, x + 1] }) + * // -> [2, 3, 4, 5, 6, 7] + * ``` + */ +export function flat_map(list, fun) { + return flatten(map(list, fun)); +} + +/** + * Reduces a list of elements into a single value by calling a given function + * on each element, going from left to right. + * + * `fold([1, 2, 3], 0, add)` is the equivalent of + * `add(add(add(0, 1), 2), 3)`. + * + * This function runs in linear time. + */ +export function fold(loop$list, loop$initial, loop$fun) { + while (true) { + let list = loop$list; + let initial = loop$initial; + let fun = loop$fun; + if (list instanceof $Empty) { + return initial; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + loop$list = rest$1; + loop$initial = fun(initial, first$1); + loop$fun = fun; + } + } +} + +/** + * Reduces a list of elements into a single value by calling a given function + * on each element, going from right to left. + * + * `fold_right([1, 2, 3], 0, add)` is the equivalent of + * `add(add(add(0, 3), 2), 1)`. + * + * This function runs in linear time. + * + * Unlike `fold` this function is not tail recursive. Where possible use + * `fold` instead as it will use less memory. + */ +export function fold_right(list, initial, fun) { + if (list instanceof $Empty) { + return initial; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + return fun(fold_right(rest$1, initial, fun), first$1); + } +} + +function index_fold_loop(loop$over, loop$acc, loop$with, loop$index) { + while (true) { + let over = loop$over; + let acc = loop$acc; + let with$ = loop$with; + let index = loop$index; + if (over instanceof $Empty) { + return acc; + } else { + let first$1 = over.head; + let rest$1 = over.tail; + loop$over = rest$1; + loop$acc = with$(acc, first$1, index); + loop$with = with$; + loop$index = index + 1; + } + } +} + +/** + * Like fold but the folding function also receives the index of the current element. + * + * ## Examples + * + * ```gleam + * ["a", "b", "c"] + * |> index_fold("", fn(acc, item, index) { + * acc <> int.to_string(index) <> ":" <> item <> " " + * }) + * // -> "0:a 1:b 2:c" + * ``` + * + * ```gleam + * [10, 20, 30] + * |> index_fold(0, fn(acc, item, index) { acc + item * index }) + * // -> 80 + * ``` + */ +export function index_fold(list, initial, fun) { + return index_fold_loop(list, initial, fun, 0); +} + +/** + * A variant of fold that might fail. + * + * The folding function should return `Result(accumulator, error)`. + * If the returned value is `Ok(accumulator)` try_fold will try the next value in the list. + * If the returned value is `Error(error)` try_fold will stop and return that error. + * + * ## Examples + * + * ```gleam + * [1, 2, 3, 4] + * |> try_fold(0, fn(acc, i) { + * case i < 3 { + * True -> Ok(acc + i) + * False -> Error(Nil) + * } + * }) + * // -> Error(Nil) + * ``` + */ +export function try_fold(loop$list, loop$initial, loop$fun) { + while (true) { + let list = loop$list; + let initial = loop$initial; + let fun = loop$fun; + if (list instanceof $Empty) { + return new Ok(initial); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = fun(initial, first$1); + if ($ instanceof Ok) { + let result = $[0]; + loop$list = rest$1; + loop$initial = result; + loop$fun = fun; + } else { + return $; + } + } + } +} + +/** + * A variant of fold that allows to stop folding earlier. + * + * The folding function should return `ContinueOrStop(accumulator)`. + * If the returned value is `Continue(accumulator)` fold_until will try the next value in the list. + * If the returned value is `Stop(accumulator)` fold_until will stop and return that accumulator. + * + * ## Examples + * + * ```gleam + * [1, 2, 3, 4] + * |> fold_until(0, fn(acc, i) { + * case i < 3 { + * True -> Continue(acc + i) + * False -> Stop(acc) + * } + * }) + * // -> 3 + * ``` + */ +export function fold_until(loop$list, loop$initial, loop$fun) { + while (true) { + let list = loop$list; + let initial = loop$initial; + let fun = loop$fun; + if (list instanceof $Empty) { + return initial; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = fun(initial, first$1); + if ($ instanceof Continue) { + let next_accumulator = $[0]; + loop$list = rest$1; + loop$initial = next_accumulator; + loop$fun = fun; + } else { + let b = $[0]; + return b; + } + } + } +} + +/** + * Finds the first element in a given list for which the given function returns + * `True`. + * + * Returns `Error(Nil)` if no such element is found. + * + * ## Examples + * + * ```gleam + * find([1, 2, 3], fn(x) { x > 2 }) + * // -> Ok(3) + * ``` + * + * ```gleam + * find([1, 2, 3], fn(x) { x > 4 }) + * // -> Error(Nil) + * ``` + * + * ```gleam + * find([], fn(_) { True }) + * // -> Error(Nil) + * ``` + */ +export function find(loop$list, loop$is_desired) { + while (true) { + let list = loop$list; + let is_desired = loop$is_desired; + if (list instanceof $Empty) { + return new Error(undefined); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = is_desired(first$1); + if ($) { + return new Ok(first$1); + } else { + loop$list = rest$1; + loop$is_desired = is_desired; + } + } + } +} + +/** + * Finds the first element in a given list for which the given function returns + * `Ok(new_value)`, then returns the wrapped `new_value`. + * + * Returns `Error(Nil)` if no such element is found. + * + * ## Examples + * + * ```gleam + * find_map([[], [2], [3]], first) + * // -> Ok(2) + * ``` + * + * ```gleam + * find_map([[], []], first) + * // -> Error(Nil) + * ``` + * + * ```gleam + * find_map([], first) + * // -> Error(Nil) + * ``` + */ +export function find_map(loop$list, loop$fun) { + while (true) { + let list = loop$list; + let fun = loop$fun; + if (list instanceof $Empty) { + return new Error(undefined); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = fun(first$1); + if ($ instanceof Ok) { + return $; + } else { + loop$list = rest$1; + loop$fun = fun; + } + } + } +} + +/** + * Returns `True` if the given function returns `True` for all the elements in + * the given list. If the function returns `False` for any of the elements it + * immediately returns `False` without checking the rest of the list. + * + * ## Examples + * + * ```gleam + * all([], fn(x) { x > 3 }) + * // -> True + * ``` + * + * ```gleam + * all([4, 5], fn(x) { x > 3 }) + * // -> True + * ``` + * + * ```gleam + * all([4, 3], fn(x) { x > 3 }) + * // -> False + * ``` + */ +export function all(loop$list, loop$predicate) { + while (true) { + let list = loop$list; + let predicate = loop$predicate; + if (list instanceof $Empty) { + return true; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = predicate(first$1); + if ($) { + loop$list = rest$1; + loop$predicate = predicate; + } else { + return $; + } + } + } +} + +/** + * Returns `True` if the given function returns `True` for any the elements in + * the given list. If the function returns `True` for any of the elements it + * immediately returns `True` without checking the rest of the list. + * + * ## Examples + * + * ```gleam + * any([], fn(x) { x > 3 }) + * // -> False + * ``` + * + * ```gleam + * any([4, 5], fn(x) { x > 3 }) + * // -> True + * ``` + * + * ```gleam + * any([4, 3], fn(x) { x > 4 }) + * // -> False + * ``` + * + * ```gleam + * any([3, 4], fn(x) { x > 3 }) + * // -> True + * ``` + */ +export function any(loop$list, loop$predicate) { + while (true) { + let list = loop$list; + let predicate = loop$predicate; + if (list instanceof $Empty) { + return false; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = predicate(first$1); + if ($) { + return $; + } else { + loop$list = rest$1; + loop$predicate = predicate; + } + } + } +} + +function zip_loop(loop$one, loop$other, loop$acc) { + while (true) { + let one = loop$one; + let other = loop$other; + let acc = loop$acc; + if (one instanceof $Empty) { + return reverse(acc); + } else if (other instanceof $Empty) { + return reverse(acc); + } else { + let first_one = one.head; + let rest_one = one.tail; + let first_other = other.head; + let rest_other = other.tail; + loop$one = rest_one; + loop$other = rest_other; + loop$acc = listPrepend([first_one, first_other], acc); + } + } +} + +/** + * Takes two lists and returns a single list of 2-element tuples. + * + * If one of the lists is longer than the other, the remaining elements from + * the longer list are not used. + * + * ## Examples + * + * ```gleam + * zip([], []) + * // -> [] + * ``` + * + * ```gleam + * zip([1, 2], [3]) + * // -> [#(1, 3)] + * ``` + * + * ```gleam + * zip([1], [3, 4]) + * // -> [#(1, 3)] + * ``` + * + * ```gleam + * zip([1, 2], [3, 4]) + * // -> [#(1, 3), #(2, 4)] + * ``` + */ +export function zip(list, other) { + return zip_loop(list, other, toList([])); +} + +function strict_zip_loop(loop$one, loop$other, loop$acc) { + while (true) { + let one = loop$one; + let other = loop$other; + let acc = loop$acc; + if (one instanceof $Empty) { + if (other instanceof $Empty) { + return new Ok(reverse(acc)); + } else { + return new Error(undefined); + } + } else if (other instanceof $Empty) { + return new Error(undefined); + } else { + let first_one = one.head; + let rest_one = one.tail; + let first_other = other.head; + let rest_other = other.tail; + loop$one = rest_one; + loop$other = rest_other; + loop$acc = listPrepend([first_one, first_other], acc); + } + } +} + +/** + * Takes two lists and returns a single list of 2-element tuples. + * + * If one of the lists is longer than the other, an `Error` is returned. + * + * ## Examples + * + * ```gleam + * strict_zip([], []) + * // -> Ok([]) + * ``` + * + * ```gleam + * strict_zip([1, 2], [3]) + * // -> Error(Nil) + * ``` + * + * ```gleam + * strict_zip([1], [3, 4]) + * // -> Error(Nil) + * ``` + * + * ```gleam + * strict_zip([1, 2], [3, 4]) + * // -> Ok([#(1, 3), #(2, 4)]) + * ``` + */ +export function strict_zip(list, other) { + return strict_zip_loop(list, other, toList([])); +} + +function unzip_loop(loop$input, loop$one, loop$other) { + while (true) { + let input = loop$input; + let one = loop$one; + let other = loop$other; + if (input instanceof $Empty) { + return [reverse(one), reverse(other)]; + } else { + let rest$1 = input.tail; + let first_one = input.head[0]; + let first_other = input.head[1]; + loop$input = rest$1; + loop$one = listPrepend(first_one, one); + loop$other = listPrepend(first_other, other); + } + } +} + +/** + * Takes a single list of 2-element tuples and returns two lists. + * + * ## Examples + * + * ```gleam + * unzip([#(1, 2), #(3, 4)]) + * // -> #([1, 3], [2, 4]) + * ``` + * + * ```gleam + * unzip([]) + * // -> #([], []) + * ``` + */ +export function unzip(input) { + return unzip_loop(input, toList([]), toList([])); +} + +function intersperse_loop(loop$list, loop$separator, loop$acc) { + while (true) { + let list = loop$list; + let separator = loop$separator; + let acc = loop$acc; + if (list instanceof $Empty) { + return reverse(acc); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + loop$list = rest$1; + loop$separator = separator; + loop$acc = listPrepend(first$1, listPrepend(separator, acc)); + } + } +} + +/** + * Inserts a given value between each existing element in a given list. + * + * This function runs in linear time and copies the list. + * + * ## Examples + * + * ```gleam + * intersperse([1, 1, 1], 2) + * // -> [1, 2, 1, 2, 1] + * ``` + * + * ```gleam + * intersperse([], 2) + * // -> [] + * ``` + */ +export function intersperse(list, elem) { + if (list instanceof $Empty) { + return list; + } else { + let $ = list.tail; + if ($ instanceof $Empty) { + return list; + } else { + let first$1 = list.head; + let rest$1 = $; + return intersperse_loop(rest$1, elem, toList([first$1])); + } + } +} + +function unique_loop(loop$list, loop$seen, loop$acc) { + while (true) { + let list = loop$list; + let seen = loop$seen; + let acc = loop$acc; + if (list instanceof $Empty) { + return reverse(acc); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = $dict.has_key(seen, first$1); + if ($) { + loop$list = rest$1; + loop$seen = seen; + loop$acc = acc; + } else { + loop$list = rest$1; + loop$seen = $dict.insert(seen, first$1, undefined); + loop$acc = listPrepend(first$1, acc); + } + } + } +} + +/** + * Removes any duplicate elements from a given list. + * + * This function returns in loglinear time. + * + * ## Examples + * + * ```gleam + * unique([1, 1, 1, 4, 7, 3, 3, 4]) + * // -> [1, 4, 7, 3] + * ``` + */ +export function unique(list) { + return unique_loop(list, $dict.new$(), toList([])); +} + +/** + * Given a list it returns slices of it that are locally sorted in ascending + * order. + * + * Imagine you have this list: + * + * ``` + * [1, 2, 3, 2, 1, 0] + * ^^^^^^^ ^^^^^^^ This is a slice in descending order + * | + * | This is a slice that is sorted in ascending order + * ``` + * + * So the produced result will contain these two slices, each one sorted in + * ascending order: `[[1, 2, 3], [0, 1, 2]]`. + * + * - `growing` is an accumulator with the current slice being grown + * - `direction` is the growing direction of the slice being grown, it could + * either be ascending or strictly descending + * - `prev` is the previous element that needs to be added to the growing slice + * it is carried around to check whether we have to keep growing the current + * slice or not + * - `acc` is the accumulator containing the slices sorted in ascending order + * + * @ignore + */ +function sequences( + loop$list, + loop$compare, + loop$growing, + loop$direction, + loop$prev, + loop$acc +) { + while (true) { + let list = loop$list; + let compare = loop$compare; + let growing = loop$growing; + let direction = loop$direction; + let prev = loop$prev; + let acc = loop$acc; + let growing$1 = listPrepend(prev, growing); + if (list instanceof $Empty) { + if (direction instanceof Ascending) { + return listPrepend(reverse(growing$1), acc); + } else { + return listPrepend(growing$1, acc); + } + } else { + let new$1 = list.head; + let rest$1 = list.tail; + let $ = compare(prev, new$1); + if (direction instanceof Ascending) { + if ($ instanceof $order.Lt) { + loop$list = rest$1; + loop$compare = compare; + loop$growing = growing$1; + loop$direction = direction; + loop$prev = new$1; + loop$acc = acc; + } else if ($ instanceof $order.Eq) { + loop$list = rest$1; + loop$compare = compare; + loop$growing = growing$1; + loop$direction = direction; + loop$prev = new$1; + loop$acc = acc; + } else { + let _block; + if (direction instanceof Ascending) { + _block = listPrepend(reverse(growing$1), acc); + } else { + _block = listPrepend(growing$1, acc); + } + let acc$1 = _block; + if (rest$1 instanceof $Empty) { + return listPrepend(toList([new$1]), acc$1); + } else { + let next = rest$1.head; + let rest$2 = rest$1.tail; + let _block$1; + let $1 = compare(new$1, next); + if ($1 instanceof $order.Lt) { + _block$1 = new Ascending(); + } else if ($1 instanceof $order.Eq) { + _block$1 = new Ascending(); + } else { + _block$1 = new Descending(); + } + let direction$1 = _block$1; + loop$list = rest$2; + loop$compare = compare; + loop$growing = toList([new$1]); + loop$direction = direction$1; + loop$prev = next; + loop$acc = acc$1; + } + } + } else if ($ instanceof $order.Lt) { + let _block; + if (direction instanceof Ascending) { + _block = listPrepend(reverse(growing$1), acc); + } else { + _block = listPrepend(growing$1, acc); + } + let acc$1 = _block; + if (rest$1 instanceof $Empty) { + return listPrepend(toList([new$1]), acc$1); + } else { + let next = rest$1.head; + let rest$2 = rest$1.tail; + let _block$1; + let $1 = compare(new$1, next); + if ($1 instanceof $order.Lt) { + _block$1 = new Ascending(); + } else if ($1 instanceof $order.Eq) { + _block$1 = new Ascending(); + } else { + _block$1 = new Descending(); + } + let direction$1 = _block$1; + loop$list = rest$2; + loop$compare = compare; + loop$growing = toList([new$1]); + loop$direction = direction$1; + loop$prev = next; + loop$acc = acc$1; + } + } else if ($ instanceof $order.Eq) { + let _block; + if (direction instanceof Ascending) { + _block = listPrepend(reverse(growing$1), acc); + } else { + _block = listPrepend(growing$1, acc); + } + let acc$1 = _block; + if (rest$1 instanceof $Empty) { + return listPrepend(toList([new$1]), acc$1); + } else { + let next = rest$1.head; + let rest$2 = rest$1.tail; + let _block$1; + let $1 = compare(new$1, next); + if ($1 instanceof $order.Lt) { + _block$1 = new Ascending(); + } else if ($1 instanceof $order.Eq) { + _block$1 = new Ascending(); + } else { + _block$1 = new Descending(); + } + let direction$1 = _block$1; + loop$list = rest$2; + loop$compare = compare; + loop$growing = toList([new$1]); + loop$direction = direction$1; + loop$prev = next; + loop$acc = acc$1; + } + } else { + loop$list = rest$1; + loop$compare = compare; + loop$growing = growing$1; + loop$direction = direction; + loop$prev = new$1; + loop$acc = acc; + } + } + } +} + +/** + * Merges two lists sorted in ascending order into a single list sorted in + * descending order according to the given comparator function. + * + * This reversing of the sort order is not avoidable if we want to implement + * merge as a tail recursive function. We could reverse the accumulator before + * returning it but that would end up being less efficient; so the merging + * algorithm has to play around this. + * + * @ignore + */ +function merge_ascendings(loop$list1, loop$list2, loop$compare, loop$acc) { + while (true) { + let list1 = loop$list1; + let list2 = loop$list2; + let compare = loop$compare; + let acc = loop$acc; + if (list1 instanceof $Empty) { + let list = list2; + return reverse_and_prepend(list, acc); + } else if (list2 instanceof $Empty) { + let list = list1; + return reverse_and_prepend(list, acc); + } else { + let first1 = list1.head; + let rest1 = list1.tail; + let first2 = list2.head; + let rest2 = list2.tail; + let $ = compare(first1, first2); + if ($ instanceof $order.Lt) { + loop$list1 = rest1; + loop$list2 = list2; + loop$compare = compare; + loop$acc = listPrepend(first1, acc); + } else if ($ instanceof $order.Eq) { + loop$list1 = list1; + loop$list2 = rest2; + loop$compare = compare; + loop$acc = listPrepend(first2, acc); + } else { + loop$list1 = list1; + loop$list2 = rest2; + loop$compare = compare; + loop$acc = listPrepend(first2, acc); + } + } + } +} + +/** + * Given a list of ascending lists, it merges adjacent pairs into a single + * descending list, halving their number. + * It returns a list of the remaining descending lists. + * + * @ignore + */ +function merge_ascending_pairs(loop$sequences, loop$compare, loop$acc) { + while (true) { + let sequences = loop$sequences; + let compare = loop$compare; + let acc = loop$acc; + if (sequences instanceof $Empty) { + return reverse(acc); + } else { + let $ = sequences.tail; + if ($ instanceof $Empty) { + let sequence = sequences.head; + return reverse(listPrepend(reverse(sequence), acc)); + } else { + let ascending1 = sequences.head; + let ascending2 = $.head; + let rest$1 = $.tail; + let descending = merge_ascendings( + ascending1, + ascending2, + compare, + toList([]), + ); + loop$sequences = rest$1; + loop$compare = compare; + loop$acc = listPrepend(descending, acc); + } + } + } +} + +/** + * This is exactly the same as merge_ascendings but mirrored: it merges two + * lists sorted in descending order into a single list sorted in ascending + * order according to the given comparator function. + * + * This reversing of the sort order is not avoidable if we want to implement + * merge as a tail recursive function. We could reverse the accumulator before + * returning it but that would end up being less efficient; so the merging + * algorithm has to play around this. + * + * @ignore + */ +function merge_descendings(loop$list1, loop$list2, loop$compare, loop$acc) { + while (true) { + let list1 = loop$list1; + let list2 = loop$list2; + let compare = loop$compare; + let acc = loop$acc; + if (list1 instanceof $Empty) { + let list = list2; + return reverse_and_prepend(list, acc); + } else if (list2 instanceof $Empty) { + let list = list1; + return reverse_and_prepend(list, acc); + } else { + let first1 = list1.head; + let rest1 = list1.tail; + let first2 = list2.head; + let rest2 = list2.tail; + let $ = compare(first1, first2); + if ($ instanceof $order.Lt) { + loop$list1 = list1; + loop$list2 = rest2; + loop$compare = compare; + loop$acc = listPrepend(first2, acc); + } else if ($ instanceof $order.Eq) { + loop$list1 = rest1; + loop$list2 = list2; + loop$compare = compare; + loop$acc = listPrepend(first1, acc); + } else { + loop$list1 = rest1; + loop$list2 = list2; + loop$compare = compare; + loop$acc = listPrepend(first1, acc); + } + } + } +} + +/** + * This is the same as merge_ascending_pairs but flipped for descending lists. + * + * @ignore + */ +function merge_descending_pairs(loop$sequences, loop$compare, loop$acc) { + while (true) { + let sequences = loop$sequences; + let compare = loop$compare; + let acc = loop$acc; + if (sequences instanceof $Empty) { + return reverse(acc); + } else { + let $ = sequences.tail; + if ($ instanceof $Empty) { + let sequence = sequences.head; + return reverse(listPrepend(reverse(sequence), acc)); + } else { + let descending1 = sequences.head; + let descending2 = $.head; + let rest$1 = $.tail; + let ascending = merge_descendings( + descending1, + descending2, + compare, + toList([]), + ); + loop$sequences = rest$1; + loop$compare = compare; + loop$acc = listPrepend(ascending, acc); + } + } + } +} + +/** + * Given some some sorted sequences (assumed to be sorted in `direction`) it + * merges them all together until we're left with just a list sorted in + * ascending order. + * + * @ignore + */ +function merge_all(loop$sequences, loop$direction, loop$compare) { + while (true) { + let sequences = loop$sequences; + let direction = loop$direction; + let compare = loop$compare; + if (sequences instanceof $Empty) { + return sequences; + } else if (direction instanceof Ascending) { + let $ = sequences.tail; + if ($ instanceof $Empty) { + let sequence = sequences.head; + return sequence; + } else { + let sequences$1 = merge_ascending_pairs(sequences, compare, toList([])); + loop$sequences = sequences$1; + loop$direction = new Descending(); + loop$compare = compare; + } + } else { + let $ = sequences.tail; + if ($ instanceof $Empty) { + let sequence = sequences.head; + return reverse(sequence); + } else { + let sequences$1 = merge_descending_pairs(sequences, compare, toList([])); + loop$sequences = sequences$1; + loop$direction = new Ascending(); + loop$compare = compare; + } + } + } +} + +/** + * Sorts from smallest to largest based upon the ordering specified by a given + * function. + * + * ## Examples + * + * ```gleam + * import gleam/int + * + * sort([4, 3, 6, 5, 4, 1, 2], by: int.compare) + * // -> [1, 2, 3, 4, 4, 5, 6] + * ``` + */ +export function sort(list, compare) { + if (list instanceof $Empty) { + return list; + } else { + let $ = list.tail; + if ($ instanceof $Empty) { + return list; + } else { + let x = list.head; + let y = $.head; + let rest$1 = $.tail; + let _block; + let $1 = compare(x, y); + if ($1 instanceof $order.Lt) { + _block = new Ascending(); + } else if ($1 instanceof $order.Eq) { + _block = new Ascending(); + } else { + _block = new Descending(); + } + let direction = _block; + let sequences$1 = sequences( + rest$1, + compare, + toList([x]), + direction, + y, + toList([]), + ); + return merge_all(sequences$1, new Ascending(), compare); + } + } +} + +function range_loop(loop$start, loop$stop, loop$acc) { + while (true) { + let start = loop$start; + let stop = loop$stop; + let acc = loop$acc; + let $ = $int.compare(start, stop); + if ($ instanceof $order.Lt) { + loop$start = start; + loop$stop = stop - 1; + loop$acc = listPrepend(stop, acc); + } else if ($ instanceof $order.Eq) { + return listPrepend(stop, acc); + } else { + loop$start = start; + loop$stop = stop + 1; + loop$acc = listPrepend(stop, acc); + } + } +} + +/** + * Creates a list of ints ranging from a given start and finish. + * + * ## Examples + * + * ```gleam + * range(0, 0) + * // -> [0] + * ``` + * + * ```gleam + * range(0, 5) + * // -> [0, 1, 2, 3, 4, 5] + * ``` + * + * ```gleam + * range(1, -5) + * // -> [1, 0, -1, -2, -3, -4, -5] + * ``` + */ +export function range(start, stop) { + return range_loop(start, stop, toList([])); +} + +function repeat_loop(loop$item, loop$times, loop$acc) { + while (true) { + let item = loop$item; + let times = loop$times; + let acc = loop$acc; + let $ = times <= 0; + if ($) { + return acc; + } else { + loop$item = item; + loop$times = times - 1; + loop$acc = listPrepend(item, acc); + } + } +} + +/** + * Builds a list of a given value a given number of times. + * + * ## Examples + * + * ```gleam + * repeat("a", times: 0) + * // -> [] + * ``` + * + * ```gleam + * repeat("a", times: 5) + * // -> ["a", "a", "a", "a", "a"] + * ``` + */ +export function repeat(a, times) { + return repeat_loop(a, times, toList([])); +} + +function split_loop(loop$list, loop$n, loop$taken) { + while (true) { + let list = loop$list; + let n = loop$n; + let taken = loop$taken; + let $ = n <= 0; + if ($) { + return [reverse(taken), list]; + } else { + if (list instanceof $Empty) { + return [reverse(taken), toList([])]; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + loop$list = rest$1; + loop$n = n - 1; + loop$taken = listPrepend(first$1, taken); + } + } + } +} + +/** + * Splits a list in two before the given index. + * + * If the list is not long enough to have the given index the before list will + * be the input list, and the after list will be empty. + * + * ## Examples + * + * ```gleam + * split([6, 7, 8, 9], 0) + * // -> #([], [6, 7, 8, 9]) + * ``` + * + * ```gleam + * split([6, 7, 8, 9], 2) + * // -> #([6, 7], [8, 9]) + * ``` + * + * ```gleam + * split([6, 7, 8, 9], 4) + * // -> #([6, 7, 8, 9], []) + * ``` + */ +export function split(list, index) { + return split_loop(list, index, toList([])); +} + +function split_while_loop(loop$list, loop$f, loop$acc) { + while (true) { + let list = loop$list; + let f = loop$f; + let acc = loop$acc; + if (list instanceof $Empty) { + return [reverse(acc), toList([])]; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = f(first$1); + if ($) { + loop$list = rest$1; + loop$f = f; + loop$acc = listPrepend(first$1, acc); + } else { + return [reverse(acc), list]; + } + } + } +} + +/** + * Splits a list in two before the first element that a given function returns + * `False` for. + * + * If the function returns `True` for all elements the first list will be the + * input list, and the second list will be empty. + * + * ## Examples + * + * ```gleam + * split_while([1, 2, 3, 4, 5], fn(x) { x <= 3 }) + * // -> #([1, 2, 3], [4, 5]) + * ``` + * + * ```gleam + * split_while([1, 2, 3, 4, 5], fn(x) { x <= 5 }) + * // -> #([1, 2, 3, 4, 5], []) + * ``` + */ +export function split_while(list, predicate) { + return split_while_loop(list, predicate, toList([])); +} + +/** + * Given a list of 2-element tuples, finds the first tuple that has a given + * key as the first element and returns the second element. + * + * If no tuple is found with the given key then `Error(Nil)` is returned. + * + * This function may be useful for interacting with Erlang code where lists of + * tuples are common. + * + * ## Examples + * + * ```gleam + * key_find([#("a", 0), #("b", 1)], "a") + * // -> Ok(0) + * ``` + * + * ```gleam + * key_find([#("a", 0), #("b", 1)], "b") + * // -> Ok(1) + * ``` + * + * ```gleam + * key_find([#("a", 0), #("b", 1)], "c") + * // -> Error(Nil) + * ``` + */ +export function key_find(keyword_list, desired_key) { + return find_map( + keyword_list, + (keyword) => { + let key; + let value; + key = keyword[0]; + value = keyword[1]; + let $ = isEqual(key, desired_key); + if ($) { + return new Ok(value); + } else { + return new Error(undefined); + } + }, + ); +} + +/** + * Given a list of 2-element tuples, finds all tuples that have a given + * key as the first element and returns the second element. + * + * This function may be useful for interacting with Erlang code where lists of + * tuples are common. + * + * ## Examples + * + * ```gleam + * key_filter([#("a", 0), #("b", 1), #("a", 2)], "a") + * // -> [0, 2] + * ``` + * + * ```gleam + * key_filter([#("a", 0), #("b", 1)], "c") + * // -> [] + * ``` + */ +export function key_filter(keyword_list, desired_key) { + return filter_map( + keyword_list, + (keyword) => { + let key; + let value; + key = keyword[0]; + value = keyword[1]; + let $ = isEqual(key, desired_key); + if ($) { + return new Ok(value); + } else { + return new Error(undefined); + } + }, + ); +} + +function key_pop_loop(loop$list, loop$key, loop$checked) { + while (true) { + let list = loop$list; + let key = loop$key; + let checked = loop$checked; + if (list instanceof $Empty) { + return new Error(undefined); + } else { + let k = list.head[0]; + if (isEqual(k, key)) { + let rest$1 = list.tail; + let v = list.head[1]; + return new Ok([v, reverse_and_prepend(checked, rest$1)]); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + loop$list = rest$1; + loop$key = key; + loop$checked = listPrepend(first$1, checked); + } + } + } +} + +/** + * Given a list of 2-element tuples, finds the first tuple that has a given + * key as the first element. This function will return the second element + * of the found tuple and list with tuple removed. + * + * If no tuple is found with the given key then `Error(Nil)` is returned. + * + * ## Examples + * + * ```gleam + * key_pop([#("a", 0), #("b", 1)], "a") + * // -> Ok(#(0, [#("b", 1)])) + * ``` + * + * ```gleam + * key_pop([#("a", 0), #("b", 1)], "b") + * // -> Ok(#(1, [#("a", 0)])) + * ``` + * + * ```gleam + * key_pop([#("a", 0), #("b", 1)], "c") + * // -> Error(Nil) + * ``` + */ +export function key_pop(list, key) { + return key_pop_loop(list, key, toList([])); +} + +function key_set_loop(loop$list, loop$key, loop$value, loop$inspected) { + while (true) { + let list = loop$list; + let key = loop$key; + let value = loop$value; + let inspected = loop$inspected; + if (list instanceof $Empty) { + return reverse(listPrepend([key, value], inspected)); + } else { + let k = list.head[0]; + if (isEqual(k, key)) { + let rest$1 = list.tail; + return reverse_and_prepend(inspected, listPrepend([k, value], rest$1)); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + loop$list = rest$1; + loop$key = key; + loop$value = value; + loop$inspected = listPrepend(first$1, inspected); + } + } + } +} + +/** + * Given a list of 2-element tuples, inserts a key and value into the list. + * + * If there was already a tuple with the key then it is replaced, otherwise it + * is added to the end of the list. + * + * ## Examples + * + * ```gleam + * key_set([#(5, 0), #(4, 1)], 4, 100) + * // -> [#(5, 0), #(4, 100)] + * ``` + * + * ```gleam + * key_set([#(5, 0), #(4, 1)], 1, 100) + * // -> [#(5, 0), #(4, 1), #(1, 100)] + * ``` + */ +export function key_set(list, key, value) { + return key_set_loop(list, key, value, toList([])); +} + +/** + * Calls a function for each element in a list, discarding the return value. + * + * Useful for calling a side effect for every item of a list. + * + * ```gleam + * import gleam/io + * + * each(["1", "2", "3"], io.println) + * // -> Nil + * // 1 + * // 2 + * // 3 + * ``` + */ +export function each(loop$list, loop$f) { + while (true) { + let list = loop$list; + let f = loop$f; + if (list instanceof $Empty) { + return undefined; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + f(first$1); + loop$list = rest$1; + loop$f = f; + } + } +} + +/** + * Calls a `Result` returning function for each element in a list, discarding + * the return value. If the function returns `Error` then the iteration is + * stopped and the error is returned. + * + * Useful for calling a side effect for every item of a list. + * + * ## Examples + * + * ```gleam + * try_each( + * over: [1, 2, 3], + * with: function_that_might_fail, + * ) + * // -> Ok(Nil) + * ``` + */ +export function try_each(loop$list, loop$fun) { + while (true) { + let list = loop$list; + let fun = loop$fun; + if (list instanceof $Empty) { + return new Ok(undefined); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = fun(first$1); + if ($ instanceof Ok) { + loop$list = rest$1; + loop$fun = fun; + } else { + return $; + } + } + } +} + +function partition_loop(loop$list, loop$categorise, loop$trues, loop$falses) { + while (true) { + let list = loop$list; + let categorise = loop$categorise; + let trues = loop$trues; + let falses = loop$falses; + if (list instanceof $Empty) { + return [reverse(trues), reverse(falses)]; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = categorise(first$1); + if ($) { + loop$list = rest$1; + loop$categorise = categorise; + loop$trues = listPrepend(first$1, trues); + loop$falses = falses; + } else { + loop$list = rest$1; + loop$categorise = categorise; + loop$trues = trues; + loop$falses = listPrepend(first$1, falses); + } + } + } +} + +/** + * Partitions a list into a tuple/pair of lists + * by a given categorisation function. + * + * ## Examples + * + * ```gleam + * import gleam/int + * + * [1, 2, 3, 4, 5] |> partition(int.is_odd) + * // -> #([1, 3, 5], [2, 4]) + * ``` + */ +export function partition(list, categorise) { + return partition_loop(list, categorise, toList([]), toList([])); +} + +function window_loop(loop$acc, loop$list, loop$n) { + while (true) { + let acc = loop$acc; + let list = loop$list; + let n = loop$n; + let window$1 = take(list, n); + let $ = length(window$1) === n; + if ($) { + loop$acc = listPrepend(window$1, acc); + loop$list = drop(list, 1); + loop$n = n; + } else { + return reverse(acc); + } + } +} + +/** + * Returns a list of sliding windows. + * + * ## Examples + * + * ```gleam + * window([1,2,3,4,5], 3) + * // -> [[1, 2, 3], [2, 3, 4], [3, 4, 5]] + * ``` + * + * ```gleam + * window([1, 2], 4) + * // -> [] + * ``` + */ +export function window(list, n) { + let $ = n <= 0; + if ($) { + return toList([]); + } else { + return window_loop(toList([]), list, n); + } +} + +/** + * Returns a list of tuples containing two contiguous elements. + * + * ## Examples + * + * ```gleam + * window_by_2([1,2,3,4]) + * // -> [#(1, 2), #(2, 3), #(3, 4)] + * ``` + * + * ```gleam + * window_by_2([1]) + * // -> [] + * ``` + */ +export function window_by_2(list) { + return zip(list, drop(list, 1)); +} + +/** + * Drops the first elements in a given list for which the predicate function returns `True`. + * + * ## Examples + * + * ```gleam + * drop_while([1, 2, 3, 4], fn (x) { x < 3 }) + * // -> [3, 4] + * ``` + */ +export function drop_while(loop$list, loop$predicate) { + while (true) { + let list = loop$list; + let predicate = loop$predicate; + if (list instanceof $Empty) { + return list; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = predicate(first$1); + if ($) { + loop$list = rest$1; + loop$predicate = predicate; + } else { + return listPrepend(first$1, rest$1); + } + } + } +} + +function take_while_loop(loop$list, loop$predicate, loop$acc) { + while (true) { + let list = loop$list; + let predicate = loop$predicate; + let acc = loop$acc; + if (list instanceof $Empty) { + return reverse(acc); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = predicate(first$1); + if ($) { + loop$list = rest$1; + loop$predicate = predicate; + loop$acc = listPrepend(first$1, acc); + } else { + return reverse(acc); + } + } + } +} + +/** + * Takes the first elements in a given list for which the predicate function returns `True`. + * + * ## Examples + * + * ```gleam + * take_while([1, 2, 3, 2, 4], fn (x) { x < 3 }) + * // -> [1, 2] + * ``` + */ +export function take_while(list, predicate) { + return take_while_loop(list, predicate, toList([])); +} + +function chunk_loop( + loop$list, + loop$f, + loop$previous_key, + loop$current_chunk, + loop$acc +) { + while (true) { + let list = loop$list; + let f = loop$f; + let previous_key = loop$previous_key; + let current_chunk = loop$current_chunk; + let acc = loop$acc; + if (list instanceof $Empty) { + return reverse(listPrepend(reverse(current_chunk), acc)); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let key = f(first$1); + let $ = isEqual(key, previous_key); + if ($) { + loop$list = rest$1; + loop$f = f; + loop$previous_key = key; + loop$current_chunk = listPrepend(first$1, current_chunk); + loop$acc = acc; + } else { + let new_acc = listPrepend(reverse(current_chunk), acc); + loop$list = rest$1; + loop$f = f; + loop$previous_key = key; + loop$current_chunk = toList([first$1]); + loop$acc = new_acc; + } + } + } +} + +/** + * Returns a list of chunks in which + * the return value of calling `f` on each element is the same. + * + * ## Examples + * + * ```gleam + * [1, 2, 2, 3, 4, 4, 6, 7, 7] |> chunk(by: fn(n) { n % 2 }) + * // -> [[1], [2, 2], [3], [4, 4, 6], [7, 7]] + * ``` + */ +export function chunk(list, f) { + if (list instanceof $Empty) { + return list; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + return chunk_loop(rest$1, f, f(first$1), toList([first$1]), toList([])); + } +} + +function sized_chunk_loop( + loop$list, + loop$count, + loop$left, + loop$current_chunk, + loop$acc +) { + while (true) { + let list = loop$list; + let count = loop$count; + let left = loop$left; + let current_chunk = loop$current_chunk; + let acc = loop$acc; + if (list instanceof $Empty) { + if (current_chunk instanceof $Empty) { + return reverse(acc); + } else { + let remaining = current_chunk; + return reverse(listPrepend(reverse(remaining), acc)); + } + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let chunk$1 = listPrepend(first$1, current_chunk); + let $ = left > 1; + if ($) { + loop$list = rest$1; + loop$count = count; + loop$left = left - 1; + loop$current_chunk = chunk$1; + loop$acc = acc; + } else { + loop$list = rest$1; + loop$count = count; + loop$left = count; + loop$current_chunk = toList([]); + loop$acc = listPrepend(reverse(chunk$1), acc); + } + } + } +} + +/** + * Returns a list of chunks containing `count` elements each. + * + * If the last chunk does not have `count` elements, it is instead + * a partial chunk, with less than `count` elements. + * + * For any `count` less than 1 this function behaves as if it was set to 1. + * + * ## Examples + * + * ```gleam + * [1, 2, 3, 4, 5, 6] |> sized_chunk(into: 2) + * // -> [[1, 2], [3, 4], [5, 6]] + * ``` + * + * ```gleam + * [1, 2, 3, 4, 5, 6, 7, 8] |> sized_chunk(into: 3) + * // -> [[1, 2, 3], [4, 5, 6], [7, 8]] + * ``` + */ +export function sized_chunk(list, count) { + return sized_chunk_loop(list, count, count, toList([]), toList([])); +} + +/** + * This function acts similar to fold, but does not take an initial state. + * Instead, it starts from the first element in the list + * and combines it with each subsequent element in turn using the given + * function. The function is called as `fun(accumulator, current_element)`. + * + * Returns `Ok` to indicate a successful run, and `Error` if called on an + * empty list. + * + * ## Examples + * + * ```gleam + * [] |> reduce(fn(acc, x) { acc + x }) + * // -> Error(Nil) + * ``` + * + * ```gleam + * [1, 2, 3, 4, 5] |> reduce(fn(acc, x) { acc + x }) + * // -> Ok(15) + * ``` + */ +export function reduce(list, fun) { + if (list instanceof $Empty) { + return new Error(undefined); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + return new Ok(fold(rest$1, first$1, fun)); + } +} + +function scan_loop(loop$list, loop$accumulator, loop$accumulated, loop$fun) { + while (true) { + let list = loop$list; + let accumulator = loop$accumulator; + let accumulated = loop$accumulated; + let fun = loop$fun; + if (list instanceof $Empty) { + return reverse(accumulated); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let next = fun(accumulator, first$1); + loop$list = rest$1; + loop$accumulator = next; + loop$accumulated = listPrepend(next, accumulated); + loop$fun = fun; + } + } +} + +/** + * Similar to `fold`, but yields the state of the accumulator at each stage. + * + * ## Examples + * + * ```gleam + * scan(over: [1, 2, 3], from: 100, with: fn(acc, i) { acc + i }) + * // -> [101, 103, 106] + * ``` + */ +export function scan(list, initial, fun) { + return scan_loop(list, initial, toList([]), fun); +} + +/** + * Returns the last element in the given list. + * + * Returns `Error(Nil)` if the list is empty. + * + * This function runs in linear time. + * + * ## Examples + * + * ```gleam + * last([]) + * // -> Error(Nil) + * ``` + * + * ```gleam + * last([1, 2, 3, 4, 5]) + * // -> Ok(5) + * ``` + */ +export function last(loop$list) { + while (true) { + let list = loop$list; + if (list instanceof $Empty) { + return new Error(undefined); + } else { + let $ = list.tail; + if ($ instanceof $Empty) { + let last$1 = list.head; + return new Ok(last$1); + } else { + let rest$1 = $; + loop$list = rest$1; + } + } + } +} + +/** + * Return unique combinations of elements in the list. + * + * ## Examples + * + * ```gleam + * combinations([1, 2, 3], 2) + * // -> [[1, 2], [1, 3], [2, 3]] + * ``` + * + * ```gleam + * combinations([1, 2, 3, 4], 3) + * // -> [[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]] + * ``` + */ +export function combinations(items, n) { + if (n === 0) { + return toList([toList([])]); + } else if (items instanceof $Empty) { + return items; + } else { + let first$1 = items.head; + let rest$1 = items.tail; + let _pipe = rest$1; + let _pipe$1 = combinations(_pipe, n - 1); + let _pipe$2 = map( + _pipe$1, + (combination) => { return listPrepend(first$1, combination); }, + ); + let _pipe$3 = reverse(_pipe$2); + return fold( + _pipe$3, + combinations(rest$1, n), + (acc, c) => { return listPrepend(c, acc); }, + ); + } +} + +function combination_pairs_loop(loop$items, loop$acc) { + while (true) { + let items = loop$items; + let acc = loop$acc; + if (items instanceof $Empty) { + return reverse(acc); + } else { + let first$1 = items.head; + let rest$1 = items.tail; + let first_combinations = map( + rest$1, + (other) => { return [first$1, other]; }, + ); + let acc$1 = reverse_and_prepend(first_combinations, acc); + loop$items = rest$1; + loop$acc = acc$1; + } + } +} + +/** + * Return unique pair combinations of elements in the list. + * + * ## Examples + * + * ```gleam + * combination_pairs([1, 2, 3]) + * // -> [#(1, 2), #(1, 3), #(2, 3)] + * ``` + */ +export function combination_pairs(items) { + return combination_pairs_loop(items, toList([])); +} + +function take_firsts(loop$rows, loop$column, loop$remaining_rows) { + while (true) { + let rows = loop$rows; + let column = loop$column; + let remaining_rows = loop$remaining_rows; + if (rows instanceof $Empty) { + return [reverse(column), reverse(remaining_rows)]; + } else { + let $ = rows.head; + if ($ instanceof $Empty) { + let rest$1 = rows.tail; + loop$rows = rest$1; + loop$column = column; + loop$remaining_rows = remaining_rows; + } else { + let rest_rows = rows.tail; + let first$1 = $.head; + let remaining_row = $.tail; + let remaining_rows$1 = listPrepend(remaining_row, remaining_rows); + loop$rows = rest_rows; + loop$column = listPrepend(first$1, column); + loop$remaining_rows = remaining_rows$1; + } + } + } +} + +function transpose_loop(loop$rows, loop$columns) { + while (true) { + let rows = loop$rows; + let columns = loop$columns; + if (rows instanceof $Empty) { + return reverse(columns); + } else { + let $ = take_firsts(rows, toList([]), toList([])); + let column; + let rest$1; + column = $[0]; + rest$1 = $[1]; + if (column instanceof $Empty) { + loop$rows = rest$1; + loop$columns = columns; + } else { + loop$rows = rest$1; + loop$columns = listPrepend(column, columns); + } + } + } +} + +/** + * Transpose rows and columns of the list of lists. + * + * Notice: This function is not tail recursive, + * and thus may exceed stack size if called, + * with large lists (on the JavaScript target). + * + * ## Examples + * + * ```gleam + * transpose([[1, 2, 3], [101, 102, 103]]) + * // -> [[1, 101], [2, 102], [3, 103]] + * ``` + */ +export function transpose(list_of_lists) { + return transpose_loop(list_of_lists, toList([])); +} + +/** + * Make a list alternating the elements from the given lists + * + * ## Examples + * + * ```gleam + * interleave([[1, 2], [101, 102], [201, 202]]) + * // -> [1, 101, 201, 2, 102, 202] + * ``` + */ +export function interleave(list) { + let _pipe = list; + let _pipe$1 = transpose(_pipe); + return flatten(_pipe$1); +} + +function shuffle_pair_unwrap_loop(loop$list, loop$acc) { + while (true) { + let list = loop$list; + let acc = loop$acc; + if (list instanceof $Empty) { + return acc; + } else { + let elem_pair = list.head; + let enumerable = list.tail; + loop$list = enumerable; + loop$acc = listPrepend(elem_pair[1], acc); + } + } +} + +function do_shuffle_by_pair_indexes(list_of_pairs) { + return sort( + list_of_pairs, + (a_pair, b_pair) => { return $float.compare(a_pair[0], b_pair[0]); }, + ); +} + +/** + * Takes a list, randomly sorts all items and returns the shuffled list. + * + * This function uses `float.random` to decide the order of the elements. + * + * ## Example + * + * ```gleam + * range(1, 10) |> shuffle() + * // -> [1, 6, 9, 10, 3, 8, 4, 2, 7, 5] + * ``` + */ +export function shuffle(list) { + let _pipe = list; + let _pipe$1 = fold( + _pipe, + toList([]), + (acc, a) => { return listPrepend([$float.random(), a], acc); }, + ); + let _pipe$2 = do_shuffle_by_pair_indexes(_pipe$1); + return shuffle_pair_unwrap_loop(_pipe$2, toList([])); +} + +function max_loop(loop$list, loop$compare, loop$max) { + while (true) { + let list = loop$list; + let compare = loop$compare; + let max = loop$max; + if (list instanceof $Empty) { + return max; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let $ = compare(first$1, max); + if ($ instanceof $order.Lt) { + loop$list = rest$1; + loop$compare = compare; + loop$max = max; + } else if ($ instanceof $order.Eq) { + loop$list = rest$1; + loop$compare = compare; + loop$max = max; + } else { + loop$list = rest$1; + loop$compare = compare; + loop$max = first$1; + } + } + } +} + +/** + * Takes a list and a comparator, and returns the maximum element in the list + * + * + * ## Example + * + * ```gleam + * range(1, 10) |> list.max(int.compare) + * // -> Ok(10) + * ``` + * + * ```gleam + * ["a", "c", "b"] |> list.max(string.compare) + * // -> Ok("c") + * ``` + */ +export function max(list, compare) { + if (list instanceof $Empty) { + return new Error(undefined); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + return new Ok(max_loop(rest$1, compare, first$1)); + } +} + +function build_reservoir_loop(loop$list, loop$size, loop$reservoir) { + while (true) { + let list = loop$list; + let size = loop$size; + let reservoir = loop$reservoir; + let reservoir_size = $dict.size(reservoir); + let $ = reservoir_size >= size; + if ($) { + return [reservoir, list]; + } else { + if (list instanceof $Empty) { + return [reservoir, toList([])]; + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let reservoir$1 = $dict.insert(reservoir, reservoir_size, first$1); + loop$list = rest$1; + loop$size = size; + loop$reservoir = reservoir$1; + } + } + } +} + +/** + * Builds the initial reservoir used by Algorithm L. + * This is a dictionary with keys ranging from `0` up to `n - 1` where each + * value is the corresponding element at that position in `list`. + * + * This also returns the remaining elements of `list` that didn't end up in + * the reservoir. + * + * @ignore + */ +function build_reservoir(list, n) { + return build_reservoir_loop(list, n, $dict.new$()); +} + +const min_positive = 2.2250738585072014e-308; + +function log_random() { + let $ = $float.logarithm($float.random() + min_positive); + let random; + if ($ instanceof Ok) { + random = $[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "gleam/list", + 2391, + "log_random", + "Pattern match failed, no pattern matched the value.", + { + value: $, + start: 56078, + end: 56149, + pattern_start: 56089, + pattern_end: 56099 + } + ) + } + return random; +} + +function sample_loop(loop$list, loop$reservoir, loop$n, loop$w) { + while (true) { + let list = loop$list; + let reservoir = loop$reservoir; + let n = loop$n; + let w = loop$w; + let _block; + { + let $ = $float.logarithm(1.0 - w); + let log; + if ($ instanceof Ok) { + log = $[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "gleam/list", + 2374, + "sample_loop", + "Pattern match failed, no pattern matched the value.", + { + value: $, + start: 55639, + end: 55685, + pattern_start: 55650, + pattern_end: 55657 + } + ) + } + _block = $float.round($float.floor(divideFloat(log_random(), log))); + } + let skip = _block; + let $ = drop(list, skip); + if ($ instanceof $Empty) { + return reservoir; + } else { + let first$1 = $.head; + let rest$1 = $.tail; + let reservoir$1 = $dict.insert(reservoir, $int.random(n), first$1); + let w$1 = w * $float.exponential( + divideFloat(log_random(), $int.to_float(n)), + ); + loop$list = rest$1; + loop$reservoir = reservoir$1; + loop$n = n; + loop$w = w$1; + } + } +} + +/** + * Returns a random sample of up to n elements from a list using reservoir + * sampling via [Algorithm L](https://en.wikipedia.org/wiki/Reservoir_sampling#Optimal:_Algorithm_L). + * Returns an empty list if the sample size is less than or equal to 0. + * + * Order is not random, only selection is. + * + * ## Examples + * + * ```gleam + * reservoir_sample([1, 2, 3, 4, 5], 3) + * // -> [2, 4, 5] // A random sample of 3 items + * ``` + */ +export function sample(list, n) { + let $ = build_reservoir(list, n); + let reservoir; + let rest$1; + reservoir = $[0]; + rest$1 = $[1]; + let $1 = $dict.is_empty(reservoir); + if ($1) { + return toList([]); + } else { + let w = $float.exponential(divideFloat(log_random(), $int.to_float(n))); + return $dict.values(sample_loop(rest$1, reservoir, n, w)); + } +} + +function permutation_zip(list, rest, acc) { + if (list instanceof $Empty) { + return reverse(acc); + } else { + let head = list.head; + let tail = list.tail; + return permutation_prepend( + head, + permutations(reverse_and_prepend(rest, tail)), + tail, + listPrepend(head, rest), + acc, + ); + } +} + +function permutation_prepend( + loop$el, + loop$permutations, + loop$list_1, + loop$list_2, + loop$acc +) { + while (true) { + let el = loop$el; + let permutations = loop$permutations; + let list_1 = loop$list_1; + let list_2 = loop$list_2; + let acc = loop$acc; + if (permutations instanceof $Empty) { + return permutation_zip(list_1, list_2, acc); + } else { + let head = permutations.head; + let tail = permutations.tail; + loop$el = el; + loop$permutations = tail; + loop$list_1 = list_1; + loop$list_2 = list_2; + loop$acc = listPrepend(listPrepend(el, head), acc); + } + } +} + +/** + * Returns all the permutations of a list. + * + * ## Examples + * + * ```gleam + * permutations([1, 2]) + * // -> [[1, 2], [2, 1]] + * ``` + */ +export function permutations(list) { + if (list instanceof $Empty) { + return toList([toList([])]); + } else { + let l = list; + return permutation_zip(l, toList([]), toList([])); + } +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/option.mjs b/build/dev/javascript/gleam_stdlib/gleam/option.mjs new file mode 100644 index 0000000..5e820a3 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/option.mjs @@ -0,0 +1,419 @@ +import { + Ok, + Error, + toList, + Empty as $Empty, + prepend as listPrepend, + CustomType as $CustomType, + isEqual, +} from "../gleam.mjs"; + +export class Some extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const Option$Some = ($0) => new Some($0); +export const Option$isSome = (value) => value instanceof Some; +export const Option$Some$0 = (value) => value[0]; + +export class None extends $CustomType {} +export const Option$None = () => new None(); +export const Option$isNone = (value) => value instanceof None; + +function reverse_and_prepend(loop$prefix, loop$suffix) { + while (true) { + let prefix = loop$prefix; + let suffix = loop$suffix; + if (prefix instanceof $Empty) { + return suffix; + } else { + let first = prefix.head; + let rest = prefix.tail; + loop$prefix = rest; + loop$suffix = listPrepend(first, suffix); + } + } +} + +function reverse(list) { + return reverse_and_prepend(list, toList([])); +} + +function all_loop(loop$list, loop$acc) { + while (true) { + let list = loop$list; + let acc = loop$acc; + if (list instanceof $Empty) { + return new Some(reverse(acc)); + } else { + let $ = list.head; + if ($ instanceof Some) { + let rest = list.tail; + let first = $[0]; + loop$list = rest; + loop$acc = listPrepend(first, acc); + } else { + return new None(); + } + } + } +} + +/** + * Combines a list of `Option`s into a single `Option`. + * If all elements in the list are `Some` then returns a `Some` holding the list of values. + * If any element is `None` then returns`None`. + * + * ## Examples + * + * ```gleam + * all([Some(1), Some(2)]) + * // -> Some([1, 2]) + * ``` + * + * ```gleam + * all([Some(1), None]) + * // -> None + * ``` + */ +export function all(list) { + return all_loop(list, toList([])); +} + +/** + * Checks whether the `Option` is a `Some` value. + * + * ## Examples + * + * ```gleam + * is_some(Some(1)) + * // -> True + * ``` + * + * ```gleam + * is_some(None) + * // -> False + * ``` + */ +export function is_some(option) { + return !isEqual(option, new None()); +} + +/** + * Checks whether the `Option` is a `None` value. + * + * ## Examples + * + * ```gleam + * is_none(Some(1)) + * // -> False + * ``` + * + * ```gleam + * is_none(None) + * // -> True + * ``` + */ +export function is_none(option) { + return isEqual(option, new None()); +} + +/** + * Converts an `Option` type to a `Result` type. + * + * ## Examples + * + * ```gleam + * to_result(Some(1), "some_error") + * // -> Ok(1) + * ``` + * + * ```gleam + * to_result(None, "some_error") + * // -> Error("some_error") + * ``` + */ +export function to_result(option, e) { + if (option instanceof Some) { + let a = option[0]; + return new Ok(a); + } else { + return new Error(e); + } +} + +/** + * Converts a `Result` type to an `Option` type. + * + * ## Examples + * + * ```gleam + * from_result(Ok(1)) + * // -> Some(1) + * ``` + * + * ```gleam + * from_result(Error("some_error")) + * // -> None + * ``` + */ +export function from_result(result) { + if (result instanceof Ok) { + let a = result[0]; + return new Some(a); + } else { + return new None(); + } +} + +/** + * Extracts the value from an `Option`, returning a default value if there is none. + * + * ## Examples + * + * ```gleam + * unwrap(Some(1), 0) + * // -> 1 + * ``` + * + * ```gleam + * unwrap(None, 0) + * // -> 0 + * ``` + */ +export function unwrap(option, default$) { + if (option instanceof Some) { + let x = option[0]; + return x; + } else { + return default$; + } +} + +/** + * Extracts the value from an `Option`, evaluating the default function if the option is `None`. + * + * ## Examples + * + * ```gleam + * lazy_unwrap(Some(1), fn() { 0 }) + * // -> 1 + * ``` + * + * ```gleam + * lazy_unwrap(None, fn() { 0 }) + * // -> 0 + * ``` + */ +export function lazy_unwrap(option, default$) { + if (option instanceof Some) { + let x = option[0]; + return x; + } else { + return default$(); + } +} + +/** + * Updates a value held within the `Some` of an `Option` by calling a given function + * on it. + * + * If the `Option` is a `None` rather than `Some`, the function is not called and the + * `Option` stays the same. + * + * ## Examples + * + * ```gleam + * map(over: Some(1), with: fn(x) { x + 1 }) + * // -> Some(2) + * ``` + * + * ```gleam + * map(over: None, with: fn(x) { x + 1 }) + * // -> None + * ``` + */ +export function map(option, fun) { + if (option instanceof Some) { + let x = option[0]; + return new Some(fun(x)); + } else { + return option; + } +} + +/** + * Merges a nested `Option` into a single layer. + * + * ## Examples + * + * ```gleam + * flatten(Some(Some(1))) + * // -> Some(1) + * ``` + * + * ```gleam + * flatten(Some(None)) + * // -> None + * ``` + * + * ```gleam + * flatten(None) + * // -> None + * ``` + */ +export function flatten(option) { + if (option instanceof Some) { + let x = option[0]; + return x; + } else { + return option; + } +} + +/** + * Updates a value held within the `Some` of an `Option` by calling a given function + * on it, where the given function also returns an `Option`. The two options are + * then merged together into one `Option`. + * + * If the `Option` is a `None` rather than `Some` the function is not called and the + * option stays the same. + * + * This function is the equivalent of calling `map` followed by `flatten`, and + * it is useful for chaining together multiple functions that return `Option`. + * + * ## Examples + * + * ```gleam + * then(Some(1), fn(x) { Some(x + 1) }) + * // -> Some(2) + * ``` + * + * ```gleam + * then(Some(1), fn(x) { Some(#("a", x)) }) + * // -> Some(#("a", 1)) + * ``` + * + * ```gleam + * then(Some(1), fn(_) { None }) + * // -> None + * ``` + * + * ```gleam + * then(None, fn(x) { Some(x + 1) }) + * // -> None + * ``` + */ +export function then$(option, fun) { + if (option instanceof Some) { + let x = option[0]; + return fun(x); + } else { + return option; + } +} + +/** + * Returns the first value if it is `Some`, otherwise returns the second value. + * + * ## Examples + * + * ```gleam + * or(Some(1), Some(2)) + * // -> Some(1) + * ``` + * + * ```gleam + * or(Some(1), None) + * // -> Some(1) + * ``` + * + * ```gleam + * or(None, Some(2)) + * // -> Some(2) + * ``` + * + * ```gleam + * or(None, None) + * // -> None + * ``` + */ +export function or(first, second) { + if (first instanceof Some) { + return first; + } else { + return second; + } +} + +/** + * Returns the first value if it is `Some`, otherwise evaluates the given function for a fallback value. + * + * ## Examples + * + * ```gleam + * lazy_or(Some(1), fn() { Some(2) }) + * // -> Some(1) + * ``` + * + * ```gleam + * lazy_or(Some(1), fn() { None }) + * // -> Some(1) + * ``` + * + * ```gleam + * lazy_or(None, fn() { Some(2) }) + * // -> Some(2) + * ``` + * + * ```gleam + * lazy_or(None, fn() { None }) + * // -> None + * ``` + */ +export function lazy_or(first, second) { + if (first instanceof Some) { + return first; + } else { + return second(); + } +} + +function values_loop(loop$list, loop$acc) { + while (true) { + let list = loop$list; + let acc = loop$acc; + if (list instanceof $Empty) { + return reverse(acc); + } else { + let $ = list.head; + if ($ instanceof Some) { + let rest = list.tail; + let first = $[0]; + loop$list = rest; + loop$acc = listPrepend(first, acc); + } else { + let rest = list.tail; + loop$list = rest; + loop$acc = acc; + } + } + } +} + +/** + * Given a list of `Option`s, + * returns only the values inside `Some`. + * + * ## Examples + * + * ```gleam + * values([Some(1), None, Some(3)]) + * // -> [1, 3] + * ``` + */ +export function values(options) { + return values_loop(options, toList([])); +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/order.mjs b/build/dev/javascript/gleam_stdlib/gleam/order.mjs new file mode 100644 index 0000000..617938c --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/order.mjs @@ -0,0 +1,178 @@ +import { CustomType as $CustomType, isEqual } from "../gleam.mjs"; + +export class Lt extends $CustomType {} +export const Order$Lt = () => new Lt(); +export const Order$isLt = (value) => value instanceof Lt; + +export class Eq extends $CustomType {} +export const Order$Eq = () => new Eq(); +export const Order$isEq = (value) => value instanceof Eq; + +export class Gt extends $CustomType {} +export const Order$Gt = () => new Gt(); +export const Order$isGt = (value) => value instanceof Gt; + +/** + * Inverts an order, so less-than becomes greater-than and greater-than + * becomes less-than. + * + * ## Examples + * + * ```gleam + * negate(Lt) + * // -> Gt + * ``` + * + * ```gleam + * negate(Eq) + * // -> Eq + * ``` + * + * ```gleam + * negate(Gt) + * // -> Lt + * ``` + */ +export function negate(order) { + if (order instanceof Lt) { + return new Gt(); + } else if (order instanceof Eq) { + return order; + } else { + return new Lt(); + } +} + +/** + * Produces a numeric representation of the order. + * + * ## Examples + * + * ```gleam + * to_int(Lt) + * // -> -1 + * ``` + * + * ```gleam + * to_int(Eq) + * // -> 0 + * ``` + * + * ```gleam + * to_int(Gt) + * // -> 1 + * ``` + */ +export function to_int(order) { + if (order instanceof Lt) { + return -1; + } else if (order instanceof Eq) { + return 0; + } else { + return 1; + } +} + +/** + * Compares two `Order` values to one another, producing a new `Order`. + * + * ## Examples + * + * ```gleam + * compare(Eq, with: Lt) + * // -> Gt + * ``` + */ +export function compare(a, b) { + let x = a; + let y = b; + if (isEqual(x, y)) { + return new Eq(); + } else if (a instanceof Lt) { + return new Lt(); + } else if (a instanceof Eq && b instanceof Gt) { + return new Lt(); + } else { + return new Gt(); + } +} + +/** + * Inverts an ordering function, so less-than becomes greater-than and greater-than + * becomes less-than. + * + * ## Examples + * + * ```gleam + * import gleam/int + * import gleam/list + * + * list.sort([1, 5, 4], by: reverse(int.compare)) + * // -> [5, 4, 1] + * ``` + */ +export function reverse(orderer) { + return (a, b) => { return orderer(b, a); }; +} + +/** + * Return a fallback `Order` in case the first argument is `Eq`. + * + * ## Examples + * + * ```gleam + * import gleam/int + * + * break_tie(in: int.compare(1, 1), with: Lt) + * // -> Lt + * ``` + * + * ```gleam + * import gleam/int + * + * break_tie(in: int.compare(1, 0), with: Eq) + * // -> Gt + * ``` + */ +export function break_tie(order, other) { + if (order instanceof Lt) { + return order; + } else if (order instanceof Eq) { + return other; + } else { + return order; + } +} + +/** + * Invokes a fallback function returning an `Order` in case the first argument + * is `Eq`. + * + * This can be useful when the fallback comparison might be expensive and it + * needs to be delayed until strictly necessary. + * + * ## Examples + * + * ```gleam + * import gleam/int + * + * lazy_break_tie(in: int.compare(1, 1), with: fn() { Lt }) + * // -> Lt + * ``` + * + * ```gleam + * import gleam/int + * + * lazy_break_tie(in: int.compare(1, 0), with: fn() { Eq }) + * // -> Gt + * ``` + */ +export function lazy_break_tie(order, comparison) { + if (order instanceof Lt) { + return order; + } else if (order instanceof Eq) { + return comparison(); + } else { + return order; + } +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/pair.mjs b/build/dev/javascript/gleam_stdlib/gleam/pair.mjs new file mode 100644 index 0000000..13f0cd5 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/pair.mjs @@ -0,0 +1,102 @@ +/** + * Returns the first element in a pair. + * + * ## Examples + * + * ```gleam + * first(#(1, 2)) + * // -> 1 + * ``` + */ +export function first(pair) { + let a; + a = pair[0]; + return a; +} + +/** + * Returns the second element in a pair. + * + * ## Examples + * + * ```gleam + * second(#(1, 2)) + * // -> 2 + * ``` + */ +export function second(pair) { + let a; + a = pair[1]; + return a; +} + +/** + * Returns a new pair with the elements swapped. + * + * ## Examples + * + * ```gleam + * swap(#(1, 2)) + * // -> #(2, 1) + * ``` + */ +export function swap(pair) { + let a; + let b; + a = pair[0]; + b = pair[1]; + return [b, a]; +} + +/** + * Returns a new pair with the first element having had `with` applied to + * it. + * + * ## Examples + * + * ```gleam + * #(1, 2) |> map_first(fn(n) { n * 2 }) + * // -> #(2, 2) + * ``` + */ +export function map_first(pair, fun) { + let a; + let b; + a = pair[0]; + b = pair[1]; + return [fun(a), b]; +} + +/** + * Returns a new pair with the second element having had `with` applied to + * it. + * + * ## Examples + * + * ```gleam + * #(1, 2) |> map_second(fn(n) { n * 2 }) + * // -> #(1, 4) + * ``` + */ +export function map_second(pair, fun) { + let a; + let b; + a = pair[0]; + b = pair[1]; + return [a, fun(b)]; +} + +/** + * Returns a new pair with the given elements. This can also be done using the dedicated + * syntax instead: `new(1, 2) == #(1, 2)`. + * + * ## Examples + * + * ```gleam + * new(1, 2) + * // -> #(1, 2) + * ``` + */ +export function new$(first, second) { + return [first, second]; +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/result.mjs b/build/dev/javascript/gleam_stdlib/gleam/result.mjs new file mode 100644 index 0000000..527de4f --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/result.mjs @@ -0,0 +1,494 @@ +import { Ok, Error, toList, Empty as $Empty, prepend as listPrepend } from "../gleam.mjs"; +import * as $list from "../gleam/list.mjs"; + +/** + * Checks whether the result is an `Ok` value. + * + * ## Examples + * + * ```gleam + * is_ok(Ok(1)) + * // -> True + * ``` + * + * ```gleam + * is_ok(Error(Nil)) + * // -> False + * ``` + */ +export function is_ok(result) { + if (result instanceof Ok) { + return true; + } else { + return false; + } +} + +/** + * Checks whether the result is an `Error` value. + * + * ## Examples + * + * ```gleam + * is_error(Ok(1)) + * // -> False + * ``` + * + * ```gleam + * is_error(Error(Nil)) + * // -> True + * ``` + */ +export function is_error(result) { + if (result instanceof Ok) { + return false; + } else { + return true; + } +} + +/** + * Updates a value held within the `Ok` of a result by calling a given function + * on it. + * + * If the result is an `Error` rather than `Ok` the function is not called and the + * result stays the same. + * + * ## Examples + * + * ```gleam + * map(over: Ok(1), with: fn(x) { x + 1 }) + * // -> Ok(2) + * ``` + * + * ```gleam + * map(over: Error(1), with: fn(x) { x + 1 }) + * // -> Error(1) + * ``` + */ +export function map(result, fun) { + if (result instanceof Ok) { + let x = result[0]; + return new Ok(fun(x)); + } else { + return result; + } +} + +/** + * Updates a value held within the `Error` of a result by calling a given function + * on it. + * + * If the result is `Ok` rather than `Error` the function is not called and the + * result stays the same. + * + * ## Examples + * + * ```gleam + * map_error(over: Error(1), with: fn(x) { x + 1 }) + * // -> Error(2) + * ``` + * + * ```gleam + * map_error(over: Ok(1), with: fn(x) { x + 1 }) + * // -> Ok(1) + * ``` + */ +export function map_error(result, fun) { + if (result instanceof Ok) { + return result; + } else { + let error = result[0]; + return new Error(fun(error)); + } +} + +/** + * Merges a nested `Result` into a single layer. + * + * ## Examples + * + * ```gleam + * flatten(Ok(Ok(1))) + * // -> Ok(1) + * ``` + * + * ```gleam + * flatten(Ok(Error(""))) + * // -> Error("") + * ``` + * + * ```gleam + * flatten(Error(Nil)) + * // -> Error(Nil) + * ``` + */ +export function flatten(result) { + if (result instanceof Ok) { + let x = result[0]; + return x; + } else { + return result; + } +} + +/** + * "Updates" an `Ok` result by passing its value to a function that yields a result, + * and returning the yielded result. (This may "replace" the `Ok` with an `Error`.) + * + * If the input is an `Error` rather than an `Ok`, the function is not called and + * the original `Error` is returned. + * + * This function is the equivalent of calling `map` followed by `flatten`, and + * it is useful for chaining together multiple functions that may fail. + * + * ## Examples + * + * ```gleam + * try(Ok(1), fn(x) { Ok(x + 1) }) + * // -> Ok(2) + * ``` + * + * ```gleam + * try(Ok(1), fn(x) { Ok(#("a", x)) }) + * // -> Ok(#("a", 1)) + * ``` + * + * ```gleam + * try(Ok(1), fn(_) { Error("Oh no") }) + * // -> Error("Oh no") + * ``` + * + * ```gleam + * try(Error(Nil), fn(x) { Ok(x + 1) }) + * // -> Error(Nil) + * ``` + */ +export function try$(result, fun) { + if (result instanceof Ok) { + let x = result[0]; + return fun(x); + } else { + return result; + } +} + +export function then$(result, fun) { + return try$(result, fun); +} + +/** + * Extracts the `Ok` value from a result, returning a default value if the result + * is an `Error`. + * + * ## Examples + * + * ```gleam + * unwrap(Ok(1), 0) + * // -> 1 + * ``` + * + * ```gleam + * unwrap(Error(""), 0) + * // -> 0 + * ``` + */ +export function unwrap(result, default$) { + if (result instanceof Ok) { + let v = result[0]; + return v; + } else { + return default$; + } +} + +/** + * Extracts the `Ok` value from a result, evaluating the default function if the result + * is an `Error`. + * + * ## Examples + * + * ```gleam + * lazy_unwrap(Ok(1), fn() { 0 }) + * // -> 1 + * ``` + * + * ```gleam + * lazy_unwrap(Error(""), fn() { 0 }) + * // -> 0 + * ``` + */ +export function lazy_unwrap(result, default$) { + if (result instanceof Ok) { + let v = result[0]; + return v; + } else { + return default$(); + } +} + +/** + * Extracts the `Error` value from a result, returning a default value if the result + * is an `Ok`. + * + * ## Examples + * + * ```gleam + * unwrap_error(Error(1), 0) + * // -> 1 + * ``` + * + * ```gleam + * unwrap_error(Ok(""), 0) + * // -> 0 + * ``` + */ +export function unwrap_error(result, default$) { + if (result instanceof Ok) { + return default$; + } else { + let e = result[0]; + return e; + } +} + +export function unwrap_both(result) { + if (result instanceof Ok) { + let a = result[0]; + return a; + } else { + let a = result[0]; + return a; + } +} + +/** + * Returns the first value if it is `Ok`, otherwise returns the second value. + * + * ## Examples + * + * ```gleam + * or(Ok(1), Ok(2)) + * // -> Ok(1) + * ``` + * + * ```gleam + * or(Ok(1), Error("Error 2")) + * // -> Ok(1) + * ``` + * + * ```gleam + * or(Error("Error 1"), Ok(2)) + * // -> Ok(2) + * ``` + * + * ```gleam + * or(Error("Error 1"), Error("Error 2")) + * // -> Error("Error 2") + * ``` + */ +export function or(first, second) { + if (first instanceof Ok) { + return first; + } else { + return second; + } +} + +/** + * Returns the first value if it is `Ok`, otherwise evaluates the given function for a fallback value. + * + * If you need access to the initial error value, use `result.try_recover`. + * + * ## Examples + * + * ```gleam + * lazy_or(Ok(1), fn() { Ok(2) }) + * // -> Ok(1) + * ``` + * + * ```gleam + * lazy_or(Ok(1), fn() { Error("Error 2") }) + * // -> Ok(1) + * ``` + * + * ```gleam + * lazy_or(Error("Error 1"), fn() { Ok(2) }) + * // -> Ok(2) + * ``` + * + * ```gleam + * lazy_or(Error("Error 1"), fn() { Error("Error 2") }) + * // -> Error("Error 2") + * ``` + */ +export function lazy_or(first, second) { + if (first instanceof Ok) { + return first; + } else { + return second(); + } +} + +/** + * Combines a list of results into a single result. + * If all elements in the list are `Ok` then returns an `Ok` holding the list of values. + * If any element is `Error` then returns the first error. + * + * ## Examples + * + * ```gleam + * all([Ok(1), Ok(2)]) + * // -> Ok([1, 2]) + * ``` + * + * ```gleam + * all([Ok(1), Error("e")]) + * // -> Error("e") + * ``` + */ +export function all(results) { + return $list.try_map(results, (result) => { return result; }); +} + +function partition_loop(loop$results, loop$oks, loop$errors) { + while (true) { + let results = loop$results; + let oks = loop$oks; + let errors = loop$errors; + if (results instanceof $Empty) { + return [oks, errors]; + } else { + let $ = results.head; + if ($ instanceof Ok) { + let rest = results.tail; + let a = $[0]; + loop$results = rest; + loop$oks = listPrepend(a, oks); + loop$errors = errors; + } else { + let rest = results.tail; + let e = $[0]; + loop$results = rest; + loop$oks = oks; + loop$errors = listPrepend(e, errors); + } + } + } +} + +/** + * Given a list of results, returns a pair where the first element is a list + * of all the values inside `Ok` and the second element is a list with all the + * values inside `Error`. The values in both lists appear in reverse order with + * respect to their position in the original list of results. + * + * ## Examples + * + * ```gleam + * partition([Ok(1), Error("a"), Error("b"), Ok(2)]) + * // -> #([2, 1], ["b", "a"]) + * ``` + */ +export function partition(results) { + return partition_loop(results, toList([]), toList([])); +} + +/** + * Replace the value within a result + * + * ## Examples + * + * ```gleam + * replace(Ok(1), Nil) + * // -> Ok(Nil) + * ``` + * + * ```gleam + * replace(Error(1), Nil) + * // -> Error(1) + * ``` + */ +export function replace(result, value) { + if (result instanceof Ok) { + return new Ok(value); + } else { + return result; + } +} + +/** + * Replace the error within a result + * + * ## Examples + * + * ```gleam + * replace_error(Error(1), Nil) + * // -> Error(Nil) + * ``` + * + * ```gleam + * replace_error(Ok(1), Nil) + * // -> Ok(1) + * ``` + */ +export function replace_error(result, error) { + if (result instanceof Ok) { + return result; + } else { + return new Error(error); + } +} + +/** + * Given a list of results, returns only the values inside `Ok`. + * + * ## Examples + * + * ```gleam + * values([Ok(1), Error("a"), Ok(3)]) + * // -> [1, 3] + * ``` + */ +export function values(results) { + return $list.filter_map(results, (result) => { return result; }); +} + +/** + * Updates a value held within the `Error` of a result by calling a given function + * on it, where the given function also returns a result. The two results are + * then merged together into one result. + * + * If the result is an `Ok` rather than `Error` the function is not called and the + * result stays the same. + * + * This function is useful for chaining together computations that may fail + * and trying to recover from possible errors. + * + * If you do not need access to the initial error value, use `result.lazy_or`. + * + * ## Examples + * + * ```gleam + * Ok(1) |> try_recover(with: fn(_) { Error("failed to recover") }) + * // -> Ok(1) + * ``` + * + * ```gleam + * Error(1) |> try_recover(with: fn(error) { Ok(error + 1) }) + * // -> Ok(2) + * ``` + * + * ```gleam + * Error(1) |> try_recover(with: fn(error) { Error("failed to recover") }) + * // -> Error("failed to recover") + * ``` + */ +export function try_recover(result, fun) { + if (result instanceof Ok) { + return result; + } else { + let error = result[0]; + return fun(error); + } +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/set.mjs b/build/dev/javascript/gleam_stdlib/gleam/set.mjs new file mode 100644 index 0000000..dc1ae52 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/set.mjs @@ -0,0 +1,412 @@ +import { CustomType as $CustomType, isEqual } from "../gleam.mjs"; +import * as $dict from "../gleam/dict.mjs"; +import * as $list from "../gleam/list.mjs"; +import * as $result from "../gleam/result.mjs"; + +class Set extends $CustomType { + constructor(dict) { + super(); + this.dict = dict; + } +} + +/** + * Creates a new empty set. + */ +export function new$() { + return new Set($dict.new$()); +} + +/** + * Gets the number of members in a set. + * + * This function runs in constant time. + * + * ## Examples + * + * ```gleam + * new() + * |> insert(1) + * |> insert(2) + * |> size + * // -> 2 + * ``` + */ +export function size(set) { + return $dict.size(set.dict); +} + +/** + * Determines whether or not the set is empty. + * + * ## Examples + * + * ```gleam + * new() |> is_empty + * // -> True + * ``` + * + * ```gleam + * new() |> insert(1) |> is_empty + * // -> False + * ``` + */ +export function is_empty(set) { + return isEqual(set, new$()); +} + +/** + * Checks whether a set contains a given member. + * + * This function runs in logarithmic time. + * + * ## Examples + * + * ```gleam + * new() + * |> insert(2) + * |> contains(2) + * // -> True + * ``` + * + * ```gleam + * new() + * |> insert(2) + * |> contains(1) + * // -> False + * ``` + */ +export function contains(set, member) { + let _pipe = set.dict; + let _pipe$1 = $dict.get(_pipe, member); + return $result.is_ok(_pipe$1); +} + +/** + * Removes a member from a set. If the set does not contain the member then + * the set is returned unchanged. + * + * This function runs in logarithmic time. + * + * ## Examples + * + * ```gleam + * new() + * |> insert(2) + * |> delete(2) + * |> contains(1) + * // -> False + * ``` + */ +export function delete$(set, member) { + return new Set($dict.delete$(set.dict, member)); +} + +/** + * Converts the set into a list of the contained members. + * + * The list has no specific ordering, any unintentional ordering may change in + * future versions of Gleam or Erlang. + * + * This function runs in linear time. + * + * ## Examples + * + * ```gleam + * new() |> insert(2) |> to_list + * // -> [2] + * ``` + */ +export function to_list(set) { + return $dict.keys(set.dict); +} + +/** + * Combines all entries into a single value by calling a given function on each + * one. + * + * Sets are not ordered so the values are not returned in any specific order. + * Do not write code that relies on the order entries are used by this + * function as it may change in later versions of Gleam or Erlang. + * + * # Examples + * + * ```gleam + * from_list([1, 3, 9]) + * |> fold(0, fn(accumulator, member) { accumulator + member }) + * // -> 13 + * ``` + */ +export function fold(set, initial, reducer) { + return $dict.fold(set.dict, initial, (a, k, _) => { return reducer(a, k); }); +} + +/** + * Creates a new set from an existing set, minus any members that a given + * function returns `False` for. + * + * This function runs in loglinear time. + * + * ## Examples + * + * ```gleam + * import gleam/int + * + * from_list([1, 4, 6, 3, 675, 44, 67]) + * |> filter(keeping: int.is_even) + * |> to_list + * // -> [4, 6, 44] + * ``` + */ +export function filter(set, predicate) { + return new Set($dict.filter(set.dict, (m, _) => { return predicate(m); })); +} + +/** + * Creates a new set from a given set with all the same entries except any + * entry found on the given list. + * + * ## Examples + * + * ```gleam + * from_list([1, 2, 3, 4]) + * |> drop([1, 3]) + * |> to_list + * // -> [2, 4] + * ``` + */ +export function drop(set, disallowed) { + return $list.fold(disallowed, set, delete$); +} + +/** + * Creates a new set from a given set, only including any members which are in + * a given list. + * + * This function runs in loglinear time. + * + * ## Examples + * + * ```gleam + * from_list([1, 2, 3]) + * |> take([1, 3, 5]) + * |> to_list + * // -> [1, 3] + * ``` + */ +export function take(set, desired) { + return new Set($dict.take(set.dict, desired)); +} + +function order(first, second) { + let $ = $dict.size(first.dict) > $dict.size(second.dict); + if ($) { + return [first, second]; + } else { + return [second, first]; + } +} + +/** + * Creates a new set that contains members that are present in both given sets. + * + * This function runs in loglinear time. + * + * ## Examples + * + * ```gleam + * intersection(from_list([1, 2]), from_list([2, 3])) |> to_list + * // -> [2] + * ``` + */ +export function intersection(first, second) { + let $ = order(first, second); + let larger; + let smaller; + larger = $[0]; + smaller = $[1]; + return take(larger, to_list(smaller)); +} + +/** + * Creates a new set that contains members that are present in the first set + * but not the second. + * + * ## Examples + * + * ```gleam + * difference(from_list([1, 2]), from_list([2, 3, 4])) |> to_list + * // -> [1] + * ``` + */ +export function difference(first, second) { + return drop(first, to_list(second)); +} + +/** + * Determines if a set is fully contained by another. + * + * ## Examples + * + * ```gleam + * is_subset(from_list([1]), from_list([1, 2])) + * // -> True + * ``` + * + * ```gleam + * is_subset(from_list([1, 2, 3]), from_list([3, 4, 5])) + * // -> False + * ``` + */ +export function is_subset(first, second) { + return isEqual(intersection(first, second), first); +} + +/** + * Determines if two sets contain no common members + * + * ## Examples + * + * ```gleam + * is_disjoint(from_list([1, 2, 3]), from_list([4, 5, 6])) + * // -> True + * ``` + * + * ```gleam + * is_disjoint(from_list([1, 2, 3]), from_list([3, 4, 5])) + * // -> False + * ``` + */ +export function is_disjoint(first, second) { + return isEqual(intersection(first, second), new$()); +} + +/** + * Calls a function for each member in a set, discarding the return + * value. + * + * Useful for producing a side effect for every item of a set. + * + * ```gleam + * let set = from_list(["apple", "banana", "cherry"]) + * + * each(set, io.println) + * // -> Nil + * // apple + * // banana + * // cherry + * ``` + * + * The order of elements in the iteration is an implementation detail that + * should not be relied upon. + */ +export function each(set, fun) { + return fold( + set, + undefined, + (nil, member) => { + fun(member); + return nil; + }, + ); +} + +const token = undefined; + +/** + * Inserts an member into the set. + * + * This function runs in logarithmic time. + * + * ## Examples + * + * ```gleam + * new() + * |> insert(1) + * |> insert(2) + * |> size + * // -> 2 + * ``` + */ +export function insert(set, member) { + return new Set($dict.insert(set.dict, member, token)); +} + +/** + * Creates a new set of the members in a given list. + * + * This function runs in loglinear time. + * + * ## Examples + * + * ```gleam + * import gleam/int + * import gleam/list + * + * [1, 1, 2, 4, 3, 2] |> from_list |> to_list |> list.sort(by: int.compare) + * // -> [1, 2, 3, 4] + * ``` + */ +export function from_list(members) { + let dict = $list.fold( + members, + $dict.new$(), + (m, k) => { return $dict.insert(m, k, token); }, + ); + return new Set(dict); +} + +/** + * Creates a new set from a given set with the result of applying the given + * function to each member. + * + * ## Examples + * + * ```gleam + * from_list([1, 2, 3, 4]) + * |> map(with: fn(x) { x * 2 }) + * |> to_list + * // -> [2, 4, 6, 8] + * ``` + */ +export function map(set, fun) { + return fold( + set, + new$(), + (acc, member) => { return insert(acc, fun(member)); }, + ); +} + +/** + * Creates a new set that contains all members of both given sets. + * + * This function runs in loglinear time. + * + * ## Examples + * + * ```gleam + * union(from_list([1, 2]), from_list([2, 3])) |> to_list + * // -> [1, 2, 3] + * ``` + */ +export function union(first, second) { + let $ = order(first, second); + let larger; + let smaller; + larger = $[0]; + smaller = $[1]; + return fold(smaller, larger, insert); +} + +/** + * Creates a new set that contains members that are present in either set, but + * not both. + * + * ```gleam + * symmetric_difference(from_list([1, 2, 3]), from_list([3, 4])) |> to_list + * // -> [1, 2, 4] + * ``` + */ +export function symmetric_difference(first, second) { + return difference(union(first, second), intersection(first, second)); +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/string.mjs b/build/dev/javascript/gleam_stdlib/gleam/string.mjs new file mode 100644 index 0000000..d15468e --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/string.mjs @@ -0,0 +1,723 @@ +import { + Ok, + Error, + Empty as $Empty, + prepend as listPrepend, + CustomType as $CustomType, + remainderInt, + divideInt, +} from "../gleam.mjs"; +import * as $list from "../gleam/list.mjs"; +import * as $option from "../gleam/option.mjs"; +import { None, Some } from "../gleam/option.mjs"; +import * as $order from "../gleam/order.mjs"; +import * as $string_tree from "../gleam/string_tree.mjs"; +import { + string_length as length, + lowercase, + uppercase, + less_than, + string_grapheme_slice as grapheme_slice, + string_byte_slice as unsafe_byte_slice, + crop_string as crop, + contains_string as contains, + starts_with, + ends_with, + split_once, + trim_start, + trim_end, + pop_grapheme, + graphemes as to_graphemes, + codepoint as unsafe_int_to_utf_codepoint, + string_to_codepoint_integer_list, + utf_codepoint_list_to_string as from_utf_codepoints, + utf_codepoint_to_int, + inspect as do_inspect, + byte_size, +} from "../gleam_stdlib.mjs"; + +export { + byte_size, + contains, + crop, + ends_with, + from_utf_codepoints, + length, + lowercase, + pop_grapheme, + split_once, + starts_with, + to_graphemes, + trim_end, + trim_start, + uppercase, + utf_codepoint_to_int, +}; + +class Leading extends $CustomType {} + +class Trailing extends $CustomType {} + +/** + * Determines if a `String` is empty. + * + * ## Examples + * + * ```gleam + * is_empty("") + * // -> True + * ``` + * + * ```gleam + * is_empty("the world") + * // -> False + * ``` + */ +export function is_empty(str) { + return str === ""; +} + +/** + * Reverses a `String`. + * + * This function has to iterate across the whole `String` so it runs in linear + * time. Avoid using this in a loop. + * + * ## Examples + * + * ```gleam + * reverse("stressed") + * // -> "desserts" + * ``` + */ +export function reverse(string) { + let _pipe = string; + let _pipe$1 = $string_tree.from_string(_pipe); + let _pipe$2 = $string_tree.reverse(_pipe$1); + return $string_tree.to_string(_pipe$2); +} + +/** + * Creates a new `String` by replacing all occurrences of a given substring. + * + * ## Examples + * + * ```gleam + * replace("www.example.com", each: ".", with: "-") + * // -> "www-example-com" + * ``` + * + * ```gleam + * replace("a,b,c,d,e", each: ",", with: "/") + * // -> "a/b/c/d/e" + * ``` + */ +export function replace(string, pattern, substitute) { + let _pipe = string; + let _pipe$1 = $string_tree.from_string(_pipe); + let _pipe$2 = $string_tree.replace(_pipe$1, pattern, substitute); + return $string_tree.to_string(_pipe$2); +} + +/** + * Compares two `String`s to see which is "larger" by comparing their graphemes. + * + * This does not compare the size or length of the given `String`s. + * + * ## Examples + * + * ```gleam + * compare("Anthony", "Anthony") + * // -> order.Eq + * ``` + * + * ```gleam + * compare("A", "B") + * // -> order.Lt + * ``` + */ +export function compare(a, b) { + let $ = a === b; + if ($) { + return new $order.Eq(); + } else { + let $1 = less_than(a, b); + if ($1) { + return new $order.Lt(); + } else { + return new $order.Gt(); + } + } +} + +/** + * Takes a substring given a start grapheme index and a length. Negative indexes + * are taken starting from the *end* of the list. + * + * This function runs in linear time with the size of the index and the + * length. Negative indexes are linear with the size of the input string in + * addition to the other costs. + * + * ## Examples + * + * ```gleam + * slice(from: "gleam", at_index: 1, length: 2) + * // -> "le" + * ``` + * + * ```gleam + * slice(from: "gleam", at_index: 1, length: 10) + * // -> "leam" + * ``` + * + * ```gleam + * slice(from: "gleam", at_index: 10, length: 3) + * // -> "" + * ``` + * + * ```gleam + * slice(from: "gleam", at_index: -2, length: 2) + * // -> "am" + * ``` + * + * ```gleam + * slice(from: "gleam", at_index: -12, length: 2) + * // -> "" + * ``` + */ +export function slice(string, idx, len) { + let $ = len <= 0; + if ($) { + return ""; + } else { + let $1 = idx < 0; + if ($1) { + let translated_idx = length(string) + idx; + let $2 = translated_idx < 0; + if ($2) { + return ""; + } else { + return grapheme_slice(string, translated_idx, len); + } + } else { + return grapheme_slice(string, idx, len); + } + } +} + +/** + * Drops *n* graphemes from the end of a `String`. + * + * This function traverses the full string, so it runs in linear time with the + * size of the string. Avoid using this in a loop. + * + * ## Examples + * + * ```gleam + * drop_end(from: "Cigarette Smoking Man", up_to: 2) + * // -> "Cigarette Smoking M" + * ``` + */ +export function drop_end(string, num_graphemes) { + let $ = num_graphemes <= 0; + if ($) { + return string; + } else { + return slice(string, 0, length(string) - num_graphemes); + } +} + +/** + * Creates a new `String` by joining two `String`s together. + * + * This function typically copies both `String`s and runs in linear time, but + * the exact behaviour will depend on how the runtime you are using optimises + * your code. Benchmark and profile your code if you need to understand its + * performance better. + * + * If you are joining together large string and want to avoid copying any data + * you may want to investigate using the [`string_tree`](../gleam/string_tree.html) + * module. + * + * ## Examples + * + * ```gleam + * append(to: "butter", suffix: "fly") + * // -> "butterfly" + * ``` + */ +export function append(first, second) { + return first + second; +} + +function concat_loop(loop$strings, loop$accumulator) { + while (true) { + let strings = loop$strings; + let accumulator = loop$accumulator; + if (strings instanceof $Empty) { + return accumulator; + } else { + let string = strings.head; + let strings$1 = strings.tail; + loop$strings = strings$1; + loop$accumulator = accumulator + string; + } + } +} + +/** + * Creates a new `String` by joining many `String`s together. + * + * This function copies all the `String`s and runs in linear time. + * + * ## Examples + * + * ```gleam + * concat(["never", "the", "less"]) + * // -> "nevertheless" + * ``` + */ +export function concat(strings) { + return concat_loop(strings, ""); +} + +function repeat_loop(loop$times, loop$doubling_acc, loop$acc) { + while (true) { + let times = loop$times; + let doubling_acc = loop$doubling_acc; + let acc = loop$acc; + let _block; + let $ = times % 2; + if ($ === 0) { + _block = acc; + } else { + _block = acc + doubling_acc; + } + let acc$1 = _block; + let times$1 = globalThis.Math.trunc(times / 2); + let $1 = times$1 <= 0; + if ($1) { + return acc$1; + } else { + loop$times = times$1; + loop$doubling_acc = doubling_acc + doubling_acc; + loop$acc = acc$1; + } + } +} + +/** + * Creates a new `String` by repeating a `String` a given number of times. + * + * This function runs in loglinear time. + * + * ## Examples + * + * ```gleam + * repeat("ha", times: 3) + * // -> "hahaha" + * ``` + */ +export function repeat(string, times) { + let $ = times <= 0; + if ($) { + return ""; + } else { + return repeat_loop(times, string, ""); + } +} + +function join_loop(loop$strings, loop$separator, loop$accumulator) { + while (true) { + let strings = loop$strings; + let separator = loop$separator; + let accumulator = loop$accumulator; + if (strings instanceof $Empty) { + return accumulator; + } else { + let string = strings.head; + let strings$1 = strings.tail; + loop$strings = strings$1; + loop$separator = separator; + loop$accumulator = (accumulator + separator) + string; + } + } +} + +/** + * Joins many `String`s together with a given separator. + * + * This function runs in linear time. + * + * ## Examples + * + * ```gleam + * join(["home","evan","Desktop"], with: "/") + * // -> "home/evan/Desktop" + * ``` + */ +export function join(strings, separator) { + if (strings instanceof $Empty) { + return ""; + } else { + let first$1 = strings.head; + let rest = strings.tail; + return join_loop(rest, separator, first$1); + } +} + +function padding(size, pad_string) { + let pad_string_length = length(pad_string); + let num_pads = divideInt(size, pad_string_length); + let extra = remainderInt(size, pad_string_length); + return repeat(pad_string, num_pads) + slice(pad_string, 0, extra); +} + +/** + * Pads the start of a `String` until it has a given length. + * + * ## Examples + * + * ```gleam + * pad_start("121", to: 5, with: ".") + * // -> "..121" + * ``` + * + * ```gleam + * pad_start("121", to: 3, with: ".") + * // -> "121" + * ``` + * + * ```gleam + * pad_start("121", to: 2, with: ".") + * // -> "121" + * ``` + */ +export function pad_start(string, desired_length, pad_string) { + let current_length = length(string); + let to_pad_length = desired_length - current_length; + let $ = to_pad_length <= 0; + if ($) { + return string; + } else { + return padding(to_pad_length, pad_string) + string; + } +} + +/** + * Pads the end of a `String` until it has a given length. + * + * ## Examples + * + * ```gleam + * pad_end("123", to: 5, with: ".") + * // -> "123.." + * ``` + * + * ```gleam + * pad_end("123", to: 3, with: ".") + * // -> "123" + * ``` + * + * ```gleam + * pad_end("123", to: 2, with: ".") + * // -> "123" + * ``` + */ +export function pad_end(string, desired_length, pad_string) { + let current_length = length(string); + let to_pad_length = desired_length - current_length; + let $ = to_pad_length <= 0; + if ($) { + return string; + } else { + return string + padding(to_pad_length, pad_string); + } +} + +/** + * Removes whitespace on both sides of a `String`. + * + * Whitespace in this function is the set of nonbreakable whitespace + * codepoints, defined as Pattern_White_Space in [Unicode Standard Annex #31][1]. + * + * [1]: https://unicode.org/reports/tr31/ + * + * ## Examples + * + * ```gleam + * trim(" hats \n") + * // -> "hats" + * ``` + */ +export function trim(string) { + let _pipe = string; + let _pipe$1 = trim_start(_pipe); + return trim_end(_pipe$1); +} + +function to_graphemes_loop(loop$string, loop$acc) { + while (true) { + let string = loop$string; + let acc = loop$acc; + let $ = pop_grapheme(string); + if ($ instanceof Ok) { + let grapheme = $[0][0]; + let rest = $[0][1]; + loop$string = rest; + loop$acc = listPrepend(grapheme, acc); + } else { + return acc; + } + } +} + +/** + * Creates a list of `String`s by splitting a given string on a given substring. + * + * ## Examples + * + * ```gleam + * split("home/gleam/desktop/", on: "/") + * // -> ["home", "gleam", "desktop", ""] + * ``` + */ +export function split(x, substring) { + if (substring === "") { + return to_graphemes(x); + } else { + let _pipe = x; + let _pipe$1 = $string_tree.from_string(_pipe); + let _pipe$2 = $string_tree.split(_pipe$1, substring); + return $list.map(_pipe$2, $string_tree.to_string); + } +} + +function do_to_utf_codepoints(string) { + let _pipe = string; + let _pipe$1 = string_to_codepoint_integer_list(_pipe); + return $list.map(_pipe$1, unsafe_int_to_utf_codepoint); +} + +/** + * Converts a `String` to a `List` of `UtfCodepoint`. + * + * See and + * for an + * explanation on code points. + * + * ## Examples + * + * ```gleam + * "a" |> to_utf_codepoints + * // -> [UtfCodepoint(97)] + * ``` + * + * ```gleam + * // Semantically the same as: + * // ["🏳", "️", "‍", "🌈"] or: + * // [waving_white_flag, variant_selector_16, zero_width_joiner, rainbow] + * "🏳️‍🌈" |> to_utf_codepoints + * // -> [ + * // UtfCodepoint(127987), + * // UtfCodepoint(65039), + * // UtfCodepoint(8205), + * // UtfCodepoint(127752), + * // ] + * ``` + */ +export function to_utf_codepoints(string) { + return do_to_utf_codepoints(string); +} + +/** + * Converts an integer to a `UtfCodepoint`. + * + * Returns an `Error` if the integer does not represent a valid UTF codepoint. + */ +export function utf_codepoint(value) { + let i = value; + if (i > 1_114_111) { + return new Error(undefined); + } else { + let i$1 = value; + if ((i$1 >= 55_296) && (i$1 <= 57_343)) { + return new Error(undefined); + } else { + let i$2 = value; + if (i$2 < 0) { + return new Error(undefined); + } else { + let i$3 = value; + return new Ok(unsafe_int_to_utf_codepoint(i$3)); + } + } + } +} + +/** + * Converts a `String` into `Option(String)` where an empty `String` becomes + * `None`. + * + * ## Examples + * + * ```gleam + * to_option("") + * // -> None + * ``` + * + * ```gleam + * to_option("hats") + * // -> Some("hats") + * ``` + */ +export function to_option(string) { + if (string === "") { + return new None(); + } else { + return new Some(string); + } +} + +/** + * Returns the first grapheme cluster in a given `String` and wraps it in a + * `Result(String, Nil)`. If the `String` is empty, it returns `Error(Nil)`. + * Otherwise, it returns `Ok(String)`. + * + * ## Examples + * + * ```gleam + * first("") + * // -> Error(Nil) + * ``` + * + * ```gleam + * first("icecream") + * // -> Ok("i") + * ``` + */ +export function first(string) { + let $ = pop_grapheme(string); + if ($ instanceof Ok) { + let first$1 = $[0][0]; + return new Ok(first$1); + } else { + return $; + } +} + +/** + * Returns the last grapheme cluster in a given `String` and wraps it in a + * `Result(String, Nil)`. If the `String` is empty, it returns `Error(Nil)`. + * Otherwise, it returns `Ok(String)`. + * + * This function traverses the full string, so it runs in linear time with the + * length of the string. Avoid using this in a loop. + * + * ## Examples + * + * ```gleam + * last("") + * // -> Error(Nil) + * ``` + * + * ```gleam + * last("icecream") + * // -> Ok("m") + * ``` + */ +export function last(string) { + let $ = pop_grapheme(string); + if ($ instanceof Ok) { + let $1 = $[0][1]; + if ($1 === "") { + let first$1 = $[0][0]; + return new Ok(first$1); + } else { + let rest = $1; + return new Ok(slice(rest, -1, 1)); + } + } else { + return $; + } +} + +/** + * Creates a new `String` with the first grapheme in the input `String` + * converted to uppercase and the remaining graphemes to lowercase. + * + * ## Examples + * + * ```gleam + * capitalise("mamouna") + * // -> "Mamouna" + * ``` + */ +export function capitalise(string) { + let $ = pop_grapheme(string); + if ($ instanceof Ok) { + let first$1 = $[0][0]; + let rest = $[0][1]; + return append(uppercase(first$1), lowercase(rest)); + } else { + return ""; + } +} + +/** + * Returns a `String` representation of a term in Gleam syntax. + * + * This may be occasionally useful for quick-and-dirty printing of values in + * scripts. For error reporting and other uses prefer constructing strings by + * pattern matching on the values. + * + * ## Limitations + * + * The output format of this function is not stable and could change at any + * time. The output is not suitable for parsing. + * + * This function works using runtime reflection, so the output may not be + * perfectly accurate for data structures where the runtime structure doesn't + * hold enough information to determine the original syntax. For example, + * tuples with an Erlang atom in the first position will be mistaken for Gleam + * records. + * + * ## Security and safety + * + * There is no limit to how large the strings that this function can produce. + * Be careful not to call this function with large data structures or you + * could use very large amounts of memory, potentially causing runtime + * problems. + */ +export function inspect(term) { + let _pipe = term; + let _pipe$1 = do_inspect(_pipe); + return $string_tree.to_string(_pipe$1); +} + +/** + * Drops *n* graphemes from the start of a `String`. + * + * This function runs in linear time with the number of graphemes to drop. + * + * ## Examples + * + * ```gleam + * drop_start(from: "The Lone Gunmen", up_to: 2) + * // -> "e Lone Gunmen" + * ``` + */ +export function drop_start(string, num_graphemes) { + let $ = num_graphemes <= 0; + if ($) { + return string; + } else { + let prefix = grapheme_slice(string, 0, num_graphemes); + let prefix_size = byte_size(prefix); + return unsafe_byte_slice( + string, + prefix_size, + byte_size(string) - prefix_size, + ); + } +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/string_tree.mjs b/build/dev/javascript/gleam_stdlib/gleam/string_tree.mjs new file mode 100644 index 0000000..a8b24ce --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/string_tree.mjs @@ -0,0 +1,133 @@ +import { toList, CustomType as $CustomType, isEqual } from "../gleam.mjs"; +import * as $list from "../gleam/list.mjs"; +import { + add as append_tree, + concat as from_strings, + concat, + identity as from_string, + identity as to_string, + length as byte_size, + lowercase, + uppercase, + graphemes as do_to_graphemes, + split, + string_replace as replace, +} from "../gleam_stdlib.mjs"; + +export { + append_tree, + byte_size, + concat, + from_string, + from_strings, + lowercase, + replace, + split, + to_string, + uppercase, +}; + +class All extends $CustomType {} + +/** + * Prepends some `StringTree` onto the start of another. + * + * Runs in constant time. + */ +export function prepend_tree(tree, prefix) { + return append_tree(prefix, tree); +} + +/** + * Create an empty `StringTree`. Useful as the start of a pipe chaining many + * trees together. + */ +export function new$() { + return from_strings(toList([])); +} + +/** + * Prepends a `String` onto the start of some `StringTree`. + * + * Runs in constant time. + */ +export function prepend(tree, prefix) { + return append_tree(from_string(prefix), tree); +} + +/** + * Appends a `String` onto the end of some `StringTree`. + * + * Runs in constant time. + */ +export function append(tree, second) { + return append_tree(tree, from_string(second)); +} + +/** + * Joins the given trees into a new tree separated with the given string. + */ +export function join(trees, sep) { + let _pipe = trees; + let _pipe$1 = $list.intersperse(_pipe, from_string(sep)); + return concat(_pipe$1); +} + +/** + * Converts a `StringTree` to a new one with the contents reversed. + */ +export function reverse(tree) { + let _pipe = tree; + let _pipe$1 = to_string(_pipe); + let _pipe$2 = do_to_graphemes(_pipe$1); + let _pipe$3 = $list.reverse(_pipe$2); + return from_strings(_pipe$3); +} + +/** + * Compares two string trees to determine if they have the same textual + * content. + * + * Comparing two string trees using the `==` operator may return `False` even + * if they have the same content as they may have been build in different ways, + * so using this function is often preferred. + * + * ## Examples + * + * ```gleam + * from_strings(["a", "b"]) == from_string("ab") + * // -> False + * ``` + * + * ```gleam + * is_equal(from_strings(["a", "b"]), from_string("ab")) + * // -> True + * ``` + */ +export function is_equal(a, b) { + return isEqual(a, b); +} + +/** + * Inspects a `StringTree` to determine if it is equivalent to an empty string. + * + * ## Examples + * + * ```gleam + * from_string("ok") |> is_empty + * // -> False + * ``` + * + * ```gleam + * from_string("") |> is_empty + * // -> True + * ``` + * + * ```gleam + * from_strings([]) |> is_empty + * // -> True + * ``` + */ +export function is_empty(tree) { + return isEqual(from_string(""), tree); +} diff --git a/build/dev/javascript/gleam_stdlib/gleam/uri.mjs b/build/dev/javascript/gleam_stdlib/gleam/uri.mjs new file mode 100644 index 0000000..01f4bba --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam/uri.mjs @@ -0,0 +1,1147 @@ +import { + Ok, + Error, + toList, + Empty as $Empty, + prepend as listPrepend, + CustomType as $CustomType, + isEqual, +} from "../gleam.mjs"; +import * as $int from "../gleam/int.mjs"; +import * as $list from "../gleam/list.mjs"; +import * as $option from "../gleam/option.mjs"; +import { None, Some } from "../gleam/option.mjs"; +import * as $string from "../gleam/string.mjs"; +import * as $string_tree from "../gleam/string_tree.mjs"; +import { + pop_codeunit, + string_codeunit_slice as codeunit_slice, + parse_query, + percent_encode, + percent_decode, +} from "../gleam_stdlib.mjs"; + +export { parse_query, percent_decode, percent_encode }; + +export class Uri extends $CustomType { + constructor(scheme, userinfo, host, port, path, query, fragment) { + super(); + this.scheme = scheme; + this.userinfo = userinfo; + this.host = host; + this.port = port; + this.path = path; + this.query = query; + this.fragment = fragment; + } +} +export const Uri$Uri = (scheme, userinfo, host, port, path, query, fragment) => + new Uri(scheme, userinfo, host, port, path, query, fragment); +export const Uri$isUri = (value) => value instanceof Uri; +export const Uri$Uri$scheme = (value) => value.scheme; +export const Uri$Uri$0 = (value) => value.scheme; +export const Uri$Uri$userinfo = (value) => value.userinfo; +export const Uri$Uri$1 = (value) => value.userinfo; +export const Uri$Uri$host = (value) => value.host; +export const Uri$Uri$2 = (value) => value.host; +export const Uri$Uri$port = (value) => value.port; +export const Uri$Uri$3 = (value) => value.port; +export const Uri$Uri$path = (value) => value.path; +export const Uri$Uri$4 = (value) => value.path; +export const Uri$Uri$query = (value) => value.query; +export const Uri$Uri$5 = (value) => value.query; +export const Uri$Uri$fragment = (value) => value.fragment; +export const Uri$Uri$6 = (value) => value.fragment; + +function is_valid_host_within_brackets_char(char) { + return (((((48 >= char) && (char <= 57)) || ((65 >= char) && (char <= 90))) || ((97 >= char) && (char <= 122))) || (char === 58)) || (char === 46); +} + +function parse_fragment(rest, pieces) { + return new Ok( + new Uri( + pieces.scheme, + pieces.userinfo, + pieces.host, + pieces.port, + pieces.path, + pieces.query, + new Some(rest), + ), + ); +} + +function parse_query_with_question_mark_loop( + loop$original, + loop$uri_string, + loop$pieces, + loop$size +) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size = loop$size; + if (uri_string.startsWith("#")) { + if (size === 0) { + let rest = uri_string.slice(1); + return parse_fragment(rest, pieces); + } else { + let rest = uri_string.slice(1); + let query = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + pieces.host, + pieces.port, + pieces.path, + new Some(query), + pieces.fragment, + ); + return parse_fragment(rest, pieces$1); + } + } else if (uri_string === "") { + return new Ok( + new Uri( + pieces.scheme, + pieces.userinfo, + pieces.host, + pieces.port, + pieces.path, + new Some(original), + pieces.fragment, + ), + ); + } else { + let $ = pop_codeunit(uri_string); + let rest; + rest = $[1]; + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size + 1; + } + } +} + +function parse_query_with_question_mark(uri_string, pieces) { + return parse_query_with_question_mark_loop(uri_string, uri_string, pieces, 0); +} + +function parse_path_loop(loop$original, loop$uri_string, loop$pieces, loop$size) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size = loop$size; + if (uri_string.startsWith("?")) { + let rest = uri_string.slice(1); + let path = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + pieces.host, + pieces.port, + path, + pieces.query, + pieces.fragment, + ); + return parse_query_with_question_mark(rest, pieces$1); + } else if (uri_string.startsWith("#")) { + let rest = uri_string.slice(1); + let path = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + pieces.host, + pieces.port, + path, + pieces.query, + pieces.fragment, + ); + return parse_fragment(rest, pieces$1); + } else if (uri_string === "") { + return new Ok( + new Uri( + pieces.scheme, + pieces.userinfo, + pieces.host, + pieces.port, + original, + pieces.query, + pieces.fragment, + ), + ); + } else { + let $ = pop_codeunit(uri_string); + let rest; + rest = $[1]; + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size + 1; + } + } +} + +function parse_path(uri_string, pieces) { + return parse_path_loop(uri_string, uri_string, pieces, 0); +} + +function parse_port_loop(loop$uri_string, loop$pieces, loop$port) { + while (true) { + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let port = loop$port; + if (uri_string.startsWith("0")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10; + } else if (uri_string.startsWith("1")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 1; + } else if (uri_string.startsWith("2")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 2; + } else if (uri_string.startsWith("3")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 3; + } else if (uri_string.startsWith("4")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 4; + } else if (uri_string.startsWith("5")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 5; + } else if (uri_string.startsWith("6")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 6; + } else if (uri_string.startsWith("7")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 7; + } else if (uri_string.startsWith("8")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 8; + } else if (uri_string.startsWith("9")) { + let rest = uri_string.slice(1); + loop$uri_string = rest; + loop$pieces = pieces; + loop$port = port * 10 + 9; + } else if (uri_string.startsWith("?")) { + let rest = uri_string.slice(1); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + pieces.host, + new Some(port), + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_query_with_question_mark(rest, pieces$1); + } else if (uri_string.startsWith("#")) { + let rest = uri_string.slice(1); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + pieces.host, + new Some(port), + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_fragment(rest, pieces$1); + } else if (uri_string.startsWith("/")) { + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + pieces.host, + new Some(port), + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_path(uri_string, pieces$1); + } else if (uri_string === "") { + return new Ok( + new Uri( + pieces.scheme, + pieces.userinfo, + pieces.host, + new Some(port), + pieces.path, + pieces.query, + pieces.fragment, + ), + ); + } else { + return new Error(undefined); + } + } +} + +function parse_port(uri_string, pieces) { + if (uri_string.startsWith(":0")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 0); + } else if (uri_string.startsWith(":1")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 1); + } else if (uri_string.startsWith(":2")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 2); + } else if (uri_string.startsWith(":3")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 3); + } else if (uri_string.startsWith(":4")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 4); + } else if (uri_string.startsWith(":5")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 5); + } else if (uri_string.startsWith(":6")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 6); + } else if (uri_string.startsWith(":7")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 7); + } else if (uri_string.startsWith(":8")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 8); + } else if (uri_string.startsWith(":9")) { + let rest = uri_string.slice(2); + return parse_port_loop(rest, pieces, 9); + } else if (uri_string === ":") { + return new Ok(pieces); + } else if (uri_string === "") { + return new Ok(pieces); + } else if (uri_string.startsWith("?")) { + let rest = uri_string.slice(1); + return parse_query_with_question_mark(rest, pieces); + } else if (uri_string.startsWith(":?")) { + let rest = uri_string.slice(2); + return parse_query_with_question_mark(rest, pieces); + } else if (uri_string.startsWith("#")) { + let rest = uri_string.slice(1); + return parse_fragment(rest, pieces); + } else if (uri_string.startsWith(":#")) { + let rest = uri_string.slice(2); + return parse_fragment(rest, pieces); + } else if (uri_string.startsWith("/")) { + return parse_path(uri_string, pieces); + } else if (uri_string.startsWith(":")) { + let rest = uri_string.slice(1); + if (rest.startsWith("/")) { + return parse_path(rest, pieces); + } else { + return new Error(undefined); + } + } else { + return new Error(undefined); + } +} + +function parse_host_outside_of_brackets_loop( + loop$original, + loop$uri_string, + loop$pieces, + loop$size +) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size = loop$size; + if (uri_string === "") { + return new Ok( + new Uri( + pieces.scheme, + pieces.userinfo, + new Some(original), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ), + ); + } else if (uri_string.startsWith(":")) { + let host = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + new Some(host), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_port(uri_string, pieces$1); + } else if (uri_string.startsWith("/")) { + let host = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + new Some(host), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_path(uri_string, pieces$1); + } else if (uri_string.startsWith("?")) { + let rest = uri_string.slice(1); + let host = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + new Some(host), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_query_with_question_mark(rest, pieces$1); + } else if (uri_string.startsWith("#")) { + let rest = uri_string.slice(1); + let host = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + new Some(host), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_fragment(rest, pieces$1); + } else { + let $ = pop_codeunit(uri_string); + let rest; + rest = $[1]; + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size + 1; + } + } +} + +function parse_host_within_brackets_loop( + loop$original, + loop$uri_string, + loop$pieces, + loop$size +) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size = loop$size; + if (uri_string === "") { + return new Ok( + new Uri( + pieces.scheme, + pieces.userinfo, + new Some(uri_string), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ), + ); + } else if (uri_string.startsWith("]")) { + if (size === 0) { + let rest = uri_string.slice(1); + return parse_port(rest, pieces); + } else { + let rest = uri_string.slice(1); + let host = codeunit_slice(original, 0, size + 1); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + new Some(host), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_port(rest, pieces$1); + } + } else if (uri_string.startsWith("/")) { + if (size === 0) { + return parse_path(uri_string, pieces); + } else { + let host = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + new Some(host), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_path(uri_string, pieces$1); + } + } else if (uri_string.startsWith("?")) { + if (size === 0) { + let rest = uri_string.slice(1); + return parse_query_with_question_mark(rest, pieces); + } else { + let rest = uri_string.slice(1); + let host = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + new Some(host), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_query_with_question_mark(rest, pieces$1); + } + } else if (uri_string.startsWith("#")) { + if (size === 0) { + let rest = uri_string.slice(1); + return parse_fragment(rest, pieces); + } else { + let rest = uri_string.slice(1); + let host = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + new Some(host), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_fragment(rest, pieces$1); + } + } else { + let $ = pop_codeunit(uri_string); + let char; + let rest; + char = $[0]; + rest = $[1]; + let $1 = is_valid_host_within_brackets_char(char); + if ($1) { + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size + 1; + } else { + return parse_host_outside_of_brackets_loop( + original, + original, + pieces, + 0, + ); + } + } + } +} + +function parse_host_within_brackets(uri_string, pieces) { + return parse_host_within_brackets_loop(uri_string, uri_string, pieces, 0); +} + +function parse_host_outside_of_brackets(uri_string, pieces) { + return parse_host_outside_of_brackets_loop(uri_string, uri_string, pieces, 0); +} + +function parse_host(uri_string, pieces) { + if (uri_string.startsWith("[")) { + return parse_host_within_brackets(uri_string, pieces); + } else if (uri_string.startsWith(":")) { + let pieces$1 = new Uri( + pieces.scheme, + pieces.userinfo, + new Some(""), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_port(uri_string, pieces$1); + } else if (uri_string === "") { + return new Ok( + new Uri( + pieces.scheme, + pieces.userinfo, + new Some(""), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ), + ); + } else { + return parse_host_outside_of_brackets(uri_string, pieces); + } +} + +function parse_userinfo_loop( + loop$original, + loop$uri_string, + loop$pieces, + loop$size +) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size = loop$size; + if (uri_string.startsWith("@")) { + if (size === 0) { + let rest = uri_string.slice(1); + return parse_host(rest, pieces); + } else { + let rest = uri_string.slice(1); + let userinfo = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + pieces.scheme, + new Some(userinfo), + pieces.host, + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_host(rest, pieces$1); + } + } else if (uri_string === "") { + return parse_host(original, pieces); + } else if (uri_string.startsWith("/")) { + return parse_host(original, pieces); + } else if (uri_string.startsWith("?")) { + return parse_host(original, pieces); + } else if (uri_string.startsWith("#")) { + return parse_host(original, pieces); + } else { + let $ = pop_codeunit(uri_string); + let rest; + rest = $[1]; + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size + 1; + } + } +} + +function parse_authority_pieces(string, pieces) { + return parse_userinfo_loop(string, string, pieces, 0); +} + +function parse_authority_with_slashes(uri_string, pieces) { + if (uri_string === "//") { + return new Ok( + new Uri( + pieces.scheme, + pieces.userinfo, + new Some(""), + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ), + ); + } else if (uri_string.startsWith("//")) { + let rest = uri_string.slice(2); + return parse_authority_pieces(rest, pieces); + } else { + return parse_path(uri_string, pieces); + } +} + +function parse_scheme_loop( + loop$original, + loop$uri_string, + loop$pieces, + loop$size +) { + while (true) { + let original = loop$original; + let uri_string = loop$uri_string; + let pieces = loop$pieces; + let size = loop$size; + if (uri_string.startsWith("/")) { + if (size === 0) { + return parse_authority_with_slashes(uri_string, pieces); + } else { + let scheme = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + new Some($string.lowercase(scheme)), + pieces.userinfo, + pieces.host, + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_authority_with_slashes(uri_string, pieces$1); + } + } else if (uri_string.startsWith("?")) { + if (size === 0) { + let rest = uri_string.slice(1); + return parse_query_with_question_mark(rest, pieces); + } else { + let rest = uri_string.slice(1); + let scheme = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + new Some($string.lowercase(scheme)), + pieces.userinfo, + pieces.host, + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_query_with_question_mark(rest, pieces$1); + } + } else if (uri_string.startsWith("#")) { + if (size === 0) { + let rest = uri_string.slice(1); + return parse_fragment(rest, pieces); + } else { + let rest = uri_string.slice(1); + let scheme = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + new Some($string.lowercase(scheme)), + pieces.userinfo, + pieces.host, + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_fragment(rest, pieces$1); + } + } else if (uri_string.startsWith(":")) { + if (size === 0) { + return new Error(undefined); + } else { + let rest = uri_string.slice(1); + let scheme = codeunit_slice(original, 0, size); + let pieces$1 = new Uri( + new Some($string.lowercase(scheme)), + pieces.userinfo, + pieces.host, + pieces.port, + pieces.path, + pieces.query, + pieces.fragment, + ); + return parse_authority_with_slashes(rest, pieces$1); + } + } else if (uri_string === "") { + return new Ok( + new Uri( + pieces.scheme, + pieces.userinfo, + pieces.host, + pieces.port, + original, + pieces.query, + pieces.fragment, + ), + ); + } else { + let $ = pop_codeunit(uri_string); + let rest; + rest = $[1]; + loop$original = original; + loop$uri_string = rest; + loop$pieces = pieces; + loop$size = size + 1; + } + } +} + +function query_pair(pair) { + return $string_tree.from_strings( + toList([percent_encode(pair[0]), "=", percent_encode(pair[1])]), + ); +} + +/** + * Encodes a list of key value pairs as a URI query string. + * + * The opposite operation is `uri.parse_query`. + * + * ## Examples + * + * ```gleam + * query_to_string([#("a", "1"), #("b", "2")]) + * // -> "a=1&b=2" + * ``` + */ +export function query_to_string(query) { + let _pipe = query; + let _pipe$1 = $list.map(_pipe, query_pair); + let _pipe$2 = $list.intersperse(_pipe$1, $string_tree.from_string("&")); + let _pipe$3 = $string_tree.concat(_pipe$2); + return $string_tree.to_string(_pipe$3); +} + +function remove_dot_segments_loop(loop$input, loop$accumulator) { + while (true) { + let input = loop$input; + let accumulator = loop$accumulator; + if (input instanceof $Empty) { + return $list.reverse(accumulator); + } else { + let segment = input.head; + let rest = input.tail; + let _block; + if (segment === "") { + _block = accumulator; + } else if (segment === ".") { + _block = accumulator; + } else if (segment === "..") { + if (accumulator instanceof $Empty) { + _block = accumulator; + } else { + let accumulator$1 = accumulator.tail; + _block = accumulator$1; + } + } else { + let segment$1 = segment; + let accumulator$1 = accumulator; + _block = listPrepend(segment$1, accumulator$1); + } + let accumulator$1 = _block; + loop$input = rest; + loop$accumulator = accumulator$1; + } + } +} + +function remove_dot_segments(input) { + return remove_dot_segments_loop(input, toList([])); +} + +/** + * Splits the path section of a URI into it's constituent segments. + * + * Removes empty segments and resolves dot-segments as specified in + * [section 5.2](https://www.ietf.org/rfc/rfc3986.html#section-5.2) of the RFC. + * + * ## Examples + * + * ```gleam + * path_segments("/users/1") + * // -> ["users" ,"1"] + * ``` + */ +export function path_segments(path) { + return remove_dot_segments($string.split(path, "/")); +} + +/** + * Encodes a `Uri` value as a URI string. + * + * The opposite operation is `uri.parse`. + * + * ## Examples + * + * ```gleam + * let uri = Uri(..empty, scheme: Some("https"), host: Some("example.com")) + * to_string(uri) + * // -> "https://example.com" + * ``` + */ +export function to_string(uri) { + let _block; + let $ = uri.fragment; + if ($ instanceof Some) { + let fragment = $[0]; + _block = toList(["#", fragment]); + } else { + _block = toList([]); + } + let parts = _block; + let _block$1; + let $1 = uri.query; + if ($1 instanceof Some) { + let query = $1[0]; + _block$1 = listPrepend("?", listPrepend(query, parts)); + } else { + _block$1 = parts; + } + let parts$1 = _block$1; + let parts$2 = listPrepend(uri.path, parts$1); + let _block$2; + let $2 = uri.host; + let $3 = $string.starts_with(uri.path, "/"); + if ($2 instanceof Some && !$3) { + let host = $2[0]; + if (host !== "") { + _block$2 = listPrepend("/", parts$2); + } else { + _block$2 = parts$2; + } + } else { + _block$2 = parts$2; + } + let parts$3 = _block$2; + let _block$3; + let $4 = uri.host; + let $5 = uri.port; + if ($4 instanceof Some && $5 instanceof Some) { + let port = $5[0]; + _block$3 = listPrepend(":", listPrepend($int.to_string(port), parts$3)); + } else { + _block$3 = parts$3; + } + let parts$4 = _block$3; + let _block$4; + let $6 = uri.scheme; + let $7 = uri.userinfo; + let $8 = uri.host; + if ($6 instanceof Some) { + if ($7 instanceof Some) { + if ($8 instanceof Some) { + let s = $6[0]; + let u = $7[0]; + let h = $8[0]; + _block$4 = listPrepend( + s, + listPrepend( + "://", + listPrepend(u, listPrepend("@", listPrepend(h, parts$4))), + ), + ); + } else { + let s = $6[0]; + _block$4 = listPrepend(s, listPrepend(":", parts$4)); + } + } else if ($8 instanceof Some) { + let s = $6[0]; + let h = $8[0]; + _block$4 = listPrepend(s, listPrepend("://", listPrepend(h, parts$4))); + } else { + let s = $6[0]; + _block$4 = listPrepend(s, listPrepend(":", parts$4)); + } + } else if ($7 instanceof None && $8 instanceof Some) { + let h = $8[0]; + _block$4 = listPrepend("//", listPrepend(h, parts$4)); + } else { + _block$4 = parts$4; + } + let parts$5 = _block$4; + return $string.concat(parts$5); +} + +/** + * Fetches the origin of a URI. + * + * Returns the origin of a uri as defined in + * [RFC 6454](https://tools.ietf.org/html/rfc6454) + * + * The supported URI schemes are `http` and `https`. + * URLs without a scheme will return `Error`. + * + * ## Examples + * + * ```gleam + * let assert Ok(uri) = parse("https://example.com/path?foo#bar") + * origin(uri) + * // -> Ok("https://example.com") + * ``` + */ +export function origin(uri) { + let scheme; + let host; + let port; + scheme = uri.scheme; + host = uri.host; + port = uri.port; + if (host instanceof Some && scheme instanceof Some) { + let $ = scheme[0]; + if ($ === "https" && isEqual(port, new Some(443))) { + let h = host[0]; + return new Ok($string.concat(toList(["https://", h]))); + } else if ($ === "http" && isEqual(port, new Some(80))) { + let h = host[0]; + return new Ok($string.concat(toList(["http://", h]))); + } else { + let s = $; + if ((s === "http") || (s === "https")) { + let h = host[0]; + if (port instanceof Some) { + let p = port[0]; + return new Ok( + $string.concat(toList([s, "://", h, ":", $int.to_string(p)])), + ); + } else { + return new Ok($string.concat(toList([s, "://", h]))); + } + } else { + return new Error(undefined); + } + } + } else { + return new Error(undefined); + } +} + +function drop_last(elements) { + return $list.take(elements, $list.length(elements) - 1); +} + +function join_segments(segments) { + return $string.join(listPrepend("", segments), "/"); +} + +/** + * Resolves a URI with respect to the given base URI. + * + * The base URI must be an absolute URI or this function will return an error. + * The algorithm for merging uris is described in + * [RFC 3986](https://tools.ietf.org/html/rfc3986#section-5.2). + */ +export function merge(base, relative) { + let $ = base.scheme; + if ($ instanceof Some) { + let $1 = base.host; + if ($1 instanceof Some) { + let $2 = relative.host; + if ($2 instanceof Some) { + let _block; + let _pipe = relative.path; + let _pipe$1 = $string.split(_pipe, "/"); + let _pipe$2 = remove_dot_segments(_pipe$1); + _block = join_segments(_pipe$2); + let path = _block; + let resolved = new Uri( + $option.or(relative.scheme, base.scheme), + new None(), + relative.host, + $option.or(relative.port, base.port), + path, + relative.query, + relative.fragment, + ); + return new Ok(resolved); + } else { + let _block; + let $4 = relative.path; + if ($4 === "") { + _block = [base.path, $option.or(relative.query, base.query)]; + } else { + let _block$1; + let $5 = $string.starts_with(relative.path, "/"); + if ($5) { + _block$1 = $string.split(relative.path, "/"); + } else { + let _pipe = base.path; + let _pipe$1 = $string.split(_pipe, "/"); + let _pipe$2 = drop_last(_pipe$1); + _block$1 = $list.append(_pipe$2, $string.split(relative.path, "/")); + } + let path_segments$1 = _block$1; + let _block$2; + let _pipe = path_segments$1; + let _pipe$1 = remove_dot_segments(_pipe); + _block$2 = join_segments(_pipe$1); + let path = _block$2; + _block = [path, relative.query]; + } + let $3 = _block; + let new_path; + let new_query; + new_path = $3[0]; + new_query = $3[1]; + let resolved = new Uri( + base.scheme, + new None(), + base.host, + base.port, + new_path, + new_query, + relative.fragment, + ); + return new Ok(resolved); + } + } else { + return new Error(undefined); + } + } else { + return new Error(undefined); + } +} + +/** + * Constant representing an empty URI, equivalent to "". + * + * ## Examples + * + * ```gleam + * let uri = Uri(..empty, scheme: Some("https"), host: Some("example.com")) + * // -> Uri( + * // scheme: Some("https"), + * // userinfo: None, + * // host: Some("example.com"), + * // port: None, + * // path: "", + * // query: None, + * // fragment: None, + * // ) + * ``` + */ +export const empty = /* @__PURE__ */ new Uri( + /* @__PURE__ */ new None(), + /* @__PURE__ */ new None(), + /* @__PURE__ */ new None(), + /* @__PURE__ */ new None(), + "", + /* @__PURE__ */ new None(), + /* @__PURE__ */ new None(), +); + +/** + * Parses a compliant URI string into the `Uri` Type. + * If the string is not a valid URI string then an error is returned. + * + * The opposite operation is `uri.to_string`. + * + * ## Examples + * + * ```gleam + * parse("https://example.com:1234/a/b?query=true#fragment") + * // -> Ok( + * // Uri( + * // scheme: Some("https"), + * // userinfo: None, + * // host: Some("example.com"), + * // port: Some(1234), + * // path: "/a/b", + * // query: Some("query=true"), + * // fragment: Some("fragment") + * // ) + * // ) + * ``` + */ +export function parse(uri_string) { + return parse_scheme_loop(uri_string, uri_string, empty, 0); +} diff --git a/build/dev/javascript/gleam_stdlib/gleam@bit_array.erl b/build/dev/javascript/gleam_stdlib/gleam@bit_array.erl new file mode 100644 index 0000000..7df56ce --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@bit_array.erl @@ -0,0 +1,347 @@ +-module(gleam@bit_array). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/bit_array.gleam"). +-export([from_string/1, bit_size/1, byte_size/1, pad_to_bytes/1, slice/3, is_utf8/1, to_string/1, concat/1, append/2, base64_encode/2, base64_decode/1, base64_url_encode/2, base64_url_decode/1, base16_encode/1, base16_decode/1, inspect/1, compare/2, starts_with/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC(" BitArrays are a sequence of binary data of any length.\n"). + +-file("src/gleam/bit_array.gleam", 11). +?DOC(" Converts a UTF-8 `String` type into a `BitArray`.\n"). +-spec from_string(binary()) -> bitstring(). +from_string(X) -> + gleam_stdlib:identity(X). + +-file("src/gleam/bit_array.gleam", 17). +?DOC(" Returns an integer which is the number of bits in the bit array.\n"). +-spec bit_size(bitstring()) -> integer(). +bit_size(X) -> + erlang:bit_size(X). + +-file("src/gleam/bit_array.gleam", 23). +?DOC(" Returns an integer which is the number of bytes in the bit array.\n"). +-spec byte_size(bitstring()) -> integer(). +byte_size(X) -> + erlang:byte_size(X). + +-file("src/gleam/bit_array.gleam", 29). +?DOC(" Pads a bit array with zeros so that it is a whole number of bytes.\n"). +-spec pad_to_bytes(bitstring()) -> bitstring(). +pad_to_bytes(X) -> + gleam_stdlib:bit_array_pad_to_bytes(X). + +-file("src/gleam/bit_array.gleam", 54). +?DOC( + " Extracts a sub-section of a bit array.\n" + "\n" + " The slice will start at given position and continue up to specified\n" + " length.\n" + " A negative length can be used to extract bytes at the end of a bit array.\n" + "\n" + " This function runs in constant time.\n" +). +-spec slice(bitstring(), integer(), integer()) -> {ok, bitstring()} | + {error, nil}. +slice(String, Position, Length) -> + gleam_stdlib:bit_array_slice(String, Position, Length). + +-file("src/gleam/bit_array.gleam", 67). +-spec is_utf8_loop(bitstring()) -> boolean(). +is_utf8_loop(Bits) -> + case Bits of + <<>> -> + true; + + <<_/utf8, Rest/binary>> -> + is_utf8_loop(Rest); + + _ -> + false + end. + +-file("src/gleam/bit_array.gleam", 62). +?DOC(" Tests to see whether a bit array is valid UTF-8.\n"). +-spec is_utf8(bitstring()) -> boolean(). +is_utf8(Bits) -> + is_utf8_loop(Bits). + +-file("src/gleam/bit_array.gleam", 88). +?DOC( + " Converts a bit array to a string.\n" + "\n" + " Returns an error if the bit array is invalid UTF-8 data.\n" +). +-spec to_string(bitstring()) -> {ok, binary()} | {error, nil}. +to_string(Bits) -> + case is_utf8(Bits) of + true -> + {ok, gleam_stdlib:identity(Bits)}; + + false -> + {error, nil} + end. + +-file("src/gleam/bit_array.gleam", 109). +?DOC( + " Creates a new bit array by joining multiple binaries.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " concat([from_string(\"butter\"), from_string(\"fly\")])\n" + " // -> from_string(\"butterfly\")\n" + " ```\n" +). +-spec concat(list(bitstring())) -> bitstring(). +concat(Bit_arrays) -> + gleam_stdlib:bit_array_concat(Bit_arrays). + +-file("src/gleam/bit_array.gleam", 40). +?DOC( + " Creates a new bit array by joining two bit arrays.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " append(to: from_string(\"butter\"), suffix: from_string(\"fly\"))\n" + " // -> from_string(\"butterfly\")\n" + " ```\n" +). +-spec append(bitstring(), bitstring()) -> bitstring(). +append(First, Second) -> + gleam_stdlib:bit_array_concat([First, Second]). + +-file("src/gleam/bit_array.gleam", 118). +?DOC( + " Encodes a BitArray into a base 64 encoded string.\n" + "\n" + " If the bit array does not contain a whole number of bytes then it is padded\n" + " with zero bits prior to being encoded.\n" +). +-spec base64_encode(bitstring(), boolean()) -> binary(). +base64_encode(Input, Padding) -> + gleam_stdlib:base64_encode(Input, Padding). + +-file("src/gleam/bit_array.gleam", 122). +?DOC(" Decodes a base 64 encoded string into a `BitArray`.\n"). +-spec base64_decode(binary()) -> {ok, bitstring()} | {error, nil}. +base64_decode(Encoded) -> + Padded = case erlang:byte_size(gleam_stdlib:identity(Encoded)) rem 4 of + 0 -> + Encoded; + + N -> + gleam@string:append( + Encoded, + gleam@string:repeat(<<"="/utf8>>, 4 - N) + ) + end, + gleam_stdlib:base64_decode(Padded). + +-file("src/gleam/bit_array.gleam", 140). +?DOC( + " Encodes a `BitArray` into a base 64 encoded string with URL and filename\n" + " safe alphabet.\n" + "\n" + " If the bit array does not contain a whole number of bytes then it is padded\n" + " with zero bits prior to being encoded.\n" +). +-spec base64_url_encode(bitstring(), boolean()) -> binary(). +base64_url_encode(Input, Padding) -> + _pipe = Input, + _pipe@1 = gleam_stdlib:base64_encode(_pipe, Padding), + _pipe@2 = gleam@string:replace(_pipe@1, <<"+"/utf8>>, <<"-"/utf8>>), + gleam@string:replace(_pipe@2, <<"/"/utf8>>, <<"_"/utf8>>). + +-file("src/gleam/bit_array.gleam", 150). +?DOC( + " Decodes a base 64 encoded string with URL and filename safe alphabet into a\n" + " `BitArray`.\n" +). +-spec base64_url_decode(binary()) -> {ok, bitstring()} | {error, nil}. +base64_url_decode(Encoded) -> + _pipe = Encoded, + _pipe@1 = gleam@string:replace(_pipe, <<"-"/utf8>>, <<"+"/utf8>>), + _pipe@2 = gleam@string:replace(_pipe@1, <<"_"/utf8>>, <<"/"/utf8>>), + base64_decode(_pipe@2). + +-file("src/gleam/bit_array.gleam", 164). +?DOC( + " Encodes a `BitArray` into a base 16 encoded string.\n" + "\n" + " If the bit array does not contain a whole number of bytes then it is padded\n" + " with zero bits prior to being encoded.\n" +). +-spec base16_encode(bitstring()) -> binary(). +base16_encode(Input) -> + gleam_stdlib:base16_encode(Input). + +-file("src/gleam/bit_array.gleam", 170). +?DOC(" Decodes a base 16 encoded string into a `BitArray`.\n"). +-spec base16_decode(binary()) -> {ok, bitstring()} | {error, nil}. +base16_decode(Input) -> + gleam_stdlib:base16_decode(Input). + +-file("src/gleam/bit_array.gleam", 191). +-spec inspect_loop(bitstring(), binary()) -> binary(). +inspect_loop(Input, Accumulator) -> + case Input of + <<>> -> + Accumulator; + + <> -> + <<<>/binary, + ":size(1)"/utf8>>; + + <> -> + <<<>/binary, + ":size(2)"/utf8>>; + + <> -> + <<<>/binary, + ":size(3)"/utf8>>; + + <> -> + <<<>/binary, + ":size(4)"/utf8>>; + + <> -> + <<<>/binary, + ":size(5)"/utf8>>; + + <> -> + <<<>/binary, + ":size(6)"/utf8>>; + + <> -> + <<<>/binary, + ":size(7)"/utf8>>; + + <> -> + Suffix = case Rest of + <<>> -> + <<""/utf8>>; + + _ -> + <<", "/utf8>> + end, + Accumulator@1 = <<<>/binary, + Suffix/binary>>, + inspect_loop(Rest, Accumulator@1); + + _ -> + Accumulator + end. + +-file("src/gleam/bit_array.gleam", 187). +?DOC( + " Converts a bit array to a string containing the decimal value of each byte.\n" + "\n" + " Use this over `string.inspect` when you have a bit array you want printed\n" + " in the array syntax even if it is valid UTF-8.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " inspect(<<0, 20, 0x20, 255>>)\n" + " // -> \"<<0, 20, 32, 255>>\"\n" + "\n" + " inspect(<<100, 5:3>>)\n" + " // -> \"<<100, 5:size(3)>>\"\n" + " ```\n" +). +-spec inspect(bitstring()) -> binary(). +inspect(Input) -> + <<(inspect_loop(Input, <<"<<"/utf8>>))/binary, ">>"/utf8>>. + +-file("src/gleam/bit_array.gleam", 232). +?DOC( + " Compare two bit arrays as sequences of bytes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " compare(<<1>>, <<2>>)\n" + " // -> Lt\n" + "\n" + " compare(<<\"AB\":utf8>>, <<\"AA\":utf8>>)\n" + " // -> Gt\n" + "\n" + " compare(<<1, 2:size(2)>>, with: <<1, 2:size(2)>>)\n" + " // -> Eq\n" + " ```\n" +). +-spec compare(bitstring(), bitstring()) -> gleam@order:order(). +compare(A, B) -> + case {A, B} of + {<>, + <>} -> + case {First_byte, Second_byte} of + {F, S} when F > S -> + gt; + + {F@1, S@1} when F@1 < S@1 -> + lt; + + {_, _} -> + compare(First_rest, Second_rest) + end; + + {<<>>, <<>>} -> + eq; + + {_, <<>>} -> + gt; + + {<<>>, _} -> + lt; + + {First, Second} -> + case {gleam_stdlib:bit_array_to_int_and_size(First), + gleam_stdlib:bit_array_to_int_and_size(Second)} of + {{A@1, _}, {B@1, _}} when A@1 > B@1 -> + gt; + + {{A@2, _}, {B@2, _}} when A@2 < B@2 -> + lt; + + {{_, Size_a}, {_, Size_b}} when Size_a > Size_b -> + gt; + + {{_, Size_a@1}, {_, Size_b@1}} when Size_a@1 < Size_b@1 -> + lt; + + {_, _} -> + eq + end + end. + +-file("src/gleam/bit_array.gleam", 273). +?DOC( + " Checks whether the first `BitArray` starts with the second one.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " starts_with(<<1, 2, 3, 4>>, <<1, 2>>)\n" + " // -> True\n" + " ```\n" +). +-spec starts_with(bitstring(), bitstring()) -> boolean(). +starts_with(Bits, Prefix) -> + Prefix_size = erlang:bit_size(Prefix), + case Bits of + <> when Pref =:= Prefix -> + true; + + _ -> + false + end. diff --git a/build/dev/javascript/gleam_stdlib/gleam@bool.erl b/build/dev/javascript/gleam_stdlib/gleam@bool.erl new file mode 100644 index 0000000..01307b3 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@bool.erl @@ -0,0 +1,352 @@ +-module(gleam@bool). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/bool.gleam"). +-export(['and'/2, 'or'/2, negate/1, nor/2, nand/2, exclusive_or/2, exclusive_nor/2, to_string/1, guard/3, lazy_guard/3]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " A type with two possible values, `True` and `False`. Used to indicate whether\n" + " things are... true or false!\n" + "\n" + " Often is it clearer and offers more type safety to define a custom type\n" + " than to use `Bool`. For example, rather than having a `is_teacher: Bool`\n" + " field consider having a `role: SchoolRole` field where `SchoolRole` is a custom\n" + " type that can be either `Student` or `Teacher`.\n" +). + +-file("src/gleam/bool.gleam", 31). +?DOC( + " Returns the and of two bools, but it evaluates both arguments.\n" + "\n" + " It's the function equivalent of the `&&` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " and(True, True)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " and(False, True)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " False |> and(True)\n" + " // -> False\n" + " ```\n" +). +-spec 'and'(boolean(), boolean()) -> boolean(). +'and'(A, B) -> + A andalso B. + +-file("src/gleam/bool.gleam", 57). +?DOC( + " Returns the or of two bools, but it evaluates both arguments.\n" + "\n" + " It's the function equivalent of the `||` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " or(True, True)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " or(False, True)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " False |> or(True)\n" + " // -> True\n" + " ```\n" +). +-spec 'or'(boolean(), boolean()) -> boolean(). +'or'(A, B) -> + A orelse B. + +-file("src/gleam/bool.gleam", 77). +?DOC( + " Returns the opposite bool value.\n" + "\n" + " This is the same as the `!` or `not` operators in some other languages.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " negate(True)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " negate(False)\n" + " // -> True\n" + " ```\n" +). +-spec negate(boolean()) -> boolean(). +negate(Bool) -> + not Bool. + +-file("src/gleam/bool.gleam", 105). +?DOC( + " Returns the nor of two bools.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " nor(False, False)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " nor(False, True)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " nor(True, False)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " nor(True, True)\n" + " // -> False\n" + " ```\n" +). +-spec nor(boolean(), boolean()) -> boolean(). +nor(A, B) -> + not (A orelse B). + +-file("src/gleam/bool.gleam", 133). +?DOC( + " Returns the nand of two bools.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " nand(False, False)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " nand(False, True)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " nand(True, False)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " nand(True, True)\n" + " // -> False\n" + " ```\n" +). +-spec nand(boolean(), boolean()) -> boolean(). +nand(A, B) -> + not (A andalso B). + +-file("src/gleam/bool.gleam", 161). +?DOC( + " Returns the exclusive or of two bools.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " exclusive_or(False, False)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_or(False, True)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_or(True, False)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_or(True, True)\n" + " // -> False\n" + " ```\n" +). +-spec exclusive_or(boolean(), boolean()) -> boolean(). +exclusive_or(A, B) -> + A /= B. + +-file("src/gleam/bool.gleam", 189). +?DOC( + " Returns the exclusive nor of two bools.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " exclusive_nor(False, False)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_nor(False, True)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_nor(True, False)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_nor(True, True)\n" + " // -> True\n" + " ```\n" +). +-spec exclusive_nor(boolean(), boolean()) -> boolean(). +exclusive_nor(A, B) -> + A =:= B. + +-file("src/gleam/bool.gleam", 207). +?DOC( + " Returns a string representation of the given bool.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_string(True)\n" + " // -> \"True\"\n" + " ```\n" + "\n" + " ```gleam\n" + " to_string(False)\n" + " // -> \"False\"\n" + " ```\n" +). +-spec to_string(boolean()) -> binary(). +to_string(Bool) -> + case Bool of + false -> + <<"False"/utf8>>; + + true -> + <<"True"/utf8>> + end. + +-file("src/gleam/bool.gleam", 266). +?DOC( + " Run a callback function if the given bool is `False`, otherwise return a\n" + " default value.\n" + "\n" + " With a `use` expression this function can simulate the early-return pattern\n" + " found in some other programming languages.\n" + "\n" + " In a procedural language:\n" + "\n" + " ```js\n" + " if (predicate) return value;\n" + " // ...\n" + " ```\n" + "\n" + " In Gleam with a `use` expression:\n" + "\n" + " ```gleam\n" + " use <- guard(when: predicate, return: value)\n" + " // ...\n" + " ```\n" + "\n" + " Like everything in Gleam `use` is an expression, so it short circuits the\n" + " current block, not the entire function. As a result you can assign the value\n" + " to a variable:\n" + "\n" + " ```gleam\n" + " let x = {\n" + " use <- guard(when: predicate, return: value)\n" + " // ...\n" + " }\n" + " ```\n" + "\n" + " Note that unlike in procedural languages the `return` value is evaluated\n" + " even when the predicate is `False`, so it is advisable not to perform\n" + " expensive computation nor side-effects there.\n" + "\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let name = \"\"\n" + " use <- guard(when: name == \"\", return: \"Welcome!\")\n" + " \"Hello, \" <> name\n" + " // -> \"Welcome!\"\n" + " ```\n" + "\n" + " ```gleam\n" + " let name = \"Kamaka\"\n" + " use <- guard(when: name == \"\", return: \"Welcome!\")\n" + " \"Hello, \" <> name\n" + " // -> \"Hello, Kamaka\"\n" + " ```\n" +). +-spec guard(boolean(), BSY, fun(() -> BSY)) -> BSY. +guard(Requirement, Consequence, Alternative) -> + case Requirement of + true -> + Consequence; + + false -> + Alternative() + end. + +-file("src/gleam/bool.gleam", 307). +?DOC( + " Runs a callback function if the given bool is `True`, otherwise runs an\n" + " alternative callback function.\n" + "\n" + " Useful when further computation should be delayed regardless of the given\n" + " bool's value.\n" + "\n" + " See [`guard`](#guard) for more info.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let name = \"Kamaka\"\n" + " let inquiry = fn() { \"How may we address you?\" }\n" + " use <- lazy_guard(when: name == \"\", return: inquiry)\n" + " \"Hello, \" <> name\n" + " // -> \"Hello, Kamaka\"\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " let name = \"\"\n" + " let greeting = fn() { \"Hello, \" <> name }\n" + " use <- lazy_guard(when: name == \"\", otherwise: greeting)\n" + " let number = int.random(99)\n" + " let name = \"User \" <> int.to_string(number)\n" + " \"Welcome, \" <> name\n" + " // -> \"Welcome, User 54\"\n" + " ```\n" +). +-spec lazy_guard(boolean(), fun(() -> BSZ), fun(() -> BSZ)) -> BSZ. +lazy_guard(Requirement, Consequence, Alternative) -> + case Requirement of + true -> + Consequence(); + + false -> + Alternative() + end. diff --git a/build/dev/javascript/gleam_stdlib/gleam@bytes_tree.erl b/build/dev/javascript/gleam_stdlib/gleam@bytes_tree.erl new file mode 100644 index 0000000..a96eaa2 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@bytes_tree.erl @@ -0,0 +1,211 @@ +-module(gleam@bytes_tree). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/bytes_tree.gleam"). +-export([append_tree/2, prepend_tree/2, concat/1, new/0, from_string/1, prepend_string/2, append_string/2, from_string_tree/1, from_bit_array/1, prepend/2, append/2, concat_bit_arrays/1, to_bit_array/1, byte_size/1]). +-export_type([bytes_tree/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " `BytesTree` is a type used for efficiently building binary content to be\n" + " written to a file or a socket. Internally it is represented as tree so to\n" + " append or prepend to a bytes tree is a constant time operation that\n" + " allocates a new node in the tree without copying any of the content. When\n" + " writing to an output stream the tree is traversed and the content is sent\n" + " directly rather than copying it into a single buffer beforehand.\n" + "\n" + " If we append one bit array to another the bit arrays must be copied to a\n" + " new location in memory so that they can sit together. This behaviour\n" + " enables efficient reading of the data but copying can be expensive,\n" + " especially if we want to join many bit arrays together.\n" + "\n" + " BytesTree is different in that it can be joined together in constant\n" + " time using minimal memory, and then can be efficiently converted to a\n" + " bit array using the `to_bit_array` function.\n" + "\n" + " Byte trees are always byte aligned, so that a number of bits that is not\n" + " divisible by 8 will be padded with 0s.\n" + "\n" + " On Erlang this type is compatible with Erlang's iolists.\n" +). + +-opaque bytes_tree() :: {bytes, bitstring()} | + {text, gleam@string_tree:string_tree()} | + {many, list(bytes_tree())}. + +-file("src/gleam/bytes_tree.gleam", 68). +?DOC( + " Appends a bytes tree onto the end of another.\n" + "\n" + " Runs in constant time.\n" +). +-spec append_tree(bytes_tree(), bytes_tree()) -> bytes_tree(). +append_tree(First, Second) -> + gleam_stdlib:iodata_append(First, Second). + +-file("src/gleam/bytes_tree.gleam", 59). +?DOC( + " Prepends a bytes tree onto the start of another.\n" + "\n" + " Runs in constant time.\n" +). +-spec prepend_tree(bytes_tree(), bytes_tree()) -> bytes_tree(). +prepend_tree(Second, First) -> + gleam_stdlib:iodata_append(First, Second). + +-file("src/gleam/bytes_tree.gleam", 98). +?DOC( + " Joins a list of bytes trees into a single one.\n" + "\n" + " Runs in constant time.\n" +). +-spec concat(list(bytes_tree())) -> bytes_tree(). +concat(Trees) -> + gleam_stdlib:identity(Trees). + +-file("src/gleam/bytes_tree.gleam", 35). +?DOC( + " Create an empty `BytesTree`. Useful as the start of a pipe chaining many\n" + " trees together.\n" +). +-spec new() -> bytes_tree(). +new() -> + gleam_stdlib:identity([]). + +-file("src/gleam/bytes_tree.gleam", 118). +?DOC( + " Creates a new bytes tree from a string.\n" + "\n" + " Runs in constant time when running on Erlang.\n" + " Runs in linear time otherwise.\n" +). +-spec from_string(binary()) -> bytes_tree(). +from_string(String) -> + gleam_stdlib:wrap_list(String). + +-file("src/gleam/bytes_tree.gleam", 80). +?DOC( + " Prepends a string onto the start of a bytes tree.\n" + "\n" + " Runs in constant time when running on Erlang.\n" + " Runs in linear time with the length of the string otherwise.\n" +). +-spec prepend_string(bytes_tree(), binary()) -> bytes_tree(). +prepend_string(Second, First) -> + gleam_stdlib:iodata_append(gleam_stdlib:wrap_list(First), Second). + +-file("src/gleam/bytes_tree.gleam", 89). +?DOC( + " Appends a string onto the end of a bytes tree.\n" + "\n" + " Runs in constant time when running on Erlang.\n" + " Runs in linear time with the length of the string otherwise.\n" +). +-spec append_string(bytes_tree(), binary()) -> bytes_tree(). +append_string(First, Second) -> + gleam_stdlib:iodata_append(First, gleam_stdlib:wrap_list(Second)). + +-file("src/gleam/bytes_tree.gleam", 128). +?DOC( + " Creates a new bytes tree from a string tree.\n" + "\n" + " Runs in constant time when running on Erlang.\n" + " Runs in linear time otherwise.\n" +). +-spec from_string_tree(gleam@string_tree:string_tree()) -> bytes_tree(). +from_string_tree(Tree) -> + gleam_stdlib:wrap_list(Tree). + +-file("src/gleam/bytes_tree.gleam", 136). +?DOC( + " Creates a new bytes tree from a bit array.\n" + "\n" + " Runs in constant time.\n" +). +-spec from_bit_array(bitstring()) -> bytes_tree(). +from_bit_array(Bits) -> + _pipe = Bits, + _pipe@1 = gleam_stdlib:bit_array_pad_to_bytes(_pipe), + gleam_stdlib:wrap_list(_pipe@1). + +-file("src/gleam/bytes_tree.gleam", 43). +?DOC( + " Prepends a bit array to the start of a bytes tree.\n" + "\n" + " Runs in constant time.\n" +). +-spec prepend(bytes_tree(), bitstring()) -> bytes_tree(). +prepend(Second, First) -> + gleam_stdlib:iodata_append(from_bit_array(First), Second). + +-file("src/gleam/bytes_tree.gleam", 51). +?DOC( + " Appends a bit array to the end of a bytes tree.\n" + "\n" + " Runs in constant time.\n" +). +-spec append(bytes_tree(), bitstring()) -> bytes_tree(). +append(First, Second) -> + gleam_stdlib:iodata_append(First, from_bit_array(Second)). + +-file("src/gleam/bytes_tree.gleam", 106). +?DOC( + " Joins a list of bit arrays into a single bytes tree.\n" + "\n" + " Runs in constant time.\n" +). +-spec concat_bit_arrays(list(bitstring())) -> bytes_tree(). +concat_bit_arrays(Bits) -> + _pipe = Bits, + _pipe@1 = gleam@list:map(_pipe, fun from_bit_array/1), + gleam_stdlib:identity(_pipe@1). + +-file("src/gleam/bytes_tree.gleam", 162). +-spec to_list(list(list(bytes_tree())), list(bitstring())) -> list(bitstring()). +to_list(Stack, Acc) -> + case Stack of + [] -> + Acc; + + [[] | Remaining_stack] -> + to_list(Remaining_stack, Acc); + + [[{bytes, Bits} | Rest] | Remaining_stack@1] -> + to_list([Rest | Remaining_stack@1], [Bits | Acc]); + + [[{text, Tree} | Rest@1] | Remaining_stack@2] -> + Bits@1 = gleam_stdlib:identity(unicode:characters_to_binary(Tree)), + to_list([Rest@1 | Remaining_stack@2], [Bits@1 | Acc]); + + [[{many, Trees} | Rest@2] | Remaining_stack@3] -> + to_list([Trees, Rest@2 | Remaining_stack@3], Acc) + end. + +-file("src/gleam/bytes_tree.gleam", 155). +?DOC( + " Turns a bytes tree into a bit array.\n" + "\n" + " Runs in linear time.\n" + "\n" + " When running on Erlang this function is implemented natively by the\n" + " virtual machine and is highly optimised.\n" +). +-spec to_bit_array(bytes_tree()) -> bitstring(). +to_bit_array(Tree) -> + erlang:list_to_bitstring(Tree). + +-file("src/gleam/bytes_tree.gleam", 186). +?DOC( + " Returns the size of the bytes tree's content in bytes.\n" + "\n" + " Runs in linear time.\n" +). +-spec byte_size(bytes_tree()) -> integer(). +byte_size(Tree) -> + erlang:iolist_size(Tree). diff --git a/build/dev/javascript/gleam_stdlib/gleam@dict.erl b/build/dev/javascript/gleam_stdlib/gleam@dict.erl new file mode 100644 index 0000000..d496afb --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@dict.erl @@ -0,0 +1,561 @@ +-module(gleam@dict). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/dict.gleam"). +-export([size/1, is_empty/1, to_list/1, new/0, get/2, has_key/2, insert/3, from_list/1, keys/1, values/1, take/2, merge/2, delete/2, drop/2, upsert/3, fold/3, map_values/2, filter/2, each/2, combine/3]). +-export_type([dict/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type dict(KG, KH) :: any() | {gleam_phantom, KG, KH}. + +-file("src/gleam/dict.gleam", 36). +?DOC( + " Determines the number of key-value pairs in the dict.\n" + " This function runs in constant time and does not need to iterate the dict.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> size\n" + " // -> 0\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(\"key\", \"value\") |> size\n" + " // -> 1\n" + " ```\n" +). +-spec size(dict(any(), any())) -> integer(). +size(Dict) -> + maps:size(Dict). + +-file("src/gleam/dict.gleam", 52). +?DOC( + " Determines whether or not the dict is empty.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> is_empty\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(\"b\", 1) |> is_empty\n" + " // -> False\n" + " ```\n" +). +-spec is_empty(dict(any(), any())) -> boolean(). +is_empty(Dict) -> + maps:size(Dict) =:= 0. + +-file("src/gleam/dict.gleam", 80). +?DOC( + " Converts the dict to a list of 2-element tuples `#(key, value)`, one for\n" + " each key-value pair in the dict.\n" + "\n" + " The tuples in the list have no specific order.\n" + "\n" + " ## Examples\n" + "\n" + " Calling `to_list` on an empty `dict` returns an empty list.\n" + "\n" + " ```gleam\n" + " new() |> to_list\n" + " // -> []\n" + " ```\n" + "\n" + " The ordering of elements in the resulting list is an implementation detail\n" + " that should not be relied upon.\n" + "\n" + " ```gleam\n" + " new() |> insert(\"b\", 1) |> insert(\"a\", 0) |> insert(\"c\", 2) |> to_list\n" + " // -> [#(\"a\", 0), #(\"b\", 1), #(\"c\", 2)]\n" + " ```\n" +). +-spec to_list(dict(KQ, KR)) -> list({KQ, KR}). +to_list(Dict) -> + maps:to_list(Dict). + +-file("src/gleam/dict.gleam", 129). +?DOC(" Creates a fresh dict that contains no values.\n"). +-spec new() -> dict(any(), any()). +new() -> + maps:new(). + +-file("src/gleam/dict.gleam", 150). +?DOC( + " Fetches a value from a dict for a given key.\n" + "\n" + " The dict may not have a value for the key, so the value is wrapped in a\n" + " `Result`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0) |> get(\"a\")\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0) |> get(\"b\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec get(dict(LT, LU), LT) -> {ok, LU} | {error, nil}. +get(From, Get) -> + gleam_stdlib:map_get(From, Get). + +-file("src/gleam/dict.gleam", 116). +?DOC( + " Determines whether or not a value present in the dict for a given key.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0) |> has_key(\"a\")\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0) |> has_key(\"b\")\n" + " // -> False\n" + " ```\n" +). +-spec has_key(dict(LH, any()), LH) -> boolean(). +has_key(Dict, Key) -> + maps:is_key(Key, Dict). + +-file("src/gleam/dict.gleam", 169). +?DOC( + " Inserts a value into the dict with the given key.\n" + "\n" + " If the dict already has a value for the given key then the value is\n" + " replaced with the new value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0)\n" + " // -> from_list([#(\"a\", 0)])\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0) |> insert(\"a\", 5)\n" + " // -> from_list([#(\"a\", 5)])\n" + " ```\n" +). +-spec insert(dict(LZ, MA), LZ, MA) -> dict(LZ, MA). +insert(Dict, Key, Value) -> + maps:put(Key, Value, Dict). + +-file("src/gleam/dict.gleam", 92). +-spec from_list_loop(list({LA, LB}), dict(LA, LB)) -> dict(LA, LB). +from_list_loop(List, Initial) -> + case List of + [] -> + Initial; + + [{Key, Value} | Rest] -> + from_list_loop(Rest, insert(Initial, Key, Value)) + end. + +-file("src/gleam/dict.gleam", 88). +?DOC( + " Converts a list of 2-element tuples `#(key, value)` to a dict.\n" + "\n" + " If two tuples have the same key the last one in the list will be the one\n" + " that is present in the dict.\n" +). +-spec from_list(list({KV, KW})) -> dict(KV, KW). +from_list(List) -> + maps:from_list(List). + +-file("src/gleam/dict.gleam", 223). +-spec reverse_and_concat(list(NJ), list(NJ)) -> list(NJ). +reverse_and_concat(Remaining, Accumulator) -> + case Remaining of + [] -> + Accumulator; + + [First | Rest] -> + reverse_and_concat(Rest, [First | Accumulator]) + end. + +-file("src/gleam/dict.gleam", 216). +-spec do_keys_loop(list({NE, any()}), list(NE)) -> list(NE). +do_keys_loop(List, Acc) -> + case List of + [] -> + reverse_and_concat(Acc, []); + + [{Key, _} | Rest] -> + do_keys_loop(Rest, [Key | Acc]) + end. + +-file("src/gleam/dict.gleam", 212). +?DOC( + " Gets a list of all keys in a given dict.\n" + "\n" + " Dicts are not ordered so the keys are not returned in any specific order. Do\n" + " not write code that relies on the order keys are returned by this function\n" + " as it may change in later versions of Gleam or Erlang.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> keys\n" + " // -> [\"a\", \"b\"]\n" + " ```\n" +). +-spec keys(dict(MZ, any())) -> list(MZ). +keys(Dict) -> + maps:keys(Dict). + +-file("src/gleam/dict.gleam", 249). +-spec do_values_loop(list({any(), NT}), list(NT)) -> list(NT). +do_values_loop(List, Acc) -> + case List of + [] -> + reverse_and_concat(Acc, []); + + [{_, Value} | Rest] -> + do_values_loop(Rest, [Value | Acc]) + end. + +-file("src/gleam/dict.gleam", 244). +?DOC( + " Gets a list of all values in a given dict.\n" + "\n" + " Dicts are not ordered so the values are not returned in any specific order. Do\n" + " not write code that relies on the order values are returned by this function\n" + " as it may change in later versions of Gleam or Erlang.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> values\n" + " // -> [0, 1]\n" + " ```\n" +). +-spec values(dict(any(), NO)) -> list(NO). +values(Dict) -> + maps:values(Dict). + +-file("src/gleam/dict.gleam", 318). +-spec do_take_loop(dict(OX, OY), list(OX), dict(OX, OY)) -> dict(OX, OY). +do_take_loop(Dict, Desired_keys, Acc) -> + Insert = fun(Taken, Key) -> case gleam_stdlib:map_get(Dict, Key) of + {ok, Value} -> + insert(Taken, Key, Value); + + {error, _} -> + Taken + end end, + case Desired_keys of + [] -> + Acc; + + [First | Rest] -> + do_take_loop(Dict, Rest, Insert(Acc, First)) + end. + +-file("src/gleam/dict.gleam", 309). +?DOC( + " Creates a new dict from a given dict, only including any entries for which the\n" + " keys are in a given list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " |> take([\"b\"])\n" + " // -> from_list([#(\"b\", 1)])\n" + " ```\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " |> take([\"a\", \"b\", \"c\"])\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " ```\n" +). +-spec take(dict(OJ, OK), list(OJ)) -> dict(OJ, OK). +take(Dict, Desired_keys) -> + maps:with(Desired_keys, Dict). + +-file("src/gleam/dict.gleam", 363). +-spec insert_pair(dict(PV, PW), {PV, PW}) -> dict(PV, PW). +insert_pair(Dict, Pair) -> + insert(Dict, erlang:element(1, Pair), erlang:element(2, Pair)). + +-file("src/gleam/dict.gleam", 356). +-spec fold_inserts(list({PO, PP}), dict(PO, PP)) -> dict(PO, PP). +fold_inserts(New_entries, Dict) -> + case New_entries of + [] -> + Dict; + + [First | Rest] -> + fold_inserts(Rest, insert_pair(Dict, First)) + end. + +-file("src/gleam/dict.gleam", 350). +?DOC( + " Creates a new dict from a pair of given dicts by combining their entries.\n" + "\n" + " If there are entries with the same keys in both dicts the entry from the\n" + " second dict takes precedence.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let a = from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " let b = from_list([#(\"b\", 2), #(\"c\", 3)])\n" + " merge(a, b)\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 2), #(\"c\", 3)])\n" + " ```\n" +). +-spec merge(dict(PG, PH), dict(PG, PH)) -> dict(PG, PH). +merge(Dict, New_entries) -> + maps:merge(Dict, New_entries). + +-file("src/gleam/dict.gleam", 382). +?DOC( + " Creates a new dict from a given dict with all the same entries except for the\n" + " one with a given key, if it exists.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> delete(\"a\")\n" + " // -> from_list([#(\"b\", 1)])\n" + " ```\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> delete(\"c\")\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " ```\n" +). +-spec delete(dict(QB, QC), QB) -> dict(QB, QC). +delete(Dict, Key) -> + maps:remove(Key, Dict). + +-file("src/gleam/dict.gleam", 410). +?DOC( + " Creates a new dict from a given dict with all the same entries except any with\n" + " keys found in a given list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> drop([\"a\"])\n" + " // -> from_list([#(\"b\", 1)])\n" + " ```\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> drop([\"c\"])\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " ```\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> drop([\"a\", \"b\", \"c\"])\n" + " // -> from_list([])\n" + " ```\n" +). +-spec drop(dict(QN, QO), list(QN)) -> dict(QN, QO). +drop(Dict, Disallowed_keys) -> + case Disallowed_keys of + [] -> + Dict; + + [First | Rest] -> + drop(delete(Dict, First), Rest) + end. + +-file("src/gleam/dict.gleam", 440). +?DOC( + " Creates a new dict with one entry inserted or updated using a given function.\n" + "\n" + " If there was not an entry in the dict for the given key then the function\n" + " gets `None` as its argument, otherwise it gets `Some(value)`.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " let dict = from_list([#(\"a\", 0)])\n" + " let increment = fn(x) {\n" + " case x {\n" + " Some(i) -> i + 1\n" + " None -> 0\n" + " }\n" + " }\n" + "\n" + " upsert(dict, \"a\", increment)\n" + " // -> from_list([#(\"a\", 1)])\n" + "\n" + " upsert(dict, \"b\", increment)\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 0)])\n" + " ```\n" +). +-spec upsert(dict(QU, QV), QU, fun((gleam@option:option(QV)) -> QV)) -> dict(QU, QV). +upsert(Dict, Key, Fun) -> + case gleam_stdlib:map_get(Dict, Key) of + {ok, Value} -> + insert(Dict, Key, Fun({some, Value})); + + {error, _} -> + insert(Dict, Key, Fun(none)) + end. + +-file("src/gleam/dict.gleam", 484). +-spec fold_loop(list({RG, RH}), RJ, fun((RJ, RG, RH) -> RJ)) -> RJ. +fold_loop(List, Initial, Fun) -> + case List of + [] -> + Initial; + + [{K, V} | Rest] -> + fold_loop(Rest, Fun(Initial, K, V), Fun) + end. + +-file("src/gleam/dict.gleam", 476). +?DOC( + " Combines all entries into a single value by calling a given function on each\n" + " one.\n" + "\n" + " Dicts are not ordered so the values are not returned in any specific order. Do\n" + " not write code that relies on the order entries are used by this function\n" + " as it may change in later versions of Gleam or Erlang.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let dict = from_list([#(\"a\", 1), #(\"b\", 3), #(\"c\", 9)])\n" + " fold(dict, 0, fn(accumulator, key, value) { accumulator + value })\n" + " // -> 13\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/string\n" + "\n" + " let dict = from_list([#(\"a\", 1), #(\"b\", 3), #(\"c\", 9)])\n" + " fold(dict, \"\", fn(accumulator, key, value) {\n" + " string.append(accumulator, key)\n" + " })\n" + " // -> \"abc\"\n" + " ```\n" +). +-spec fold(dict(RB, RC), RF, fun((RF, RB, RC) -> RF)) -> RF. +fold(Dict, Initial, Fun) -> + fold_loop(maps:to_list(Dict), Initial, Fun). + +-file("src/gleam/dict.gleam", 188). +?DOC( + " Updates all values in a given dict by calling a given function on each key\n" + " and value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(3, 3), #(2, 4)])\n" + " |> map_values(fn(key, value) { key * value })\n" + " // -> from_list([#(3, 9), #(2, 8)])\n" + " ```\n" +). +-spec map_values(dict(ML, MM), fun((ML, MM) -> MP)) -> dict(ML, MP). +map_values(Dict, Fun) -> + maps:map(Fun, Dict). + +-file("src/gleam/dict.gleam", 273). +?DOC( + " Creates a new dict from a given dict, minus any entries that a given function\n" + " returns `False` for.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " |> filter(fn(key, value) { value != 0 })\n" + " // -> from_list([#(\"b\", 1)])\n" + " ```\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " |> filter(fn(key, value) { True })\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " ```\n" +). +-spec filter(dict(NX, NY), fun((NX, NY) -> boolean())) -> dict(NX, NY). +filter(Dict, Predicate) -> + maps:filter(Predicate, Dict). + +-file("src/gleam/dict.gleam", 517). +?DOC( + " Calls a function for each key and value in a dict, discarding the return\n" + " value.\n" + "\n" + " Useful for producing a side effect for every item of a dict.\n" + "\n" + " ```gleam\n" + " import gleam/io\n" + "\n" + " let dict = from_list([#(\"a\", \"apple\"), #(\"b\", \"banana\"), #(\"c\", \"cherry\")])\n" + "\n" + " each(dict, fn(k, v) {\n" + " io.println(key <> \" => \" <> value)\n" + " })\n" + " // -> Nil\n" + " // a => apple\n" + " // b => banana\n" + " // c => cherry\n" + " ```\n" + "\n" + " The order of elements in the iteration is an implementation detail that\n" + " should not be relied upon.\n" +). +-spec each(dict(RK, RL), fun((RK, RL) -> any())) -> nil. +each(Dict, Fun) -> + fold( + Dict, + nil, + fun(Nil, K, V) -> + Fun(K, V), + Nil + end + ). + +-file("src/gleam/dict.gleam", 538). +?DOC( + " Creates a new dict from a pair of given dicts by combining their entries.\n" + "\n" + " If there are entries with the same keys in both dicts the given function is\n" + " used to determine the new value to use in the resulting dict.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let a = from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " let b = from_list([#(\"a\", 2), #(\"c\", 3)])\n" + " combine(a, b, fn(one, other) { one + other })\n" + " // -> from_list([#(\"a\", 2), #(\"b\", 1), #(\"c\", 3)])\n" + " ```\n" +). +-spec combine(dict(RP, RQ), dict(RP, RQ), fun((RQ, RQ) -> RQ)) -> dict(RP, RQ). +combine(Dict, Other, Fun) -> + fold( + Dict, + Other, + fun(Acc, Key, Value) -> case gleam_stdlib:map_get(Acc, Key) of + {ok, Other_value} -> + insert(Acc, Key, Fun(Value, Other_value)); + + {error, _} -> + insert(Acc, Key, Value) + end end + ). diff --git a/build/dev/javascript/gleam_stdlib/gleam@dynamic.erl b/build/dev/javascript/gleam_stdlib/gleam@dynamic.erl new file mode 100644 index 0000000..f057ca2 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@dynamic.erl @@ -0,0 +1,106 @@ +-module(gleam@dynamic). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/dynamic.gleam"). +-export([classify/1, bool/1, string/1, float/1, int/1, bit_array/1, list/1, array/1, properties/1, nil/0]). +-export_type([dynamic_/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type dynamic_() :: any(). + +-file("src/gleam/dynamic.gleam", 30). +?DOC( + " Return a string indicating the type of the dynamic value.\n" + "\n" + " This function may be useful for constructing error messages or logs. If you\n" + " want to turn dynamic data into well typed data then you want the\n" + " `gleam/dynamic/decode` module.\n" + "\n" + " ```gleam\n" + " classify(string(\"Hello\"))\n" + " // -> \"String\"\n" + " ```\n" +). +-spec classify(dynamic_()) -> binary(). +classify(Data) -> + gleam_stdlib:classify_dynamic(Data). + +-file("src/gleam/dynamic.gleam", 36). +?DOC(" Create a dynamic value from a bool.\n"). +-spec bool(boolean()) -> dynamic_(). +bool(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 44). +?DOC( + " Create a dynamic value from a string.\n" + "\n" + " On Erlang this will be a binary string rather than a character list.\n" +). +-spec string(binary()) -> dynamic_(). +string(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 50). +?DOC(" Create a dynamic value from a float.\n"). +-spec float(float()) -> dynamic_(). +float(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 56). +?DOC(" Create a dynamic value from an int.\n"). +-spec int(integer()) -> dynamic_(). +int(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 62). +?DOC(" Create a dynamic value from a bit array.\n"). +-spec bit_array(bitstring()) -> dynamic_(). +bit_array(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 68). +?DOC(" Create a dynamic value from a list.\n"). +-spec list(list(dynamic_())) -> dynamic_(). +list(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 77). +?DOC( + " Create a dynamic value from a list, converting it to a sequential runtime\n" + " format rather than the regular list format.\n" + "\n" + " On Erlang this will be a tuple, on JavaScript this will be an array.\n" +). +-spec array(list(dynamic_())) -> dynamic_(). +array(A) -> + erlang:list_to_tuple(A). + +-file("src/gleam/dynamic.gleam", 85). +?DOC( + " Create a dynamic value made an unordered series of keys and values, where\n" + " the keys are unique.\n" + "\n" + " On Erlang this will be a map, on JavaScript this will be a Gleam dict\n" + " object.\n" +). +-spec properties(list({dynamic_(), dynamic_()})) -> dynamic_(). +properties(Entries) -> + gleam_stdlib:identity(maps:from_list(Entries)). + +-file("src/gleam/dynamic.gleam", 94). +?DOC( + " A dynamic value representing nothing.\n" + "\n" + " On Erlang this will be the atom `nil`, on JavaScript this will be\n" + " `undefined`.\n" +). +-spec nil() -> dynamic_(). +nil() -> + gleam_stdlib:identity(nil). diff --git a/build/dev/javascript/gleam_stdlib/gleam@dynamic@decode.erl b/build/dev/javascript/gleam_stdlib/gleam@dynamic@decode.erl new file mode 100644 index 0000000..bf4b951 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@dynamic@decode.erl @@ -0,0 +1,1088 @@ +-module(gleam@dynamic@decode). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/dynamic/decode.gleam"). +-export([run/2, success/1, decode_dynamic/1, map/2, map_errors/2, then/2, one_of/2, recursive/1, optional/1, decode_error/2, decode_bool/1, decode_int/1, decode_float/1, decode_bit_array/1, collapse_errors/2, failure/2, new_primitive_decoder/2, decode_string/1, dict/2, list/1, subfield/3, at/2, field/3, optional_field/4, optionally_at/3]). +-export_type([decode_error/0, decoder/1]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " The `Dynamic` type is used to represent dynamically typed data. That is, data\n" + " that we don't know the precise type of yet, so we need to introspect the data to\n" + " see if it is of the desired type before we can use it. Typically data like this\n" + " would come from user input or from untyped languages such as Erlang or JavaScript.\n" + "\n" + " This module provides the `Decoder` type and associated functions, which provides\n" + " a type-safe and composable way to convert dynamic data into some desired type,\n" + " or into errors if the data doesn't have the desired structure.\n" + "\n" + " The `Decoder` type is generic and has 1 type parameter, which is the type that\n" + " it attempts to decode. A `Decoder(String)` can be used to decode strings, and a\n" + " `Decoder(Option(Int))` can be used to decode `Option(Int)`s\n" + "\n" + " Decoders work using _runtime reflection_ and the data structures of the target\n" + " platform. Differences between Erlang and JavaScript data structures may impact\n" + " your decoders, so it is important to test your decoders on all supported\n" + " platforms.\n" + "\n" + " The decoding technique used by this module was inspired by Juraj Petráš'\n" + " [Toy](https://github.com/Hackder/toy), Go's `encoding/json`, and Elm's\n" + " `Json.Decode`. Thank you to them!\n" + "\n" + " # Examples\n" + "\n" + " Dynamic data may come from various sources and so many different syntaxes could\n" + " be used to describe or construct them. In these examples a pseudocode\n" + " syntax is used to describe the data.\n" + "\n" + " ## Simple types\n" + "\n" + " This module defines decoders for simple data types such as [`string`](#string),\n" + " [`int`](#int), [`float`](#float), [`bit_array`](#bit_array), and [`bool`](#bool).\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // \"Hello, Joe!\"\n" + "\n" + " let result = decode.run(data, decode.string)\n" + " assert result == Ok(\"Hello, Joe!\")\n" + " ```\n" + "\n" + " ## Lists\n" + "\n" + " The [`list`](#list) decoder decodes `List`s. To use it you must construct it by\n" + " passing in another decoder into the `list` function, which is the decoder that\n" + " is to be used for the elements of the list, type checking both the list and its\n" + " elements.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // [1, 2, 3, 4]\n" + "\n" + " let result = decode.run(data, decode.list(decode.int))\n" + " assert result == Ok([1, 2, 3, 4])\n" + " ```\n" + "\n" + " On Erlang this decoder can decode from lists, and on JavaScript it can\n" + " decode from lists as well as JavaScript arrays.\n" + "\n" + " ## Options\n" + "\n" + " The [`optional`](#optional) decoder is used to decode values that may or may not\n" + " be present. In other environment these might be called \"nullable\" values.\n" + "\n" + " Like the `list` decoder, the `optional` decoder takes another decoder,\n" + " which is used to decode the value if it is present.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // 12.45\n" + "\n" + " let result = decode.run(data, decode.optional(decode.float))\n" + " assert result == Ok(option.Some(12.45))\n" + " ```\n" + " ```gleam\n" + " // Data:\n" + " // null\n" + "\n" + " let result = decode.run(data, decode.optional(decode.int))\n" + " assert result == Ok(option.None)\n" + " ```\n" + "\n" + " This decoder knows how to handle multiple different runtime representations of\n" + " absent values, including `Nil`, `None`, `null`, and `undefined`.\n" + "\n" + " ## Dicts\n" + "\n" + " The [`dict`](#dict) decoder decodes `Dicts` and contains two other decoders, one\n" + " for the keys, one for the values.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // { \"Lucy\" -> 10, \"Nubi\" -> 20 }\n" + "\n" + " let result = decode.run(data, decode.dict(decode.string, decode.int))\n" + " assert result == Ok(dict.from_list([\n" + " #(\"Lucy\", 10),\n" + " #(\"Nubi\", 20),\n" + " ]))\n" + " ```\n" + "\n" + " ## Indexing objects\n" + "\n" + " The [`at`](#at) decoder can be used to decode a value that is nested within\n" + " key-value containers such as Gleam dicts, Erlang maps, or JavaScript objects.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // { \"one\" -> { \"two\" -> 123 } }\n" + "\n" + " let result = decode.run(data, decode.at([\"one\", \"two\"], decode.int))\n" + " assert result == Ok(123)\n" + " ```\n" + "\n" + " ## Indexing arrays\n" + "\n" + " If you use ints as keys then the [`at`](#at) decoder can be used to index into\n" + " array-like containers such as Gleam or Erlang tuples, or JavaScript arrays.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // [\"one\", \"two\", \"three\"]\n" + "\n" + " let result = decode.run(data, decode.at([1], decode.string))\n" + " assert result == Ok(\"two\")\n" + " ```\n" + "\n" + " ## Records\n" + "\n" + " Decoding records from dynamic data is more complex and requires combining a\n" + " decoder for each field and a special constructor that builds your records with\n" + " the decoded field values.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // {\n" + " // \"score\" -> 180,\n" + " // \"name\" -> \"Mel Smith\",\n" + " // \"is-admin\" -> false,\n" + " // \"enrolled\" -> true,\n" + " // \"colour\" -> \"Red\",\n" + " // }\n" + "\n" + " let decoder = {\n" + " use name <- decode.field(\"name\", decode.string)\n" + " use score <- decode.field(\"score\", decode.int)\n" + " use colour <- decode.field(\"colour\", decode.string)\n" + " use enrolled <- decode.field(\"enrolled\", decode.bool)\n" + " decode.success(Player(name:, score:, colour:, enrolled:))\n" + " }\n" + "\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(Player(\"Mel Smith\", 180, \"Red\", True))\n" + " ```\n" + "\n" + " ## Enum variants\n" + "\n" + " Imagine you have a custom type where all the variants do not contain any values.\n" + "\n" + " ```gleam\n" + " pub type PocketMonsterType {\n" + " Fire\n" + " Water\n" + " Grass\n" + " Electric\n" + " }\n" + " ```\n" + "\n" + " You might choose to encode these variants as strings, `\"fire\"` for `Fire`,\n" + " `\"water\"` for `Water`, and so on. To decode them you'll need to decode the dynamic\n" + " data as a string, but then you'll need to decode it further still as not all\n" + " strings are valid values for the enum. This can be done with the `then`\n" + " function, which enables running a second decoder after the first one\n" + " succeeds.\n" + "\n" + " ```gleam\n" + " let decoder = {\n" + " use decoded_string <- decode.then(decode.string)\n" + " case decoded_string {\n" + " // Return succeeding decoders for valid strings\n" + " \"fire\" -> decode.success(Fire)\n" + " \"water\" -> decode.success(Water)\n" + " \"grass\" -> decode.success(Grass)\n" + " \"electric\" -> decode.success(Electric)\n" + " // Return a failing decoder for any other strings\n" + " _ -> decode.failure(Fire, \"PocketMonsterType\")\n" + " }\n" + " }\n" + "\n" + " let result = decode.run(dynamic.string(\"water\"), decoder)\n" + " assert result == Ok(Water)\n" + "\n" + " let result = decode.run(dynamic.string(\"wobble\"), decoder)\n" + " assert result == Error([DecodeError(\"PocketMonsterType\", \"String\", [])])\n" + " ```\n" + "\n" + " ## Record variants\n" + "\n" + " Decoding type variants that contain other values is done by combining the\n" + " techniques from the \"enum variants\" and \"records\" examples. Imagine you have\n" + " this custom type that you want to decode:\n" + "\n" + " ```gleam\n" + " pub type PocketMonsterPerson {\n" + " Trainer(name: String, badge_count: Int)\n" + " GymLeader(name: String, speciality: PocketMonsterType)\n" + " }\n" + " ```\n" + " And you would like to be able to decode these from dynamic data like this:\n" + " ```erlang\n" + " {\n" + " \"type\" -> \"trainer\",\n" + " \"name\" -> \"Ash\",\n" + " \"badge-count\" -> 1,\n" + " }\n" + " ```\n" + " ```erlang\n" + " {\n" + " \"type\" -> \"gym-leader\",\n" + " \"name\" -> \"Misty\",\n" + " \"speciality\" -> \"water\",\n" + " }\n" + " ```\n" + "\n" + " Notice how both documents have a `\"type\"` field, which is used to indicate which\n" + " variant the data is for.\n" + "\n" + " First, define decoders for each of the variants:\n" + "\n" + " ```gleam\n" + " let trainer_decoder = {\n" + " use name <- decode.field(\"name\", decode.string)\n" + " use badge_count <- decode.field(\"badge-count\", decode.int)\n" + " decode.success(Trainer(name, badge_count))\n" + " }\n" + "\n" + " let gym_leader_decoder = {\n" + " use name <- decode.field(\"name\", decode.string)\n" + " use speciality <- decode.field(\"speciality\", pocket_monster_type_decoder)\n" + " decode.success(GymLeader(name, speciality))\n" + " }\n" + " ```\n" + "\n" + " A third decoder can be used to extract and decode the `\"type\"` field, and the\n" + " expression can evaluate to whichever decoder is suitable for the document.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // {\n" + " // \"type\" -> \"gym-leader\",\n" + " // \"name\" -> \"Misty\",\n" + " // \"speciality\" -> \"water\",\n" + " // }\n" + "\n" + " let decoder = {\n" + " use tag <- decode.field(\"type\", decode.string)\n" + " case tag {\n" + " \"gym-leader\" -> gym_leader_decoder\n" + " _ -> trainer_decoder\n" + " }\n" + " }\n" + "\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(GymLeader(\"Misty\", Water))\n" + " ```\n" +). + +-type decode_error() :: {decode_error, binary(), binary(), list(binary())}. + +-opaque decoder(BUW) :: {decoder, + fun((gleam@dynamic:dynamic_()) -> {BUW, list(decode_error())})}. + +-file("src/gleam/dynamic/decode.gleam", 356). +?DOC( + " Run a decoder on a `Dynamic` value, decoding the value if it is of the\n" + " desired type, or returning errors.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = {\n" + " use name <- decode.field(\"email\", decode.string)\n" + " use email <- decode.field(\"password\", decode.string)\n" + " decode.success(SignUp(name: name, email: email))\n" + " }\n" + "\n" + " decode.run(data, decoder)\n" + " ```\n" +). +-spec run(gleam@dynamic:dynamic_(), decoder(BVE)) -> {ok, BVE} | + {error, list(decode_error())}. +run(Data, Decoder) -> + {Maybe_invalid_data, Errors} = (erlang:element(2, Decoder))(Data), + case Errors of + [] -> + {ok, Maybe_invalid_data}; + + [_ | _] -> + {error, Errors} + end. + +-file("src/gleam/dynamic/decode.gleam", 479). +?DOC( + " Finalise a decoder having successfully extracted a value.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"email\"), dynamic.string(\"lucy@example.com\")),\n" + " #(dynamic.string(\"name\"), dynamic.string(\"Lucy\")),\n" + " ]))\n" + "\n" + " let decoder = {\n" + " use name <- decode.field(\"name\", string)\n" + " use email <- decode.field(\"email\", string)\n" + " decode.success(SignUp(name: name, email: email))\n" + " }\n" + "\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(SignUp(name: \"Lucy\", email: \"lucy@example.com\"))\n" + " ```\n" +). +-spec success(BWF) -> decoder(BWF). +success(Data) -> + {decoder, fun(_) -> {Data, []} end}. + +-file("src/gleam/dynamic/decode.gleam", 718). +-spec decode_dynamic(gleam@dynamic:dynamic_()) -> {gleam@dynamic:dynamic_(), + list(decode_error())}. +decode_dynamic(Data) -> + {Data, []}. + +-file("src/gleam/dynamic/decode.gleam", 875). +?DOC( + " Apply a transformation function to any value decoded by the decoder.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = decode.int |> decode.map(int.to_string)\n" + " let result = decode.run(dynamic.int(1000), decoder)\n" + " assert result == Ok(\"1000\")\n" + " ```\n" +). +-spec map(decoder(BZC), fun((BZC) -> BZE)) -> decoder(BZE). +map(Decoder, Transformer) -> + {decoder, + fun(D) -> + {Data, Errors} = (erlang:element(2, Decoder))(D), + {Transformer(Data), Errors} + end}. + +-file("src/gleam/dynamic/decode.gleam", 884). +?DOC(" Apply a transformation function to any errors returned by the decoder.\n"). +-spec map_errors( + decoder(BZG), + fun((list(decode_error())) -> list(decode_error())) +) -> decoder(BZG). +map_errors(Decoder, Transformer) -> + {decoder, + fun(D) -> + {Data, Errors} = (erlang:element(2, Decoder))(D), + {Data, Transformer(Errors)} + end}. + +-file("src/gleam/dynamic/decode.gleam", 922). +?DOC( + " Create a new decoder based upon the value of a previous decoder.\n" + "\n" + " This may be useful to run one previous decoder to use in further decoding.\n" +). +-spec then(decoder(BZO), fun((BZO) -> decoder(BZQ))) -> decoder(BZQ). +then(Decoder, Next) -> + {decoder, + fun(Dynamic_data) -> + {Data, Errors} = (erlang:element(2, Decoder))(Dynamic_data), + Decoder@1 = Next(Data), + {Data@1, _} = Layer = (erlang:element(2, Decoder@1))(Dynamic_data), + case Errors of + [] -> + Layer; + + [_ | _] -> + {Data@1, Errors} + end + end}. + +-file("src/gleam/dynamic/decode.gleam", 965). +-spec run_decoders( + gleam@dynamic:dynamic_(), + {BZY, list(decode_error())}, + list(decoder(BZY)) +) -> {BZY, list(decode_error())}. +run_decoders(Data, Failure, Decoders) -> + case Decoders of + [] -> + Failure; + + [Decoder | Decoders@1] -> + {_, Errors} = Layer = (erlang:element(2, Decoder))(Data), + case Errors of + [] -> + Layer; + + [_ | _] -> + run_decoders(Data, Failure, Decoders@1) + end + end. + +-file("src/gleam/dynamic/decode.gleam", 952). +?DOC( + " Create a new decoder from several other decoders. Each of the inner\n" + " decoders is run in turn, and the value from the first to succeed is used.\n" + "\n" + " If no decoder succeeds then the errors from the first decoder is used.\n" + " If you wish for different errors then you may wish to use the\n" + " `collapse_errors` or `map_errors` functions.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = decode.one_of(decode.string, or: [\n" + " decode.int |> decode.map(int.to_string),\n" + " decode.float |> decode.map(float.to_string),\n" + " ])\n" + " decode.run(dynamic.int(1000), decoder)\n" + " // -> Ok(\"1000\")\n" + " ```\n" +). +-spec one_of(decoder(BZT), list(decoder(BZT))) -> decoder(BZT). +one_of(First, Alternatives) -> + {decoder, + fun(Dynamic_data) -> + {_, Errors} = Layer = (erlang:element(2, First))(Dynamic_data), + case Errors of + [] -> + Layer; + + [_ | _] -> + run_decoders(Dynamic_data, Layer, Alternatives) + end + end}. + +-file("src/gleam/dynamic/decode.gleam", 1048). +?DOC( + " Create a decoder that can refer to itself, useful for decoding deeply\n" + " nested data.\n" + "\n" + " Attempting to create a recursive decoder without this function could result\n" + " in an infinite loop. If you are using `field` or other `use`able functions\n" + " then you may not need to use this function.\n" + "\n" + " ```gleam\n" + " type Nested {\n" + " Nested(List(Nested))\n" + " Value(String)\n" + " }\n" + "\n" + " fn nested_decoder() -> decode.Decoder(Nested) {\n" + " use <- decode.recursive\n" + " decode.one_of(decode.string |> decode.map(Value), [\n" + " decode.list(nested_decoder()) |> decode.map(Nested),\n" + " ])\n" + " }\n" + " ```\n" +). +-spec recursive(fun(() -> decoder(CAJ))) -> decoder(CAJ). +recursive(Inner) -> + {decoder, + fun(Data) -> + Decoder = Inner(), + (erlang:element(2, Decoder))(Data) + end}. + +-file("src/gleam/dynamic/decode.gleam", 853). +?DOC( + " A decoder that decodes nullable values of a type decoded by with a given\n" + " decoder.\n" + "\n" + " This function can handle common representations of null on all runtimes, such as\n" + " `nil`, `null`, and `undefined` on Erlang, and `undefined` and `null` on\n" + " JavaScript.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let result = decode.run(dynamic.int(100), decode.optional(decode.int))\n" + " assert result == Ok(option.Some(100))\n" + " ```\n" + "\n" + " ```gleam\n" + " let result = decode.run(dynamic.nil(), decode.optional(decode.int))\n" + " assert result == Ok(option.None)\n" + " ```\n" +). +-spec optional(decoder(BYY)) -> decoder(gleam@option:option(BYY)). +optional(Inner) -> + {decoder, fun(Data) -> case gleam_stdlib:is_null(Data) of + true -> + {none, []}; + + false -> + {Data@1, Errors} = (erlang:element(2, Inner))(Data), + {{some, Data@1}, Errors} + end end}. + +-file("src/gleam/dynamic/decode.gleam", 485). +?DOC(" Construct a decode error for some unexpected dynamic data.\n"). +-spec decode_error(binary(), gleam@dynamic:dynamic_()) -> list(decode_error()). +decode_error(Expected, Found) -> + [{decode_error, Expected, gleam_stdlib:classify_dynamic(Found), []}]. + +-file("src/gleam/dynamic/decode.gleam", 609). +-spec run_dynamic_function( + gleam@dynamic:dynamic_(), + binary(), + fun((gleam@dynamic:dynamic_()) -> {ok, BWZ} | {error, BWZ}) +) -> {BWZ, list(decode_error())}. +run_dynamic_function(Data, Name, F) -> + case F(Data) of + {ok, Data@1} -> + {Data@1, []}; + + {error, Zero} -> + {Zero, + [{decode_error, Name, gleam_stdlib:classify_dynamic(Data), []}]} + end. + +-file("src/gleam/dynamic/decode.gleam", 658). +-spec decode_bool(gleam@dynamic:dynamic_()) -> {boolean(), list(decode_error())}. +decode_bool(Data) -> + case gleam_stdlib:identity(true) =:= Data of + true -> + {true, []}; + + false -> + case gleam_stdlib:identity(false) =:= Data of + true -> + {false, []}; + + false -> + {false, decode_error(<<"Bool"/utf8>>, Data)} + end + end. + +-file("src/gleam/dynamic/decode.gleam", 680). +-spec decode_int(gleam@dynamic:dynamic_()) -> {integer(), list(decode_error())}. +decode_int(Data) -> + run_dynamic_function(Data, <<"Int"/utf8>>, fun gleam_stdlib:int/1). + +-file("src/gleam/dynamic/decode.gleam", 699). +-spec decode_float(gleam@dynamic:dynamic_()) -> {float(), list(decode_error())}. +decode_float(Data) -> + run_dynamic_function(Data, <<"Float"/utf8>>, fun gleam_stdlib:float/1). + +-file("src/gleam/dynamic/decode.gleam", 733). +-spec decode_bit_array(gleam@dynamic:dynamic_()) -> {bitstring(), + list(decode_error())}. +decode_bit_array(Data) -> + run_dynamic_function( + Data, + <<"BitArray"/utf8>>, + fun gleam_stdlib:bit_array/1 + ). + +-file("src/gleam/dynamic/decode.gleam", 908). +?DOC( + " Replace all errors produced by a decoder with one single error for a named\n" + " expected type.\n" + "\n" + " This function may be useful if you wish to simplify errors before\n" + " presenting them to a user, particularly when using the `one_of` function.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = decode.string |> decode.collapse_errors(\"MyThing\")\n" + " let result = decode.run(dynamic.int(1000), decoder)\n" + " assert result == Error([DecodeError(\"MyThing\", \"Int\", [])])\n" + " ```\n" +). +-spec collapse_errors(decoder(BZL), binary()) -> decoder(BZL). +collapse_errors(Decoder, Name) -> + {decoder, + fun(Dynamic_data) -> + {Data, Errors} = Layer = (erlang:element(2, Decoder))(Dynamic_data), + case Errors of + [] -> + Layer; + + [_ | _] -> + {Data, decode_error(Name, Dynamic_data)} + end + end}. + +-file("src/gleam/dynamic/decode.gleam", 986). +?DOC( + " Define a decoder that always fails. The parameter for this function is the\n" + " name of the type that has failed to decode.\n" +). +-spec failure(CAD, binary()) -> decoder(CAD). +failure(Zero, Expected) -> + {decoder, fun(D) -> {Zero, decode_error(Expected, D)} end}. + +-file("src/gleam/dynamic/decode.gleam", 1015). +?DOC( + " Create a decoder for a new data type from a decoding function.\n" + "\n" + " This function is used for new primitive types. For example, you might\n" + " define a decoder for Erlang's pid type.\n" + "\n" + " A default \"zero\" value is also required to make a decoder. When this\n" + " decoder is used as part of a larger decoder this zero value used as\n" + " a placeholder so that the rest of the decoder can continue to run and\n" + " collect all decoding errors.\n" + "\n" + " If you were to make a decoder for the `String` type (rather than using the\n" + " build-in `string` decoder) you would define it like so:\n" + "\n" + " ```gleam\n" + " pub fn string_decoder() -> decode.Decoder(String) {\n" + " let default = \"\"\n" + " decode.new_primitive_decoder(\"String\", fn(data) {\n" + " case dynamic.string(data) {\n" + " Ok(x) -> Ok(x)\n" + " Error(_) -> Error(default)\n" + " }\n" + " })\n" + " }\n" + " ```\n" +). +-spec new_primitive_decoder( + binary(), + fun((gleam@dynamic:dynamic_()) -> {ok, CAF} | {error, CAF}) +) -> decoder(CAF). +new_primitive_decoder(Name, Decoding_function) -> + {decoder, fun(D) -> case Decoding_function(D) of + {ok, T} -> + {T, []}; + + {error, Zero} -> + {Zero, + [{decode_error, + Name, + gleam_stdlib:classify_dynamic(D), + []}]} + end end}. + +-file("src/gleam/dynamic/decode.gleam", 636). +-spec dynamic_string(gleam@dynamic:dynamic_()) -> {ok, binary()} | + {error, binary()}. +dynamic_string(Data) -> + case gleam_stdlib:bit_array(Data) of + {ok, Data@1} -> + case gleam@bit_array:to_string(Data@1) of + {ok, String} -> + {ok, String}; + + {error, _} -> + {error, <<""/utf8>>} + end; + + {error, _} -> + {error, <<""/utf8>>} + end. + +-file("src/gleam/dynamic/decode.gleam", 631). +-spec decode_string(gleam@dynamic:dynamic_()) -> {binary(), + list(decode_error())}. +decode_string(Data) -> + run_dynamic_function(Data, <<"String"/utf8>>, fun dynamic_string/1). + +-file("src/gleam/dynamic/decode.gleam", 807). +-spec fold_dict( + {gleam@dict:dict(BYK, BYL), list(decode_error())}, + gleam@dynamic:dynamic_(), + gleam@dynamic:dynamic_(), + fun((gleam@dynamic:dynamic_()) -> {BYK, list(decode_error())}), + fun((gleam@dynamic:dynamic_()) -> {BYL, list(decode_error())}) +) -> {gleam@dict:dict(BYK, BYL), list(decode_error())}. +fold_dict(Acc, Key, Value, Key_decoder, Value_decoder) -> + case Key_decoder(Key) of + {Key@1, []} -> + case Value_decoder(Value) of + {Value@1, []} -> + Dict = gleam@dict:insert( + erlang:element(1, Acc), + Key@1, + Value@1 + ), + {Dict, erlang:element(2, Acc)}; + + {_, Errors} -> + push_path({maps:new(), Errors}, [<<"values"/utf8>>]) + end; + + {_, Errors@1} -> + push_path({maps:new(), Errors@1}, [<<"keys"/utf8>>]) + end. + +-file("src/gleam/dynamic/decode.gleam", 787). +?DOC( + " A decoder that decodes dicts where all keys and vales are decoded with\n" + " given decoders.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let values = dynamic.properties([\n" + " #(dynamic.string(\"one\"), dynamic.int(1)),\n" + " #(dynamic.string(\"two\"), dynamic.int(2)),\n" + " ])\n" + "\n" + " let result =\n" + " decode.run(values, decode.dict(decode.string, decode.int))\n" + " assert result == Ok(values)\n" + " ```\n" +). +-spec dict(decoder(BYD), decoder(BYF)) -> decoder(gleam@dict:dict(BYD, BYF)). +dict(Key, Value) -> + {decoder, fun(Data) -> case gleam_stdlib:dict(Data) of + {error, _} -> + {maps:new(), decode_error(<<"Dict"/utf8>>, Data)}; + + {ok, Dict} -> + gleam@dict:fold( + Dict, + {maps:new(), []}, + fun(A, K, V) -> case erlang:element(2, A) of + [] -> + fold_dict( + A, + K, + V, + erlang:element(2, Key), + erlang:element(2, Value) + ); + + [_ | _] -> + A + end end + ) + end end}. + +-file("src/gleam/dynamic/decode.gleam", 755). +?DOC( + " A decoder that decodes lists where all elements are decoded with a given\n" + " decoder.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let result =\n" + " [1, 2, 3]\n" + " |> list.map(dynamic.int)\n" + " |> dynamic.list\n" + " |> decode.run(decode.list(of: decode.int))\n" + " assert result == Ok([1, 2, 3])\n" + " ```\n" +). +-spec list(decoder(BXR)) -> decoder(list(BXR)). +list(Inner) -> + {decoder, + fun(Data) -> + gleam_stdlib:list( + Data, + erlang:element(2, Inner), + fun(P, K) -> push_path(P, [K]) end, + 0, + [] + ) + end}. + +-file("src/gleam/dynamic/decode.gleam", 439). +-spec push_path({BWA, list(decode_error())}, list(any())) -> {BWA, + list(decode_error())}. +push_path(Layer, Path) -> + Decoder = one_of( + {decoder, fun decode_string/1}, + [begin + _pipe = {decoder, fun decode_int/1}, + map(_pipe, fun erlang:integer_to_binary/1) + end] + ), + Path@1 = gleam@list:map( + Path, + fun(Key) -> + Key@1 = gleam_stdlib:identity(Key), + case run(Key@1, Decoder) of + {ok, Key@2} -> + Key@2; + + {error, _} -> + <<<<"<"/utf8, + (gleam_stdlib:classify_dynamic(Key@1))/binary>>/binary, + ">"/utf8>> + end + end + ), + Errors = gleam@list:map( + erlang:element(2, Layer), + fun(Error) -> + {decode_error, + erlang:element(2, Error), + erlang:element(3, Error), + lists:append(Path@1, erlang:element(4, Error))} + end + ), + {erlang:element(1, Layer), Errors}. + +-file("src/gleam/dynamic/decode.gleam", 403). +-spec index( + list(BVO), + list(BVO), + fun((gleam@dynamic:dynamic_()) -> {BVR, list(decode_error())}), + gleam@dynamic:dynamic_(), + fun((gleam@dynamic:dynamic_(), list(BVO)) -> {BVR, list(decode_error())}) +) -> {BVR, list(decode_error())}. +index(Path, Position, Inner, Data, Handle_miss) -> + case Path of + [] -> + _pipe = Data, + _pipe@1 = Inner(_pipe), + push_path(_pipe@1, lists:reverse(Position)); + + [Key | Path@1] -> + case gleam_stdlib:index(Data, Key) of + {ok, {some, Data@1}} -> + index(Path@1, [Key | Position], Inner, Data@1, Handle_miss); + + {ok, none} -> + Handle_miss(Data, [Key | Position]); + + {error, Kind} -> + {Default, _} = Inner(Data), + _pipe@2 = {Default, + [{decode_error, + Kind, + gleam_stdlib:classify_dynamic(Data), + []}]}, + push_path(_pipe@2, lists:reverse(Position)) + end + end. + +-file("src/gleam/dynamic/decode.gleam", 324). +?DOC( + " The same as [`field`](#field), except taking a path to the value rather\n" + " than a field name.\n" + "\n" + " This function will index into dictionaries with any key type, and if the key is\n" + " an int then it'll also index into Erlang tuples and JavaScript arrays, and\n" + " the first eight elements of Gleam lists.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"data\"), dynamic.properties([\n" + " #(dynamic.string(\"email\"), dynamic.string(\"lucy@example.com\")),\n" + " #(dynamic.string(\"name\"), dynamic.string(\"Lucy\")),\n" + " ])\n" + " ]))\n" + "\n" + " let decoder = {\n" + " use name <- decode.subfield([\"data\", \"name\"], decode.string)\n" + " use email <- decode.subfield([\"data\", \"email\"], decode.string)\n" + " decode.success(SignUp(name: name, email: email))\n" + " }\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(SignUp(name: \"Lucy\", email: \"lucy@example.com\"))\n" + " ```\n" +). +-spec subfield(list(any()), decoder(BUZ), fun((BUZ) -> decoder(BVB))) -> decoder(BVB). +subfield(Field_path, Field_decoder, Next) -> + {decoder, + fun(Data) -> + {Out, Errors1} = index( + Field_path, + [], + erlang:element(2, Field_decoder), + Data, + fun(Data@1, Position) -> + {Default, _} = (erlang:element(2, Field_decoder))(Data@1), + _pipe = {Default, + [{decode_error, + <<"Field"/utf8>>, + <<"Nothing"/utf8>>, + []}]}, + push_path(_pipe, lists:reverse(Position)) + end + ), + {Out@1, Errors2} = (erlang:element(2, Next(Out)))(Data), + {Out@1, lists:append(Errors1, Errors2)} + end}. + +-file("src/gleam/dynamic/decode.gleam", 393). +?DOC( + " A decoder that decodes a value that is nested within other values. For\n" + " example, decoding a value that is within some deeply nested JSON objects.\n" + "\n" + " This function will index into dictionaries with any key type, and if the key is\n" + " an int then it'll also index into Erlang tuples and JavaScript arrays, and\n" + " the first eight elements of Gleam lists.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = decode.at([\"one\", \"two\"], decode.int)\n" + "\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"one\"), dynamic.properties([\n" + " #(dynamic.string(\"two\"), dynamic.int(1000)),\n" + " ])),\n" + " ]))\n" + "\n" + "\n" + " decode.run(data, decoder)\n" + " // -> Ok(1000)\n" + " ```\n" + "\n" + " ```gleam\n" + " dynamic.nil()\n" + " |> decode.run(decode.optional(decode.int))\n" + " // -> Ok(option.None)\n" + " ```\n" +). +-spec at(list(any()), decoder(BVL)) -> decoder(BVL). +at(Path, Inner) -> + {decoder, + fun(Data) -> + index( + Path, + [], + erlang:element(2, Inner), + Data, + fun(Data@1, Position) -> + {Default, _} = (erlang:element(2, Inner))(Data@1), + _pipe = {Default, + [{decode_error, + <<"Field"/utf8>>, + <<"Nothing"/utf8>>, + []}]}, + push_path(_pipe, lists:reverse(Position)) + end + ) + end}. + +-file("src/gleam/dynamic/decode.gleam", 524). +?DOC( + " Run a decoder on a field of a `Dynamic` value, decoding the value if it is\n" + " of the desired type, or returning errors. An error is returned if there is\n" + " no field for the specified key.\n" + "\n" + " This function will index into dictionaries with any key type, and if the key is\n" + " an int then it'll also index into Erlang tuples and JavaScript arrays, and\n" + " the first eight elements of Gleam lists.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"email\"), dynamic.string(\"lucy@example.com\")),\n" + " #(dynamic.string(\"name\"), dynamic.string(\"Lucy\")),\n" + " ]))\n" + "\n" + " let decoder = {\n" + " use name <- decode.field(\"name\", string)\n" + " use email <- decode.field(\"email\", string)\n" + " decode.success(SignUp(name: name, email: email))\n" + " }\n" + "\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(SignUp(name: \"Lucy\", email: \"lucy@example.com\"))\n" + " ```\n" + "\n" + " If you wish to decode a value that is more deeply nested within the dynamic\n" + " data, see [`subfield`](#subfield) and [`at`](#at).\n" + "\n" + " If you wish to return a default in the event that a field is not present,\n" + " see [`optional_field`](#optional_field) and / [`optionally_at`](#optionally_at).\n" +). +-spec field(any(), decoder(BWJ), fun((BWJ) -> decoder(BWL))) -> decoder(BWL). +field(Field_name, Field_decoder, Next) -> + subfield([Field_name], Field_decoder, Next). + +-file("src/gleam/dynamic/decode.gleam", 557). +?DOC( + " Run a decoder on a field of a `Dynamic` value, decoding the value if it is\n" + " of the desired type, or returning errors. The given default value is\n" + " returned if there is no field for the specified key.\n" + "\n" + " This function will index into dictionaries with any key type, and if the key is\n" + " an int then it'll also index into Erlang tuples and JavaScript arrays, and\n" + " the first eight elements of Gleam lists.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"name\"), dynamic.string(\"Lucy\")),\n" + " ]))\n" + "\n" + " let decoder = {\n" + " use name <- decode.field(\"name\", string)\n" + " use email <- decode.optional_field(\"email\", \"n/a\", string)\n" + " decode.success(SignUp(name: name, email: email))\n" + " }\n" + "\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(SignUp(name: \"Lucy\", email: \"n/a\"))\n" + " ```\n" +). +-spec optional_field(any(), BWP, decoder(BWP), fun((BWP) -> decoder(BWR))) -> decoder(BWR). +optional_field(Key, Default, Field_decoder, Next) -> + {decoder, + fun(Data) -> + {Out, Errors1} = begin + _pipe = case gleam_stdlib:index(Data, Key) of + {ok, {some, Data@1}} -> + (erlang:element(2, Field_decoder))(Data@1); + + {ok, none} -> + {Default, []}; + + {error, Kind} -> + {Default, + [{decode_error, + Kind, + gleam_stdlib:classify_dynamic(Data), + []}]} + end, + push_path(_pipe, [Key]) + end, + {Out@1, Errors2} = (erlang:element(2, Next(Out)))(Data), + {Out@1, lists:append(Errors1, Errors2)} + end}. + +-file("src/gleam/dynamic/decode.gleam", 599). +?DOC( + " A decoder that decodes a value that is nested within other values. For\n" + " example, decoding a value that is within some deeply nested JSON objects.\n" + "\n" + " This function will index into dictionaries with any key type, and if the key is\n" + " an int then it'll also index into Erlang tuples and JavaScript arrays, and\n" + " the first eight elements of Gleam lists.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = decode.optionally_at([\"one\", \"two\"], 100, decode.int)\n" + "\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"one\"), dynamic.properties([])),\n" + " ]))\n" + "\n" + "\n" + " decode.run(data, decoder)\n" + " // -> Ok(100)\n" + " ```\n" +). +-spec optionally_at(list(any()), BWW, decoder(BWW)) -> decoder(BWW). +optionally_at(Path, Default, Inner) -> + {decoder, + fun(Data) -> + index( + Path, + [], + erlang:element(2, Inner), + Data, + fun(_, _) -> {Default, []} end + ) + end}. diff --git a/build/dev/javascript/gleam_stdlib/gleam@float.erl b/build/dev/javascript/gleam_stdlib/gleam@float.erl new file mode 100644 index 0000000..6b55b47 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@float.erl @@ -0,0 +1,744 @@ +-module(gleam@float). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/float.gleam"). +-export([parse/1, to_string/1, compare/2, min/2, max/2, clamp/3, ceiling/1, floor/1, truncate/1, absolute_value/1, loosely_compare/3, loosely_equals/3, power/2, square_root/1, negate/1, round/1, to_precision/2, sum/1, product/1, random/0, modulo/2, divide/2, add/2, multiply/2, subtract/2, logarithm/1, exponential/1]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Functions for working with floats.\n" + "\n" + " ## Float representation\n" + "\n" + " Floats are represented as 64 bit floating point numbers on both the Erlang\n" + " and JavaScript runtimes. The floating point behaviour is native to their\n" + " respective runtimes, so their exact behaviour will be slightly different on\n" + " the two runtimes.\n" + "\n" + " ### Infinity and NaN\n" + "\n" + " Under the JavaScript runtime, exceeding the maximum (or minimum)\n" + " representable value for a floating point value will result in Infinity (or\n" + " -Infinity). Should you try to divide two infinities you will get NaN as a\n" + " result.\n" + "\n" + " When running on BEAM, exceeding the maximum (or minimum) representable\n" + " value for a floating point value will raise an error.\n" + "\n" + " ## Division by zero\n" + "\n" + " Gleam runs on the Erlang virtual machine, which does not follow the IEEE\n" + " 754 standard for floating point arithmetic and does not have an `Infinity`\n" + " value. In Erlang division by zero results in a crash, however Gleam does\n" + " not have partial functions and operators in core so instead division by zero\n" + " returns zero, a behaviour taken from Pony, Coq, and Lean.\n" + "\n" + " This may seem unexpected at first, but it is no less mathematically valid\n" + " than crashing or returning a special value. Division by zero is undefined\n" + " in mathematics.\n" +). + +-file("src/gleam/float.gleam", 51). +?DOC( + " Attempts to parse a string as a `Float`, returning `Error(Nil)` if it was\n" + " not possible.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " parse(\"2.3\")\n" + " // -> Ok(2.3)\n" + " ```\n" + "\n" + " ```gleam\n" + " parse(\"ABC\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec parse(binary()) -> {ok, float()} | {error, nil}. +parse(String) -> + gleam_stdlib:parse_float(String). + +-file("src/gleam/float.gleam", 64). +?DOC( + " Returns the string representation of the provided `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_string(2.3)\n" + " // -> \"2.3\"\n" + " ```\n" +). +-spec to_string(float()) -> binary(). +to_string(X) -> + gleam_stdlib:float_to_string(X). + +-file("src/gleam/float.gleam", 95). +?DOC( + " Compares two `Float`s, returning an `Order`:\n" + " `Lt` for lower than, `Eq` for equals, or `Gt` for greater than.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " compare(2.0, 2.3)\n" + " // -> Lt\n" + " ```\n" + "\n" + " To handle\n" + " [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems)\n" + " you may use [`loosely_compare`](#loosely_compare) instead.\n" +). +-spec compare(float(), float()) -> gleam@order:order(). +compare(A, B) -> + case A =:= B of + true -> + eq; + + false -> + case A < B of + true -> + lt; + + false -> + gt + end + end. + +-file("src/gleam/float.gleam", 176). +?DOC( + " Compares two `Float`s, returning the smaller of the two.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " min(2.0, 2.3)\n" + " // -> 2.0\n" + " ```\n" +). +-spec min(float(), float()) -> float(). +min(A, B) -> + case A < B of + true -> + A; + + false -> + B + end. + +-file("src/gleam/float.gleam", 192). +?DOC( + " Compares two `Float`s, returning the larger of the two.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " max(2.0, 2.3)\n" + " // -> 2.3\n" + " ```\n" +). +-spec max(float(), float()) -> float(). +max(A, B) -> + case A > B of + true -> + A; + + false -> + B + end. + +-file("src/gleam/float.gleam", 75). +?DOC( + " Restricts a `Float` between a lower and upper bound.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " clamp(1.2, min: 1.4, max: 1.6)\n" + " // -> 1.4\n" + " ```\n" +). +-spec clamp(float(), float(), float()) -> float(). +clamp(X, Min_bound, Max_bound) -> + _pipe = X, + _pipe@1 = min(_pipe, Max_bound), + max(_pipe@1, Min_bound). + +-file("src/gleam/float.gleam", 210). +?DOC( + " Rounds the value to the next highest whole number as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " ceiling(2.3)\n" + " // -> 3.0\n" + " ```\n" +). +-spec ceiling(float()) -> float(). +ceiling(X) -> + math:ceil(X). + +-file("src/gleam/float.gleam", 223). +?DOC( + " Rounds the value to the next lowest whole number as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " floor(2.3)\n" + " // -> 2.0\n" + " ```\n" +). +-spec floor(float()) -> float(). +floor(X) -> + math:floor(X). + +-file("src/gleam/float.gleam", 261). +?DOC( + " Returns the value as an `Int`, truncating all decimal digits.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " truncate(2.4343434847383438)\n" + " // -> 2\n" + " ```\n" +). +-spec truncate(float()) -> integer(). +truncate(X) -> + erlang:trunc(X). + +-file("src/gleam/float.gleam", 311). +?DOC( + " Returns the absolute value of the input as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " absolute_value(-12.5)\n" + " // -> 12.5\n" + " ```\n" + "\n" + " ```gleam\n" + " absolute_value(10.2)\n" + " // -> 10.2\n" + " ```\n" +). +-spec absolute_value(float()) -> float(). +absolute_value(X) -> + case X >= +0.0 of + true -> + X; + + false -> + +0.0 - X + end. + +-file("src/gleam/float.gleam", 125). +?DOC( + " Compares two `Float`s within a tolerance, returning an `Order`:\n" + " `Lt` for lower than, `Eq` for equals, or `Gt` for greater than.\n" + "\n" + " This function allows Float comparison while handling\n" + " [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems).\n" + "\n" + " Notice: For `Float`s the tolerance won't be exact:\n" + " `5.3 - 5.0` is not exactly `0.3`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " loosely_compare(5.0, with: 5.3, tolerating: 0.5)\n" + " // -> Eq\n" + " ```\n" + "\n" + " If you want to check only for equality you may use\n" + " [`loosely_equals`](#loosely_equals) instead.\n" +). +-spec loosely_compare(float(), float(), float()) -> gleam@order:order(). +loosely_compare(A, B, Tolerance) -> + Difference = absolute_value(A - B), + case Difference =< Tolerance of + true -> + eq; + + false -> + compare(A, B) + end. + +-file("src/gleam/float.gleam", 158). +?DOC( + " Checks for equality of two `Float`s within a tolerance,\n" + " returning an `Bool`.\n" + "\n" + " This function allows Float comparison while handling\n" + " [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems).\n" + "\n" + " Notice: For `Float`s the tolerance won't be exact:\n" + " `5.3 - 5.0` is not exactly `0.3`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " loosely_equals(5.0, with: 5.3, tolerating: 0.5)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " loosely_equals(5.0, with: 5.1, tolerating: 0.1)\n" + " // -> False\n" + " ```\n" +). +-spec loosely_equals(float(), float(), float()) -> boolean(). +loosely_equals(A, B, Tolerance) -> + Difference = absolute_value(A - B), + Difference =< Tolerance. + +-file("src/gleam/float.gleam", 348). +?DOC( + " Returns the results of the base being raised to the power of the\n" + " exponent, as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " power(2.0, -1.0)\n" + " // -> Ok(0.5)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(2.0, 2.0)\n" + " // -> Ok(4.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(8.0, 1.5)\n" + " // -> Ok(22.627416997969522)\n" + " ```\n" + "\n" + " ```gleam\n" + " 4.0 |> power(of: 2.0)\n" + " // -> Ok(16.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(-1.0, 0.5)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec power(float(), float()) -> {ok, float()} | {error, nil}. +power(Base, Exponent) -> + Fractional = (math:ceil(Exponent) - Exponent) > +0.0, + case ((Base < +0.0) andalso Fractional) orelse ((Base =:= +0.0) andalso (Exponent + < +0.0)) of + true -> + {error, nil}; + + false -> + {ok, math:pow(Base, Exponent)} + end. + +-file("src/gleam/float.gleam", 380). +?DOC( + " Returns the square root of the input as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " square_root(4.0)\n" + " // -> Ok(2.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " square_root(-16.0)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec square_root(float()) -> {ok, float()} | {error, nil}. +square_root(X) -> + power(X, 0.5). + +-file("src/gleam/float.gleam", 393). +?DOC( + " Returns the negative of the value provided.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " negate(1.0)\n" + " // -> -1.0\n" + " ```\n" +). +-spec negate(float()) -> float(). +negate(X) -> + -1.0 * X. + +-file("src/gleam/float.gleam", 240). +?DOC( + " Rounds the value to the nearest whole number as an `Int`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " round(2.3)\n" + " // -> 2\n" + " ```\n" + "\n" + " ```gleam\n" + " round(2.5)\n" + " // -> 3\n" + " ```\n" +). +-spec round(float()) -> integer(). +round(X) -> + erlang:round(X). + +-file("src/gleam/float.gleam", 280). +?DOC( + " Converts the value to a given precision as a `Float`.\n" + " The precision is the number of allowed decimal places.\n" + " Negative precisions are allowed and force rounding\n" + " to the nearest tenth, hundredth, thousandth etc.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_precision(2.43434348473, precision: 2)\n" + " // -> 2.43\n" + " ```\n" + "\n" + " ```gleam\n" + " to_precision(547890.453444, precision: -3)\n" + " // -> 548000.0\n" + " ```\n" +). +-spec to_precision(float(), integer()) -> float(). +to_precision(X, Precision) -> + case Precision =< 0 of + true -> + Factor = math:pow(10.0, erlang:float(- Precision)), + erlang:float(erlang:round(case Factor of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> X / Gleam@denominator + end)) * Factor; + + false -> + Factor@1 = math:pow(10.0, erlang:float(Precision)), + case Factor@1 of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator@1 -> erlang:float(erlang:round(X * Factor@1)) + / Gleam@denominator@1 + end + end. + +-file("src/gleam/float.gleam", 410). +-spec sum_loop(list(float()), float()) -> float(). +sum_loop(Numbers, Initial) -> + case Numbers of + [First | Rest] -> + sum_loop(Rest, First + Initial); + + [] -> + Initial + end. + +-file("src/gleam/float.gleam", 406). +?DOC( + " Sums a list of `Float`s.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " sum([1.0, 2.2, 3.3])\n" + " // -> 6.5\n" + " ```\n" +). +-spec sum(list(float())) -> float(). +sum(Numbers) -> + sum_loop(Numbers, +0.0). + +-file("src/gleam/float.gleam", 430). +-spec product_loop(list(float()), float()) -> float(). +product_loop(Numbers, Initial) -> + case Numbers of + [First | Rest] -> + product_loop(Rest, First * Initial); + + [] -> + Initial + end. + +-file("src/gleam/float.gleam", 426). +?DOC( + " Multiplies a list of `Float`s and returns the product.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " product([2.5, 3.2, 4.2])\n" + " // -> 33.6\n" + " ```\n" +). +-spec product(list(float())) -> float(). +product(Numbers) -> + product_loop(Numbers, 1.0). + +-file("src/gleam/float.gleam", 452). +?DOC( + " Generates a random float between the given zero (inclusive) and one\n" + " (exclusive).\n" + "\n" + " On Erlang this updates the random state in the process dictionary.\n" + " See: \n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " random()\n" + " // -> 0.646355926896028\n" + " ```\n" +). +-spec random() -> float(). +random() -> + rand:uniform(). + +-file("src/gleam/float.gleam", 481). +?DOC( + " Computes the modulo of an float division of inputs as a `Result`.\n" + "\n" + " Returns division of the inputs as a `Result`: If the given divisor equals\n" + " `0`, this function returns an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " modulo(13.3, by: 3.3)\n" + " // -> Ok(0.1)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(-13.3, by: 3.3)\n" + " // -> Ok(3.2)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(13.3, by: -3.3)\n" + " // -> Ok(-3.2)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(-13.3, by: -3.3)\n" + " // -> Ok(-0.1)\n" + " ```\n" +). +-spec modulo(float(), float()) -> {ok, float()} | {error, nil}. +modulo(Dividend, Divisor) -> + case Divisor of + +0.0 -> + {error, nil}; + + _ -> + {ok, Dividend - (math:floor(case Divisor of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> Dividend / Gleam@denominator + end) * Divisor)} + end. + +-file("src/gleam/float.gleam", 502). +?DOC( + " Returns division of the inputs as a `Result`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " divide(0.0, 1.0)\n" + " // -> Ok(0.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " divide(1.0, 0.0)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec divide(float(), float()) -> {ok, float()} | {error, nil}. +divide(A, B) -> + case B of + +0.0 -> + {error, nil}; + + B@1 -> + {ok, case B@1 of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> A / Gleam@denominator + end} + end. + +-file("src/gleam/float.gleam", 533). +?DOC( + " Adds two floats together.\n" + "\n" + " It's the function equivalent of the `+.` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " add(1.0, 2.0)\n" + " // -> 3.0\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + "\n" + " list.fold([1.0, 2.0, 3.0], 0.0, add)\n" + " // -> 6.0\n" + " ```\n" + "\n" + " ```gleam\n" + " 3.0 |> add(2.0)\n" + " // -> 5.0\n" + " ```\n" +). +-spec add(float(), float()) -> float(). +add(A, B) -> + A + B. + +-file("src/gleam/float.gleam", 561). +?DOC( + " Multiplies two floats together.\n" + "\n" + " It's the function equivalent of the `*.` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " multiply(2.0, 4.0)\n" + " // -> 8.0\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + "\n" + " list.fold([2.0, 3.0, 4.0], 1.0, multiply)\n" + " // -> 24.0\n" + " ```\n" + "\n" + " ```gleam\n" + " 3.0 |> multiply(2.0)\n" + " // -> 6.0\n" + " ```\n" +). +-spec multiply(float(), float()) -> float(). +multiply(A, B) -> + A * B. + +-file("src/gleam/float.gleam", 594). +?DOC( + " Subtracts one float from another.\n" + "\n" + " It's the function equivalent of the `-.` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " subtract(3.0, 1.0)\n" + " // -> 2.0\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + "\n" + " list.fold([1.0, 2.0, 3.0], 10.0, subtract)\n" + " // -> 4.0\n" + " ```\n" + "\n" + " ```gleam\n" + " 3.0 |> subtract(_, 2.0)\n" + " // -> 1.0\n" + " ```\n" + "\n" + " ```gleam\n" + " 3.0 |> subtract(2.0, _)\n" + " // -> -1.0\n" + " ```\n" +). +-spec subtract(float(), float()) -> float(). +subtract(A, B) -> + A - B. + +-file("src/gleam/float.gleam", 623). +?DOC( + " Returns the natural logarithm (base e) of the given as a `Result`. If the\n" + " input is less than or equal to 0, returns `Error(Nil)`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " logarithm(1.0)\n" + " // -> Ok(0.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " logarithm(2.718281828459045) // e\n" + " // -> Ok(1.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " logarithm(0.0)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " logarithm(-1.0)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec logarithm(float()) -> {ok, float()} | {error, nil}. +logarithm(X) -> + case X =< +0.0 of + true -> + {error, nil}; + + false -> + {ok, math:log(X)} + end. + +-file("src/gleam/float.gleam", 661). +?DOC( + " Returns e (Euler's number) raised to the power of the given exponent, as\n" + " a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " exponential(0.0)\n" + " // -> Ok(1.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " exponential(1.0)\n" + " // -> Ok(2.718281828459045)\n" + " ```\n" + "\n" + " ```gleam\n" + " exponential(-1.0)\n" + " // -> Ok(0.36787944117144233)\n" + " ```\n" +). +-spec exponential(float()) -> float(). +exponential(X) -> + math:exp(X). diff --git a/build/dev/javascript/gleam_stdlib/gleam@function.erl b/build/dev/javascript/gleam_stdlib/gleam@function.erl new file mode 100644 index 0000000..a6dec81 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@function.erl @@ -0,0 +1,30 @@ +-module(gleam@function). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/function.gleam"). +-export([identity/1, tap/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-file("src/gleam/function.gleam", 3). +?DOC(" Takes a single argument and always returns its input value.\n"). +-spec identity(CLA) -> CLA. +identity(X) -> + X. + +-file("src/gleam/function.gleam", 12). +?DOC( + " Takes an argument and a single function, calls that function with that\n" + " argument and returns that argument instead of the function return value.\n" + "\n" + " Useful for running synchronous side effects in a pipeline.\n" +). +-spec tap(CLB, fun((CLB) -> any())) -> CLB. +tap(Arg, Effect) -> + Effect(Arg), + Arg. diff --git a/build/dev/javascript/gleam_stdlib/gleam@int.erl b/build/dev/javascript/gleam_stdlib/gleam@int.erl new file mode 100644 index 0000000..5e298da --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@int.erl @@ -0,0 +1,986 @@ +-module(gleam@int). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/int.gleam"). +-export([absolute_value/1, parse/1, base_parse/2, to_string/1, to_base_string/2, to_base2/1, to_base8/1, to_base16/1, to_base36/1, to_float/1, power/2, square_root/1, compare/2, min/2, max/2, clamp/3, is_even/1, is_odd/1, negate/1, sum/1, product/1, digits/2, undigits/2, random/1, divide/2, remainder/2, modulo/2, floor_divide/2, add/2, multiply/2, subtract/2, bitwise_and/2, bitwise_not/1, bitwise_or/2, bitwise_exclusive_or/2, bitwise_shift_left/2, bitwise_shift_right/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Functions for working with integers.\n" + "\n" + " ## Division by zero\n" + "\n" + " In Erlang division by zero results in a crash, however Gleam does not have\n" + " partial functions and operators in core so instead division by zero returns\n" + " zero, a behaviour taken from Pony, Coq, and Lean.\n" + "\n" + " This may seem unexpected at first, but it is no less mathematically valid\n" + " than crashing or returning a special value. Division by zero is undefined\n" + " in mathematics.\n" +). + +-file("src/gleam/int.gleam", 30). +?DOC( + " Returns the absolute value of the input.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " absolute_value(-12)\n" + " // -> 12\n" + " ```\n" + "\n" + " ```gleam\n" + " absolute_value(10)\n" + " // -> 10\n" + " ```\n" +). +-spec absolute_value(integer()) -> integer(). +absolute_value(X) -> + case X >= 0 of + true -> + X; + + false -> + X * -1 + end. + +-file("src/gleam/int.gleam", 109). +?DOC( + " Parses a given string as an int if possible.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " parse(\"2\")\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " parse(\"ABC\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec parse(binary()) -> {ok, integer()} | {error, nil}. +parse(String) -> + gleam_stdlib:parse_int(String). + +-file("src/gleam/int.gleam", 141). +?DOC( + " Parses a given string as an int in a given base if possible.\n" + " Supports only bases 2 to 36, for values outside of which this function returns an `Error(Nil)`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " base_parse(\"10\", 2)\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " base_parse(\"30\", 16)\n" + " // -> Ok(48)\n" + " ```\n" + "\n" + " ```gleam\n" + " base_parse(\"1C\", 36)\n" + " // -> Ok(48)\n" + " ```\n" + "\n" + " ```gleam\n" + " base_parse(\"48\", 1)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " base_parse(\"48\", 37)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec base_parse(binary(), integer()) -> {ok, integer()} | {error, nil}. +base_parse(String, Base) -> + case (Base >= 2) andalso (Base =< 36) of + true -> + gleam_stdlib:int_from_base_string(String, Base); + + false -> + {error, nil} + end. + +-file("src/gleam/int.gleam", 163). +?DOC( + " Prints a given int to a string.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_string(2)\n" + " // -> \"2\"\n" + " ```\n" +). +-spec to_string(integer()) -> binary(). +to_string(X) -> + erlang:integer_to_binary(X). + +-file("src/gleam/int.gleam", 196). +?DOC( + " Prints a given int to a string using the base number provided.\n" + " Supports only bases 2 to 36, for values outside of which this function returns an `Error(Nil)`.\n" + " For common bases (2, 8, 16, 36), use the `to_baseN` functions.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_base_string(2, 2)\n" + " // -> Ok(\"10\")\n" + " ```\n" + "\n" + " ```gleam\n" + " to_base_string(48, 16)\n" + " // -> Ok(\"30\")\n" + " ```\n" + "\n" + " ```gleam\n" + " to_base_string(48, 36)\n" + " // -> Ok(\"1C\")\n" + " ```\n" + "\n" + " ```gleam\n" + " to_base_string(48, 1)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " to_base_string(48, 37)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec to_base_string(integer(), integer()) -> {ok, binary()} | {error, nil}. +to_base_string(X, Base) -> + case (Base >= 2) andalso (Base =< 36) of + true -> + {ok, erlang:integer_to_binary(X, Base)}; + + false -> + {error, nil} + end. + +-file("src/gleam/int.gleam", 216). +?DOC( + " Prints a given int to a string using base-2.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_base2(2)\n" + " // -> \"10\"\n" + " ```\n" +). +-spec to_base2(integer()) -> binary(). +to_base2(X) -> + erlang:integer_to_binary(X, 2). + +-file("src/gleam/int.gleam", 229). +?DOC( + " Prints a given int to a string using base-8.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_base8(15)\n" + " // -> \"17\"\n" + " ```\n" +). +-spec to_base8(integer()) -> binary(). +to_base8(X) -> + erlang:integer_to_binary(X, 8). + +-file("src/gleam/int.gleam", 242). +?DOC( + " Prints a given int to a string using base-16.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_base16(48)\n" + " // -> \"30\"\n" + " ```\n" +). +-spec to_base16(integer()) -> binary(). +to_base16(X) -> + erlang:integer_to_binary(X, 16). + +-file("src/gleam/int.gleam", 255). +?DOC( + " Prints a given int to a string using base-36.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_base36(48)\n" + " // -> \"1C\"\n" + " ```\n" +). +-spec to_base36(integer()) -> binary(). +to_base36(X) -> + erlang:integer_to_binary(X, 36). + +-file("src/gleam/int.gleam", 280). +?DOC( + " Takes an int and returns its value as a float.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_float(5)\n" + " // -> 5.0\n" + " ```\n" + "\n" + " ```gleam\n" + " to_float(0)\n" + " // -> 0.0\n" + " ```\n" + "\n" + " ```gleam\n" + " to_float(-3)\n" + " // -> -3.0\n" + " ```\n" +). +-spec to_float(integer()) -> float(). +to_float(X) -> + erlang:float(X). + +-file("src/gleam/int.gleam", 67). +?DOC( + " Returns the results of the base being raised to the power of the\n" + " exponent, as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " power(2, -1.0)\n" + " // -> Ok(0.5)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(2, 2.0)\n" + " // -> Ok(4.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(8, 1.5)\n" + " // -> Ok(22.627416997969522)\n" + " ```\n" + "\n" + " ```gleam\n" + " 4 |> power(of: 2.0)\n" + " // -> Ok(16.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(-1, 0.5)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec power(integer(), float()) -> {ok, float()} | {error, nil}. +power(Base, Exponent) -> + _pipe = Base, + _pipe@1 = erlang:float(_pipe), + gleam@float:power(_pipe@1, Exponent). + +-file("src/gleam/int.gleam", 87). +?DOC( + " Returns the square root of the input as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " square_root(4)\n" + " // -> Ok(2.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " square_root(-16)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec square_root(integer()) -> {ok, float()} | {error, nil}. +square_root(X) -> + _pipe = X, + _pipe@1 = erlang:float(_pipe), + gleam@float:square_root(_pipe@1). + +-file("src/gleam/int.gleam", 316). +?DOC( + " Compares two ints, returning an order.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " compare(2, 3)\n" + " // -> Lt\n" + " ```\n" + "\n" + " ```gleam\n" + " compare(4, 3)\n" + " // -> Gt\n" + " ```\n" + "\n" + " ```gleam\n" + " compare(3, 3)\n" + " // -> Eq\n" + " ```\n" +). +-spec compare(integer(), integer()) -> gleam@order:order(). +compare(A, B) -> + case A =:= B of + true -> + eq; + + false -> + case A < B of + true -> + lt; + + false -> + gt + end + end. + +-file("src/gleam/int.gleam", 336). +?DOC( + " Compares two ints, returning the smaller of the two.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " min(2, 3)\n" + " // -> 2\n" + " ```\n" +). +-spec min(integer(), integer()) -> integer(). +min(A, B) -> + case A < B of + true -> + A; + + false -> + B + end. + +-file("src/gleam/int.gleam", 352). +?DOC( + " Compares two ints, returning the larger of the two.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " max(2, 3)\n" + " // -> 3\n" + " ```\n" +). +-spec max(integer(), integer()) -> integer(). +max(A, B) -> + case A > B of + true -> + A; + + false -> + B + end. + +-file("src/gleam/int.gleam", 291). +?DOC( + " Restricts an int between a lower and upper bound.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " clamp(40, min: 50, max: 60)\n" + " // -> 50\n" + " ```\n" +). +-spec clamp(integer(), integer(), integer()) -> integer(). +clamp(X, Min_bound, Max_bound) -> + _pipe = X, + _pipe@1 = min(_pipe, Max_bound), + max(_pipe@1, Min_bound). + +-file("src/gleam/int.gleam", 373). +?DOC( + " Returns whether the value provided is even.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_even(2)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_even(3)\n" + " // -> False\n" + " ```\n" +). +-spec is_even(integer()) -> boolean(). +is_even(X) -> + (X rem 2) =:= 0. + +-file("src/gleam/int.gleam", 391). +?DOC( + " Returns whether the value provided is odd.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_odd(3)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_odd(2)\n" + " // -> False\n" + " ```\n" +). +-spec is_odd(integer()) -> boolean(). +is_odd(X) -> + (X rem 2) /= 0. + +-file("src/gleam/int.gleam", 404). +?DOC( + " Returns the negative of the value provided.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " negate(1)\n" + " // -> -1\n" + " ```\n" +). +-spec negate(integer()) -> integer(). +negate(X) -> + -1 * X. + +-file("src/gleam/int.gleam", 421). +-spec sum_loop(list(integer()), integer()) -> integer(). +sum_loop(Numbers, Initial) -> + case Numbers of + [First | Rest] -> + sum_loop(Rest, First + Initial); + + [] -> + Initial + end. + +-file("src/gleam/int.gleam", 417). +?DOC( + " Sums a list of ints.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " sum([1, 2, 3])\n" + " // -> 6\n" + " ```\n" +). +-spec sum(list(integer())) -> integer(). +sum(Numbers) -> + sum_loop(Numbers, 0). + +-file("src/gleam/int.gleam", 441). +-spec product_loop(list(integer()), integer()) -> integer(). +product_loop(Numbers, Initial) -> + case Numbers of + [First | Rest] -> + product_loop(Rest, First * Initial); + + [] -> + Initial + end. + +-file("src/gleam/int.gleam", 437). +?DOC( + " Multiplies a list of ints and returns the product.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " product([2, 3, 4])\n" + " // -> 24\n" + " ```\n" +). +-spec product(list(integer())) -> integer(). +product(Numbers) -> + product_loop(Numbers, 1). + +-file("src/gleam/int.gleam", 456). +-spec digits_loop(integer(), integer(), list(integer())) -> list(integer()). +digits_loop(X, Base, Acc) -> + case absolute_value(X) < Base of + true -> + [X | Acc]; + + false -> + digits_loop(case Base of + 0 -> 0; + Gleam@denominator -> X div Gleam@denominator + end, Base, [case Base of + 0 -> 0; + Gleam@denominator@1 -> X rem Gleam@denominator@1 + end | Acc]) + end. + +-file("src/gleam/int.gleam", 449). +-spec digits(integer(), integer()) -> {ok, list(integer())} | {error, nil}. +digits(X, Base) -> + case Base < 2 of + true -> + {error, nil}; + + false -> + {ok, digits_loop(X, Base, [])} + end. + +-file("src/gleam/int.gleam", 471). +-spec undigits_loop(list(integer()), integer(), integer()) -> {ok, integer()} | + {error, nil}. +undigits_loop(Numbers, Base, Acc) -> + case Numbers of + [] -> + {ok, Acc}; + + [Digit | _] when Digit >= Base -> + {error, nil}; + + [Digit@1 | Rest] -> + undigits_loop(Rest, Base, (Acc * Base) + Digit@1) + end. + +-file("src/gleam/int.gleam", 464). +-spec undigits(list(integer()), integer()) -> {ok, integer()} | {error, nil}. +undigits(Numbers, Base) -> + case Base < 2 of + true -> + {error, nil}; + + false -> + undigits_loop(Numbers, Base, 0) + end. + +-file("src/gleam/int.gleam", 500). +?DOC( + " Generates a random int between zero and the given maximum.\n" + "\n" + " The lower number is inclusive, the upper number is exclusive.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " random(10)\n" + " // -> 4\n" + " ```\n" + "\n" + " ```gleam\n" + " random(1)\n" + " // -> 0\n" + " ```\n" + "\n" + " ```gleam\n" + " random(-1)\n" + " // -> -1\n" + " ```\n" +). +-spec random(integer()) -> integer(). +random(Max) -> + _pipe = (rand:uniform() * erlang:float(Max)), + _pipe@1 = math:floor(_pipe), + erlang:round(_pipe@1). + +-file("src/gleam/int.gleam", 533). +?DOC( + " Performs a truncated integer division.\n" + "\n" + " Returns division of the inputs as a `Result`: If the given divisor equals\n" + " `0`, this function returns an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " divide(0, 1)\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " divide(1, 0)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " divide(5, 2)\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " divide(-99, 2)\n" + " // -> Ok(-49)\n" + " ```\n" +). +-spec divide(integer(), integer()) -> {ok, integer()} | {error, nil}. +divide(Dividend, Divisor) -> + case Divisor of + 0 -> + {error, nil}; + + Divisor@1 -> + {ok, case Divisor@1 of + 0 -> 0; + Gleam@denominator -> Dividend div Gleam@denominator + end} + end. + +-file("src/gleam/int.gleam", 585). +?DOC( + " Computes the remainder of an integer division of inputs as a `Result`.\n" + "\n" + " Returns division of the inputs as a `Result`: If the given divisor equals\n" + " `0`, this function returns an `Error`.\n" + "\n" + " Most the time you will want to use the `%` operator instead of this\n" + " function.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " remainder(3, 2)\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(1, 0)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(10, -1)\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(13, by: 3)\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(-13, by: 3)\n" + " // -> Ok(-1)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(13, by: -3)\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(-13, by: -3)\n" + " // -> Ok(-1)\n" + " ```\n" +). +-spec remainder(integer(), integer()) -> {ok, integer()} | {error, nil}. +remainder(Dividend, Divisor) -> + case Divisor of + 0 -> + {error, nil}; + + Divisor@1 -> + {ok, case Divisor@1 of + 0 -> 0; + Gleam@denominator -> Dividend rem Gleam@denominator + end} + end. + +-file("src/gleam/int.gleam", 627). +?DOC( + " Computes the modulo of an integer division of inputs as a `Result`.\n" + "\n" + " Returns division of the inputs as a `Result`: If the given divisor equals\n" + " `0`, this function returns an `Error`.\n" + "\n" + " Most the time you will want to use the `%` operator instead of this\n" + " function.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " modulo(3, 2)\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(1, 0)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(10, -1)\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(13, by: 3)\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(-13, by: 3)\n" + " // -> Ok(2)\n" + " ```\n" +). +-spec modulo(integer(), integer()) -> {ok, integer()} | {error, nil}. +modulo(Dividend, Divisor) -> + case Divisor of + 0 -> + {error, nil}; + + _ -> + Remainder = case Divisor of + 0 -> 0; + Gleam@denominator -> Dividend rem Gleam@denominator + end, + case (Remainder * Divisor) < 0 of + true -> + {ok, Remainder + Divisor}; + + false -> + {ok, Remainder} + end + end. + +-file("src/gleam/int.gleam", 671). +?DOC( + " Performs a *floored* integer division, which means that the result will\n" + " always be rounded towards negative infinity.\n" + "\n" + " If you want to perform truncated integer division (rounding towards zero),\n" + " use `int.divide()` or the `/` operator instead.\n" + "\n" + " Returns division of the inputs as a `Result`: If the given divisor equals\n" + " `0`, this function returns an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " floor_divide(1, 0)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " floor_divide(5, 2)\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " floor_divide(6, -4)\n" + " // -> Ok(-2)\n" + " ```\n" + "\n" + " ```gleam\n" + " floor_divide(-99, 2)\n" + " // -> Ok(-50)\n" + " ```\n" +). +-spec floor_divide(integer(), integer()) -> {ok, integer()} | {error, nil}. +floor_divide(Dividend, Divisor) -> + case Divisor of + 0 -> + {error, nil}; + + Divisor@1 -> + case ((Dividend * Divisor@1) < 0) andalso ((case Divisor@1 of + 0 -> 0; + Gleam@denominator -> Dividend rem Gleam@denominator + end) /= 0) of + true -> + {ok, (case Divisor@1 of + 0 -> 0; + Gleam@denominator@1 -> Dividend div Gleam@denominator@1 + end) - 1}; + + false -> + {ok, case Divisor@1 of + 0 -> 0; + Gleam@denominator@2 -> Dividend div Gleam@denominator@2 + end} + end + end. + +-file("src/gleam/int.gleam", 705). +?DOC( + " Adds two integers together.\n" + "\n" + " It's the function equivalent of the `+` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " add(1, 2)\n" + " // -> 3\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + " list.fold([1, 2, 3], 0, add)\n" + " // -> 6\n" + " ```\n" + "\n" + " ```gleam\n" + " 3 |> add(2)\n" + " // -> 5\n" + " ```\n" +). +-spec add(integer(), integer()) -> integer(). +add(A, B) -> + A + B. + +-file("src/gleam/int.gleam", 733). +?DOC( + " Multiplies two integers together.\n" + "\n" + " It's the function equivalent of the `*` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " multiply(2, 4)\n" + " // -> 8\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + "\n" + " list.fold([2, 3, 4], 1, multiply)\n" + " // -> 24\n" + " ```\n" + "\n" + " ```gleam\n" + " 3 |> multiply(2)\n" + " // -> 6\n" + " ```\n" +). +-spec multiply(integer(), integer()) -> integer(). +multiply(A, B) -> + A * B. + +-file("src/gleam/int.gleam", 766). +?DOC( + " Subtracts one int from another.\n" + "\n" + " It's the function equivalent of the `-` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " subtract(3, 1)\n" + " // -> 2\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + "\n" + " list.fold([1, 2, 3], 10, subtract)\n" + " // -> 4\n" + " ```\n" + "\n" + " ```gleam\n" + " 3 |> subtract(2)\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " 3 |> subtract(2, _)\n" + " // -> -1\n" + " ```\n" +). +-spec subtract(integer(), integer()) -> integer(). +subtract(A, B) -> + A - B. + +-file("src/gleam/int.gleam", 778). +?DOC( + " Calculates the bitwise AND of its arguments.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_and(integer(), integer()) -> integer(). +bitwise_and(X, Y) -> + erlang:'band'(X, Y). + +-file("src/gleam/int.gleam", 788). +?DOC( + " Calculates the bitwise NOT of its argument.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_not(integer()) -> integer(). +bitwise_not(X) -> + erlang:'bnot'(X). + +-file("src/gleam/int.gleam", 798). +?DOC( + " Calculates the bitwise OR of its arguments.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_or(integer(), integer()) -> integer(). +bitwise_or(X, Y) -> + erlang:'bor'(X, Y). + +-file("src/gleam/int.gleam", 808). +?DOC( + " Calculates the bitwise XOR of its arguments.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_exclusive_or(integer(), integer()) -> integer(). +bitwise_exclusive_or(X, Y) -> + erlang:'bxor'(X, Y). + +-file("src/gleam/int.gleam", 818). +?DOC( + " Calculates the result of an arithmetic left bitshift.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_shift_left(integer(), integer()) -> integer(). +bitwise_shift_left(X, Y) -> + erlang:'bsl'(X, Y). + +-file("src/gleam/int.gleam", 828). +?DOC( + " Calculates the result of an arithmetic right bitshift.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_shift_right(integer(), integer()) -> integer(). +bitwise_shift_right(X, Y) -> + erlang:'bsr'(X, Y). diff --git a/build/dev/javascript/gleam_stdlib/gleam@io.erl b/build/dev/javascript/gleam_stdlib/gleam@io.erl new file mode 100644 index 0000000..e60295e --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@io.erl @@ -0,0 +1,80 @@ +-module(gleam@io). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/io.gleam"). +-export([print/1, print_error/1, println/1, println_error/1]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-file("src/gleam/io.gleam", 15). +?DOC( + " Writes a string to standard output (stdout).\n" + "\n" + " If you want your output to be printed on its own line see `println`.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " io.print(\"Hi mum\")\n" + " // -> Nil\n" + " // Hi mum\n" + " ```\n" +). +-spec print(binary()) -> nil. +print(String) -> + gleam_stdlib:print(String). + +-file("src/gleam/io.gleam", 31). +?DOC( + " Writes a string to standard error (stderr).\n" + "\n" + " If you want your output to be printed on its own line see `println_error`.\n" + "\n" + " ## Example\n" + "\n" + " ```\n" + " io.print_error(\"Hi pop\")\n" + " // -> Nil\n" + " // Hi pop\n" + " ```\n" +). +-spec print_error(binary()) -> nil. +print_error(String) -> + gleam_stdlib:print_error(String). + +-file("src/gleam/io.gleam", 45). +?DOC( + " Writes a string to standard output (stdout), appending a newline to the end.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " io.println(\"Hi mum\")\n" + " // -> Nil\n" + " // Hi mum\n" + " ```\n" +). +-spec println(binary()) -> nil. +println(String) -> + gleam_stdlib:println(String). + +-file("src/gleam/io.gleam", 59). +?DOC( + " Writes a string to standard error (stderr), appending a newline to the end.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " io.println_error(\"Hi pop\")\n" + " // -> Nil\n" + " // Hi pop\n" + " ```\n" +). +-spec println_error(binary()) -> nil. +println_error(String) -> + gleam_stdlib:println_error(String). diff --git a/build/dev/javascript/gleam_stdlib/gleam@list.erl b/build/dev/javascript/gleam_stdlib/gleam@list.erl new file mode 100644 index 0000000..0c96ecb --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@list.erl @@ -0,0 +1,2873 @@ +-module(gleam@list). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/list.gleam"). +-export([length/1, count/2, reverse/1, is_empty/1, contains/2, first/1, rest/1, group/2, filter/2, filter_map/2, map/2, map2/3, map_fold/3, index_map/2, try_map/2, drop/2, take/2, new/0, wrap/1, append/2, prepend/2, flatten/1, flat_map/2, fold/3, fold_right/3, index_fold/3, try_fold/3, fold_until/3, find/2, find_map/2, all/2, any/2, zip/2, strict_zip/2, unzip/1, intersperse/2, unique/1, sort/2, range/2, repeat/2, split/2, split_while/2, key_find/2, key_filter/2, key_pop/2, key_set/3, each/2, try_each/2, partition/2, window/2, window_by_2/1, drop_while/2, take_while/2, chunk/2, sized_chunk/2, reduce/2, scan/3, last/1, combinations/2, combination_pairs/1, transpose/1, interleave/1, shuffle/1, max/2, sample/2, permutations/1]). +-export_type([continue_or_stop/1, sorting/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Lists are an ordered sequence of elements and are one of the most common\n" + " data types in Gleam.\n" + "\n" + " New elements can be added and removed from the front of a list in\n" + " constant time, while adding and removing from the end requires traversing\n" + " and copying the whole list, so keep this in mind when designing your\n" + " programs.\n" + "\n" + " There is a dedicated syntax for prefixing to a list:\n" + "\n" + " ```gleam\n" + " let new_list = [1, 2, ..existing_list]\n" + " ```\n" + "\n" + " And a matching syntax for getting the first elements of a list:\n" + "\n" + " ```gleam\n" + " case list {\n" + " [first_element, ..rest] -> first_element\n" + " _ -> \"this pattern matches when the list is empty\"\n" + " }\n" + " ```\n" + "\n" +). + +-type continue_or_stop(XG) :: {continue, XG} | {stop, XG}. + +-type sorting() :: ascending | descending. + +-file("src/gleam/list.gleam", 60). +-spec length_loop(list(any()), integer()) -> integer(). +length_loop(List, Count) -> + case List of + [_ | List@1] -> + length_loop(List@1, Count + 1); + + [] -> + Count + end. + +-file("src/gleam/list.gleam", 56). +?DOC( + " Counts the number of elements in a given list.\n" + "\n" + " This function has to traverse the list to determine the number of elements,\n" + " so it runs in linear time.\n" + "\n" + " This function is natively implemented by the virtual machine and is highly\n" + " optimised.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " length([])\n" + " // -> 0\n" + " ```\n" + "\n" + " ```gleam\n" + " length([1])\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " length([1, 2])\n" + " // -> 2\n" + " ```\n" +). +-spec length(list(any())) -> integer(). +length(List) -> + erlang:length(List). + +-file("src/gleam/list.gleam", 93). +-spec count_loop(list(XN), fun((XN) -> boolean()), integer()) -> integer(). +count_loop(List, Predicate, Acc) -> + case List of + [] -> + Acc; + + [First | Rest] -> + case Predicate(First) of + true -> + count_loop(Rest, Predicate, Acc + 1); + + false -> + count_loop(Rest, Predicate, Acc) + end + end. + +-file("src/gleam/list.gleam", 89). +?DOC( + " Counts the number of elements in a given list satisfying a given predicate.\n" + "\n" + " This function has to traverse the list to determine the number of elements,\n" + " so it runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " count([], fn(a) { a > 0 })\n" + " // -> 0\n" + " ```\n" + "\n" + " ```gleam\n" + " count([1], fn(a) { a > 0 })\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " count([1, 2, 3], int.is_odd)\n" + " // -> 2\n" + " ```\n" +). +-spec count(list(XL), fun((XL) -> boolean())) -> integer(). +count(List, Predicate) -> + count_loop(List, Predicate, 0). + +-file("src/gleam/list.gleam", 131). +?DOC( + " Creates a new list from a given list containing the same elements but in the\n" + " opposite order.\n" + "\n" + " This function has to traverse the list to create the new reversed list, so\n" + " it runs in linear time.\n" + "\n" + " This function is natively implemented by the virtual machine and is highly\n" + " optimised.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " reverse([])\n" + " // -> []\n" + " ```\n" + "\n" + " ```gleam\n" + " reverse([1])\n" + " // -> [1]\n" + " ```\n" + "\n" + " ```gleam\n" + " reverse([1, 2])\n" + " // -> [2, 1]\n" + " ```\n" +). +-spec reverse(list(XP)) -> list(XP). +reverse(List) -> + lists:reverse(List). + +-file("src/gleam/list.gleam", 168). +?DOC( + " Determines whether or not the list is empty.\n" + "\n" + " This function runs in constant time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_empty([])\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_empty([1])\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " is_empty([1, 1])\n" + " // -> False\n" + " ```\n" +). +-spec is_empty(list(any())) -> boolean(). +is_empty(List) -> + List =:= []. + +-file("src/gleam/list.gleam", 204). +?DOC( + " Determines whether or not a given element exists within a given list.\n" + "\n" + " This function traverses the list to find the element, so it runs in linear\n" + " time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [] |> contains(any: 0)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " [0] |> contains(any: 0)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " [1] |> contains(any: 0)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " [1, 1] |> contains(any: 0)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " [1, 0] |> contains(any: 0)\n" + " // -> True\n" + " ```\n" +). +-spec contains(list(XY), XY) -> boolean(). +contains(List, Elem) -> + case List of + [] -> + false; + + [First | _] when First =:= Elem -> + true; + + [_ | Rest] -> + contains(Rest, Elem) + end. + +-file("src/gleam/list.gleam", 231). +?DOC( + " Gets the first element from the start of the list, if there is one.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " first([])\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " first([0])\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " first([1, 2])\n" + " // -> Ok(1)\n" + " ```\n" +). +-spec first(list(YA)) -> {ok, YA} | {error, nil}. +first(List) -> + case List of + [] -> + {error, nil}; + + [First | _] -> + {ok, First} + end. + +-file("src/gleam/list.gleam", 260). +?DOC( + " Returns the list minus the first element. If the list is empty, `Error(Nil)` is\n" + " returned.\n" + "\n" + " This function runs in constant time and does not make a copy of the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " rest([])\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " rest([0])\n" + " // -> Ok([])\n" + " ```\n" + "\n" + " ```gleam\n" + " rest([1, 2])\n" + " // -> Ok([2])\n" + " ```\n" +). +-spec rest(list(YE)) -> {ok, list(YE)} | {error, nil}. +rest(List) -> + case List of + [] -> + {error, nil}; + + [_ | Rest] -> + {ok, Rest} + end. + +-file("src/gleam/list.gleam", 302). +-spec group_loop(list(YP), fun((YP) -> YR), gleam@dict:dict(YR, list(YP))) -> gleam@dict:dict(YR, list(YP)). +group_loop(List, To_key, Groups) -> + case List of + [] -> + Groups; + + [First | Rest] -> + Key = To_key(First), + Groups@1 = case gleam_stdlib:map_get(Groups, Key) of + {error, _} -> + gleam@dict:insert(Groups, Key, [First]); + + {ok, Existing} -> + gleam@dict:insert(Groups, Key, [First | Existing]) + end, + group_loop(Rest, To_key, Groups@1) + end. + +-file("src/gleam/list.gleam", 298). +?DOC( + " Groups the elements from the given list by the given key function.\n" + "\n" + " Does not preserve the initial value order.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/dict\n" + "\n" + " [Ok(3), Error(\"Wrong\"), Ok(200), Ok(73)]\n" + " |> group(by: fn(i) {\n" + " case i {\n" + " Ok(_) -> \"Successful\"\n" + " Error(_) -> \"Failed\"\n" + " }\n" + " })\n" + " |> dict.to_list\n" + " // -> [\n" + " // #(\"Failed\", [Error(\"Wrong\")]),\n" + " // #(\"Successful\", [Ok(73), Ok(200), Ok(3)])\n" + " // ]\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/dict\n" + "\n" + " group([1,2,3,4,5], by: fn(i) { i - i / 3 * 3 })\n" + " |> dict.to_list\n" + " // -> [#(0, [3]), #(1, [4, 1]), #(2, [5, 2])]\n" + " ```\n" +). +-spec group(list(YJ), fun((YJ) -> YL)) -> gleam@dict:dict(YL, list(YJ)). +group(List, Key) -> + group_loop(List, Key, maps:new()). + +-file("src/gleam/list.gleam", 339). +-spec filter_loop(list(AAB), fun((AAB) -> boolean()), list(AAB)) -> list(AAB). +filter_loop(List, Fun, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + New_acc = case Fun(First) of + true -> + [First | Acc]; + + false -> + Acc + end, + filter_loop(Rest, Fun, New_acc) + end. + +-file("src/gleam/list.gleam", 335). +?DOC( + " Returns a new list containing only the elements from the first list for\n" + " which the given functions returns `True`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " filter([2, 4, 6, 1], fn(x) { x > 2 })\n" + " // -> [4, 6]\n" + " ```\n" + "\n" + " ```gleam\n" + " filter([2, 4, 6, 1], fn(x) { x > 6 })\n" + " // -> []\n" + " ```\n" +). +-spec filter(list(YY), fun((YY) -> boolean())) -> list(YY). +filter(List, Predicate) -> + filter_loop(List, Predicate, []). + +-file("src/gleam/list.gleam", 371). +-spec filter_map_loop( + list(AAM), + fun((AAM) -> {ok, AAO} | {error, any()}), + list(AAO) +) -> list(AAO). +filter_map_loop(List, Fun, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + New_acc = case Fun(First) of + {ok, First@1} -> + [First@1 | Acc]; + + {error, _} -> + Acc + end, + filter_map_loop(Rest, Fun, New_acc) + end. + +-file("src/gleam/list.gleam", 367). +?DOC( + " Returns a new list containing only the elements from the first list for\n" + " which the given functions returns `Ok(_)`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " filter_map([2, 4, 6, 1], Error)\n" + " // -> []\n" + " ```\n" + "\n" + " ```gleam\n" + " filter_map([2, 4, 6, 1], fn(x) { Ok(x + 1) })\n" + " // -> [3, 5, 7, 2]\n" + " ```\n" +). +-spec filter_map(list(AAF), fun((AAF) -> {ok, AAH} | {error, any()})) -> list(AAH). +filter_map(List, Fun) -> + filter_map_loop(List, Fun, []). + +-file("src/gleam/list.gleam", 402). +-spec map_loop(list(AAY), fun((AAY) -> ABA), list(ABA)) -> list(ABA). +map_loop(List, Fun, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + map_loop(Rest, Fun, [Fun(First) | Acc]) + end. + +-file("src/gleam/list.gleam", 398). +?DOC( + " Returns a new list containing only the elements of the first list after the\n" + " function has been applied to each one.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map([2, 4, 6], fn(x) { x * 2 })\n" + " // -> [4, 8, 12]\n" + " ```\n" +). +-spec map(list(AAU), fun((AAU) -> AAW)) -> list(AAW). +map(List, Fun) -> + map_loop(List, Fun, []). + +-file("src/gleam/list.gleam", 429). +-spec map2_loop(list(ABJ), list(ABL), fun((ABJ, ABL) -> ABN), list(ABN)) -> list(ABN). +map2_loop(List1, List2, Fun, Acc) -> + case {List1, List2} of + {[], _} -> + lists:reverse(Acc); + + {_, []} -> + lists:reverse(Acc); + + {[A | As_], [B | Bs]} -> + map2_loop(As_, Bs, Fun, [Fun(A, B) | Acc]) + end. + +-file("src/gleam/list.gleam", 425). +?DOC( + " Combines two lists into a single list using the given function.\n" + "\n" + " If a list is longer than the other the extra elements are dropped.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map2([1, 2, 3], [4, 5, 6], fn(x, y) { x + y })\n" + " // -> [5, 7, 9]\n" + " ```\n" + "\n" + " ```gleam\n" + " map2([1, 2], [\"a\", \"b\", \"c\"], fn(i, x) { #(i, x) })\n" + " // -> [#(1, \"a\"), #(2, \"b\")]\n" + " ```\n" +). +-spec map2(list(ABD), list(ABF), fun((ABD, ABF) -> ABH)) -> list(ABH). +map2(List1, List2, Fun) -> + map2_loop(List1, List2, Fun, []). + +-file("src/gleam/list.gleam", 462). +-spec map_fold_loop(list(ABV), fun((ABX, ABV) -> {ABX, ABY}), ABX, list(ABY)) -> {ABX, + list(ABY)}. +map_fold_loop(List, Fun, Acc, List_acc) -> + case List of + [] -> + {Acc, lists:reverse(List_acc)}; + + [First | Rest] -> + {Acc@1, First@1} = Fun(Acc, First), + map_fold_loop(Rest, Fun, Acc@1, [First@1 | List_acc]) + end. + +-file("src/gleam/list.gleam", 454). +?DOC( + " Similar to `map` but also lets you pass around an accumulated value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map_fold(\n" + " over: [1, 2, 3],\n" + " from: 100,\n" + " with: fn(memo, i) { #(memo + i, i * 2) }\n" + " )\n" + " // -> #(106, [2, 4, 6])\n" + " ```\n" +). +-spec map_fold(list(ABQ), ABS, fun((ABS, ABQ) -> {ABS, ABT})) -> {ABS, + list(ABT)}. +map_fold(List, Initial, Fun) -> + map_fold_loop(List, Fun, Initial, []). + +-file("src/gleam/list.gleam", 494). +-spec index_map_loop( + list(ACF), + fun((ACF, integer()) -> ACH), + integer(), + list(ACH) +) -> list(ACH). +index_map_loop(List, Fun, Index, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + Acc@1 = [Fun(First, Index) | Acc], + index_map_loop(Rest, Fun, Index + 1, Acc@1) + end. + +-file("src/gleam/list.gleam", 490). +?DOC( + " Returns a new list containing only the elements of the first list after the\n" + " function has been applied to each one and their index.\n" + "\n" + " The index starts at 0, so the first element is 0, the second is 1, and so\n" + " on.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " index_map([\"a\", \"b\"], fn(x, i) { #(i, x) })\n" + " // -> [#(0, \"a\"), #(1, \"b\")]\n" + " ```\n" +). +-spec index_map(list(ACB), fun((ACB, integer()) -> ACD)) -> list(ACD). +index_map(List, Fun) -> + index_map_loop(List, Fun, 0, []). + +-file("src/gleam/list.gleam", 548). +-spec try_map_loop(list(ACT), fun((ACT) -> {ok, ACV} | {error, ACW}), list(ACV)) -> {ok, + list(ACV)} | + {error, ACW}. +try_map_loop(List, Fun, Acc) -> + case List of + [] -> + {ok, lists:reverse(Acc)}; + + [First | Rest] -> + case Fun(First) of + {ok, First@1} -> + try_map_loop(Rest, Fun, [First@1 | Acc]); + + {error, Error} -> + {error, Error} + end + end. + +-file("src/gleam/list.gleam", 541). +?DOC( + " Takes a function that returns a `Result` and applies it to each element in a\n" + " given list in turn.\n" + "\n" + " If the function returns `Ok(new_value)` for all elements in the list then a\n" + " list of the new values is returned.\n" + "\n" + " If the function returns `Error(reason)` for any of the elements then it is\n" + " returned immediately. None of the elements in the list are processed after\n" + " one returns an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " try_map([1, 2, 3], fn(x) { Ok(x + 2) })\n" + " // -> Ok([3, 4, 5])\n" + " ```\n" + "\n" + " ```gleam\n" + " try_map([1, 2, 3], fn(_) { Error(0) })\n" + " // -> Error(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " try_map([[1], [2, 3]], first)\n" + " // -> Ok([1, 2])\n" + " ```\n" + "\n" + " ```gleam\n" + " try_map([[1], [], [2]], first)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec try_map(list(ACK), fun((ACK) -> {ok, ACM} | {error, ACN})) -> {ok, + list(ACM)} | + {error, ACN}. +try_map(List, Fun) -> + try_map_loop(List, Fun, []). + +-file("src/gleam/list.gleam", 583). +?DOC( + " Returns a list that is the given list with up to the given number of\n" + " elements removed from the front of the list.\n" + "\n" + " If the element has less than the number of elements an empty list is\n" + " returned.\n" + "\n" + " This function runs in linear time but does not copy the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " drop([1, 2, 3, 4], 2)\n" + " // -> [3, 4]\n" + " ```\n" + "\n" + " ```gleam\n" + " drop([1, 2, 3, 4], 9)\n" + " // -> []\n" + " ```\n" +). +-spec drop(list(ADD), integer()) -> list(ADD). +drop(List, N) -> + case N =< 0 of + true -> + List; + + false -> + case List of + [] -> + []; + + [_ | Rest] -> + drop(Rest, N - 1) + end + end. + +-file("src/gleam/list.gleam", 618). +-spec take_loop(list(ADJ), integer(), list(ADJ)) -> list(ADJ). +take_loop(List, N, Acc) -> + case N =< 0 of + true -> + lists:reverse(Acc); + + false -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + take_loop(Rest, N - 1, [First | Acc]) + end + end. + +-file("src/gleam/list.gleam", 614). +?DOC( + " Returns a list containing the first given number of elements from the given\n" + " list.\n" + "\n" + " If the element has less than the number of elements then the full list is\n" + " returned.\n" + "\n" + " This function runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " take([1, 2, 3, 4], 2)\n" + " // -> [1, 2]\n" + " ```\n" + "\n" + " ```gleam\n" + " take([1, 2, 3, 4], 9)\n" + " // -> [1, 2, 3, 4]\n" + " ```\n" +). +-spec take(list(ADG), integer()) -> list(ADG). +take(List, N) -> + take_loop(List, N, []). + +-file("src/gleam/list.gleam", 638). +?DOC( + " Returns a new empty list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new()\n" + " // -> []\n" + " ```\n" +). +-spec new() -> list(any()). +new() -> + []. + +-file("src/gleam/list.gleam", 658). +?DOC( + " Returns the given item wrapped in a list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " wrap(1)\n" + " // -> [1]\n" + "\n" + " wrap([\"a\", \"b\", \"c\"])\n" + " // -> [[\"a\", \"b\", \"c\"]]\n" + "\n" + " wrap([[]])\n" + " // -> [[[]]]\n" + " ```\n" +). +-spec wrap(ADP) -> list(ADP). +wrap(Item) -> + [Item]. + +-file("src/gleam/list.gleam", 679). +-spec append_loop(list(ADV), list(ADV)) -> list(ADV). +append_loop(First, Second) -> + case First of + [] -> + Second; + + [First@1 | Rest] -> + append_loop(Rest, [First@1 | Second]) + end. + +-file("src/gleam/list.gleam", 675). +?DOC( + " Joins one list onto the end of another.\n" + "\n" + " This function runs in linear time, and it traverses and copies the first\n" + " list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " append([1, 2], [3])\n" + " // -> [1, 2, 3]\n" + " ```\n" +). +-spec append(list(ADR), list(ADR)) -> list(ADR). +append(First, Second) -> + lists:append(First, Second). + +-file("src/gleam/list.gleam", 699). +?DOC( + " Prefixes an item to a list. This can also be done using the dedicated\n" + " syntax instead\n" + "\n" + " ```gleam\n" + " let existing_list = [2, 3, 4]\n" + "\n" + " [1, ..existing_list]\n" + " // -> [1, 2, 3, 4]\n" + "\n" + " prepend(to: existing_list, this: 1)\n" + " // -> [1, 2, 3, 4]\n" + " ```\n" +). +-spec prepend(list(ADZ), ADZ) -> list(ADZ). +prepend(List, Item) -> + [Item | List]. + +-file("src/gleam/list.gleam", 720). +-spec flatten_loop(list(list(AEG)), list(AEG)) -> list(AEG). +flatten_loop(Lists, Acc) -> + case Lists of + [] -> + lists:reverse(Acc); + + [List | Further_lists] -> + flatten_loop(Further_lists, lists:reverse(List, Acc)) + end. + +-file("src/gleam/list.gleam", 716). +?DOC( + " Joins a list of lists into a single list.\n" + "\n" + " This function traverses all elements twice on the JavaScript target.\n" + " This function traverses all elements once on the Erlang target.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " flatten([[1], [2, 3], []])\n" + " // -> [1, 2, 3]\n" + " ```\n" +). +-spec flatten(list(list(AEC))) -> list(AEC). +flatten(Lists) -> + lists:append(Lists). + +-file("src/gleam/list.gleam", 737). +?DOC( + " Maps the list with the given function into a list of lists, and then flattens it.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " flat_map([2, 4, 6], fn(x) { [x, x + 1] })\n" + " // -> [2, 3, 4, 5, 6, 7]\n" + " ```\n" +). +-spec flat_map(list(AEL), fun((AEL) -> list(AEN))) -> list(AEN). +flat_map(List, Fun) -> + lists:append(map(List, Fun)). + +-file("src/gleam/list.gleam", 749). +?DOC( + " Reduces a list of elements into a single value by calling a given function\n" + " on each element, going from left to right.\n" + "\n" + " `fold([1, 2, 3], 0, add)` is the equivalent of\n" + " `add(add(add(0, 1), 2), 3)`.\n" + "\n" + " This function runs in linear time.\n" +). +-spec fold(list(AEQ), AES, fun((AES, AEQ) -> AES)) -> AES. +fold(List, Initial, Fun) -> + case List of + [] -> + Initial; + + [First | Rest] -> + fold(Rest, Fun(Initial, First), Fun) + end. + +-file("src/gleam/list.gleam", 771). +?DOC( + " Reduces a list of elements into a single value by calling a given function\n" + " on each element, going from right to left.\n" + "\n" + " `fold_right([1, 2, 3], 0, add)` is the equivalent of\n" + " `add(add(add(0, 3), 2), 1)`.\n" + "\n" + " This function runs in linear time.\n" + "\n" + " Unlike `fold` this function is not tail recursive. Where possible use\n" + " `fold` instead as it will use less memory.\n" +). +-spec fold_right(list(AET), AEV, fun((AEV, AET) -> AEV)) -> AEV. +fold_right(List, Initial, Fun) -> + case List of + [] -> + Initial; + + [First | Rest] -> + Fun(fold_right(Rest, Initial, Fun), First) + end. + +-file("src/gleam/list.gleam", 808). +-spec index_fold_loop( + list(AEZ), + AFB, + fun((AFB, AEZ, integer()) -> AFB), + integer() +) -> AFB. +index_fold_loop(Over, Acc, With, Index) -> + case Over of + [] -> + Acc; + + [First | Rest] -> + index_fold_loop(Rest, With(Acc, First, Index), With, Index + 1) + end. + +-file("src/gleam/list.gleam", 800). +?DOC( + " Like fold but the folding function also receives the index of the current element.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [\"a\", \"b\", \"c\"]\n" + " |> index_fold(\"\", fn(acc, item, index) {\n" + " acc <> int.to_string(index) <> \":\" <> item <> \" \"\n" + " })\n" + " // -> \"0:a 1:b 2:c\"\n" + " ```\n" + "\n" + " ```gleam\n" + " [10, 20, 30]\n" + " |> index_fold(0, fn(acc, item, index) { acc + item * index })\n" + " // -> 80\n" + " ```\n" +). +-spec index_fold(list(AEW), AEY, fun((AEY, AEW, integer()) -> AEY)) -> AEY. +index_fold(List, Initial, Fun) -> + index_fold_loop(List, Initial, Fun, 0). + +-file("src/gleam/list.gleam", 840). +?DOC( + " A variant of fold that might fail.\n" + "\n" + " The folding function should return `Result(accumulator, error)`.\n" + " If the returned value is `Ok(accumulator)` try_fold will try the next value in the list.\n" + " If the returned value is `Error(error)` try_fold will stop and return that error.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [1, 2, 3, 4]\n" + " |> try_fold(0, fn(acc, i) {\n" + " case i < 3 {\n" + " True -> Ok(acc + i)\n" + " False -> Error(Nil)\n" + " }\n" + " })\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec try_fold(list(AFC), AFE, fun((AFE, AFC) -> {ok, AFE} | {error, AFF})) -> {ok, + AFE} | + {error, AFF}. +try_fold(List, Initial, Fun) -> + case List of + [] -> + {ok, Initial}; + + [First | Rest] -> + case Fun(Initial, First) of + {ok, Result} -> + try_fold(Rest, Result, Fun); + + {error, _} = Error -> + Error + end + end. + +-file("src/gleam/list.gleam", 879). +?DOC( + " A variant of fold that allows to stop folding earlier.\n" + "\n" + " The folding function should return `ContinueOrStop(accumulator)`.\n" + " If the returned value is `Continue(accumulator)` fold_until will try the next value in the list.\n" + " If the returned value is `Stop(accumulator)` fold_until will stop and return that accumulator.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [1, 2, 3, 4]\n" + " |> fold_until(0, fn(acc, i) {\n" + " case i < 3 {\n" + " True -> Continue(acc + i)\n" + " False -> Stop(acc)\n" + " }\n" + " })\n" + " // -> 3\n" + " ```\n" +). +-spec fold_until(list(AFK), AFM, fun((AFM, AFK) -> continue_or_stop(AFM))) -> AFM. +fold_until(List, Initial, Fun) -> + case List of + [] -> + Initial; + + [First | Rest] -> + case Fun(Initial, First) of + {continue, Next_accumulator} -> + fold_until(Rest, Next_accumulator, Fun); + + {stop, B} -> + B + end + end. + +-file("src/gleam/list.gleam", 916). +?DOC( + " Finds the first element in a given list for which the given function returns\n" + " `True`.\n" + "\n" + " Returns `Error(Nil)` if no such element is found.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " find([1, 2, 3], fn(x) { x > 2 })\n" + " // -> Ok(3)\n" + " ```\n" + "\n" + " ```gleam\n" + " find([1, 2, 3], fn(x) { x > 4 })\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " find([], fn(_) { True })\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec find(list(AFO), fun((AFO) -> boolean())) -> {ok, AFO} | {error, nil}. +find(List, Is_desired) -> + case List of + [] -> + {error, nil}; + + [First | Rest] -> + case Is_desired(First) of + true -> + {ok, First}; + + false -> + find(Rest, Is_desired) + end + end. + +-file("src/gleam/list.gleam", 952). +?DOC( + " Finds the first element in a given list for which the given function returns\n" + " `Ok(new_value)`, then returns the wrapped `new_value`.\n" + "\n" + " Returns `Error(Nil)` if no such element is found.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " find_map([[], [2], [3]], first)\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " find_map([[], []], first)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " find_map([], first)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec find_map(list(AFS), fun((AFS) -> {ok, AFU} | {error, any()})) -> {ok, AFU} | + {error, nil}. +find_map(List, Fun) -> + case List of + [] -> + {error, nil}; + + [First | Rest] -> + case Fun(First) of + {ok, First@1} -> + {ok, First@1}; + + {error, _} -> + find_map(Rest, Fun) + end + end. + +-file("src/gleam/list.gleam", 987). +?DOC( + " Returns `True` if the given function returns `True` for all the elements in\n" + " the given list. If the function returns `False` for any of the elements it\n" + " immediately returns `False` without checking the rest of the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " all([], fn(x) { x > 3 })\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " all([4, 5], fn(x) { x > 3 })\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " all([4, 3], fn(x) { x > 3 })\n" + " // -> False\n" + " ```\n" +). +-spec all(list(AGA), fun((AGA) -> boolean())) -> boolean(). +all(List, Predicate) -> + case List of + [] -> + true; + + [First | Rest] -> + case Predicate(First) of + true -> + all(Rest, Predicate); + + false -> + false + end + end. + +-file("src/gleam/list.gleam", 1024). +?DOC( + " Returns `True` if the given function returns `True` for any the elements in\n" + " the given list. If the function returns `True` for any of the elements it\n" + " immediately returns `True` without checking the rest of the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " any([], fn(x) { x > 3 })\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " any([4, 5], fn(x) { x > 3 })\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " any([4, 3], fn(x) { x > 4 })\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " any([3, 4], fn(x) { x > 3 })\n" + " // -> True\n" + " ```\n" +). +-spec any(list(AGC), fun((AGC) -> boolean())) -> boolean(). +any(List, Predicate) -> + case List of + [] -> + false; + + [First | Rest] -> + case Predicate(First) of + true -> + true; + + false -> + any(Rest, Predicate) + end + end. + +-file("src/gleam/list.gleam", 1066). +-spec zip_loop(list(AGJ), list(AGL), list({AGJ, AGL})) -> list({AGJ, AGL}). +zip_loop(One, Other, Acc) -> + case {One, Other} of + {[First_one | Rest_one], [First_other | Rest_other]} -> + zip_loop(Rest_one, Rest_other, [{First_one, First_other} | Acc]); + + {_, _} -> + lists:reverse(Acc) + end. + +-file("src/gleam/list.gleam", 1062). +?DOC( + " Takes two lists and returns a single list of 2-element tuples.\n" + "\n" + " If one of the lists is longer than the other, the remaining elements from\n" + " the longer list are not used.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " zip([], [])\n" + " // -> []\n" + " ```\n" + "\n" + " ```gleam\n" + " zip([1, 2], [3])\n" + " // -> [#(1, 3)]\n" + " ```\n" + "\n" + " ```gleam\n" + " zip([1], [3, 4])\n" + " // -> [#(1, 3)]\n" + " ```\n" + "\n" + " ```gleam\n" + " zip([1, 2], [3, 4])\n" + " // -> [#(1, 3), #(2, 4)]\n" + " ```\n" +). +-spec zip(list(AGE), list(AGG)) -> list({AGE, AGG}). +zip(List, Other) -> + zip_loop(List, Other, []). + +-file("src/gleam/list.gleam", 1107). +-spec strict_zip_loop(list(AGW), list(AGY), list({AGW, AGY})) -> {ok, + list({AGW, AGY})} | + {error, nil}. +strict_zip_loop(One, Other, Acc) -> + case {One, Other} of + {[], []} -> + {ok, lists:reverse(Acc)}; + + {[], _} -> + {error, nil}; + + {_, []} -> + {error, nil}; + + {[First_one | Rest_one], [First_other | Rest_other]} -> + strict_zip_loop( + Rest_one, + Rest_other, + [{First_one, First_other} | Acc] + ) + end. + +-file("src/gleam/list.gleam", 1100). +?DOC( + " Takes two lists and returns a single list of 2-element tuples.\n" + "\n" + " If one of the lists is longer than the other, an `Error` is returned.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " strict_zip([], [])\n" + " // -> Ok([])\n" + " ```\n" + "\n" + " ```gleam\n" + " strict_zip([1, 2], [3])\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " strict_zip([1], [3, 4])\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " strict_zip([1, 2], [3, 4])\n" + " // -> Ok([#(1, 3), #(2, 4)])\n" + " ```\n" +). +-spec strict_zip(list(AGP), list(AGR)) -> {ok, list({AGP, AGR})} | {error, nil}. +strict_zip(List, Other) -> + strict_zip_loop(List, Other, []). + +-file("src/gleam/list.gleam", 1138). +-spec unzip_loop(list({AHJ, AHK}), list(AHJ), list(AHK)) -> {list(AHJ), + list(AHK)}. +unzip_loop(Input, One, Other) -> + case Input of + [] -> + {lists:reverse(One), lists:reverse(Other)}; + + [{First_one, First_other} | Rest] -> + unzip_loop(Rest, [First_one | One], [First_other | Other]) + end. + +-file("src/gleam/list.gleam", 1134). +?DOC( + " Takes a single list of 2-element tuples and returns two lists.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " unzip([#(1, 2), #(3, 4)])\n" + " // -> #([1, 3], [2, 4])\n" + " ```\n" + "\n" + " ```gleam\n" + " unzip([])\n" + " // -> #([], [])\n" + " ```\n" +). +-spec unzip(list({AHE, AHF})) -> {list(AHE), list(AHF)}. +unzip(Input) -> + unzip_loop(Input, [], []). + +-file("src/gleam/list.gleam", 1173). +-spec intersperse_loop(list(AHT), AHT, list(AHT)) -> list(AHT). +intersperse_loop(List, Separator, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + intersperse_loop(Rest, Separator, [First, Separator | Acc]) + end. + +-file("src/gleam/list.gleam", 1166). +?DOC( + " Inserts a given value between each existing element in a given list.\n" + "\n" + " This function runs in linear time and copies the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " intersperse([1, 1, 1], 2)\n" + " // -> [1, 2, 1, 2, 1]\n" + " ```\n" + "\n" + " ```gleam\n" + " intersperse([], 2)\n" + " // -> []\n" + " ```\n" +). +-spec intersperse(list(AHQ), AHQ) -> list(AHQ). +intersperse(List, Elem) -> + case List of + [] -> + List; + + [_] -> + List; + + [First | Rest] -> + intersperse_loop(Rest, Elem, [First]) + end. + +-file("src/gleam/list.gleam", 1196). +-spec unique_loop(list(AIA), gleam@dict:dict(AIA, nil), list(AIA)) -> list(AIA). +unique_loop(List, Seen, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + case gleam@dict:has_key(Seen, First) of + true -> + unique_loop(Rest, Seen, Acc); + + false -> + unique_loop( + Rest, + gleam@dict:insert(Seen, First, nil), + [First | Acc] + ) + end + end. + +-file("src/gleam/list.gleam", 1192). +?DOC( + " Removes any duplicate elements from a given list.\n" + "\n" + " This function returns in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " unique([1, 1, 1, 4, 7, 3, 3, 4])\n" + " // -> [1, 4, 7, 3]\n" + " ```\n" +). +-spec unique(list(AHX)) -> list(AHX). +unique(List) -> + unique_loop(List, maps:new(), []). + +-file("src/gleam/list.gleam", 1282). +?DOC( + " Given a list it returns slices of it that are locally sorted in ascending\n" + " order.\n" + "\n" + " Imagine you have this list:\n" + "\n" + " ```\n" + " [1, 2, 3, 2, 1, 0]\n" + " ^^^^^^^ ^^^^^^^ This is a slice in descending order\n" + " |\n" + " | This is a slice that is sorted in ascending order\n" + " ```\n" + "\n" + " So the produced result will contain these two slices, each one sorted in\n" + " ascending order: `[[1, 2, 3], [0, 1, 2]]`.\n" + "\n" + " - `growing` is an accumulator with the current slice being grown\n" + " - `direction` is the growing direction of the slice being grown, it could\n" + " either be ascending or strictly descending\n" + " - `prev` is the previous element that needs to be added to the growing slice\n" + " it is carried around to check whether we have to keep growing the current\n" + " slice or not\n" + " - `acc` is the accumulator containing the slices sorted in ascending order\n" +). +-spec sequences( + list(AIJ), + fun((AIJ, AIJ) -> gleam@order:order()), + list(AIJ), + sorting(), + AIJ, + list(list(AIJ)) +) -> list(list(AIJ)). +sequences(List, Compare, Growing, Direction, Prev, Acc) -> + Growing@1 = [Prev | Growing], + case List of + [] -> + case Direction of + ascending -> + [lists:reverse(Growing@1) | Acc]; + + descending -> + [Growing@1 | Acc] + end; + + [New | Rest] -> + case {Compare(Prev, New), Direction} of + {gt, descending} -> + sequences(Rest, Compare, Growing@1, Direction, New, Acc); + + {lt, ascending} -> + sequences(Rest, Compare, Growing@1, Direction, New, Acc); + + {eq, ascending} -> + sequences(Rest, Compare, Growing@1, Direction, New, Acc); + + {gt, ascending} -> + Acc@1 = case Direction of + ascending -> + [lists:reverse(Growing@1) | Acc]; + + descending -> + [Growing@1 | Acc] + end, + case Rest of + [] -> + [[New] | Acc@1]; + + [Next | Rest@1] -> + Direction@1 = case Compare(New, Next) of + lt -> + ascending; + + eq -> + ascending; + + gt -> + descending + end, + sequences( + Rest@1, + Compare, + [New], + Direction@1, + Next, + Acc@1 + ) + end; + + {lt, descending} -> + Acc@1 = case Direction of + ascending -> + [lists:reverse(Growing@1) | Acc]; + + descending -> + [Growing@1 | Acc] + end, + case Rest of + [] -> + [[New] | Acc@1]; + + [Next | Rest@1] -> + Direction@1 = case Compare(New, Next) of + lt -> + ascending; + + eq -> + ascending; + + gt -> + descending + end, + sequences( + Rest@1, + Compare, + [New], + Direction@1, + Next, + Acc@1 + ) + end; + + {eq, descending} -> + Acc@1 = case Direction of + ascending -> + [lists:reverse(Growing@1) | Acc]; + + descending -> + [Growing@1 | Acc] + end, + case Rest of + [] -> + [[New] | Acc@1]; + + [Next | Rest@1] -> + Direction@1 = case Compare(New, Next) of + lt -> + ascending; + + eq -> + ascending; + + gt -> + descending + end, + sequences( + Rest@1, + Compare, + [New], + Direction@1, + Next, + Acc@1 + ) + end + end + end. + +-file("src/gleam/list.gleam", 1430). +?DOC( + " Merges two lists sorted in ascending order into a single list sorted in\n" + " descending order according to the given comparator function.\n" + "\n" + " This reversing of the sort order is not avoidable if we want to implement\n" + " merge as a tail recursive function. We could reverse the accumulator before\n" + " returning it but that would end up being less efficient; so the merging\n" + " algorithm has to play around this.\n" +). +-spec merge_ascendings( + list(AJG), + list(AJG), + fun((AJG, AJG) -> gleam@order:order()), + list(AJG) +) -> list(AJG). +merge_ascendings(List1, List2, Compare, Acc) -> + case {List1, List2} of + {[], List} -> + lists:reverse(List, Acc); + + {List, []} -> + lists:reverse(List, Acc); + + {[First1 | Rest1], [First2 | Rest2]} -> + case Compare(First1, First2) of + lt -> + merge_ascendings(Rest1, List2, Compare, [First1 | Acc]); + + gt -> + merge_ascendings(List1, Rest2, Compare, [First2 | Acc]); + + eq -> + merge_ascendings(List1, Rest2, Compare, [First2 | Acc]) + end + end. + +-file("src/gleam/list.gleam", 1383). +?DOC( + " Given a list of ascending lists, it merges adjacent pairs into a single\n" + " descending list, halving their number.\n" + " It returns a list of the remaining descending lists.\n" +). +-spec merge_ascending_pairs( + list(list(AIU)), + fun((AIU, AIU) -> gleam@order:order()), + list(list(AIU)) +) -> list(list(AIU)). +merge_ascending_pairs(Sequences, Compare, Acc) -> + case Sequences of + [] -> + lists:reverse(Acc); + + [Sequence] -> + lists:reverse([lists:reverse(Sequence) | Acc]); + + [Ascending1, Ascending2 | Rest] -> + Descending = merge_ascendings(Ascending1, Ascending2, Compare, []), + merge_ascending_pairs(Rest, Compare, [Descending | Acc]) + end. + +-file("src/gleam/list.gleam", 1457). +?DOC( + " This is exactly the same as merge_ascendings but mirrored: it merges two\n" + " lists sorted in descending order into a single list sorted in ascending\n" + " order according to the given comparator function.\n" + "\n" + " This reversing of the sort order is not avoidable if we want to implement\n" + " merge as a tail recursive function. We could reverse the accumulator before\n" + " returning it but that would end up being less efficient; so the merging\n" + " algorithm has to play around this.\n" +). +-spec merge_descendings( + list(AJL), + list(AJL), + fun((AJL, AJL) -> gleam@order:order()), + list(AJL) +) -> list(AJL). +merge_descendings(List1, List2, Compare, Acc) -> + case {List1, List2} of + {[], List} -> + lists:reverse(List, Acc); + + {List, []} -> + lists:reverse(List, Acc); + + {[First1 | Rest1], [First2 | Rest2]} -> + case Compare(First1, First2) of + lt -> + merge_descendings(List1, Rest2, Compare, [First2 | Acc]); + + gt -> + merge_descendings(Rest1, List2, Compare, [First1 | Acc]); + + eq -> + merge_descendings(Rest1, List2, Compare, [First1 | Acc]) + end + end. + +-file("src/gleam/list.gleam", 1405). +?DOC(" This is the same as merge_ascending_pairs but flipped for descending lists.\n"). +-spec merge_descending_pairs( + list(list(AJA)), + fun((AJA, AJA) -> gleam@order:order()), + list(list(AJA)) +) -> list(list(AJA)). +merge_descending_pairs(Sequences, Compare, Acc) -> + case Sequences of + [] -> + lists:reverse(Acc); + + [Sequence] -> + lists:reverse([lists:reverse(Sequence) | Acc]); + + [Descending1, Descending2 | Rest] -> + Ascending = merge_descendings(Descending1, Descending2, Compare, []), + merge_descending_pairs(Rest, Compare, [Ascending | Acc]) + end. + +-file("src/gleam/list.gleam", 1349). +?DOC( + " Given some some sorted sequences (assumed to be sorted in `direction`) it\n" + " merges them all together until we're left with just a list sorted in\n" + " ascending order.\n" +). +-spec merge_all( + list(list(AIQ)), + sorting(), + fun((AIQ, AIQ) -> gleam@order:order()) +) -> list(AIQ). +merge_all(Sequences, Direction, Compare) -> + case {Sequences, Direction} of + {[], _} -> + []; + + {[Sequence], ascending} -> + Sequence; + + {[Sequence@1], descending} -> + lists:reverse(Sequence@1); + + {_, ascending} -> + Sequences@1 = merge_ascending_pairs(Sequences, Compare, []), + merge_all(Sequences@1, descending, Compare); + + {_, descending} -> + Sequences@2 = merge_descending_pairs(Sequences, Compare, []), + merge_all(Sequences@2, ascending, Compare) + end. + +-file("src/gleam/list.gleam", 1220). +?DOC( + " Sorts from smallest to largest based upon the ordering specified by a given\n" + " function.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " sort([4, 3, 6, 5, 4, 1, 2], by: int.compare)\n" + " // -> [1, 2, 3, 4, 4, 5, 6]\n" + " ```\n" +). +-spec sort(list(AIG), fun((AIG, AIG) -> gleam@order:order())) -> list(AIG). +sort(List, Compare) -> + case List of + [] -> + []; + + [X] -> + [X]; + + [X@1, Y | Rest] -> + Direction = case Compare(X@1, Y) of + lt -> + ascending; + + eq -> + ascending; + + gt -> + descending + end, + Sequences = sequences(Rest, Compare, [X@1], Direction, Y, []), + merge_all(Sequences, ascending, Compare) + end. + +-file("src/gleam/list.gleam", 1497). +-spec range_loop(integer(), integer(), list(integer())) -> list(integer()). +range_loop(Start, Stop, Acc) -> + case gleam@int:compare(Start, Stop) of + eq -> + [Stop | Acc]; + + gt -> + range_loop(Start, Stop + 1, [Stop | Acc]); + + lt -> + range_loop(Start, Stop - 1, [Stop | Acc]) + end. + +-file("src/gleam/list.gleam", 1493). +?DOC( + " Creates a list of ints ranging from a given start and finish.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " range(0, 0)\n" + " // -> [0]\n" + " ```\n" + "\n" + " ```gleam\n" + " range(0, 5)\n" + " // -> [0, 1, 2, 3, 4, 5]\n" + " ```\n" + "\n" + " ```gleam\n" + " range(1, -5)\n" + " // -> [1, 0, -1, -2, -3, -4, -5]\n" + " ```\n" +). +-spec range(integer(), integer()) -> list(integer()). +range(Start, Stop) -> + range_loop(Start, Stop, []). + +-file("src/gleam/list.gleam", 1523). +-spec repeat_loop(AJV, integer(), list(AJV)) -> list(AJV). +repeat_loop(Item, Times, Acc) -> + case Times =< 0 of + true -> + Acc; + + false -> + repeat_loop(Item, Times - 1, [Item | Acc]) + end. + +-file("src/gleam/list.gleam", 1519). +?DOC( + " Builds a list of a given value a given number of times.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " repeat(\"a\", times: 0)\n" + " // -> []\n" + " ```\n" + "\n" + " ```gleam\n" + " repeat(\"a\", times: 5)\n" + " // -> [\"a\", \"a\", \"a\", \"a\", \"a\"]\n" + " ```\n" +). +-spec repeat(AJT, integer()) -> list(AJT). +repeat(A, Times) -> + repeat_loop(A, Times, []). + +-file("src/gleam/list.gleam", 1556). +-spec split_loop(list(AKC), integer(), list(AKC)) -> {list(AKC), list(AKC)}. +split_loop(List, N, Taken) -> + case N =< 0 of + true -> + {lists:reverse(Taken), List}; + + false -> + case List of + [] -> + {lists:reverse(Taken), []}; + + [First | Rest] -> + split_loop(Rest, N - 1, [First | Taken]) + end + end. + +-file("src/gleam/list.gleam", 1552). +?DOC( + " Splits a list in two before the given index.\n" + "\n" + " If the list is not long enough to have the given index the before list will\n" + " be the input list, and the after list will be empty.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " split([6, 7, 8, 9], 0)\n" + " // -> #([], [6, 7, 8, 9])\n" + " ```\n" + "\n" + " ```gleam\n" + " split([6, 7, 8, 9], 2)\n" + " // -> #([6, 7], [8, 9])\n" + " ```\n" + "\n" + " ```gleam\n" + " split([6, 7, 8, 9], 4)\n" + " // -> #([6, 7, 8, 9], [])\n" + " ```\n" +). +-spec split(list(AJY), integer()) -> {list(AJY), list(AJY)}. +split(List, Index) -> + split_loop(List, Index, []). + +-file("src/gleam/list.gleam", 1592). +-spec split_while_loop(list(AKL), fun((AKL) -> boolean()), list(AKL)) -> {list(AKL), + list(AKL)}. +split_while_loop(List, F, Acc) -> + case List of + [] -> + {lists:reverse(Acc), []}; + + [First | Rest] -> + case F(First) of + true -> + split_while_loop(Rest, F, [First | Acc]); + + false -> + {lists:reverse(Acc), List} + end + end. + +-file("src/gleam/list.gleam", 1585). +?DOC( + " Splits a list in two before the first element that a given function returns\n" + " `False` for.\n" + "\n" + " If the function returns `True` for all elements the first list will be the\n" + " input list, and the second list will be empty.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " split_while([1, 2, 3, 4, 5], fn(x) { x <= 3 })\n" + " // -> #([1, 2, 3], [4, 5])\n" + " ```\n" + "\n" + " ```gleam\n" + " split_while([1, 2, 3, 4, 5], fn(x) { x <= 5 })\n" + " // -> #([1, 2, 3, 4, 5], [])\n" + " ```\n" +). +-spec split_while(list(AKH), fun((AKH) -> boolean())) -> {list(AKH), list(AKH)}. +split_while(List, Predicate) -> + split_while_loop(List, Predicate, []). + +-file("src/gleam/list.gleam", 1632). +?DOC( + " Given a list of 2-element tuples, finds the first tuple that has a given\n" + " key as the first element and returns the second element.\n" + "\n" + " If no tuple is found with the given key then `Error(Nil)` is returned.\n" + "\n" + " This function may be useful for interacting with Erlang code where lists of\n" + " tuples are common.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " key_find([#(\"a\", 0), #(\"b\", 1)], \"a\")\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " key_find([#(\"a\", 0), #(\"b\", 1)], \"b\")\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " key_find([#(\"a\", 0), #(\"b\", 1)], \"c\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec key_find(list({AKQ, AKR}), AKQ) -> {ok, AKR} | {error, nil}. +key_find(Keyword_list, Desired_key) -> + find_map( + Keyword_list, + fun(Keyword) -> + {Key, Value} = Keyword, + case Key =:= Desired_key of + true -> + {ok, Value}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/list.gleam", 1663). +?DOC( + " Given a list of 2-element tuples, finds all tuples that have a given\n" + " key as the first element and returns the second element.\n" + "\n" + " This function may be useful for interacting with Erlang code where lists of\n" + " tuples are common.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " key_filter([#(\"a\", 0), #(\"b\", 1), #(\"a\", 2)], \"a\")\n" + " // -> [0, 2]\n" + " ```\n" + "\n" + " ```gleam\n" + " key_filter([#(\"a\", 0), #(\"b\", 1)], \"c\")\n" + " // -> []\n" + " ```\n" +). +-spec key_filter(list({AKV, AKW}), AKV) -> list(AKW). +key_filter(Keyword_list, Desired_key) -> + filter_map( + Keyword_list, + fun(Keyword) -> + {Key, Value} = Keyword, + case Key =:= Desired_key of + true -> + {ok, Value}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/list.gleam", 1703). +-spec key_pop_loop(list({ALF, ALG}), ALF, list({ALF, ALG})) -> {ok, + {ALG, list({ALF, ALG})}} | + {error, nil}. +key_pop_loop(List, Key, Checked) -> + case List of + [] -> + {error, nil}; + + [{K, V} | Rest] when K =:= Key -> + {ok, {V, lists:reverse(Checked, Rest)}}; + + [First | Rest@1] -> + key_pop_loop(Rest@1, Key, [First | Checked]) + end. + +-file("src/gleam/list.gleam", 1699). +?DOC( + " Given a list of 2-element tuples, finds the first tuple that has a given\n" + " key as the first element. This function will return the second element\n" + " of the found tuple and list with tuple removed.\n" + "\n" + " If no tuple is found with the given key then `Error(Nil)` is returned.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " key_pop([#(\"a\", 0), #(\"b\", 1)], \"a\")\n" + " // -> Ok(#(0, [#(\"b\", 1)]))\n" + " ```\n" + "\n" + " ```gleam\n" + " key_pop([#(\"a\", 0), #(\"b\", 1)], \"b\")\n" + " // -> Ok(#(1, [#(\"a\", 0)]))\n" + " ```\n" + "\n" + " ```gleam\n" + " key_pop([#(\"a\", 0), #(\"b\", 1)], \"c\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec key_pop(list({AKZ, ALA}), AKZ) -> {ok, {ALA, list({AKZ, ALA})}} | + {error, nil}. +key_pop(List, Key) -> + key_pop_loop(List, Key, []). + +-file("src/gleam/list.gleam", 1737). +-spec key_set_loop(list({ALQ, ALR}), ALQ, ALR, list({ALQ, ALR})) -> list({ALQ, + ALR}). +key_set_loop(List, Key, Value, Inspected) -> + case List of + [{K, _} | Rest] when K =:= Key -> + lists:reverse(Inspected, [{K, Value} | Rest]); + + [First | Rest@1] -> + key_set_loop(Rest@1, Key, Value, [First | Inspected]); + + [] -> + lists:reverse([{Key, Value} | Inspected]) + end. + +-file("src/gleam/list.gleam", 1733). +?DOC( + " Given a list of 2-element tuples, inserts a key and value into the list.\n" + "\n" + " If there was already a tuple with the key then it is replaced, otherwise it\n" + " is added to the end of the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " key_set([#(5, 0), #(4, 1)], 4, 100)\n" + " // -> [#(5, 0), #(4, 100)]\n" + " ```\n" + "\n" + " ```gleam\n" + " key_set([#(5, 0), #(4, 1)], 1, 100)\n" + " // -> [#(5, 0), #(4, 1), #(1, 100)]\n" + " ```\n" +). +-spec key_set(list({ALM, ALN}), ALM, ALN) -> list({ALM, ALN}). +key_set(List, Key, Value) -> + key_set_loop(List, Key, Value, []). + +-file("src/gleam/list.gleam", 1765). +?DOC( + " Calls a function for each element in a list, discarding the return value.\n" + "\n" + " Useful for calling a side effect for every item of a list.\n" + "\n" + " ```gleam\n" + " import gleam/io\n" + "\n" + " each([\"1\", \"2\", \"3\"], io.println)\n" + " // -> Nil\n" + " // 1\n" + " // 2\n" + " // 3\n" + " ```\n" +). +-spec each(list(ALV), fun((ALV) -> any())) -> nil. +each(List, F) -> + case List of + [] -> + nil; + + [First | Rest] -> + F(First), + each(Rest, F) + end. + +-file("src/gleam/list.gleam", 1791). +?DOC( + " Calls a `Result` returning function for each element in a list, discarding\n" + " the return value. If the function returns `Error` then the iteration is\n" + " stopped and the error is returned.\n" + "\n" + " Useful for calling a side effect for every item of a list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " try_each(\n" + " over: [1, 2, 3],\n" + " with: function_that_might_fail,\n" + " )\n" + " // -> Ok(Nil)\n" + " ```\n" +). +-spec try_each(list(ALY), fun((ALY) -> {ok, any()} | {error, AMB})) -> {ok, nil} | + {error, AMB}. +try_each(List, Fun) -> + case List of + [] -> + {ok, nil}; + + [First | Rest] -> + case Fun(First) of + {ok, _} -> + try_each(Rest, Fun); + + {error, E} -> + {error, E} + end + end. + +-file("src/gleam/list.gleam", 1824). +-spec partition_loop(list(BFR), fun((BFR) -> boolean()), list(BFR), list(BFR)) -> {list(BFR), + list(BFR)}. +partition_loop(List, Categorise, Trues, Falses) -> + case List of + [] -> + {lists:reverse(Trues), lists:reverse(Falses)}; + + [First | Rest] -> + case Categorise(First) of + true -> + partition_loop(Rest, Categorise, [First | Trues], Falses); + + false -> + partition_loop(Rest, Categorise, Trues, [First | Falses]) + end + end. + +-file("src/gleam/list.gleam", 1817). +?DOC( + " Partitions a list into a tuple/pair of lists\n" + " by a given categorisation function.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " [1, 2, 3, 4, 5] |> partition(int.is_odd)\n" + " // -> #([1, 3, 5], [2, 4])\n" + " ```\n" +). +-spec partition(list(AMG), fun((AMG) -> boolean())) -> {list(AMG), list(AMG)}. +partition(List, Categorise) -> + partition_loop(List, Categorise, [], []). + +-file("src/gleam/list.gleam", 1904). +-spec window_loop(list(list(ANN)), list(ANN), integer()) -> list(list(ANN)). +window_loop(Acc, List, N) -> + Window = take(List, N), + case erlang:length(Window) =:= N of + true -> + window_loop([Window | Acc], drop(List, 1), N); + + false -> + lists:reverse(Acc) + end. + +-file("src/gleam/list.gleam", 1897). +?DOC( + " Returns a list of sliding windows.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " window([1,2,3,4,5], 3)\n" + " // -> [[1, 2, 3], [2, 3, 4], [3, 4, 5]]\n" + " ```\n" + "\n" + " ```gleam\n" + " window([1, 2], 4)\n" + " // -> []\n" + " ```\n" +). +-spec window(list(ANJ), integer()) -> list(list(ANJ)). +window(List, N) -> + case N =< 0 of + true -> + []; + + false -> + window_loop([], List, N) + end. + +-file("src/gleam/list.gleam", 1927). +?DOC( + " Returns a list of tuples containing two contiguous elements.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " window_by_2([1,2,3,4])\n" + " // -> [#(1, 2), #(2, 3), #(3, 4)]\n" + " ```\n" + "\n" + " ```gleam\n" + " window_by_2([1])\n" + " // -> []\n" + " ```\n" +). +-spec window_by_2(list(ANT)) -> list({ANT, ANT}). +window_by_2(List) -> + zip(List, drop(List, 1)). + +-file("src/gleam/list.gleam", 1940). +?DOC( + " Drops the first elements in a given list for which the predicate function returns `True`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " drop_while([1, 2, 3, 4], fn (x) { x < 3 })\n" + " // -> [3, 4]\n" + " ```\n" +). +-spec drop_while(list(ANW), fun((ANW) -> boolean())) -> list(ANW). +drop_while(List, Predicate) -> + case List of + [] -> + []; + + [First | Rest] -> + case Predicate(First) of + true -> + drop_while(Rest, Predicate); + + false -> + [First | Rest] + end + end. + +-file("src/gleam/list.gleam", 1970). +-spec take_while_loop(list(AOC), fun((AOC) -> boolean()), list(AOC)) -> list(AOC). +take_while_loop(List, Predicate, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + case Predicate(First) of + true -> + take_while_loop(Rest, Predicate, [First | Acc]); + + false -> + lists:reverse(Acc) + end + end. + +-file("src/gleam/list.gleam", 1963). +?DOC( + " Takes the first elements in a given list for which the predicate function returns `True`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " take_while([1, 2, 3, 2, 4], fn (x) { x < 3 })\n" + " // -> [1, 2]\n" + " ```\n" +). +-spec take_while(list(ANZ), fun((ANZ) -> boolean())) -> list(ANZ). +take_while(List, Predicate) -> + take_while_loop(List, Predicate, []). + +-file("src/gleam/list.gleam", 2002). +-spec chunk_loop(list(AOL), fun((AOL) -> AON), AON, list(AOL), list(list(AOL))) -> list(list(AOL)). +chunk_loop(List, F, Previous_key, Current_chunk, Acc) -> + case List of + [First | Rest] -> + Key = F(First), + case Key =:= Previous_key of + true -> + chunk_loop(Rest, F, Key, [First | Current_chunk], Acc); + + false -> + New_acc = [lists:reverse(Current_chunk) | Acc], + chunk_loop(Rest, F, Key, [First], New_acc) + end; + + [] -> + lists:reverse([lists:reverse(Current_chunk) | Acc]) + end. + +-file("src/gleam/list.gleam", 1995). +?DOC( + " Returns a list of chunks in which\n" + " the return value of calling `f` on each element is the same.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [1, 2, 2, 3, 4, 4, 6, 7, 7] |> chunk(by: fn(n) { n % 2 })\n" + " // -> [[1], [2, 2], [3], [4, 4, 6], [7, 7]]\n" + " ```\n" +). +-spec chunk(list(AOG), fun((AOG) -> any())) -> list(list(AOG)). +chunk(List, F) -> + case List of + [] -> + []; + + [First | Rest] -> + chunk_loop(Rest, F, F(First), [First], []) + end. + +-file("src/gleam/list.gleam", 2047). +-spec sized_chunk_loop( + list(AOX), + integer(), + integer(), + list(AOX), + list(list(AOX)) +) -> list(list(AOX)). +sized_chunk_loop(List, Count, Left, Current_chunk, Acc) -> + case List of + [] -> + case Current_chunk of + [] -> + lists:reverse(Acc); + + Remaining -> + lists:reverse([lists:reverse(Remaining) | Acc]) + end; + + [First | Rest] -> + Chunk = [First | Current_chunk], + case Left > 1 of + true -> + sized_chunk_loop(Rest, Count, Left - 1, Chunk, Acc); + + false -> + sized_chunk_loop( + Rest, + Count, + Count, + [], + [lists:reverse(Chunk) | Acc] + ) + end + end. + +-file("src/gleam/list.gleam", 2043). +?DOC( + " Returns a list of chunks containing `count` elements each.\n" + "\n" + " If the last chunk does not have `count` elements, it is instead\n" + " a partial chunk, with less than `count` elements.\n" + "\n" + " For any `count` less than 1 this function behaves as if it was set to 1.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [1, 2, 3, 4, 5, 6] |> sized_chunk(into: 2)\n" + " // -> [[1, 2], [3, 4], [5, 6]]\n" + " ```\n" + "\n" + " ```gleam\n" + " [1, 2, 3, 4, 5, 6, 7, 8] |> sized_chunk(into: 3)\n" + " // -> [[1, 2, 3], [4, 5, 6], [7, 8]]\n" + " ```\n" +). +-spec sized_chunk(list(AOT), integer()) -> list(list(AOT)). +sized_chunk(List, Count) -> + sized_chunk_loop(List, Count, Count, [], []). + +-file("src/gleam/list.gleam", 2091). +?DOC( + " This function acts similar to fold, but does not take an initial state.\n" + " Instead, it starts from the first element in the list\n" + " and combines it with each subsequent element in turn using the given\n" + " function. The function is called as `fun(accumulator, current_element)`.\n" + "\n" + " Returns `Ok` to indicate a successful run, and `Error` if called on an\n" + " empty list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [] |> reduce(fn(acc, x) { acc + x })\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " [1, 2, 3, 4, 5] |> reduce(fn(acc, x) { acc + x })\n" + " // -> Ok(15)\n" + " ```\n" +). +-spec reduce(list(APE), fun((APE, APE) -> APE)) -> {ok, APE} | {error, nil}. +reduce(List, Fun) -> + case List of + [] -> + {error, nil}; + + [First | Rest] -> + {ok, fold(Rest, First, Fun)} + end. + +-file("src/gleam/list.gleam", 2115). +-spec scan_loop(list(APM), APO, list(APO), fun((APO, APM) -> APO)) -> list(APO). +scan_loop(List, Accumulator, Accumulated, Fun) -> + case List of + [] -> + lists:reverse(Accumulated); + + [First | Rest] -> + Next = Fun(Accumulator, First), + scan_loop(Rest, Next, [Next | Accumulated], Fun) + end. + +-file("src/gleam/list.gleam", 2107). +?DOC( + " Similar to `fold`, but yields the state of the accumulator at each stage.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " scan(over: [1, 2, 3], from: 100, with: fn(acc, i) { acc + i })\n" + " // -> [101, 103, 106]\n" + " ```\n" +). +-spec scan(list(API), APK, fun((APK, API) -> APK)) -> list(APK). +scan(List, Initial, Fun) -> + scan_loop(List, Initial, [], Fun). + +-file("src/gleam/list.gleam", 2148). +?DOC( + " Returns the last element in the given list.\n" + "\n" + " Returns `Error(Nil)` if the list is empty.\n" + "\n" + " This function runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " last([])\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " last([1, 2, 3, 4, 5])\n" + " // -> Ok(5)\n" + " ```\n" +). +-spec last(list(APR)) -> {ok, APR} | {error, nil}. +last(List) -> + case List of + [] -> + {error, nil}; + + [Last] -> + {ok, Last}; + + [_ | Rest] -> + last(Rest) + end. + +-file("src/gleam/list.gleam", 2170). +?DOC( + " Return unique combinations of elements in the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " combinations([1, 2, 3], 2)\n" + " // -> [[1, 2], [1, 3], [2, 3]]\n" + " ```\n" + "\n" + " ```gleam\n" + " combinations([1, 2, 3, 4], 3)\n" + " // -> [[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]]\n" + " ```\n" +). +-spec combinations(list(APV), integer()) -> list(list(APV)). +combinations(Items, N) -> + case {N, Items} of + {0, _} -> + [[]]; + + {_, []} -> + []; + + {_, [First | Rest]} -> + _pipe = Rest, + _pipe@1 = combinations(_pipe, N - 1), + _pipe@2 = map( + _pipe@1, + fun(Combination) -> [First | Combination] end + ), + _pipe@3 = lists:reverse(_pipe@2), + fold(_pipe@3, combinations(Rest, N), fun(Acc, C) -> [C | Acc] end) + end. + +-file("src/gleam/list.gleam", 2196). +-spec combination_pairs_loop(list(AQC), list({AQC, AQC})) -> list({AQC, AQC}). +combination_pairs_loop(Items, Acc) -> + case Items of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + First_combinations = map(Rest, fun(Other) -> {First, Other} end), + Acc@1 = lists:reverse(First_combinations, Acc), + combination_pairs_loop(Rest, Acc@1) + end. + +-file("src/gleam/list.gleam", 2192). +?DOC( + " Return unique pair combinations of elements in the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " combination_pairs([1, 2, 3])\n" + " // -> [#(1, 2), #(1, 3), #(2, 3)]\n" + " ```\n" +). +-spec combination_pairs(list(APZ)) -> list({APZ, APZ}). +combination_pairs(Items) -> + combination_pairs_loop(Items, []). + +-file("src/gleam/list.gleam", 2252). +-spec take_firsts(list(list(AQW)), list(AQW), list(list(AQW))) -> {list(AQW), + list(list(AQW))}. +take_firsts(Rows, Column, Remaining_rows) -> + case Rows of + [] -> + {lists:reverse(Column), lists:reverse(Remaining_rows)}; + + [[] | Rest] -> + take_firsts(Rest, Column, Remaining_rows); + + [[First | Remaining_row] | Rest_rows] -> + Remaining_rows@1 = [Remaining_row | Remaining_rows], + take_firsts(Rest_rows, [First | Column], Remaining_rows@1) + end. + +-file("src/gleam/list.gleam", 2239). +-spec transpose_loop(list(list(AQP)), list(list(AQP))) -> list(list(AQP)). +transpose_loop(Rows, Columns) -> + case Rows of + [] -> + lists:reverse(Columns); + + _ -> + {Column, Rest} = take_firsts(Rows, [], []), + case Column of + [_ | _] -> + transpose_loop(Rest, [Column | Columns]); + + [] -> + transpose_loop(Rest, Columns) + end + end. + +-file("src/gleam/list.gleam", 2235). +?DOC( + " Transpose rows and columns of the list of lists.\n" + "\n" + " Notice: This function is not tail recursive,\n" + " and thus may exceed stack size if called,\n" + " with large lists (on the JavaScript target).\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " transpose([[1, 2, 3], [101, 102, 103]])\n" + " // -> [[1, 101], [2, 102], [3, 103]]\n" + " ```\n" +). +-spec transpose(list(list(AQK))) -> list(list(AQK)). +transpose(List_of_lists) -> + transpose_loop(List_of_lists, []). + +-file("src/gleam/list.gleam", 2216). +?DOC( + " Make a list alternating the elements from the given lists\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " interleave([[1, 2], [101, 102], [201, 202]])\n" + " // -> [1, 101, 201, 2, 102, 202]\n" + " ```\n" +). +-spec interleave(list(list(AQG))) -> list(AQG). +interleave(List) -> + _pipe = List, + _pipe@1 = transpose(_pipe), + lists:append(_pipe@1). + +-file("src/gleam/list.gleam", 2285). +-spec shuffle_pair_unwrap_loop(list({float(), ARI}), list(ARI)) -> list(ARI). +shuffle_pair_unwrap_loop(List, Acc) -> + case List of + [] -> + Acc; + + [Elem_pair | Enumerable] -> + shuffle_pair_unwrap_loop( + Enumerable, + [erlang:element(2, Elem_pair) | Acc] + ) + end. + +-file("src/gleam/list.gleam", 2293). +-spec do_shuffle_by_pair_indexes(list({float(), ARM})) -> list({float(), ARM}). +do_shuffle_by_pair_indexes(List_of_pairs) -> + sort( + List_of_pairs, + fun(A_pair, B_pair) -> + gleam@float:compare( + erlang:element(1, A_pair), + erlang:element(1, B_pair) + ) + end + ). + +-file("src/gleam/list.gleam", 2278). +?DOC( + " Takes a list, randomly sorts all items and returns the shuffled list.\n" + "\n" + " This function uses `float.random` to decide the order of the elements.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " range(1, 10) |> shuffle()\n" + " // -> [1, 6, 9, 10, 3, 8, 4, 2, 7, 5]\n" + " ```\n" +). +-spec shuffle(list(ARF)) -> list(ARF). +shuffle(List) -> + _pipe = List, + _pipe@1 = fold(_pipe, [], fun(Acc, A) -> [{rand:uniform(), A} | Acc] end), + _pipe@2 = do_shuffle_by_pair_indexes(_pipe@1), + shuffle_pair_unwrap_loop(_pipe@2, []). + +-file("src/gleam/list.gleam", 2325). +-spec max_loop(list(ARW), fun((ARW, ARW) -> gleam@order:order()), ARW) -> ARW. +max_loop(List, Compare, Max) -> + case List of + [] -> + Max; + + [First | Rest] -> + case Compare(First, Max) of + gt -> + max_loop(Rest, Compare, First); + + lt -> + max_loop(Rest, Compare, Max); + + eq -> + max_loop(Rest, Compare, Max) + end + end. + +-file("src/gleam/list.gleam", 2315). +?DOC( + " Takes a list and a comparator, and returns the maximum element in the list\n" + "\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " range(1, 10) |> list.max(int.compare)\n" + " // -> Ok(10)\n" + " ```\n" + "\n" + " ```gleam\n" + " [\"a\", \"c\", \"b\"] |> list.max(string.compare)\n" + " // -> Ok(\"c\")\n" + " ```\n" +). +-spec max(list(ARP), fun((ARP, ARP) -> gleam@order:order())) -> {ok, ARP} | + {error, nil}. +max(List, Compare) -> + case List of + [] -> + {error, nil}; + + [First | Rest] -> + {ok, max_loop(Rest, Compare, First)} + end. + +-file("src/gleam/list.gleam", 2406). +-spec build_reservoir_loop( + list(ASL), + integer(), + gleam@dict:dict(integer(), ASL) +) -> {gleam@dict:dict(integer(), ASL), list(ASL)}. +build_reservoir_loop(List, Size, Reservoir) -> + Reservoir_size = maps:size(Reservoir), + case Reservoir_size >= Size of + true -> + {Reservoir, List}; + + false -> + case List of + [] -> + {Reservoir, []}; + + [First | Rest] -> + Reservoir@1 = gleam@dict:insert( + Reservoir, + Reservoir_size, + First + ), + build_reservoir_loop(Rest, Size, Reservoir@1) + end + end. + +-file("src/gleam/list.gleam", 2402). +?DOC( + " Builds the initial reservoir used by Algorithm L.\n" + " This is a dictionary with keys ranging from `0` up to `n - 1` where each\n" + " value is the corresponding element at that position in `list`.\n" + "\n" + " This also returns the remaining elements of `list` that didn't end up in\n" + " the reservoir.\n" +). +-spec build_reservoir(list(ASG), integer()) -> {gleam@dict:dict(integer(), ASG), + list(ASG)}. +build_reservoir(List, N) -> + build_reservoir_loop(List, N, maps:new()). + +-file("src/gleam/list.gleam", 2390). +-spec log_random() -> float(). +log_random() -> + Random@1 = case gleam@float:logarithm( + rand:uniform() + 2.2250738585072014e-308 + ) of + {ok, Random} -> Random; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam/list"/utf8>>, + function => <<"log_random"/utf8>>, + line => 2391, + value => _assert_fail, + start => 56078, + 'end' => 56149, + pattern_start => 56089, + pattern_end => 56099}) + end, + Random@1. + +-file("src/gleam/list.gleam", 2367). +-spec sample_loop( + list(ASA), + gleam@dict:dict(integer(), ASA), + integer(), + float() +) -> gleam@dict:dict(integer(), ASA). +sample_loop(List, Reservoir, N, W) -> + Skip = begin + Log@1 = case gleam@float:logarithm(1.0 - W) of + {ok, Log} -> Log; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam/list"/utf8>>, + function => <<"sample_loop"/utf8>>, + line => 2374, + value => _assert_fail, + start => 55639, + 'end' => 55685, + pattern_start => 55650, + pattern_end => 55657}) + end, + erlang:round(math:floor(case Log@1 of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> log_random() / Gleam@denominator + end)) + end, + case drop(List, Skip) of + [] -> + Reservoir; + + [First | Rest] -> + Reservoir@1 = gleam@dict:insert( + Reservoir, + gleam@int:random(N), + First + ), + W@1 = W * math:exp(case erlang:float(N) of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator@1 -> log_random() / Gleam@denominator@1 + end), + sample_loop(Rest, Reservoir@1, N, W@1) + end. + +-file("src/gleam/list.gleam", 2349). +?DOC( + " Returns a random sample of up to n elements from a list using reservoir\n" + " sampling via [Algorithm L](https://en.wikipedia.org/wiki/Reservoir_sampling#Optimal:_Algorithm_L).\n" + " Returns an empty list if the sample size is less than or equal to 0.\n" + "\n" + " Order is not random, only selection is.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " reservoir_sample([1, 2, 3, 4, 5], 3)\n" + " // -> [2, 4, 5] // A random sample of 3 items\n" + " ```\n" +). +-spec sample(list(ARX), integer()) -> list(ARX). +sample(List, N) -> + {Reservoir, Rest} = build_reservoir(List, N), + case gleam@dict:is_empty(Reservoir) of + true -> + []; + + false -> + W = math:exp(case erlang:float(N) of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> log_random() / Gleam@denominator + end), + maps:values(sample_loop(Rest, Reservoir, N, W)) + end. + +-file("src/gleam/list.gleam", 1851). +-spec permutation_zip(list(AMT), list(AMT), list(list(AMT))) -> list(list(AMT)). +permutation_zip(List, Rest, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [Head | Tail] -> + permutation_prepend( + Head, + permutations(lists:reverse(Rest, Tail)), + Tail, + [Head | Rest], + Acc + ) + end. + +-file("src/gleam/list.gleam", 1869). +-spec permutation_prepend( + ANA, + list(list(ANA)), + list(ANA), + list(ANA), + list(list(ANA)) +) -> list(list(ANA)). +permutation_prepend(El, Permutations, List_1, List_2, Acc) -> + case Permutations of + [] -> + permutation_zip(List_1, List_2, Acc); + + [Head | Tail] -> + permutation_prepend(El, Tail, List_1, List_2, [[El | Head] | Acc]) + end. + +-file("src/gleam/list.gleam", 1844). +?DOC( + " Returns all the permutations of a list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " permutations([1, 2])\n" + " // -> [[1, 2], [2, 1]]\n" + " ```\n" +). +-spec permutations(list(AMP)) -> list(list(AMP)). +permutations(List) -> + case List of + [] -> + [[]]; + + L -> + permutation_zip(L, [], []) + end. diff --git a/build/dev/javascript/gleam_stdlib/gleam@option.erl b/build/dev/javascript/gleam_stdlib/gleam@option.erl new file mode 100644 index 0000000..8e86a8e --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@option.erl @@ -0,0 +1,413 @@ +-module(gleam@option). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/option.gleam"). +-export([all/1, is_some/1, is_none/1, to_result/2, from_result/1, unwrap/2, lazy_unwrap/2, map/2, flatten/1, then/2, 'or'/2, lazy_or/2, values/1]). +-export_type([option/1]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type option(FG) :: {some, FG} | none. + +-file("src/gleam/option.gleam", 59). +-spec reverse_and_prepend(list(FV), list(FV)) -> list(FV). +reverse_and_prepend(Prefix, Suffix) -> + case Prefix of + [] -> + Suffix; + + [First | Rest] -> + reverse_and_prepend(Rest, [First | Suffix]) + end. + +-file("src/gleam/option.gleam", 44). +-spec all_loop(list(option(FM)), list(FM)) -> option(list(FM)). +all_loop(List, Acc) -> + case List of + [] -> + {some, lists:reverse(Acc)}; + + [none | _] -> + none; + + [{some, First} | Rest] -> + all_loop(Rest, [First | Acc]) + end. + +-file("src/gleam/option.gleam", 40). +?DOC( + " Combines a list of `Option`s into a single `Option`.\n" + " If all elements in the list are `Some` then returns a `Some` holding the list of values.\n" + " If any element is `None` then returns`None`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " all([Some(1), Some(2)])\n" + " // -> Some([1, 2])\n" + " ```\n" + "\n" + " ```gleam\n" + " all([Some(1), None])\n" + " // -> None\n" + " ```\n" +). +-spec all(list(option(FH))) -> option(list(FH)). +all(List) -> + all_loop(List, []). + +-file("src/gleam/option.gleam", 80). +?DOC( + " Checks whether the `Option` is a `Some` value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_some(Some(1))\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_some(None)\n" + " // -> False\n" + " ```\n" +). +-spec is_some(option(any())) -> boolean(). +is_some(Option) -> + Option /= none. + +-file("src/gleam/option.gleam", 98). +?DOC( + " Checks whether the `Option` is a `None` value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_none(Some(1))\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " is_none(None)\n" + " // -> True\n" + " ```\n" +). +-spec is_none(option(any())) -> boolean(). +is_none(Option) -> + Option =:= none. + +-file("src/gleam/option.gleam", 116). +?DOC( + " Converts an `Option` type to a `Result` type.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_result(Some(1), \"some_error\")\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " to_result(None, \"some_error\")\n" + " // -> Error(\"some_error\")\n" + " ```\n" +). +-spec to_result(option(GD), GG) -> {ok, GD} | {error, GG}. +to_result(Option, E) -> + case Option of + {some, A} -> + {ok, A}; + + none -> + {error, E} + end. + +-file("src/gleam/option.gleam", 137). +?DOC( + " Converts a `Result` type to an `Option` type.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_result(Ok(1))\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " from_result(Error(\"some_error\"))\n" + " // -> None\n" + " ```\n" +). +-spec from_result({ok, GJ} | {error, any()}) -> option(GJ). +from_result(Result) -> + case Result of + {ok, A} -> + {some, A}; + + {error, _} -> + none + end. + +-file("src/gleam/option.gleam", 158). +?DOC( + " Extracts the value from an `Option`, returning a default value if there is none.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " unwrap(Some(1), 0)\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " unwrap(None, 0)\n" + " // -> 0\n" + " ```\n" +). +-spec unwrap(option(GO), GO) -> GO. +unwrap(Option, Default) -> + case Option of + {some, X} -> + X; + + none -> + Default + end. + +-file("src/gleam/option.gleam", 179). +?DOC( + " Extracts the value from an `Option`, evaluating the default function if the option is `None`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " lazy_unwrap(Some(1), fn() { 0 })\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_unwrap(None, fn() { 0 })\n" + " // -> 0\n" + " ```\n" +). +-spec lazy_unwrap(option(GQ), fun(() -> GQ)) -> GQ. +lazy_unwrap(Option, Default) -> + case Option of + {some, X} -> + X; + + none -> + Default() + end. + +-file("src/gleam/option.gleam", 204). +?DOC( + " Updates a value held within the `Some` of an `Option` by calling a given function\n" + " on it.\n" + "\n" + " If the `Option` is a `None` rather than `Some`, the function is not called and the\n" + " `Option` stays the same.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map(over: Some(1), with: fn(x) { x + 1 })\n" + " // -> Some(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " map(over: None, with: fn(x) { x + 1 })\n" + " // -> None\n" + " ```\n" +). +-spec map(option(GS), fun((GS) -> GU)) -> option(GU). +map(Option, Fun) -> + case Option of + {some, X} -> + {some, Fun(X)}; + + none -> + none + end. + +-file("src/gleam/option.gleam", 230). +?DOC( + " Merges a nested `Option` into a single layer.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " flatten(Some(Some(1)))\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " flatten(Some(None))\n" + " // -> None\n" + " ```\n" + "\n" + " ```gleam\n" + " flatten(None)\n" + " // -> None\n" + " ```\n" +). +-spec flatten(option(option(GW))) -> option(GW). +flatten(Option) -> + case Option of + {some, X} -> + X; + + none -> + none + end. + +-file("src/gleam/option.gleam", 269). +?DOC( + " Updates a value held within the `Some` of an `Option` by calling a given function\n" + " on it, where the given function also returns an `Option`. The two options are\n" + " then merged together into one `Option`.\n" + "\n" + " If the `Option` is a `None` rather than `Some` the function is not called and the\n" + " option stays the same.\n" + "\n" + " This function is the equivalent of calling `map` followed by `flatten`, and\n" + " it is useful for chaining together multiple functions that return `Option`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " then(Some(1), fn(x) { Some(x + 1) })\n" + " // -> Some(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " then(Some(1), fn(x) { Some(#(\"a\", x)) })\n" + " // -> Some(#(\"a\", 1))\n" + " ```\n" + "\n" + " ```gleam\n" + " then(Some(1), fn(_) { None })\n" + " // -> None\n" + " ```\n" + "\n" + " ```gleam\n" + " then(None, fn(x) { Some(x + 1) })\n" + " // -> None\n" + " ```\n" +). +-spec then(option(HA), fun((HA) -> option(HC))) -> option(HC). +then(Option, Fun) -> + case Option of + {some, X} -> + Fun(X); + + none -> + none + end. + +-file("src/gleam/option.gleam", 300). +?DOC( + " Returns the first value if it is `Some`, otherwise returns the second value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " or(Some(1), Some(2))\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(Some(1), None)\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(None, Some(2))\n" + " // -> Some(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(None, None)\n" + " // -> None\n" + " ```\n" +). +-spec 'or'(option(HF), option(HF)) -> option(HF). +'or'(First, Second) -> + case First of + {some, _} -> + First; + + none -> + Second + end. + +-file("src/gleam/option.gleam", 331). +?DOC( + " Returns the first value if it is `Some`, otherwise evaluates the given function for a fallback value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " lazy_or(Some(1), fn() { Some(2) })\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(Some(1), fn() { None })\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(None, fn() { Some(2) })\n" + " // -> Some(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(None, fn() { None })\n" + " // -> None\n" + " ```\n" +). +-spec lazy_or(option(HJ), fun(() -> option(HJ))) -> option(HJ). +lazy_or(First, Second) -> + case First of + {some, _} -> + First; + + none -> + Second() + end. + +-file("src/gleam/option.gleam", 352). +-spec values_loop(list(option(HR)), list(HR)) -> list(HR). +values_loop(List, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [none | Rest] -> + values_loop(Rest, Acc); + + [{some, First} | Rest@1] -> + values_loop(Rest@1, [First | Acc]) + end. + +-file("src/gleam/option.gleam", 348). +?DOC( + " Given a list of `Option`s,\n" + " returns only the values inside `Some`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " values([Some(1), None, Some(3)])\n" + " // -> [1, 3]\n" + " ```\n" +). +-spec values(list(option(HN))) -> list(HN). +values(Options) -> + values_loop(Options, []). diff --git a/build/dev/javascript/gleam_stdlib/gleam@order.erl b/build/dev/javascript/gleam_stdlib/gleam@order.erl new file mode 100644 index 0000000..ec2bb84 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@order.erl @@ -0,0 +1,200 @@ +-module(gleam@order). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/order.gleam"). +-export([negate/1, to_int/1, compare/2, reverse/1, break_tie/2, lazy_break_tie/2]). +-export_type([order/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type order() :: lt | eq | gt. + +-file("src/gleam/order.gleam", 35). +?DOC( + " Inverts an order, so less-than becomes greater-than and greater-than\n" + " becomes less-than.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " negate(Lt)\n" + " // -> Gt\n" + " ```\n" + "\n" + " ```gleam\n" + " negate(Eq)\n" + " // -> Eq\n" + " ```\n" + "\n" + " ```gleam\n" + " negate(Gt)\n" + " // -> Lt\n" + " ```\n" +). +-spec negate(order()) -> order(). +negate(Order) -> + case Order of + lt -> + gt; + + eq -> + eq; + + gt -> + lt + end. + +-file("src/gleam/order.gleam", 62). +?DOC( + " Produces a numeric representation of the order.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_int(Lt)\n" + " // -> -1\n" + " ```\n" + "\n" + " ```gleam\n" + " to_int(Eq)\n" + " // -> 0\n" + " ```\n" + "\n" + " ```gleam\n" + " to_int(Gt)\n" + " // -> 1\n" + " ```\n" +). +-spec to_int(order()) -> integer(). +to_int(Order) -> + case Order of + lt -> + -1; + + eq -> + 0; + + gt -> + 1 + end. + +-file("src/gleam/order.gleam", 79). +?DOC( + " Compares two `Order` values to one another, producing a new `Order`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " compare(Eq, with: Lt)\n" + " // -> Gt\n" + " ```\n" +). +-spec compare(order(), order()) -> order(). +compare(A, B) -> + case {A, B} of + {X, Y} when X =:= Y -> + eq; + + {lt, _} -> + lt; + + {eq, gt} -> + lt; + + {_, _} -> + gt + end. + +-file("src/gleam/order.gleam", 100). +?DOC( + " Inverts an ordering function, so less-than becomes greater-than and greater-than\n" + " becomes less-than.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + " import gleam/list\n" + "\n" + " list.sort([1, 5, 4], by: reverse(int.compare))\n" + " // -> [5, 4, 1]\n" + " ```\n" +). +-spec reverse(fun((I, I) -> order())) -> fun((I, I) -> order()). +reverse(Orderer) -> + fun(A, B) -> Orderer(B, A) end. + +-file("src/gleam/order.gleam", 122). +?DOC( + " Return a fallback `Order` in case the first argument is `Eq`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " break_tie(in: int.compare(1, 1), with: Lt)\n" + " // -> Lt\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " break_tie(in: int.compare(1, 0), with: Eq)\n" + " // -> Gt\n" + " ```\n" +). +-spec break_tie(order(), order()) -> order(). +break_tie(Order, Other) -> + case Order of + lt -> + Order; + + gt -> + Order; + + eq -> + Other + end. + +-file("src/gleam/order.gleam", 151). +?DOC( + " Invokes a fallback function returning an `Order` in case the first argument\n" + " is `Eq`.\n" + "\n" + " This can be useful when the fallback comparison might be expensive and it\n" + " needs to be delayed until strictly necessary.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " lazy_break_tie(in: int.compare(1, 1), with: fn() { Lt })\n" + " // -> Lt\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " lazy_break_tie(in: int.compare(1, 0), with: fn() { Eq })\n" + " // -> Gt\n" + " ```\n" +). +-spec lazy_break_tie(order(), fun(() -> order())) -> order(). +lazy_break_tie(Order, Comparison) -> + case Order of + lt -> + Order; + + gt -> + Order; + + eq -> + Comparison() + end. diff --git a/build/dev/javascript/gleam_stdlib/gleam@pair.erl b/build/dev/javascript/gleam_stdlib/gleam@pair.erl new file mode 100644 index 0000000..cb18264 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@pair.erl @@ -0,0 +1,110 @@ +-module(gleam@pair). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/pair.gleam"). +-export([first/1, second/1, swap/1, map_first/2, map_second/2, new/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-file("src/gleam/pair.gleam", 10). +?DOC( + " Returns the first element in a pair.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " first(#(1, 2))\n" + " // -> 1\n" + " ```\n" +). +-spec first({CLF, any()}) -> CLF. +first(Pair) -> + {A, _} = Pair, + A. + +-file("src/gleam/pair.gleam", 24). +?DOC( + " Returns the second element in a pair.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " second(#(1, 2))\n" + " // -> 2\n" + " ```\n" +). +-spec second({any(), CLI}) -> CLI. +second(Pair) -> + {_, A} = Pair, + A. + +-file("src/gleam/pair.gleam", 38). +?DOC( + " Returns a new pair with the elements swapped.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " swap(#(1, 2))\n" + " // -> #(2, 1)\n" + " ```\n" +). +-spec swap({CLJ, CLK}) -> {CLK, CLJ}. +swap(Pair) -> + {A, B} = Pair, + {B, A}. + +-file("src/gleam/pair.gleam", 53). +?DOC( + " Returns a new pair with the first element having had `with` applied to\n" + " it.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " #(1, 2) |> map_first(fn(n) { n * 2 })\n" + " // -> #(2, 2)\n" + " ```\n" +). +-spec map_first({CLL, CLM}, fun((CLL) -> CLN)) -> {CLN, CLM}. +map_first(Pair, Fun) -> + {A, B} = Pair, + {Fun(A), B}. + +-file("src/gleam/pair.gleam", 68). +?DOC( + " Returns a new pair with the second element having had `with` applied to\n" + " it.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " #(1, 2) |> map_second(fn(n) { n * 2 })\n" + " // -> #(1, 4)\n" + " ```\n" +). +-spec map_second({CLO, CLP}, fun((CLP) -> CLQ)) -> {CLO, CLQ}. +map_second(Pair, Fun) -> + {A, B} = Pair, + {A, Fun(B)}. + +-file("src/gleam/pair.gleam", 83). +?DOC( + " Returns a new pair with the given elements. This can also be done using the dedicated\n" + " syntax instead: `new(1, 2) == #(1, 2)`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new(1, 2)\n" + " // -> #(1, 2)\n" + " ```\n" +). +-spec new(CLR, CLS) -> {CLR, CLS}. +new(First, Second) -> + {First, Second}. diff --git a/build/dev/javascript/gleam_stdlib/gleam@result.erl b/build/dev/javascript/gleam_stdlib/gleam@result.erl new file mode 100644 index 0000000..9d89ff7 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@result.erl @@ -0,0 +1,550 @@ +-module(gleam@result). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/result.gleam"). +-export([is_ok/1, is_error/1, map/2, map_error/2, flatten/1, 'try'/2, then/2, unwrap/2, lazy_unwrap/2, unwrap_error/2, unwrap_both/1, 'or'/2, lazy_or/2, all/1, partition/1, replace/2, replace_error/2, values/1, try_recover/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Result represents the result of something that may succeed or not.\n" + " `Ok` means it was successful, `Error` means it was not successful.\n" +). + +-file("src/gleam/result.gleam", 20). +?DOC( + " Checks whether the result is an `Ok` value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_ok(Ok(1))\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_ok(Error(Nil))\n" + " // -> False\n" + " ```\n" +). +-spec is_ok({ok, any()} | {error, any()}) -> boolean(). +is_ok(Result) -> + case Result of + {error, _} -> + false; + + {ok, _} -> + true + end. + +-file("src/gleam/result.gleam", 41). +?DOC( + " Checks whether the result is an `Error` value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_error(Ok(1))\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " is_error(Error(Nil))\n" + " // -> True\n" + " ```\n" +). +-spec is_error({ok, any()} | {error, any()}) -> boolean(). +is_error(Result) -> + case Result of + {ok, _} -> + false; + + {error, _} -> + true + end. + +-file("src/gleam/result.gleam", 66). +?DOC( + " Updates a value held within the `Ok` of a result by calling a given function\n" + " on it.\n" + "\n" + " If the result is an `Error` rather than `Ok` the function is not called and the\n" + " result stays the same.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map(over: Ok(1), with: fn(x) { x + 1 })\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " map(over: Error(1), with: fn(x) { x + 1 })\n" + " // -> Error(1)\n" + " ```\n" +). +-spec map({ok, CMC} | {error, CMD}, fun((CMC) -> CMG)) -> {ok, CMG} | + {error, CMD}. +map(Result, Fun) -> + case Result of + {ok, X} -> + {ok, Fun(X)}; + + {error, E} -> + {error, E} + end. + +-file("src/gleam/result.gleam", 91). +?DOC( + " Updates a value held within the `Error` of a result by calling a given function\n" + " on it.\n" + "\n" + " If the result is `Ok` rather than `Error` the function is not called and the\n" + " result stays the same.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map_error(over: Error(1), with: fn(x) { x + 1 })\n" + " // -> Error(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " map_error(over: Ok(1), with: fn(x) { x + 1 })\n" + " // -> Ok(1)\n" + " ```\n" +). +-spec map_error({ok, CMJ} | {error, CMK}, fun((CMK) -> CMN)) -> {ok, CMJ} | + {error, CMN}. +map_error(Result, Fun) -> + case Result of + {ok, X} -> + {ok, X}; + + {error, Error} -> + {error, Fun(Error)} + end. + +-file("src/gleam/result.gleam", 120). +?DOC( + " Merges a nested `Result` into a single layer.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " flatten(Ok(Ok(1)))\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " flatten(Ok(Error(\"\")))\n" + " // -> Error(\"\")\n" + " ```\n" + "\n" + " ```gleam\n" + " flatten(Error(Nil))\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec flatten({ok, {ok, CMQ} | {error, CMR}} | {error, CMR}) -> {ok, CMQ} | + {error, CMR}. +flatten(Result) -> + case Result of + {ok, X} -> + X; + + {error, Error} -> + {error, Error} + end. + +-file("src/gleam/result.gleam", 158). +?DOC( + " \"Updates\" an `Ok` result by passing its value to a function that yields a result,\n" + " and returning the yielded result. (This may \"replace\" the `Ok` with an `Error`.)\n" + "\n" + " If the input is an `Error` rather than an `Ok`, the function is not called and\n" + " the original `Error` is returned.\n" + "\n" + " This function is the equivalent of calling `map` followed by `flatten`, and\n" + " it is useful for chaining together multiple functions that may fail.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " try(Ok(1), fn(x) { Ok(x + 1) })\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " try(Ok(1), fn(x) { Ok(#(\"a\", x)) })\n" + " // -> Ok(#(\"a\", 1))\n" + " ```\n" + "\n" + " ```gleam\n" + " try(Ok(1), fn(_) { Error(\"Oh no\") })\n" + " // -> Error(\"Oh no\")\n" + " ```\n" + "\n" + " ```gleam\n" + " try(Error(Nil), fn(x) { Ok(x + 1) })\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec 'try'({ok, CMY} | {error, CMZ}, fun((CMY) -> {ok, CNC} | {error, CMZ})) -> {ok, + CNC} | + {error, CMZ}. +'try'(Result, Fun) -> + case Result of + {ok, X} -> + Fun(X); + + {error, E} -> + {error, E} + end. + +-file("src/gleam/result.gleam", 169). +-spec then({ok, CNH} | {error, CNI}, fun((CNH) -> {ok, CNL} | {error, CNI})) -> {ok, + CNL} | + {error, CNI}. +then(Result, Fun) -> + 'try'(Result, Fun). + +-file("src/gleam/result.gleam", 191). +?DOC( + " Extracts the `Ok` value from a result, returning a default value if the result\n" + " is an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " unwrap(Ok(1), 0)\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " unwrap(Error(\"\"), 0)\n" + " // -> 0\n" + " ```\n" +). +-spec unwrap({ok, CNQ} | {error, any()}, CNQ) -> CNQ. +unwrap(Result, Default) -> + case Result of + {ok, V} -> + V; + + {error, _} -> + Default + end. + +-file("src/gleam/result.gleam", 213). +?DOC( + " Extracts the `Ok` value from a result, evaluating the default function if the result\n" + " is an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " lazy_unwrap(Ok(1), fn() { 0 })\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_unwrap(Error(\"\"), fn() { 0 })\n" + " // -> 0\n" + " ```\n" +). +-spec lazy_unwrap({ok, CNU} | {error, any()}, fun(() -> CNU)) -> CNU. +lazy_unwrap(Result, Default) -> + case Result of + {ok, V} -> + V; + + {error, _} -> + Default() + end. + +-file("src/gleam/result.gleam", 235). +?DOC( + " Extracts the `Error` value from a result, returning a default value if the result\n" + " is an `Ok`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " unwrap_error(Error(1), 0)\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " unwrap_error(Ok(\"\"), 0)\n" + " // -> 0\n" + " ```\n" +). +-spec unwrap_error({ok, any()} | {error, CNZ}, CNZ) -> CNZ. +unwrap_error(Result, Default) -> + case Result of + {ok, _} -> + Default; + + {error, E} -> + E + end. + +-file("src/gleam/result.gleam", 243). +-spec unwrap_both({ok, COC} | {error, COC}) -> COC. +unwrap_both(Result) -> + case Result of + {ok, A} -> + A; + + {error, A@1} -> + A@1 + end. + +-file("src/gleam/result.gleam", 274). +?DOC( + " Returns the first value if it is `Ok`, otherwise returns the second value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " or(Ok(1), Ok(2))\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(Ok(1), Error(\"Error 2\"))\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(Error(\"Error 1\"), Ok(2))\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(Error(\"Error 1\"), Error(\"Error 2\"))\n" + " // -> Error(\"Error 2\")\n" + " ```\n" +). +-spec 'or'({ok, COF} | {error, COG}, {ok, COF} | {error, COG}) -> {ok, COF} | + {error, COG}. +'or'(First, Second) -> + case First of + {ok, _} -> + First; + + {error, _} -> + Second + end. + +-file("src/gleam/result.gleam", 307). +?DOC( + " Returns the first value if it is `Ok`, otherwise evaluates the given function for a fallback value.\n" + "\n" + " If you need access to the initial error value, use `result.try_recover`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " lazy_or(Ok(1), fn() { Ok(2) })\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(Ok(1), fn() { Error(\"Error 2\") })\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(Error(\"Error 1\"), fn() { Ok(2) })\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(Error(\"Error 1\"), fn() { Error(\"Error 2\") })\n" + " // -> Error(\"Error 2\")\n" + " ```\n" +). +-spec lazy_or({ok, CON} | {error, COO}, fun(() -> {ok, CON} | {error, COO})) -> {ok, + CON} | + {error, COO}. +lazy_or(First, Second) -> + case First of + {ok, _} -> + First; + + {error, _} -> + Second() + end. + +-file("src/gleam/result.gleam", 333). +?DOC( + " Combines a list of results into a single result.\n" + " If all elements in the list are `Ok` then returns an `Ok` holding the list of values.\n" + " If any element is `Error` then returns the first error.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " all([Ok(1), Ok(2)])\n" + " // -> Ok([1, 2])\n" + " ```\n" + "\n" + " ```gleam\n" + " all([Ok(1), Error(\"e\")])\n" + " // -> Error(\"e\")\n" + " ```\n" +). +-spec all(list({ok, COV} | {error, COW})) -> {ok, list(COV)} | {error, COW}. +all(Results) -> + gleam@list:try_map(Results, fun(Result) -> Result end). + +-file("src/gleam/result.gleam", 353). +-spec partition_loop(list({ok, CPK} | {error, CPL}), list(CPK), list(CPL)) -> {list(CPK), + list(CPL)}. +partition_loop(Results, Oks, Errors) -> + case Results of + [] -> + {Oks, Errors}; + + [{ok, A} | Rest] -> + partition_loop(Rest, [A | Oks], Errors); + + [{error, E} | Rest@1] -> + partition_loop(Rest@1, Oks, [E | Errors]) + end. + +-file("src/gleam/result.gleam", 349). +?DOC( + " Given a list of results, returns a pair where the first element is a list\n" + " of all the values inside `Ok` and the second element is a list with all the\n" + " values inside `Error`. The values in both lists appear in reverse order with\n" + " respect to their position in the original list of results.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " partition([Ok(1), Error(\"a\"), Error(\"b\"), Ok(2)])\n" + " // -> #([2, 1], [\"b\", \"a\"])\n" + " ```\n" +). +-spec partition(list({ok, CPD} | {error, CPE})) -> {list(CPD), list(CPE)}. +partition(Results) -> + partition_loop(Results, [], []). + +-file("src/gleam/result.gleam", 375). +?DOC( + " Replace the value within a result\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " replace(Ok(1), Nil)\n" + " // -> Ok(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " replace(Error(1), Nil)\n" + " // -> Error(1)\n" + " ```\n" +). +-spec replace({ok, any()} | {error, CPT}, CPW) -> {ok, CPW} | {error, CPT}. +replace(Result, Value) -> + case Result of + {ok, _} -> + {ok, Value}; + + {error, Error} -> + {error, Error} + end. + +-file("src/gleam/result.gleam", 396). +?DOC( + " Replace the error within a result\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " replace_error(Error(1), Nil)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " replace_error(Ok(1), Nil)\n" + " // -> Ok(1)\n" + " ```\n" +). +-spec replace_error({ok, CPZ} | {error, any()}, CQD) -> {ok, CPZ} | {error, CQD}. +replace_error(Result, Error) -> + case Result of + {ok, X} -> + {ok, X}; + + {error, _} -> + {error, Error} + end. + +-file("src/gleam/result.gleam", 412). +?DOC( + " Given a list of results, returns only the values inside `Ok`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " values([Ok(1), Error(\"a\"), Ok(3)])\n" + " // -> [1, 3]\n" + " ```\n" +). +-spec values(list({ok, CQG} | {error, any()})) -> list(CQG). +values(Results) -> + gleam@list:filter_map(Results, fun(Result) -> Result end). + +-file("src/gleam/result.gleam", 445). +?DOC( + " Updates a value held within the `Error` of a result by calling a given function\n" + " on it, where the given function also returns a result. The two results are\n" + " then merged together into one result.\n" + "\n" + " If the result is an `Ok` rather than `Error` the function is not called and the\n" + " result stays the same.\n" + "\n" + " This function is useful for chaining together computations that may fail\n" + " and trying to recover from possible errors.\n" + "\n" + " If you do not need access to the initial error value, use `result.lazy_or`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " Ok(1) |> try_recover(with: fn(_) { Error(\"failed to recover\") })\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " Error(1) |> try_recover(with: fn(error) { Ok(error + 1) })\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " Error(1) |> try_recover(with: fn(error) { Error(\"failed to recover\") })\n" + " // -> Error(\"failed to recover\")\n" + " ```\n" +). +-spec try_recover( + {ok, CQM} | {error, CQN}, + fun((CQN) -> {ok, CQM} | {error, CQQ}) +) -> {ok, CQM} | {error, CQQ}. +try_recover(Result, Fun) -> + case Result of + {ok, Value} -> + {ok, Value}; + + {error, Error} -> + Fun(Error) + end. diff --git a/build/dev/javascript/gleam_stdlib/gleam@set.erl b/build/dev/javascript/gleam_stdlib/gleam@set.erl new file mode 100644 index 0000000..bb3c417 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@set.erl @@ -0,0 +1,429 @@ +-module(gleam@set). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/set.gleam"). +-export([new/0, size/1, is_empty/1, contains/2, delete/2, to_list/1, fold/3, filter/2, drop/2, take/2, intersection/2, difference/2, is_subset/2, is_disjoint/2, each/2, insert/2, from_list/1, map/2, union/2, symmetric_difference/2]). +-export_type([set/1]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-opaque set(CVL) :: {set, gleam@dict:dict(CVL, list(nil))}. + +-file("src/gleam/set.gleam", 32). +?DOC(" Creates a new empty set.\n"). +-spec new() -> set(any()). +new() -> + {set, maps:new()}. + +-file("src/gleam/set.gleam", 50). +?DOC( + " Gets the number of members in a set.\n" + "\n" + " This function runs in constant time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new()\n" + " |> insert(1)\n" + " |> insert(2)\n" + " |> size\n" + " // -> 2\n" + " ```\n" +). +-spec size(set(any())) -> integer(). +size(Set) -> + maps:size(erlang:element(2, Set)). + +-file("src/gleam/set.gleam", 68). +?DOC( + " Determines whether or not the set is empty.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> is_empty\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(1) |> is_empty\n" + " // -> False\n" + " ```\n" +). +-spec is_empty(set(any())) -> boolean(). +is_empty(Set) -> + Set =:= new(). + +-file("src/gleam/set.gleam", 110). +?DOC( + " Checks whether a set contains a given member.\n" + "\n" + " This function runs in logarithmic time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new()\n" + " |> insert(2)\n" + " |> contains(2)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " new()\n" + " |> insert(2)\n" + " |> contains(1)\n" + " // -> False\n" + " ```\n" +). +-spec contains(set(CVW), CVW) -> boolean(). +contains(Set, Member) -> + _pipe = erlang:element(2, Set), + _pipe@1 = gleam_stdlib:map_get(_pipe, Member), + gleam@result:is_ok(_pipe@1). + +-file("src/gleam/set.gleam", 131). +?DOC( + " Removes a member from a set. If the set does not contain the member then\n" + " the set is returned unchanged.\n" + "\n" + " This function runs in logarithmic time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new()\n" + " |> insert(2)\n" + " |> delete(2)\n" + " |> contains(1)\n" + " // -> False\n" + " ```\n" +). +-spec delete(set(CVY), CVY) -> set(CVY). +delete(Set, Member) -> + {set, gleam@dict:delete(erlang:element(2, Set), Member)}. + +-file("src/gleam/set.gleam", 149). +?DOC( + " Converts the set into a list of the contained members.\n" + "\n" + " The list has no specific ordering, any unintentional ordering may change in\n" + " future versions of Gleam or Erlang.\n" + "\n" + " This function runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> insert(2) |> to_list\n" + " // -> [2]\n" + " ```\n" +). +-spec to_list(set(CWB)) -> list(CWB). +to_list(Set) -> + maps:keys(erlang:element(2, Set)). + +-file("src/gleam/set.gleam", 190). +?DOC( + " Combines all entries into a single value by calling a given function on each\n" + " one.\n" + "\n" + " Sets are not ordered so the values are not returned in any specific order.\n" + " Do not write code that relies on the order entries are used by this\n" + " function as it may change in later versions of Gleam or Erlang.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " from_list([1, 3, 9])\n" + " |> fold(0, fn(accumulator, member) { accumulator + member })\n" + " // -> 13\n" + " ```\n" +). +-spec fold(set(CWH), CWJ, fun((CWJ, CWH) -> CWJ)) -> CWJ. +fold(Set, Initial, Reducer) -> + gleam@dict:fold( + erlang:element(2, Set), + Initial, + fun(A, K, _) -> Reducer(A, K) end + ). + +-file("src/gleam/set.gleam", 214). +?DOC( + " Creates a new set from an existing set, minus any members that a given\n" + " function returns `False` for.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " from_list([1, 4, 6, 3, 675, 44, 67])\n" + " |> filter(keeping: int.is_even)\n" + " |> to_list\n" + " // -> [4, 6, 44]\n" + " ```\n" +). +-spec filter(set(CWK), fun((CWK) -> boolean())) -> set(CWK). +filter(Set, Predicate) -> + {set, + gleam@dict:filter(erlang:element(2, Set), fun(M, _) -> Predicate(M) end)}. + +-file("src/gleam/set.gleam", 249). +?DOC( + " Creates a new set from a given set with all the same entries except any\n" + " entry found on the given list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([1, 2, 3, 4])\n" + " |> drop([1, 3])\n" + " |> to_list\n" + " // -> [2, 4]\n" + " ```\n" +). +-spec drop(set(CWR), list(CWR)) -> set(CWR). +drop(Set, Disallowed) -> + gleam@list:fold(Disallowed, Set, fun delete/2). + +-file("src/gleam/set.gleam", 267). +?DOC( + " Creates a new set from a given set, only including any members which are in\n" + " a given list.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([1, 2, 3])\n" + " |> take([1, 3, 5])\n" + " |> to_list\n" + " // -> [1, 3]\n" + " ```\n" +). +-spec take(set(CWV), list(CWV)) -> set(CWV). +take(Set, Desired) -> + {set, gleam@dict:take(erlang:element(2, Set), Desired)}. + +-file("src/gleam/set.gleam", 287). +-spec order(set(CXD), set(CXD)) -> {set(CXD), set(CXD)}. +order(First, Second) -> + case maps:size(erlang:element(2, First)) > maps:size( + erlang:element(2, Second) + ) of + true -> + {First, Second}; + + false -> + {Second, First} + end. + +-file("src/gleam/set.gleam", 305). +?DOC( + " Creates a new set that contains members that are present in both given sets.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " intersection(from_list([1, 2]), from_list([2, 3])) |> to_list\n" + " // -> [2]\n" + " ```\n" +). +-spec intersection(set(CXI), set(CXI)) -> set(CXI). +intersection(First, Second) -> + {Larger, Smaller} = order(First, Second), + take(Larger, to_list(Smaller)). + +-file("src/gleam/set.gleam", 323). +?DOC( + " Creates a new set that contains members that are present in the first set\n" + " but not the second.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " difference(from_list([1, 2]), from_list([2, 3, 4])) |> to_list\n" + " // -> [1]\n" + " ```\n" +). +-spec difference(set(CXM), set(CXM)) -> set(CXM). +difference(First, Second) -> + drop(First, to_list(Second)). + +-file("src/gleam/set.gleam", 344). +?DOC( + " Determines if a set is fully contained by another.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_subset(from_list([1]), from_list([1, 2]))\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_subset(from_list([1, 2, 3]), from_list([3, 4, 5]))\n" + " // -> False\n" + " ```\n" +). +-spec is_subset(set(CXQ), set(CXQ)) -> boolean(). +is_subset(First, Second) -> + intersection(First, Second) =:= First. + +-file("src/gleam/set.gleam", 362). +?DOC( + " Determines if two sets contain no common members\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_disjoint(from_list([1, 2, 3]), from_list([4, 5, 6]))\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_disjoint(from_list([1, 2, 3]), from_list([3, 4, 5]))\n" + " // -> False\n" + " ```\n" +). +-spec is_disjoint(set(CXT), set(CXT)) -> boolean(). +is_disjoint(First, Second) -> + intersection(First, Second) =:= new(). + +-file("src/gleam/set.gleam", 402). +?DOC( + " Calls a function for each member in a set, discarding the return\n" + " value.\n" + "\n" + " Useful for producing a side effect for every item of a set.\n" + "\n" + " ```gleam\n" + " let set = from_list([\"apple\", \"banana\", \"cherry\"])\n" + "\n" + " each(set, io.println)\n" + " // -> Nil\n" + " // apple\n" + " // banana\n" + " // cherry\n" + " ```\n" + "\n" + " The order of elements in the iteration is an implementation detail that\n" + " should not be relied upon.\n" +). +-spec each(set(CYA), fun((CYA) -> any())) -> nil. +each(Set, Fun) -> + fold( + Set, + nil, + fun(Nil, Member) -> + Fun(Member), + Nil + end + ). + +-file("src/gleam/set.gleam", 86). +?DOC( + " Inserts an member into the set.\n" + "\n" + " This function runs in logarithmic time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new()\n" + " |> insert(1)\n" + " |> insert(2)\n" + " |> size\n" + " // -> 2\n" + " ```\n" +). +-spec insert(set(CVT), CVT) -> set(CVT). +insert(Set, Member) -> + {set, gleam@dict:insert(erlang:element(2, Set), Member, [])}. + +-file("src/gleam/set.gleam", 167). +?DOC( + " Creates a new set of the members in a given list.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + " import gleam/list\n" + "\n" + " [1, 1, 2, 4, 3, 2] |> from_list |> to_list |> list.sort(by: int.compare)\n" + " // -> [1, 2, 3, 4]\n" + " ```\n" +). +-spec from_list(list(CWE)) -> set(CWE). +from_list(Members) -> + Dict = gleam@list:fold( + Members, + maps:new(), + fun(M, K) -> gleam@dict:insert(M, K, []) end + ), + {set, Dict}. + +-file("src/gleam/set.gleam", 232). +?DOC( + " Creates a new set from a given set with the result of applying the given\n" + " function to each member.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([1, 2, 3, 4])\n" + " |> map(with: fn(x) { x * 2 })\n" + " |> to_list\n" + " // -> [2, 4, 6, 8]\n" + " ```\n" +). +-spec map(set(CWN), fun((CWN) -> CWP)) -> set(CWP). +map(Set, Fun) -> + fold(Set, new(), fun(Acc, Member) -> insert(Acc, Fun(Member)) end). + +-file("src/gleam/set.gleam", 282). +?DOC( + " Creates a new set that contains all members of both given sets.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " union(from_list([1, 2]), from_list([2, 3])) |> to_list\n" + " // -> [1, 2, 3]\n" + " ```\n" +). +-spec union(set(CWZ), set(CWZ)) -> set(CWZ). +union(First, Second) -> + {Larger, Smaller} = order(First, Second), + fold(Smaller, Larger, fun insert/2). + +-file("src/gleam/set.gleam", 374). +?DOC( + " Creates a new set that contains members that are present in either set, but\n" + " not both.\n" + "\n" + " ```gleam\n" + " symmetric_difference(from_list([1, 2, 3]), from_list([3, 4])) |> to_list\n" + " // -> [1, 2, 4]\n" + " ```\n" +). +-spec symmetric_difference(set(CXW), set(CXW)) -> set(CXW). +symmetric_difference(First, Second) -> + difference(union(First, Second), intersection(First, Second)). diff --git a/build/dev/javascript/gleam_stdlib/gleam@string.erl b/build/dev/javascript/gleam_stdlib/gleam@string.erl new file mode 100644 index 0000000..63006b8 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@string.erl @@ -0,0 +1,1012 @@ +-module(gleam@string). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/string.gleam"). +-export([is_empty/1, length/1, reverse/1, replace/3, lowercase/1, uppercase/1, compare/2, slice/3, crop/2, drop_end/2, contains/2, starts_with/2, ends_with/2, split_once/2, append/2, concat/1, repeat/2, join/2, pad_start/3, pad_end/3, trim_start/1, trim_end/1, trim/1, pop_grapheme/1, to_graphemes/1, split/2, to_utf_codepoints/1, from_utf_codepoints/1, utf_codepoint/1, utf_codepoint_to_int/1, to_option/1, first/1, last/1, capitalise/1, inspect/1, byte_size/1, drop_start/2]). +-export_type([direction/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Strings in Gleam are UTF-8 binaries. They can be written in your code as\n" + " text surrounded by `\"double quotes\"`.\n" +). + +-type direction() :: leading | trailing. + +-file("src/gleam/string.gleam", 23). +?DOC( + " Determines if a `String` is empty.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_empty(\"\")\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_empty(\"the world\")\n" + " // -> False\n" + " ```\n" +). +-spec is_empty(binary()) -> boolean(). +is_empty(Str) -> + Str =:= <<""/utf8>>. + +-file("src/gleam/string.gleam", 51). +?DOC( + " Gets the number of grapheme clusters in a given `String`.\n" + "\n" + " This function has to iterate across the whole string to count the number of\n" + " graphemes, so it runs in linear time. Avoid using this in a loop.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " length(\"Gleam\")\n" + " // -> 5\n" + " ```\n" + "\n" + " ```gleam\n" + " length(\"ß↑e̊\")\n" + " // -> 3\n" + " ```\n" + "\n" + " ```gleam\n" + " length(\"\")\n" + " // -> 0\n" + " ```\n" +). +-spec length(binary()) -> integer(). +length(String) -> + string:length(String). + +-file("src/gleam/string.gleam", 65). +?DOC( + " Reverses a `String`.\n" + "\n" + " This function has to iterate across the whole `String` so it runs in linear\n" + " time. Avoid using this in a loop.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " reverse(\"stressed\")\n" + " // -> \"desserts\"\n" + " ```\n" +). +-spec reverse(binary()) -> binary(). +reverse(String) -> + _pipe = String, + _pipe@1 = gleam_stdlib:identity(_pipe), + _pipe@2 = string:reverse(_pipe@1), + unicode:characters_to_binary(_pipe@2). + +-file("src/gleam/string.gleam", 86). +?DOC( + " Creates a new `String` by replacing all occurrences of a given substring.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " replace(\"www.example.com\", each: \".\", with: \"-\")\n" + " // -> \"www-example-com\"\n" + " ```\n" + "\n" + " ```gleam\n" + " replace(\"a,b,c,d,e\", each: \",\", with: \"/\")\n" + " // -> \"a/b/c/d/e\"\n" + " ```\n" +). +-spec replace(binary(), binary(), binary()) -> binary(). +replace(String, Pattern, Substitute) -> + _pipe = String, + _pipe@1 = gleam_stdlib:identity(_pipe), + _pipe@2 = gleam_stdlib:string_replace(_pipe@1, Pattern, Substitute), + unicode:characters_to_binary(_pipe@2). + +-file("src/gleam/string.gleam", 111). +?DOC( + " Creates a new `String` with all the graphemes in the input `String` converted to\n" + " lowercase.\n" + "\n" + " Useful for case-insensitive comparisons.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " lowercase(\"X-FILES\")\n" + " // -> \"x-files\"\n" + " ```\n" +). +-spec lowercase(binary()) -> binary(). +lowercase(String) -> + string:lowercase(String). + +-file("src/gleam/string.gleam", 127). +?DOC( + " Creates a new `String` with all the graphemes in the input `String` converted to\n" + " uppercase.\n" + "\n" + " Useful for case-insensitive comparisons and VIRTUAL YELLING.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " uppercase(\"skinner\")\n" + " // -> \"SKINNER\"\n" + " ```\n" +). +-spec uppercase(binary()) -> binary(). +uppercase(String) -> + string:uppercase(String). + +-file("src/gleam/string.gleam", 145). +?DOC( + " Compares two `String`s to see which is \"larger\" by comparing their graphemes.\n" + "\n" + " This does not compare the size or length of the given `String`s.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " compare(\"Anthony\", \"Anthony\")\n" + " // -> order.Eq\n" + " ```\n" + "\n" + " ```gleam\n" + " compare(\"A\", \"B\")\n" + " // -> order.Lt\n" + " ```\n" +). +-spec compare(binary(), binary()) -> gleam@order:order(). +compare(A, B) -> + case A =:= B of + true -> + eq; + + _ -> + case gleam_stdlib:less_than(A, B) of + true -> + lt; + + false -> + gt + end + end. + +-file("src/gleam/string.gleam", 194). +?DOC( + " Takes a substring given a start grapheme index and a length. Negative indexes\n" + " are taken starting from the *end* of the list.\n" + "\n" + " This function runs in linear time with the size of the index and the\n" + " length. Negative indexes are linear with the size of the input string in\n" + " addition to the other costs.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " slice(from: \"gleam\", at_index: 1, length: 2)\n" + " // -> \"le\"\n" + " ```\n" + "\n" + " ```gleam\n" + " slice(from: \"gleam\", at_index: 1, length: 10)\n" + " // -> \"leam\"\n" + " ```\n" + "\n" + " ```gleam\n" + " slice(from: \"gleam\", at_index: 10, length: 3)\n" + " // -> \"\"\n" + " ```\n" + "\n" + " ```gleam\n" + " slice(from: \"gleam\", at_index: -2, length: 2)\n" + " // -> \"am\"\n" + " ```\n" + "\n" + " ```gleam\n" + " slice(from: \"gleam\", at_index: -12, length: 2)\n" + " // -> \"\"\n" + " ```\n" +). +-spec slice(binary(), integer(), integer()) -> binary(). +slice(String, Idx, Len) -> + case Len =< 0 of + true -> + <<""/utf8>>; + + false -> + case Idx < 0 of + true -> + Translated_idx = string:length(String) + Idx, + case Translated_idx < 0 of + true -> + <<""/utf8>>; + + false -> + gleam_stdlib:slice(String, Translated_idx, Len) + end; + + false -> + gleam_stdlib:slice(String, Idx, Len) + end + end. + +-file("src/gleam/string.gleam", 232). +?DOC( + " Drops contents of the first `String` that occur before the second `String`.\n" + " If the `from` string does not contain the `before` string, `from` is\n" + " returned unchanged.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " crop(from: \"The Lone Gunmen\", before: \"Lone\")\n" + " // -> \"Lone Gunmen\"\n" + " ```\n" +). +-spec crop(binary(), binary()) -> binary(). +crop(String, Substring) -> + gleam_stdlib:crop_string(String, Substring). + +-file("src/gleam/string.gleam", 268). +?DOC( + " Drops *n* graphemes from the end of a `String`.\n" + "\n" + " This function traverses the full string, so it runs in linear time with the\n" + " size of the string. Avoid using this in a loop.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " drop_end(from: \"Cigarette Smoking Man\", up_to: 2)\n" + " // -> \"Cigarette Smoking M\"\n" + " ```\n" +). +-spec drop_end(binary(), integer()) -> binary(). +drop_end(String, Num_graphemes) -> + case Num_graphemes =< 0 of + true -> + String; + + false -> + slice(String, 0, string:length(String) - Num_graphemes) + end. + +-file("src/gleam/string.gleam", 296). +?DOC( + " Checks if the first `String` contains the second.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " contains(does: \"theory\", contain: \"ory\")\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " contains(does: \"theory\", contain: \"the\")\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " contains(does: \"theory\", contain: \"THE\")\n" + " // -> False\n" + " ```\n" +). +-spec contains(binary(), binary()) -> boolean(). +contains(Haystack, Needle) -> + gleam_stdlib:contains_string(Haystack, Needle). + +-file("src/gleam/string.gleam", 309). +?DOC( + " Checks whether the first `String` starts with the second one.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " starts_with(\"theory\", \"ory\")\n" + " // -> False\n" + " ```\n" +). +-spec starts_with(binary(), binary()) -> boolean(). +starts_with(String, Prefix) -> + gleam_stdlib:string_starts_with(String, Prefix). + +-file("src/gleam/string.gleam", 322). +?DOC( + " Checks whether the first `String` ends with the second one.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " ends_with(\"theory\", \"ory\")\n" + " // -> True\n" + " ```\n" +). +-spec ends_with(binary(), binary()) -> boolean(). +ends_with(String, Suffix) -> + gleam_stdlib:string_ends_with(String, Suffix). + +-file("src/gleam/string.gleam", 361). +?DOC( + " Splits a `String` a single time on the given substring.\n" + "\n" + " Returns an `Error` if substring not present.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " split_once(\"home/gleam/desktop/\", on: \"/\")\n" + " // -> Ok(#(\"home\", \"gleam/desktop/\"))\n" + " ```\n" + "\n" + " ```gleam\n" + " split_once(\"home/gleam/desktop/\", on: \"?\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec split_once(binary(), binary()) -> {ok, {binary(), binary()}} | + {error, nil}. +split_once(String, Substring) -> + case string:split(String, Substring) of + [First, Rest] -> + {ok, {First, Rest}}; + + _ -> + {error, nil} + end. + +-file("src/gleam/string.gleam", 392). +?DOC( + " Creates a new `String` by joining two `String`s together.\n" + "\n" + " This function typically copies both `String`s and runs in linear time, but\n" + " the exact behaviour will depend on how the runtime you are using optimises\n" + " your code. Benchmark and profile your code if you need to understand its\n" + " performance better.\n" + "\n" + " If you are joining together large string and want to avoid copying any data\n" + " you may want to investigate using the [`string_tree`](../gleam/string_tree.html)\n" + " module.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " append(to: \"butter\", suffix: \"fly\")\n" + " // -> \"butterfly\"\n" + " ```\n" +). +-spec append(binary(), binary()) -> binary(). +append(First, Second) -> + <>. + +-file("src/gleam/string.gleam", 412). +-spec concat_loop(list(binary()), binary()) -> binary(). +concat_loop(Strings, Accumulator) -> + case Strings of + [String | Strings@1] -> + concat_loop(Strings@1, <>); + + [] -> + Accumulator + end. + +-file("src/gleam/string.gleam", 408). +?DOC( + " Creates a new `String` by joining many `String`s together.\n" + "\n" + " This function copies all the `String`s and runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " concat([\"never\", \"the\", \"less\"])\n" + " // -> \"nevertheless\"\n" + " ```\n" +). +-spec concat(list(binary())) -> binary(). +concat(Strings) -> + erlang:list_to_binary(Strings). + +-file("src/gleam/string.gleam", 437). +-spec repeat_loop(integer(), binary(), binary()) -> binary(). +repeat_loop(Times, Doubling_acc, Acc) -> + Acc@1 = case Times rem 2 of + 0 -> + Acc; + + _ -> + <> + end, + Times@1 = Times div 2, + case Times@1 =< 0 of + true -> + Acc@1; + + false -> + repeat_loop( + Times@1, + <>, + Acc@1 + ) + end. + +-file("src/gleam/string.gleam", 430). +?DOC( + " Creates a new `String` by repeating a `String` a given number of times.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " repeat(\"ha\", times: 3)\n" + " // -> \"hahaha\"\n" + " ```\n" +). +-spec repeat(binary(), integer()) -> binary(). +repeat(String, Times) -> + case Times =< 0 of + true -> + <<""/utf8>>; + + false -> + repeat_loop(Times, String, <<""/utf8>>) + end. + +-file("src/gleam/string.gleam", 467). +-spec join_loop(list(binary()), binary(), binary()) -> binary(). +join_loop(Strings, Separator, Accumulator) -> + case Strings of + [] -> + Accumulator; + + [String | Strings@1] -> + join_loop( + Strings@1, + Separator, + <<<>/binary, + String/binary>> + ) + end. + +-file("src/gleam/string.gleam", 460). +?DOC( + " Joins many `String`s together with a given separator.\n" + "\n" + " This function runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " join([\"home\",\"evan\",\"Desktop\"], with: \"/\")\n" + " // -> \"home/evan/Desktop\"\n" + " ```\n" +). +-spec join(list(binary()), binary()) -> binary(). +join(Strings, Separator) -> + case Strings of + [] -> + <<""/utf8>>; + + [First | Rest] -> + join_loop(Rest, Separator, First) + end. + +-file("src/gleam/string.gleam", 545). +-spec padding(integer(), binary()) -> binary(). +padding(Size, Pad_string) -> + Pad_string_length = string:length(Pad_string), + Num_pads = case Pad_string_length of + 0 -> 0; + Gleam@denominator -> Size div Gleam@denominator + end, + Extra = case Pad_string_length of + 0 -> 0; + Gleam@denominator@1 -> Size rem Gleam@denominator@1 + end, + <<(repeat(Pad_string, Num_pads))/binary, + (slice(Pad_string, 0, Extra))/binary>>. + +-file("src/gleam/string.gleam", 498). +?DOC( + " Pads the start of a `String` until it has a given length.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " pad_start(\"121\", to: 5, with: \".\")\n" + " // -> \"..121\"\n" + " ```\n" + "\n" + " ```gleam\n" + " pad_start(\"121\", to: 3, with: \".\")\n" + " // -> \"121\"\n" + " ```\n" + "\n" + " ```gleam\n" + " pad_start(\"121\", to: 2, with: \".\")\n" + " // -> \"121\"\n" + " ```\n" +). +-spec pad_start(binary(), integer(), binary()) -> binary(). +pad_start(String, Desired_length, Pad_string) -> + Current_length = string:length(String), + To_pad_length = Desired_length - Current_length, + case To_pad_length =< 0 of + true -> + String; + + false -> + <<(padding(To_pad_length, Pad_string))/binary, String/binary>> + end. + +-file("src/gleam/string.gleam", 531). +?DOC( + " Pads the end of a `String` until it has a given length.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " pad_end(\"123\", to: 5, with: \".\")\n" + " // -> \"123..\"\n" + " ```\n" + "\n" + " ```gleam\n" + " pad_end(\"123\", to: 3, with: \".\")\n" + " // -> \"123\"\n" + " ```\n" + "\n" + " ```gleam\n" + " pad_end(\"123\", to: 2, with: \".\")\n" + " // -> \"123\"\n" + " ```\n" +). +-spec pad_end(binary(), integer(), binary()) -> binary(). +pad_end(String, Desired_length, Pad_string) -> + Current_length = string:length(String), + To_pad_length = Desired_length - Current_length, + case To_pad_length =< 0 of + true -> + String; + + false -> + <> + end. + +-file("src/gleam/string.gleam", 589). +?DOC( + " Removes whitespace at the start of a `String`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " trim_start(\" hats \\n\")\n" + " // -> \"hats \\n\"\n" + " ```\n" +). +-spec trim_start(binary()) -> binary(). +trim_start(String) -> + string:trim(String, leading). + +-file("src/gleam/string.gleam", 603). +?DOC( + " Removes whitespace at the end of a `String`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " trim_end(\" hats \\n\")\n" + " // -> \" hats\"\n" + " ```\n" +). +-spec trim_end(binary()) -> binary(). +trim_end(String) -> + string:trim(String, trailing). + +-file("src/gleam/string.gleam", 567). +?DOC( + " Removes whitespace on both sides of a `String`.\n" + "\n" + " Whitespace in this function is the set of nonbreakable whitespace\n" + " codepoints, defined as Pattern_White_Space in [Unicode Standard Annex #31][1].\n" + "\n" + " [1]: https://unicode.org/reports/tr31/\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " trim(\" hats \\n\")\n" + " // -> \"hats\"\n" + " ```\n" +). +-spec trim(binary()) -> binary(). +trim(String) -> + _pipe = String, + _pipe@1 = trim_start(_pipe), + trim_end(_pipe@1). + +-file("src/gleam/string.gleam", 630). +?DOC( + " Splits a non-empty `String` into its first element (head) and rest (tail).\n" + " This lets you pattern match on `String`s exactly as you would with lists.\n" + "\n" + " ## Performance\n" + "\n" + " There is a notable overhead to using this function, so you may not want to\n" + " use it in a tight loop. If you wish to efficiently parse a string you may\n" + " want to use alternatives such as the [splitter package](https://hex.pm/packages/splitter).\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " pop_grapheme(\"gleam\")\n" + " // -> Ok(#(\"g\", \"leam\"))\n" + " ```\n" + "\n" + " ```gleam\n" + " pop_grapheme(\"\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec pop_grapheme(binary()) -> {ok, {binary(), binary()}} | {error, nil}. +pop_grapheme(String) -> + gleam_stdlib:string_pop_grapheme(String). + +-file("src/gleam/string.gleam", 647). +-spec to_graphemes_loop(binary(), list(binary())) -> list(binary()). +to_graphemes_loop(String, Acc) -> + case gleam_stdlib:string_pop_grapheme(String) of + {ok, {Grapheme, Rest}} -> + to_graphemes_loop(Rest, [Grapheme | Acc]); + + {error, _} -> + Acc + end. + +-file("src/gleam/string.gleam", 641). +?DOC( + " Converts a `String` to a list of\n" + " [graphemes](https://en.wikipedia.org/wiki/Grapheme).\n" + "\n" + " ```gleam\n" + " to_graphemes(\"abc\")\n" + " // -> [\"a\", \"b\", \"c\"]\n" + " ```\n" +). +-spec to_graphemes(binary()) -> list(binary()). +to_graphemes(String) -> + _pipe = String, + _pipe@1 = to_graphemes_loop(_pipe, []), + lists:reverse(_pipe@1). + +-file("src/gleam/string.gleam", 333). +?DOC( + " Creates a list of `String`s by splitting a given string on a given substring.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " split(\"home/gleam/desktop/\", on: \"/\")\n" + " // -> [\"home\", \"gleam\", \"desktop\", \"\"]\n" + " ```\n" +). +-spec split(binary(), binary()) -> list(binary()). +split(X, Substring) -> + case Substring of + <<""/utf8>> -> + to_graphemes(X); + + _ -> + _pipe = X, + _pipe@1 = gleam_stdlib:identity(_pipe), + _pipe@2 = gleam@string_tree:split(_pipe@1, Substring), + gleam@list:map(_pipe@2, fun unicode:characters_to_binary/1) + end. + +-file("src/gleam/string.gleam", 694). +-spec to_utf_codepoints_loop(bitstring(), list(integer())) -> list(integer()). +to_utf_codepoints_loop(Bit_array, Acc) -> + case Bit_array of + <> -> + to_utf_codepoints_loop(Rest, [First | Acc]); + + _ -> + lists:reverse(Acc) + end. + +-file("src/gleam/string.gleam", 689). +-spec do_to_utf_codepoints(binary()) -> list(integer()). +do_to_utf_codepoints(String) -> + to_utf_codepoints_loop(<>, []). + +-file("src/gleam/string.gleam", 684). +?DOC( + " Converts a `String` to a `List` of `UtfCodepoint`.\n" + "\n" + " See and\n" + " for an\n" + " explanation on code points.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " \"a\" |> to_utf_codepoints\n" + " // -> [UtfCodepoint(97)]\n" + " ```\n" + "\n" + " ```gleam\n" + " // Semantically the same as:\n" + " // [\"🏳\", \"️\", \"‍\", \"🌈\"] or:\n" + " // [waving_white_flag, variant_selector_16, zero_width_joiner, rainbow]\n" + " \"🏳️‍🌈\" |> to_utf_codepoints\n" + " // -> [\n" + " // UtfCodepoint(127987),\n" + " // UtfCodepoint(65039),\n" + " // UtfCodepoint(8205),\n" + " // UtfCodepoint(127752),\n" + " // ]\n" + " ```\n" +). +-spec to_utf_codepoints(binary()) -> list(integer()). +to_utf_codepoints(String) -> + do_to_utf_codepoints(String). + +-file("src/gleam/string.gleam", 734). +?DOC( + " Converts a `List` of `UtfCodepoint`s to a `String`.\n" + "\n" + " See and\n" + " for an\n" + " explanation on code points.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let assert Ok(a) = utf_codepoint(97)\n" + " let assert Ok(b) = utf_codepoint(98)\n" + " let assert Ok(c) = utf_codepoint(99)\n" + " from_utf_codepoints([a, b, c])\n" + " // -> \"abc\"\n" + " ```\n" +). +-spec from_utf_codepoints(list(integer())) -> binary(). +from_utf_codepoints(Utf_codepoints) -> + gleam_stdlib:utf_codepoint_list_to_string(Utf_codepoints). + +-file("src/gleam/string.gleam", 740). +?DOC( + " Converts an integer to a `UtfCodepoint`.\n" + "\n" + " Returns an `Error` if the integer does not represent a valid UTF codepoint.\n" +). +-spec utf_codepoint(integer()) -> {ok, integer()} | {error, nil}. +utf_codepoint(Value) -> + case Value of + I when I > 1114111 -> + {error, nil}; + + I@1 when (I@1 >= 55296) andalso (I@1 =< 57343) -> + {error, nil}; + + I@2 when I@2 < 0 -> + {error, nil}; + + I@3 -> + {ok, gleam_stdlib:identity(I@3)} + end. + +-file("src/gleam/string.gleam", 761). +?DOC( + " Converts an UtfCodepoint to its ordinal code point value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let assert [utf_codepoint, ..] = to_utf_codepoints(\"💜\")\n" + " utf_codepoint_to_int(utf_codepoint)\n" + " // -> 128156\n" + " ```\n" +). +-spec utf_codepoint_to_int(integer()) -> integer(). +utf_codepoint_to_int(Cp) -> + gleam_stdlib:identity(Cp). + +-file("src/gleam/string.gleam", 778). +?DOC( + " Converts a `String` into `Option(String)` where an empty `String` becomes\n" + " `None`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_option(\"\")\n" + " // -> None\n" + " ```\n" + "\n" + " ```gleam\n" + " to_option(\"hats\")\n" + " // -> Some(\"hats\")\n" + " ```\n" +). +-spec to_option(binary()) -> gleam@option:option(binary()). +to_option(String) -> + case String of + <<""/utf8>> -> + none; + + _ -> + {some, String} + end. + +-file("src/gleam/string.gleam", 801). +?DOC( + " Returns the first grapheme cluster in a given `String` and wraps it in a\n" + " `Result(String, Nil)`. If the `String` is empty, it returns `Error(Nil)`.\n" + " Otherwise, it returns `Ok(String)`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " first(\"\")\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " first(\"icecream\")\n" + " // -> Ok(\"i\")\n" + " ```\n" +). +-spec first(binary()) -> {ok, binary()} | {error, nil}. +first(String) -> + case gleam_stdlib:string_pop_grapheme(String) of + {ok, {First, _}} -> + {ok, First}; + + {error, E} -> + {error, E} + end. + +-file("src/gleam/string.gleam", 827). +?DOC( + " Returns the last grapheme cluster in a given `String` and wraps it in a\n" + " `Result(String, Nil)`. If the `String` is empty, it returns `Error(Nil)`.\n" + " Otherwise, it returns `Ok(String)`.\n" + "\n" + " This function traverses the full string, so it runs in linear time with the\n" + " length of the string. Avoid using this in a loop.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " last(\"\")\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " last(\"icecream\")\n" + " // -> Ok(\"m\")\n" + " ```\n" +). +-spec last(binary()) -> {ok, binary()} | {error, nil}. +last(String) -> + case gleam_stdlib:string_pop_grapheme(String) of + {ok, {First, <<""/utf8>>}} -> + {ok, First}; + + {ok, {_, Rest}} -> + {ok, slice(Rest, -1, 1)}; + + {error, E} -> + {error, E} + end. + +-file("src/gleam/string.gleam", 845). +?DOC( + " Creates a new `String` with the first grapheme in the input `String`\n" + " converted to uppercase and the remaining graphemes to lowercase.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " capitalise(\"mamouna\")\n" + " // -> \"Mamouna\"\n" + " ```\n" +). +-spec capitalise(binary()) -> binary(). +capitalise(String) -> + case gleam_stdlib:string_pop_grapheme(String) of + {ok, {First, Rest}} -> + append(string:uppercase(First), string:lowercase(Rest)); + + {error, _} -> + <<""/utf8>> + end. + +-file("src/gleam/string.gleam", 876). +?DOC( + " Returns a `String` representation of a term in Gleam syntax.\n" + "\n" + " This may be occasionally useful for quick-and-dirty printing of values in\n" + " scripts. For error reporting and other uses prefer constructing strings by\n" + " pattern matching on the values.\n" + "\n" + " ## Limitations\n" + "\n" + " The output format of this function is not stable and could change at any\n" + " time. The output is not suitable for parsing.\n" + "\n" + " This function works using runtime reflection, so the output may not be\n" + " perfectly accurate for data structures where the runtime structure doesn't\n" + " hold enough information to determine the original syntax. For example,\n" + " tuples with an Erlang atom in the first position will be mistaken for Gleam\n" + " records.\n" + "\n" + " ## Security and safety\n" + "\n" + " There is no limit to how large the strings that this function can produce.\n" + " Be careful not to call this function with large data structures or you\n" + " could use very large amounts of memory, potentially causing runtime\n" + " problems.\n" +). +-spec inspect(any()) -> binary(). +inspect(Term) -> + _pipe = Term, + _pipe@1 = gleam_stdlib:inspect(_pipe), + unicode:characters_to_binary(_pipe@1). + +-file("src/gleam/string.gleam", 900). +?DOC( + " Returns the number of bytes in a `String`.\n" + "\n" + " This function runs in constant time on Erlang and in linear time on\n" + " JavaScript.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " byte_size(\"🏳️‍⚧️🏳️‍🌈👩🏾‍❤️‍👨🏻\")\n" + " // -> 58\n" + " ```\n" +). +-spec byte_size(binary()) -> integer(). +byte_size(String) -> + erlang:byte_size(String). + +-file("src/gleam/string.gleam", 245). +?DOC( + " Drops *n* graphemes from the start of a `String`.\n" + "\n" + " This function runs in linear time with the number of graphemes to drop.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " drop_start(from: \"The Lone Gunmen\", up_to: 2)\n" + " // -> \"e Lone Gunmen\"\n" + " ```\n" +). +-spec drop_start(binary(), integer()) -> binary(). +drop_start(String, Num_graphemes) -> + case Num_graphemes =< 0 of + true -> + String; + + false -> + Prefix = gleam_stdlib:slice(String, 0, Num_graphemes), + Prefix_size = erlang:byte_size(Prefix), + binary:part( + String, + Prefix_size, + erlang:byte_size(String) - Prefix_size + ) + end. diff --git a/build/dev/javascript/gleam_stdlib/gleam@string_tree.erl b/build/dev/javascript/gleam_stdlib/gleam@string_tree.erl new file mode 100644 index 0000000..0d72b4d --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@string_tree.erl @@ -0,0 +1,207 @@ +-module(gleam@string_tree). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/string_tree.gleam"). +-export([append_tree/2, prepend_tree/2, from_strings/1, new/0, concat/1, from_string/1, prepend/2, append/2, to_string/1, byte_size/1, join/2, lowercase/1, uppercase/1, reverse/1, split/2, replace/3, is_equal/2, is_empty/1]). +-export_type([string_tree/0, direction/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type string_tree() :: any(). + +-type direction() :: all. + +-file("src/gleam/string_tree.gleam", 61). +?DOC( + " Appends some `StringTree` onto the end of another.\n" + "\n" + " Runs in constant time.\n" +). +-spec append_tree(string_tree(), string_tree()) -> string_tree(). +append_tree(Tree, Suffix) -> + gleam_stdlib:iodata_append(Tree, Suffix). + +-file("src/gleam/string_tree.gleam", 48). +?DOC( + " Prepends some `StringTree` onto the start of another.\n" + "\n" + " Runs in constant time.\n" +). +-spec prepend_tree(string_tree(), string_tree()) -> string_tree(). +prepend_tree(Tree, Prefix) -> + gleam_stdlib:iodata_append(Prefix, Tree). + +-file("src/gleam/string_tree.gleam", 69). +?DOC( + " Converts a list of strings into a `StringTree`.\n" + "\n" + " Runs in constant time.\n" +). +-spec from_strings(list(binary())) -> string_tree(). +from_strings(Strings) -> + gleam_stdlib:identity(Strings). + +-file("src/gleam/string_tree.gleam", 24). +?DOC( + " Create an empty `StringTree`. Useful as the start of a pipe chaining many\n" + " trees together.\n" +). +-spec new() -> string_tree(). +new() -> + gleam_stdlib:identity([]). + +-file("src/gleam/string_tree.gleam", 77). +?DOC( + " Joins a list of trees into a single tree.\n" + "\n" + " Runs in constant time.\n" +). +-spec concat(list(string_tree())) -> string_tree(). +concat(Trees) -> + gleam_stdlib:identity(Trees). + +-file("src/gleam/string_tree.gleam", 85). +?DOC( + " Converts a string into a `StringTree`.\n" + "\n" + " Runs in constant time.\n" +). +-spec from_string(binary()) -> string_tree(). +from_string(String) -> + gleam_stdlib:identity(String). + +-file("src/gleam/string_tree.gleam", 32). +?DOC( + " Prepends a `String` onto the start of some `StringTree`.\n" + "\n" + " Runs in constant time.\n" +). +-spec prepend(string_tree(), binary()) -> string_tree(). +prepend(Tree, Prefix) -> + gleam_stdlib:iodata_append(gleam_stdlib:identity(Prefix), Tree). + +-file("src/gleam/string_tree.gleam", 40). +?DOC( + " Appends a `String` onto the end of some `StringTree`.\n" + "\n" + " Runs in constant time.\n" +). +-spec append(string_tree(), binary()) -> string_tree(). +append(Tree, Second) -> + gleam_stdlib:iodata_append(Tree, gleam_stdlib:identity(Second)). + +-file("src/gleam/string_tree.gleam", 94). +?DOC( + " Turns a `StringTree` into a `String`\n" + "\n" + " This function is implemented natively by the virtual machine and is highly\n" + " optimised.\n" +). +-spec to_string(string_tree()) -> binary(). +to_string(Tree) -> + unicode:characters_to_binary(Tree). + +-file("src/gleam/string_tree.gleam", 100). +?DOC(" Returns the size of the `StringTree` in bytes.\n"). +-spec byte_size(string_tree()) -> integer(). +byte_size(Tree) -> + erlang:iolist_size(Tree). + +-file("src/gleam/string_tree.gleam", 104). +?DOC(" Joins the given trees into a new tree separated with the given string.\n"). +-spec join(list(string_tree()), binary()) -> string_tree(). +join(Trees, Sep) -> + _pipe = Trees, + _pipe@1 = gleam@list:intersperse(_pipe, gleam_stdlib:identity(Sep)), + gleam_stdlib:identity(_pipe@1). + +-file("src/gleam/string_tree.gleam", 115). +?DOC( + " Converts a `StringTree` to a new one where the contents have been\n" + " lowercased.\n" +). +-spec lowercase(string_tree()) -> string_tree(). +lowercase(Tree) -> + string:lowercase(Tree). + +-file("src/gleam/string_tree.gleam", 122). +?DOC( + " Converts a `StringTree` to a new one where the contents have been\n" + " uppercased.\n" +). +-spec uppercase(string_tree()) -> string_tree(). +uppercase(Tree) -> + string:uppercase(Tree). + +-file("src/gleam/string_tree.gleam", 127). +?DOC(" Converts a `StringTree` to a new one with the contents reversed.\n"). +-spec reverse(string_tree()) -> string_tree(). +reverse(Tree) -> + string:reverse(Tree). + +-file("src/gleam/string_tree.gleam", 145). +?DOC(" Splits a `StringTree` on a given pattern into a list of trees.\n"). +-spec split(string_tree(), binary()) -> list(string_tree()). +split(Tree, Pattern) -> + string:split(Tree, Pattern, all). + +-file("src/gleam/string_tree.gleam", 156). +?DOC(" Replaces all instances of a pattern with a given string substitute.\n"). +-spec replace(string_tree(), binary(), binary()) -> string_tree(). +replace(Tree, Pattern, Substitute) -> + gleam_stdlib:string_replace(Tree, Pattern, Substitute). + +-file("src/gleam/string_tree.gleam", 182). +?DOC( + " Compares two string trees to determine if they have the same textual\n" + " content.\n" + "\n" + " Comparing two string trees using the `==` operator may return `False` even\n" + " if they have the same content as they may have been build in different ways,\n" + " so using this function is often preferred.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_strings([\"a\", \"b\"]) == from_string(\"ab\")\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " is_equal(from_strings([\"a\", \"b\"]), from_string(\"ab\"))\n" + " // -> True\n" + " ```\n" +). +-spec is_equal(string_tree(), string_tree()) -> boolean(). +is_equal(A, B) -> + string:equal(A, B). + +-file("src/gleam/string_tree.gleam", 206). +?DOC( + " Inspects a `StringTree` to determine if it is equivalent to an empty string.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_string(\"ok\") |> is_empty\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " from_string(\"\") |> is_empty\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " from_strings([]) |> is_empty\n" + " // -> True\n" + " ```\n" +). +-spec is_empty(string_tree()) -> boolean(). +is_empty(Tree) -> + string:is_empty(Tree). diff --git a/build/dev/javascript/gleam_stdlib/gleam@uri.erl b/build/dev/javascript/gleam_stdlib/gleam@uri.erl new file mode 100644 index 0000000..0819463 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam@uri.erl @@ -0,0 +1,1044 @@ +-module(gleam@uri). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/uri.gleam"). +-export([parse_query/1, percent_encode/1, query_to_string/1, percent_decode/1, path_segments/1, to_string/1, origin/1, merge/2, parse/1]). +-export_type([uri/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Utilities for working with URIs\n" + "\n" + " This module provides functions for working with URIs (for example, parsing\n" + " URIs or encoding query strings). The functions in this module are implemented\n" + " according to [RFC 3986](https://tools.ietf.org/html/rfc3986).\n" + "\n" + " Query encoding (Form encoding) is defined in the\n" + " [W3C specification](https://www.w3.org/TR/html52/sec-forms.html#urlencoded-form-data).\n" +). + +-type uri() :: {uri, + gleam@option:option(binary()), + gleam@option:option(binary()), + gleam@option:option(binary()), + gleam@option:option(integer()), + binary(), + gleam@option:option(binary()), + gleam@option:option(binary())}. + +-file("src/gleam/uri.gleam", 289). +-spec is_valid_host_within_brackets_char(integer()) -> boolean(). +is_valid_host_within_brackets_char(Char) -> + (((((48 >= Char) andalso (Char =< 57)) orelse ((65 >= Char) andalso (Char =< 90))) + orelse ((97 >= Char) andalso (Char =< 122))) + orelse (Char =:= 58)) + orelse (Char =:= 46). + +-file("src/gleam/uri.gleam", 506). +-spec parse_fragment(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_fragment(Rest, Pieces) -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + {some, Rest}}}. + +-file("src/gleam/uri.gleam", 478). +-spec parse_query_with_question_mark_loop(binary(), binary(), uri(), integer()) -> {ok, + uri()} | + {error, nil}. +parse_query_with_question_mark_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<"#"/utf8, Rest/binary>> when Size =:= 0 -> + parse_fragment(Rest, Pieces); + + <<"#"/utf8, Rest@1/binary>> -> + Query = binary:part(Original, 0, Size), + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + {some, Query}, + erlang:element(8, Pieces)}, + parse_fragment(Rest@1, Pieces@1); + + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + {some, Original}, + erlang:element(8, Pieces)}}; + + _ -> + {_, Rest@2} = gleam_stdlib:string_pop_codeunit(Uri_string), + parse_query_with_question_mark_loop( + Original, + Rest@2, + Pieces, + Size + 1 + ) + end. + +-file("src/gleam/uri.gleam", 471). +-spec parse_query_with_question_mark(binary(), uri()) -> {ok, uri()} | + {error, nil}. +parse_query_with_question_mark(Uri_string, Pieces) -> + parse_query_with_question_mark_loop(Uri_string, Uri_string, Pieces, 0). + +-file("src/gleam/uri.gleam", 437). +-spec parse_path_loop(binary(), binary(), uri(), integer()) -> {ok, uri()} | + {error, nil}. +parse_path_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<"?"/utf8, Rest/binary>> -> + Path = binary:part(Original, 0, Size), + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + Path, + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_query_with_question_mark(Rest, Pieces@1); + + <<"#"/utf8, Rest@1/binary>> -> + Path@1 = binary:part(Original, 0, Size), + Pieces@2 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + Path@1, + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_fragment(Rest@1, Pieces@2); + + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + Original, + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + _ -> + {_, Rest@2} = gleam_stdlib:string_pop_codeunit(Uri_string), + parse_path_loop(Original, Rest@2, Pieces, Size + 1) + end. + +-file("src/gleam/uri.gleam", 433). +-spec parse_path(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_path(Uri_string, Pieces) -> + parse_path_loop(Uri_string, Uri_string, Pieces, 0). + +-file("src/gleam/uri.gleam", 388). +-spec parse_port_loop(binary(), uri(), integer()) -> {ok, uri()} | {error, nil}. +parse_port_loop(Uri_string, Pieces, Port) -> + case Uri_string of + <<"0"/utf8, Rest/binary>> -> + parse_port_loop(Rest, Pieces, Port * 10); + + <<"1"/utf8, Rest@1/binary>> -> + parse_port_loop(Rest@1, Pieces, (Port * 10) + 1); + + <<"2"/utf8, Rest@2/binary>> -> + parse_port_loop(Rest@2, Pieces, (Port * 10) + 2); + + <<"3"/utf8, Rest@3/binary>> -> + parse_port_loop(Rest@3, Pieces, (Port * 10) + 3); + + <<"4"/utf8, Rest@4/binary>> -> + parse_port_loop(Rest@4, Pieces, (Port * 10) + 4); + + <<"5"/utf8, Rest@5/binary>> -> + parse_port_loop(Rest@5, Pieces, (Port * 10) + 5); + + <<"6"/utf8, Rest@6/binary>> -> + parse_port_loop(Rest@6, Pieces, (Port * 10) + 6); + + <<"7"/utf8, Rest@7/binary>> -> + parse_port_loop(Rest@7, Pieces, (Port * 10) + 7); + + <<"8"/utf8, Rest@8/binary>> -> + parse_port_loop(Rest@8, Pieces, (Port * 10) + 8); + + <<"9"/utf8, Rest@9/binary>> -> + parse_port_loop(Rest@9, Pieces, (Port * 10) + 9); + + <<"?"/utf8, Rest@10/binary>> -> + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + {some, Port}, + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_query_with_question_mark(Rest@10, Pieces@1); + + <<"#"/utf8, Rest@11/binary>> -> + Pieces@2 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + {some, Port}, + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_fragment(Rest@11, Pieces@2); + + <<"/"/utf8, _/binary>> -> + Pieces@3 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + {some, Port}, + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_path(Uri_string, Pieces@3); + + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + {some, Port}, + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + _ -> + {error, nil} + end. + +-file("src/gleam/uri.gleam", 353). +-spec parse_port(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_port(Uri_string, Pieces) -> + case Uri_string of + <<":0"/utf8, Rest/binary>> -> + parse_port_loop(Rest, Pieces, 0); + + <<":1"/utf8, Rest@1/binary>> -> + parse_port_loop(Rest@1, Pieces, 1); + + <<":2"/utf8, Rest@2/binary>> -> + parse_port_loop(Rest@2, Pieces, 2); + + <<":3"/utf8, Rest@3/binary>> -> + parse_port_loop(Rest@3, Pieces, 3); + + <<":4"/utf8, Rest@4/binary>> -> + parse_port_loop(Rest@4, Pieces, 4); + + <<":5"/utf8, Rest@5/binary>> -> + parse_port_loop(Rest@5, Pieces, 5); + + <<":6"/utf8, Rest@6/binary>> -> + parse_port_loop(Rest@6, Pieces, 6); + + <<":7"/utf8, Rest@7/binary>> -> + parse_port_loop(Rest@7, Pieces, 7); + + <<":8"/utf8, Rest@8/binary>> -> + parse_port_loop(Rest@8, Pieces, 8); + + <<":9"/utf8, Rest@9/binary>> -> + parse_port_loop(Rest@9, Pieces, 9); + + <<":"/utf8>> -> + {ok, Pieces}; + + <<""/utf8>> -> + {ok, Pieces}; + + <<"?"/utf8, Rest@10/binary>> -> + parse_query_with_question_mark(Rest@10, Pieces); + + <<":?"/utf8, Rest@10/binary>> -> + parse_query_with_question_mark(Rest@10, Pieces); + + <<"#"/utf8, Rest@11/binary>> -> + parse_fragment(Rest@11, Pieces); + + <<":#"/utf8, Rest@11/binary>> -> + parse_fragment(Rest@11, Pieces); + + <<"/"/utf8, _/binary>> -> + parse_path(Uri_string, Pieces); + + <<":"/utf8, Rest@12/binary>> -> + case Rest@12 of + <<"/"/utf8, _/binary>> -> + parse_path(Rest@12, Pieces); + + _ -> + {error, nil} + end; + + _ -> + {error, nil} + end. + +-file("src/gleam/uri.gleam", 309). +-spec parse_host_outside_of_brackets_loop(binary(), binary(), uri(), integer()) -> {ok, + uri()} | + {error, nil}. +parse_host_outside_of_brackets_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Original}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + <<":"/utf8, _/binary>> -> + Host = binary:part(Original, 0, Size), + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_port(Uri_string, Pieces@1); + + <<"/"/utf8, _/binary>> -> + Host@1 = binary:part(Original, 0, Size), + Pieces@2 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@1}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_path(Uri_string, Pieces@2); + + <<"?"/utf8, Rest/binary>> -> + Host@2 = binary:part(Original, 0, Size), + Pieces@3 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@2}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_query_with_question_mark(Rest, Pieces@3); + + <<"#"/utf8, Rest@1/binary>> -> + Host@3 = binary:part(Original, 0, Size), + Pieces@4 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@3}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_fragment(Rest@1, Pieces@4); + + _ -> + {_, Rest@2} = gleam_stdlib:string_pop_codeunit(Uri_string), + parse_host_outside_of_brackets_loop( + Original, + Rest@2, + Pieces, + Size + 1 + ) + end. + +-file("src/gleam/uri.gleam", 229). +-spec parse_host_within_brackets_loop(binary(), binary(), uri(), integer()) -> {ok, + uri()} | + {error, nil}. +parse_host_within_brackets_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Uri_string}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + <<"]"/utf8, Rest/binary>> when Size =:= 0 -> + parse_port(Rest, Pieces); + + <<"]"/utf8, Rest@1/binary>> -> + Host = binary:part(Original, 0, Size + 1), + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_port(Rest@1, Pieces@1); + + <<"/"/utf8, _/binary>> when Size =:= 0 -> + parse_path(Uri_string, Pieces); + + <<"/"/utf8, _/binary>> -> + Host@1 = binary:part(Original, 0, Size), + Pieces@2 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@1}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_path(Uri_string, Pieces@2); + + <<"?"/utf8, Rest@2/binary>> when Size =:= 0 -> + parse_query_with_question_mark(Rest@2, Pieces); + + <<"?"/utf8, Rest@3/binary>> -> + Host@2 = binary:part(Original, 0, Size), + Pieces@3 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@2}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_query_with_question_mark(Rest@3, Pieces@3); + + <<"#"/utf8, Rest@4/binary>> when Size =:= 0 -> + parse_fragment(Rest@4, Pieces); + + <<"#"/utf8, Rest@5/binary>> -> + Host@3 = binary:part(Original, 0, Size), + Pieces@4 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@3}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_fragment(Rest@5, Pieces@4); + + _ -> + {Char, Rest@6} = gleam_stdlib:string_pop_codeunit(Uri_string), + case is_valid_host_within_brackets_char(Char) of + true -> + parse_host_within_brackets_loop( + Original, + Rest@6, + Pieces, + Size + 1 + ); + + false -> + parse_host_outside_of_brackets_loop( + Original, + Original, + Pieces, + 0 + ) + end + end. + +-file("src/gleam/uri.gleam", 222). +-spec parse_host_within_brackets(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_host_within_brackets(Uri_string, Pieces) -> + parse_host_within_brackets_loop(Uri_string, Uri_string, Pieces, 0). + +-file("src/gleam/uri.gleam", 302). +-spec parse_host_outside_of_brackets(binary(), uri()) -> {ok, uri()} | + {error, nil}. +parse_host_outside_of_brackets(Uri_string, Pieces) -> + parse_host_outside_of_brackets_loop(Uri_string, Uri_string, Pieces, 0). + +-file("src/gleam/uri.gleam", 199). +-spec parse_host(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_host(Uri_string, Pieces) -> + case Uri_string of + <<"["/utf8, _/binary>> -> + parse_host_within_brackets(Uri_string, Pieces); + + <<":"/utf8, _/binary>> -> + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, <<""/utf8>>}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_port(Uri_string, Pieces@1); + + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, <<""/utf8>>}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + _ -> + parse_host_outside_of_brackets(Uri_string, Pieces) + end. + +-file("src/gleam/uri.gleam", 167). +-spec parse_userinfo_loop(binary(), binary(), uri(), integer()) -> {ok, uri()} | + {error, nil}. +parse_userinfo_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<"@"/utf8, Rest/binary>> when Size =:= 0 -> + parse_host(Rest, Pieces); + + <<"@"/utf8, Rest@1/binary>> -> + Userinfo = binary:part(Original, 0, Size), + Pieces@1 = {uri, + erlang:element(2, Pieces), + {some, Userinfo}, + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_host(Rest@1, Pieces@1); + + <<""/utf8>> -> + parse_host(Original, Pieces); + + <<"/"/utf8, _/binary>> -> + parse_host(Original, Pieces); + + <<"?"/utf8, _/binary>> -> + parse_host(Original, Pieces); + + <<"#"/utf8, _/binary>> -> + parse_host(Original, Pieces); + + _ -> + {_, Rest@2} = gleam_stdlib:string_pop_codeunit(Uri_string), + parse_userinfo_loop(Original, Rest@2, Pieces, Size + 1) + end. + +-file("src/gleam/uri.gleam", 163). +-spec parse_authority_pieces(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_authority_pieces(String, Pieces) -> + parse_userinfo_loop(String, String, Pieces, 0). + +-file("src/gleam/uri.gleam", 150). +-spec parse_authority_with_slashes(binary(), uri()) -> {ok, uri()} | + {error, nil}. +parse_authority_with_slashes(Uri_string, Pieces) -> + case Uri_string of + <<"//"/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, <<""/utf8>>}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + <<"//"/utf8, Rest/binary>> -> + parse_authority_pieces(Rest, Pieces); + + _ -> + parse_path(Uri_string, Pieces) + end. + +-file("src/gleam/uri.gleam", 91). +-spec parse_scheme_loop(binary(), binary(), uri(), integer()) -> {ok, uri()} | + {error, nil}. +parse_scheme_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<"/"/utf8, _/binary>> when Size =:= 0 -> + parse_authority_with_slashes(Uri_string, Pieces); + + <<"/"/utf8, _/binary>> -> + Scheme = binary:part(Original, 0, Size), + Pieces@1 = {uri, + {some, string:lowercase(Scheme)}, + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_authority_with_slashes(Uri_string, Pieces@1); + + <<"?"/utf8, Rest/binary>> when Size =:= 0 -> + parse_query_with_question_mark(Rest, Pieces); + + <<"?"/utf8, Rest@1/binary>> -> + Scheme@1 = binary:part(Original, 0, Size), + Pieces@2 = {uri, + {some, string:lowercase(Scheme@1)}, + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_query_with_question_mark(Rest@1, Pieces@2); + + <<"#"/utf8, Rest@2/binary>> when Size =:= 0 -> + parse_fragment(Rest@2, Pieces); + + <<"#"/utf8, Rest@3/binary>> -> + Scheme@2 = binary:part(Original, 0, Size), + Pieces@3 = {uri, + {some, string:lowercase(Scheme@2)}, + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_fragment(Rest@3, Pieces@3); + + <<":"/utf8, _/binary>> when Size =:= 0 -> + {error, nil}; + + <<":"/utf8, Rest@4/binary>> -> + Scheme@3 = binary:part(Original, 0, Size), + Pieces@4 = {uri, + {some, string:lowercase(Scheme@3)}, + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_authority_with_slashes(Rest@4, Pieces@4); + + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + Original, + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + _ -> + {_, Rest@5} = gleam_stdlib:string_pop_codeunit(Uri_string), + parse_scheme_loop(Original, Rest@5, Pieces, Size + 1) + end. + +-file("src/gleam/uri.gleam", 537). +?DOC( + " Parses an urlencoded query string into a list of key value pairs.\n" + " Returns an error for invalid encoding.\n" + "\n" + " The opposite operation is `uri.query_to_string`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " parse_query(\"a=1&b=2\")\n" + " // -> Ok([#(\"a\", \"1\"), #(\"b\", \"2\")])\n" + " ```\n" +). +-spec parse_query(binary()) -> {ok, list({binary(), binary()})} | {error, nil}. +parse_query(Query) -> + gleam_stdlib:parse_query(Query). + +-file("src/gleam/uri.gleam", 573). +?DOC( + " Encodes a string into a percent encoded representation.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " percent_encode(\"100% great\")\n" + " // -> \"100%25%20great\"\n" + " ```\n" +). +-spec percent_encode(binary()) -> binary(). +percent_encode(Value) -> + gleam_stdlib:percent_encode(Value). + +-file("src/gleam/uri.gleam", 558). +-spec query_pair({binary(), binary()}) -> gleam@string_tree:string_tree(). +query_pair(Pair) -> + gleam_stdlib:identity( + [gleam_stdlib:percent_encode(erlang:element(1, Pair)), + <<"="/utf8>>, + gleam_stdlib:percent_encode(erlang:element(2, Pair))] + ). + +-file("src/gleam/uri.gleam", 550). +?DOC( + " Encodes a list of key value pairs as a URI query string.\n" + "\n" + " The opposite operation is `uri.parse_query`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " query_to_string([#(\"a\", \"1\"), #(\"b\", \"2\")])\n" + " // -> \"a=1&b=2\"\n" + " ```\n" +). +-spec query_to_string(list({binary(), binary()})) -> binary(). +query_to_string(Query) -> + _pipe = Query, + _pipe@1 = gleam@list:map(_pipe, fun query_pair/1), + _pipe@2 = gleam@list:intersperse( + _pipe@1, + gleam_stdlib:identity(<<"&"/utf8>>) + ), + _pipe@3 = gleam_stdlib:identity(_pipe@2), + unicode:characters_to_binary(_pipe@3). + +-file("src/gleam/uri.gleam", 586). +?DOC( + " Decodes a percent encoded string.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " percent_decode(\"100%25%20great+fun\")\n" + " // -> Ok(\"100% great+fun\")\n" + " ```\n" +). +-spec percent_decode(binary()) -> {ok, binary()} | {error, nil}. +percent_decode(Value) -> + gleam_stdlib:percent_decode(Value). + +-file("src/gleam/uri.gleam", 608). +-spec remove_dot_segments_loop(list(binary()), list(binary())) -> list(binary()). +remove_dot_segments_loop(Input, Accumulator) -> + case Input of + [] -> + lists:reverse(Accumulator); + + [Segment | Rest] -> + Accumulator@5 = case {Segment, Accumulator} of + {<<""/utf8>>, Accumulator@1} -> + Accumulator@1; + + {<<"."/utf8>>, Accumulator@2} -> + Accumulator@2; + + {<<".."/utf8>>, []} -> + []; + + {<<".."/utf8>>, [_ | Accumulator@3]} -> + Accumulator@3; + + {Segment@1, Accumulator@4} -> + [Segment@1 | Accumulator@4] + end, + remove_dot_segments_loop(Rest, Accumulator@5) + end. + +-file("src/gleam/uri.gleam", 604). +-spec remove_dot_segments(list(binary())) -> list(binary()). +remove_dot_segments(Input) -> + remove_dot_segments_loop(Input, []). + +-file("src/gleam/uri.gleam", 600). +?DOC( + " Splits the path section of a URI into it's constituent segments.\n" + "\n" + " Removes empty segments and resolves dot-segments as specified in\n" + " [section 5.2](https://www.ietf.org/rfc/rfc3986.html#section-5.2) of the RFC.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " path_segments(\"/users/1\")\n" + " // -> [\"users\" ,\"1\"]\n" + " ```\n" +). +-spec path_segments(binary()) -> list(binary()). +path_segments(Path) -> + remove_dot_segments(gleam@string:split(Path, <<"/"/utf8>>)). + +-file("src/gleam/uri.gleam", 639). +?DOC( + " Encodes a `Uri` value as a URI string.\n" + "\n" + " The opposite operation is `uri.parse`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let uri = Uri(..empty, scheme: Some(\"https\"), host: Some(\"example.com\"))\n" + " to_string(uri)\n" + " // -> \"https://example.com\"\n" + " ```\n" +). +-spec to_string(uri()) -> binary(). +to_string(Uri) -> + Parts = case erlang:element(8, Uri) of + {some, Fragment} -> + [<<"#"/utf8>>, Fragment]; + + none -> + [] + end, + Parts@1 = case erlang:element(7, Uri) of + {some, Query} -> + [<<"?"/utf8>>, Query | Parts]; + + none -> + Parts + end, + Parts@2 = [erlang:element(6, Uri) | Parts@1], + Parts@3 = case {erlang:element(4, Uri), + gleam_stdlib:string_starts_with(erlang:element(6, Uri), <<"/"/utf8>>)} of + {{some, Host}, false} when Host =/= <<""/utf8>> -> + [<<"/"/utf8>> | Parts@2]; + + {_, _} -> + Parts@2 + end, + Parts@4 = case {erlang:element(4, Uri), erlang:element(5, Uri)} of + {{some, _}, {some, Port}} -> + [<<":"/utf8>>, erlang:integer_to_binary(Port) | Parts@3]; + + {_, _} -> + Parts@3 + end, + Parts@5 = case {erlang:element(2, Uri), + erlang:element(3, Uri), + erlang:element(4, Uri)} of + {{some, S}, {some, U}, {some, H}} -> + [S, <<"://"/utf8>>, U, <<"@"/utf8>>, H | Parts@4]; + + {{some, S@1}, none, {some, H@1}} -> + [S@1, <<"://"/utf8>>, H@1 | Parts@4]; + + {{some, S@2}, {some, _}, none} -> + [S@2, <<":"/utf8>> | Parts@4]; + + {{some, S@2}, none, none} -> + [S@2, <<":"/utf8>> | Parts@4]; + + {none, none, {some, H@2}} -> + [<<"//"/utf8>>, H@2 | Parts@4]; + + {_, _, _} -> + Parts@4 + end, + erlang:list_to_binary(Parts@5). + +-file("src/gleam/uri.gleam", 683). +?DOC( + " Fetches the origin of a URI.\n" + "\n" + " Returns the origin of a uri as defined in\n" + " [RFC 6454](https://tools.ietf.org/html/rfc6454)\n" + "\n" + " The supported URI schemes are `http` and `https`.\n" + " URLs without a scheme will return `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let assert Ok(uri) = parse(\"https://example.com/path?foo#bar\")\n" + " origin(uri)\n" + " // -> Ok(\"https://example.com\")\n" + " ```\n" +). +-spec origin(uri()) -> {ok, binary()} | {error, nil}. +origin(Uri) -> + {uri, Scheme, _, Host, Port, _, _, _} = Uri, + case {Host, Scheme} of + {{some, H}, {some, <<"https"/utf8>>}} when Port =:= {some, 443} -> + {ok, erlang:list_to_binary([<<"https://"/utf8>>, H])}; + + {{some, H@1}, {some, <<"http"/utf8>>}} when Port =:= {some, 80} -> + {ok, erlang:list_to_binary([<<"http://"/utf8>>, H@1])}; + + {{some, H@2}, {some, S}} when (S =:= <<"http"/utf8>>) orelse (S =:= <<"https"/utf8>>) -> + case Port of + {some, P} -> + {ok, + erlang:list_to_binary( + [S, + <<"://"/utf8>>, + H@2, + <<":"/utf8>>, + erlang:integer_to_binary(P)] + )}; + + none -> + {ok, erlang:list_to_binary([S, <<"://"/utf8>>, H@2])} + end; + + {_, _} -> + {error, nil} + end. + +-file("src/gleam/uri.gleam", 764). +-spec drop_last(list(DDO)) -> list(DDO). +drop_last(Elements) -> + gleam@list:take(Elements, erlang:length(Elements) - 1). + +-file("src/gleam/uri.gleam", 768). +-spec join_segments(list(binary())) -> binary(). +join_segments(Segments) -> + gleam@string:join([<<""/utf8>> | Segments], <<"/"/utf8>>). + +-file("src/gleam/uri.gleam", 706). +?DOC( + " Resolves a URI with respect to the given base URI.\n" + "\n" + " The base URI must be an absolute URI or this function will return an error.\n" + " The algorithm for merging uris is described in\n" + " [RFC 3986](https://tools.ietf.org/html/rfc3986#section-5.2).\n" +). +-spec merge(uri(), uri()) -> {ok, uri()} | {error, nil}. +merge(Base, Relative) -> + case Base of + {uri, {some, _}, _, {some, _}, _, _, _, _} -> + case Relative of + {uri, _, _, {some, _}, _, _, _, _} -> + Path = begin + _pipe = erlang:element(6, Relative), + _pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>), + _pipe@2 = remove_dot_segments(_pipe@1), + join_segments(_pipe@2) + end, + Resolved = {uri, + gleam@option:'or'( + erlang:element(2, Relative), + erlang:element(2, Base) + ), + none, + erlang:element(4, Relative), + gleam@option:'or'( + erlang:element(5, Relative), + erlang:element(5, Base) + ), + Path, + erlang:element(7, Relative), + erlang:element(8, Relative)}, + {ok, Resolved}; + + _ -> + {New_path, New_query} = case erlang:element(6, Relative) of + <<""/utf8>> -> + {erlang:element(6, Base), + gleam@option:'or'( + erlang:element(7, Relative), + erlang:element(7, Base) + )}; + + _ -> + Path_segments = case gleam_stdlib:string_starts_with( + erlang:element(6, Relative), + <<"/"/utf8>> + ) of + true -> + gleam@string:split( + erlang:element(6, Relative), + <<"/"/utf8>> + ); + + false -> + _pipe@3 = erlang:element(6, Base), + _pipe@4 = gleam@string:split( + _pipe@3, + <<"/"/utf8>> + ), + _pipe@5 = drop_last(_pipe@4), + lists:append( + _pipe@5, + gleam@string:split( + erlang:element(6, Relative), + <<"/"/utf8>> + ) + ) + end, + Path@1 = begin + _pipe@6 = Path_segments, + _pipe@7 = remove_dot_segments(_pipe@6), + join_segments(_pipe@7) + end, + {Path@1, erlang:element(7, Relative)} + end, + Resolved@1 = {uri, + erlang:element(2, Base), + none, + erlang:element(4, Base), + erlang:element(5, Base), + New_path, + New_query, + erlang:element(8, Relative)}, + {ok, Resolved@1} + end; + + _ -> + {error, nil} + end. + +-file("src/gleam/uri.gleam", 81). +?DOC( + " Parses a compliant URI string into the `Uri` Type.\n" + " If the string is not a valid URI string then an error is returned.\n" + "\n" + " The opposite operation is `uri.to_string`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " parse(\"https://example.com:1234/a/b?query=true#fragment\")\n" + " // -> Ok(\n" + " // Uri(\n" + " // scheme: Some(\"https\"),\n" + " // userinfo: None,\n" + " // host: Some(\"example.com\"),\n" + " // port: Some(1234),\n" + " // path: \"/a/b\",\n" + " // query: Some(\"query=true\"),\n" + " // fragment: Some(\"fragment\")\n" + " // )\n" + " // )\n" + " ```\n" +). +-spec parse(binary()) -> {ok, uri()} | {error, nil}. +parse(Uri_string) -> + gleam_stdlib:uri_parse(Uri_string). diff --git a/build/dev/javascript/gleam_stdlib/gleam_stdlib.erl b/build/dev/javascript/gleam_stdlib/gleam_stdlib.erl new file mode 100644 index 0000000..2c416f4 --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam_stdlib.erl @@ -0,0 +1,534 @@ +-module(gleam_stdlib). + +-export([ + map_get/2, iodata_append/2, identity/1, parse_int/1, parse_float/1, + less_than/2, string_pop_grapheme/1, string_pop_codeunit/1, + string_starts_with/2, wrap_list/1, string_ends_with/2, string_pad/4, + uri_parse/1, bit_array_slice/3, percent_encode/1, percent_decode/1, + base64_decode/1, parse_query/1, bit_array_concat/1, + base64_encode/2, tuple_get/2, classify_dynamic/1, print/1, + println/1, print_error/1, println_error/1, inspect/1, float_to_string/1, + int_from_base_string/2, utf_codepoint_list_to_string/1, contains_string/2, + crop_string/2, base16_encode/1, base16_decode/1, string_replace/3, slice/3, + bit_array_to_int_and_size/1, bit_array_pad_to_bytes/1, index/2, list/5, + dict/1, int/1, float/1, bit_array/1, is_null/1 +]). + +%% Taken from OTP's uri_string module +-define(DEC2HEX(X), + if ((X) >= 0) andalso ((X) =< 9) -> (X) + $0; + ((X) >= 10) andalso ((X) =< 15) -> (X) + $A - 10 + end). + +%% Taken from OTP's uri_string module +-define(HEX2DEC(X), + if ((X) >= $0) andalso ((X) =< $9) -> (X) - $0; + ((X) >= $A) andalso ((X) =< $F) -> (X) - $A + 10; + ((X) >= $a) andalso ((X) =< $f) -> (X) - $a + 10 + end). + +-define(is_lowercase_char(X), + (X > 96 andalso X < 123)). +-define(is_underscore_char(X), + (X == 95)). +-define(is_digit_char(X), + (X > 47 andalso X < 58)). +-define(is_ascii_character(X), + (erlang:is_integer(X) andalso X >= 32 andalso X =< 126)). + +uppercase(X) -> X - 32. + +map_get(Map, Key) -> + case maps:find(Key, Map) of + error -> {error, nil}; + OkFound -> OkFound + end. + +iodata_append(Iodata, String) -> [Iodata, String]. + +identity(X) -> X. + +classify_dynamic(nil) -> <<"Nil">>; +classify_dynamic(null) -> <<"Nil">>; +classify_dynamic(undefined) -> <<"Nil">>; +classify_dynamic(X) when is_boolean(X) -> <<"Bool">>; +classify_dynamic(X) when is_atom(X) -> <<"Atom">>; +classify_dynamic(X) when is_binary(X) -> <<"String">>; +classify_dynamic(X) when is_bitstring(X) -> <<"BitArray">>; +classify_dynamic(X) when is_integer(X) -> <<"Int">>; +classify_dynamic(X) when is_float(X) -> <<"Float">>; +classify_dynamic(X) when is_list(X) -> <<"List">>; +classify_dynamic(X) when is_map(X) -> <<"Dict">>; +classify_dynamic(X) when is_tuple(X) -> <<"Array">>; +classify_dynamic(X) when is_reference(X) -> <<"Reference">>; +classify_dynamic(X) when is_pid(X) -> <<"Pid">>; +classify_dynamic(X) when is_port(X) -> <<"Port">>; +classify_dynamic(X) when + is_function(X, 0) orelse is_function(X, 1) orelse is_function(X, 2) orelse + is_function(X, 3) orelse is_function(X, 4) orelse is_function(X, 5) orelse + is_function(X, 6) orelse is_function(X, 7) orelse is_function(X, 8) orelse + is_function(X, 9) orelse is_function(X, 10) orelse is_function(X, 11) orelse + is_function(X, 12) -> <<"Function">>; +classify_dynamic(_) -> <<"Unknown">>. + +tuple_get(_tup, Index) when Index < 0 -> {error, nil}; +tuple_get(Data, Index) when Index >= tuple_size(Data) -> {error, nil}; +tuple_get(Data, Index) -> {ok, element(Index + 1, Data)}. + +int_from_base_string(String, Base) -> + case catch binary_to_integer(String, Base) of + Int when is_integer(Int) -> {ok, Int}; + _ -> {error, nil} + end. + +parse_int(String) -> + case catch binary_to_integer(String) of + Int when is_integer(Int) -> {ok, Int}; + _ -> {error, nil} + end. + +parse_float(String) -> + case catch binary_to_float(String) of + Float when is_float(Float) -> {ok, Float}; + _ -> {error, nil} + end. + +less_than(Lhs, Rhs) -> + Lhs < Rhs. + +string_starts_with(_, <<>>) -> true; +string_starts_with(String, Prefix) when byte_size(Prefix) > byte_size(String) -> false; +string_starts_with(String, Prefix) -> + PrefixSize = byte_size(Prefix), + Prefix == binary_part(String, 0, PrefixSize). + +string_ends_with(_, <<>>) -> true; +string_ends_with(String, Suffix) when byte_size(Suffix) > byte_size(String) -> false; +string_ends_with(String, Suffix) -> + SuffixSize = byte_size(Suffix), + Suffix == binary_part(String, byte_size(String) - SuffixSize, SuffixSize). + +string_pad(String, Length, Dir, PadString) -> + Chars = string:pad(String, Length, Dir, binary_to_list(PadString)), + case unicode:characters_to_binary(Chars) of + Bin when is_binary(Bin) -> Bin; + Error -> erlang:error({gleam_error, {string_invalid_utf8, Error}}) + end. + +string_pop_grapheme(String) -> + case string:next_grapheme(String) of + [ Next | Rest ] when is_binary(Rest) -> + {ok, {unicode:characters_to_binary([Next]), Rest}}; + + [ Next | Rest ] -> + {ok, {unicode:characters_to_binary([Next]), unicode:characters_to_binary(Rest)}}; + + _ -> {error, nil} + end. + +string_pop_codeunit(<>) -> {Cp, Rest}; +string_pop_codeunit(Binary) -> {0, Binary}. + +bit_array_pad_to_bytes(Bin) -> + case erlang:bit_size(Bin) rem 8 of + 0 -> Bin; + TrailingBits -> + PaddingBits = 8 - TrailingBits, + <> + end. + +bit_array_concat(BitArrays) -> + list_to_bitstring(BitArrays). + +-if(?OTP_RELEASE >= 26). +base64_encode(Bin, Padding) -> + PaddedBin = bit_array_pad_to_bytes(Bin), + base64:encode(PaddedBin, #{padding => Padding}). +-else. +base64_encode(_Bin, _Padding) -> + erlang:error(<<"Erlang OTP/26 or higher is required to use base64:encode">>). +-endif. + +bit_array_slice(Bin, Pos, Len) -> + try {ok, binary:part(Bin, Pos, Len)} + catch error:badarg -> {error, nil} + end. + +base64_decode(S) -> + try {ok, base64:decode(S)} + catch error:_ -> {error, nil} + end. + +wrap_list(X) when is_list(X) -> X; +wrap_list(X) -> [X]. + +parse_query(Query) -> + case uri_string:dissect_query(Query) of + {error, _, _} -> {error, nil}; + Pairs -> + Pairs1 = lists:map(fun + ({K, true}) -> {K, <<"">>}; + (Pair) -> Pair + end, Pairs), + {ok, Pairs1} + end. + +percent_encode(B) -> percent_encode(B, <<>>). +percent_encode(<<>>, Acc) -> + Acc; +percent_encode(<>, Acc) -> + case percent_ok(H) of + true -> + percent_encode(T, <>); + false -> + <> = <>, + percent_encode(T, <>) + end. + +percent_decode(Cs) -> percent_decode(Cs, <<>>). +percent_decode(<<$%, C0, C1, Cs/binary>>, Acc) -> + case is_hex_digit(C0) andalso is_hex_digit(C1) of + true -> + B = ?HEX2DEC(C0)*16+?HEX2DEC(C1), + percent_decode(Cs, <>); + false -> + {error, nil} + end; +percent_decode(<>, Acc) -> + percent_decode(Cs, <>); +percent_decode(<<>>, Acc) -> + check_utf8(Acc). + +percent_ok($!) -> true; +percent_ok($$) -> true; +percent_ok($') -> true; +percent_ok($() -> true; +percent_ok($)) -> true; +percent_ok($*) -> true; +percent_ok($+) -> true; +percent_ok($-) -> true; +percent_ok($.) -> true; +percent_ok($_) -> true; +percent_ok($~) -> true; +percent_ok(C) when $0 =< C, C =< $9 -> true; +percent_ok(C) when $A =< C, C =< $Z -> true; +percent_ok(C) when $a =< C, C =< $z -> true; +percent_ok(_) -> false. + +is_hex_digit(C) -> + ($0 =< C andalso C =< $9) orelse ($a =< C andalso C =< $f) orelse ($A =< C andalso C =< $F). + +check_utf8(Cs) -> + case unicode:characters_to_list(Cs) of + {incomplete, _, _} -> {error, nil}; + {error, _, _} -> {error, nil}; + _ -> {ok, Cs} + end. + +uri_parse(String) -> + case uri_string:parse(String) of + {error, _, _} -> {error, nil}; + Uri -> + Port = + try maps:get(port, Uri) of + undefined -> none; + Value -> {some, Value} + catch _:_ -> none + end, + {ok, {uri, + maps_get_optional(Uri, scheme), + maps_get_optional(Uri, userinfo), + maps_get_optional(Uri, host), + Port, + maps_get_or(Uri, path, <<>>), + maps_get_optional(Uri, query), + maps_get_optional(Uri, fragment) + }} + end. + +maps_get_optional(Map, Key) -> + try {some, maps:get(Key, Map)} + catch _:_ -> none + end. + +maps_get_or(Map, Key, Default) -> + try maps:get(Key, Map) + catch _:_ -> Default + end. + +print(String) -> + io:put_chars(String), + nil. + +println(String) -> + io:put_chars([String, $\n]), + nil. + +print_error(String) -> + io:put_chars(standard_error, String), + nil. + +println_error(String) -> + io:put_chars(standard_error, [String, $\n]), + nil. + +inspect(true) -> + "True"; +inspect(false) -> + "False"; +inspect(nil) -> + "Nil"; +inspect(Data) when is_map(Data) -> + Fields = [ + [<<"#(">>, inspect(Key), <<", ">>, inspect(Value), <<")">>] + || {Key, Value} <- maps:to_list(Data) + ], + ["dict.from_list([", lists:join(", ", Fields), "])"]; +inspect(Atom) when is_atom(Atom) -> + erlang:element(2, inspect_atom(Atom)); +inspect(Any) when is_integer(Any) -> + erlang:integer_to_list(Any); +inspect(Any) when is_float(Any) -> + io_lib_format:fwrite_g(Any); +inspect(Binary) when is_binary(Binary) -> + case inspect_maybe_utf8_string(Binary, <<>>) of + {ok, InspectedUtf8String} -> InspectedUtf8String; + {error, not_a_utf8_string} -> + Segments = [erlang:integer_to_list(X) || <> <= Binary], + ["<<", lists:join(", ", Segments), ">>"] + end; +inspect(Bits) when is_bitstring(Bits) -> + inspect_bit_array(Bits); +inspect(List) when is_list(List) -> + case inspect_list(List, true) of + {charlist, _} -> ["charlist.from_string(\"", list_to_binary(List), "\")"]; + {proper, Elements} -> ["[", Elements, "]"]; + {improper, Elements} -> ["//erl([", Elements, "])"] + end; +inspect(Any) when is_tuple(Any) % Record constructors + andalso is_atom(element(1, Any)) + andalso element(1, Any) =/= false + andalso element(1, Any) =/= true + andalso element(1, Any) =/= nil +-> + [Atom | ArgsList] = erlang:tuple_to_list(Any), + InspectedArgs = lists:map(fun inspect/1, ArgsList), + case inspect_atom(Atom) of + {gleam_atom, GleamAtom} -> + Args = lists:join(<<", ">>, InspectedArgs), + [GleamAtom, "(", Args, ")"]; + {erlang_atom, ErlangAtom} -> + Args = lists:join(<<", ">>, [ErlangAtom | InspectedArgs]), + ["#(", Args, ")"] + end; +inspect(Tuple) when is_tuple(Tuple) -> + Elements = lists:map(fun inspect/1, erlang:tuple_to_list(Tuple)), + ["#(", lists:join(", ", Elements), ")"]; +inspect(Any) when is_function(Any) -> + {arity, Arity} = erlang:fun_info(Any, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(<<", ">>, + lists:map(fun(Arg) -> <> end, ArgsAsciiCodes) + ), + ["//fn(", Args, ") { ... }"]; +inspect(Any) -> + ["//erl(", io_lib:format("~p", [Any]), ")"]. + +inspect_atom(Atom) -> + Binary = erlang:atom_to_binary(Atom), + case inspect_maybe_gleam_atom(Binary, none, <<>>) of + {ok, Inspected} -> {gleam_atom, Inspected}; + {error, _} -> {erlang_atom, ["atom.create(\"", Binary, "\")"]} + end. + +inspect_maybe_gleam_atom(<<>>, none, _) -> + {error, nil}; +inspect_maybe_gleam_atom(<>, none, _) when ?is_digit_char(First) -> + {error, nil}; +inspect_maybe_gleam_atom(<<"_", _Rest/binary>>, none, _) -> + {error, nil}; +inspect_maybe_gleam_atom(<<"_">>, _PrevChar, _Acc) -> + {error, nil}; +inspect_maybe_gleam_atom(<<"_", _Rest/binary>>, $_, _Acc) -> + {error, nil}; +inspect_maybe_gleam_atom(<>, _PrevChar, _Acc) + when not (?is_lowercase_char(First) orelse ?is_underscore_char(First) orelse ?is_digit_char(First)) -> + {error, nil}; +inspect_maybe_gleam_atom(<>, none, Acc) -> + inspect_maybe_gleam_atom(Rest, First, <>); +inspect_maybe_gleam_atom(<<"_", Rest/binary>>, _PrevChar, Acc) -> + inspect_maybe_gleam_atom(Rest, $_, Acc); +inspect_maybe_gleam_atom(<>, $_, Acc) -> + inspect_maybe_gleam_atom(Rest, First, <>); +inspect_maybe_gleam_atom(<>, _PrevChar, Acc) -> + inspect_maybe_gleam_atom(Rest, First, <>); +inspect_maybe_gleam_atom(<<>>, _PrevChar, Acc) -> + {ok, Acc}; +inspect_maybe_gleam_atom(A, B, C) -> + erlang:display({A, B, C}), + throw({gleam_error, A, B, C}). + +inspect_list([], _) -> + {proper, []}; +inspect_list([First], true) when ?is_ascii_character(First) -> + {charlist, nil}; +inspect_list([First], _) -> + {proper, [inspect(First)]}; +inspect_list([First | Rest], ValidCharlist) when is_list(Rest) -> + StillValidCharlist = ValidCharlist andalso ?is_ascii_character(First), + {Kind, Inspected} = inspect_list(Rest, StillValidCharlist), + {Kind, [inspect(First), <<", ">> | Inspected]}; +inspect_list([First | ImproperTail], _) -> + {improper, [inspect(First), <<" | ">>, inspect(ImproperTail)]}. + +inspect_bit_array(Bits) -> + Text = inspect_bit_array(Bits, <<"<<">>), + <>">>. + +inspect_bit_array(<<>>, Acc) -> + Acc; +inspect_bit_array(<>, Acc) -> + inspect_bit_array(Rest, append_segment(Acc, erlang:integer_to_binary(X))); +inspect_bit_array(Rest, Acc) -> + Size = bit_size(Rest), + <> = Rest, + X1 = erlang:integer_to_binary(X), + Size1 = erlang:integer_to_binary(Size), + Segment = <>, + inspect_bit_array(<<>>, append_segment(Acc, Segment)). + +bit_array_to_int_and_size(A) -> + Size = bit_size(A), + <> = A, + {A1, Size}. + +append_segment(<<"<<">>, Segment) -> + <<"<<", Segment/binary>>; +append_segment(Acc, Segment) -> + <>. + + +inspect_maybe_utf8_string(Binary, Acc) -> + case Binary of + <<>> -> {ok, <<$", Acc/binary, $">>}; + <> -> + Escaped = case First of + $" -> <<$\\, $">>; + $\\ -> <<$\\, $\\>>; + $\r -> <<$\\, $r>>; + $\n -> <<$\\, $n>>; + $\t -> <<$\\, $t>>; + $\f -> <<$\\, $f>>; + X when X > 126, X < 160 -> convert_to_u(X); + X when X < 32 -> convert_to_u(X); + Other -> <> + end, + inspect_maybe_utf8_string(Rest, <>); + _ -> {error, not_a_utf8_string} + end. + +convert_to_u(Code) -> + list_to_binary(io_lib:format("\\u{~4.16.0B}", [Code])). + +float_to_string(Float) when is_float(Float) -> + erlang:iolist_to_binary(io_lib_format:fwrite_g(Float)). + +utf_codepoint_list_to_string(List) -> + case unicode:characters_to_binary(List) of + {error, _} -> erlang:error({gleam_error, {string_invalid_utf8, List}}); + Binary -> Binary + end. + +crop_string(String, Prefix) -> + case string:find(String, Prefix) of + nomatch -> String; + New -> New + end. + +contains_string(String, Substring) -> + is_bitstring(string:find(String, Substring)). + +base16_encode(Bin) -> + PaddedBin = bit_array_pad_to_bytes(Bin), + binary:encode_hex(PaddedBin). + +base16_decode(String) -> + try + {ok, binary:decode_hex(String)} + catch + _:_ -> {error, nil} + end. + +string_replace(String, Pattern, Replacement) -> + string:replace(String, Pattern, Replacement, all). + +slice(String, Index, Length) -> + case string:slice(String, Index, Length) of + X when is_binary(X) -> X; + X when is_list(X) -> unicode:characters_to_binary(X) + end. + +index([X | _], 0) -> + {ok, {some, X}}; +index([_, X | _], 1) -> + {ok, {some, X}}; +index([_, _, X | _], 2) -> + {ok, {some, X}}; +index([_, _, _, X | _], 3) -> + {ok, {some, X}}; +index([_, _, _, _, X | _], 4) -> + {ok, {some, X}}; +index([_, _, _, _, _, X | _], 5) -> + {ok, {some, X}}; +index([_, _, _, _, _, _, X | _], 6) -> + {ok, {some, X}}; +index([_, _, _, _, _, _, _, X | _], 7) -> + {ok, {some, X}}; +index(Tuple, Index) when is_tuple(Tuple) andalso is_integer(Index) -> + {ok, try + {some, element(Index + 1, Tuple)} + catch _:_ -> + none + end}; +index(Map, Key) when is_map(Map) -> + {ok, try + {some, maps:get(Key, Map)} + catch _:_ -> + none + end}; +index(_, Index) when is_integer(Index) -> + {error, <<"Indexable">>}; +index(_, _) -> + {error, <<"Dict">>}. + +list(T, A, B, C, D) when is_tuple(T) -> + list(tuple_to_list(T), A, B, C, D); +list([], _, _, _, Acc) -> + {lists:reverse(Acc), []}; +list([X | Xs], Decode, PushPath, Index, Acc) -> + {Out, Errors} = Decode(X), + case Errors of + [] -> list(Xs, Decode, PushPath, Index + 1, [Out | Acc]); + _ -> PushPath({[], Errors}, integer_to_binary(Index)) + end; +list(Unexpected, _, _, _, []) -> + Found = gleam@dynamic:classify(Unexpected), + Error = {decode_error, <<"List"/utf8>>, Found, []}, + {[], [Error]}; +list(_, _, _, _, Acc) -> + {lists:reverse(Acc), []}. + +dict(#{} = Data) -> {ok, Data}; +dict(_) -> {error, nil}. + +int(I) when is_integer(I) -> {ok, I}; +int(_) -> {error, 0}. + +float(F) when is_float(F) -> {ok, F}; +float(_) -> {error, 0.0}. + +bit_array(B) when is_bitstring(B) -> {ok, B}; +bit_array(_) -> {error, <<>>}. + +is_null(X) -> + X =:= undefined orelse X =:= null orelse X =:= nil. diff --git a/build/dev/javascript/gleam_stdlib/gleam_stdlib.mjs b/build/dev/javascript/gleam_stdlib/gleam_stdlib.mjs new file mode 100644 index 0000000..ebac45f --- /dev/null +++ b/build/dev/javascript/gleam_stdlib/gleam_stdlib.mjs @@ -0,0 +1,1048 @@ +import { + BitArray, + Error, + List, + Ok, + Result, + UtfCodepoint, + stringBits, + toBitArray, + bitArraySlice, + NonEmpty, + Empty, + CustomType, +} from "./gleam.mjs"; +import { Some, None } from "./gleam/option.mjs"; +import Dict from "./dict.mjs"; +import { classify } from "./gleam/dynamic.mjs"; +import { DecodeError } from "./gleam/dynamic/decode.mjs"; + +const Nil = undefined; +const NOT_FOUND = {}; + +export function identity(x) { + return x; +} + +export function parse_int(value) { + if (/^[-+]?(\d+)$/.test(value)) { + return new Ok(parseInt(value)); + } else { + return new Error(Nil); + } +} + +export function parse_float(value) { + if (/^[-+]?(\d+)\.(\d+)([eE][-+]?\d+)?$/.test(value)) { + return new Ok(parseFloat(value)); + } else { + return new Error(Nil); + } +} + +export function to_string(term) { + return term.toString(); +} + +export function int_to_base_string(int, base) { + return int.toString(base).toUpperCase(); +} + +const int_base_patterns = { + 2: /[^0-1]/, + 3: /[^0-2]/, + 4: /[^0-3]/, + 5: /[^0-4]/, + 6: /[^0-5]/, + 7: /[^0-6]/, + 8: /[^0-7]/, + 9: /[^0-8]/, + 10: /[^0-9]/, + 11: /[^0-9a]/, + 12: /[^0-9a-b]/, + 13: /[^0-9a-c]/, + 14: /[^0-9a-d]/, + 15: /[^0-9a-e]/, + 16: /[^0-9a-f]/, + 17: /[^0-9a-g]/, + 18: /[^0-9a-h]/, + 19: /[^0-9a-i]/, + 20: /[^0-9a-j]/, + 21: /[^0-9a-k]/, + 22: /[^0-9a-l]/, + 23: /[^0-9a-m]/, + 24: /[^0-9a-n]/, + 25: /[^0-9a-o]/, + 26: /[^0-9a-p]/, + 27: /[^0-9a-q]/, + 28: /[^0-9a-r]/, + 29: /[^0-9a-s]/, + 30: /[^0-9a-t]/, + 31: /[^0-9a-u]/, + 32: /[^0-9a-v]/, + 33: /[^0-9a-w]/, + 34: /[^0-9a-x]/, + 35: /[^0-9a-y]/, + 36: /[^0-9a-z]/, +}; + +export function int_from_base_string(string, base) { + if (int_base_patterns[base].test(string.replace(/^-/, "").toLowerCase())) { + return new Error(Nil); + } + + const result = parseInt(string, base); + + if (isNaN(result)) { + return new Error(Nil); + } + + return new Ok(result); +} + +export function string_replace(string, target, substitute) { + return string.replaceAll(target, substitute); +} + +export function string_reverse(string) { + return [...string].reverse().join(""); +} + +export function string_length(string) { + if (string === "") { + return 0; + } + const iterator = graphemes_iterator(string); + if (iterator) { + let i = 0; + for (const _ of iterator) { + i++; + } + return i; + } else { + return string.match(/./gsu).length; + } +} + +export function graphemes(string) { + const iterator = graphemes_iterator(string); + if (iterator) { + return List.fromArray(Array.from(iterator).map((item) => item.segment)); + } else { + return List.fromArray(string.match(/./gsu)); + } +} + +let segmenter = undefined; + +function graphemes_iterator(string) { + if (globalThis.Intl && Intl.Segmenter) { + segmenter ||= new Intl.Segmenter(); + return segmenter.segment(string)[Symbol.iterator](); + } +} + +export function pop_grapheme(string) { + let first; + const iterator = graphemes_iterator(string); + if (iterator) { + first = iterator.next().value?.segment; + } else { + first = string.match(/./su)?.[0]; + } + if (first) { + return new Ok([first, string.slice(first.length)]); + } else { + return new Error(Nil); + } +} + +export function pop_codeunit(str) { + return [str.charCodeAt(0) | 0, str.slice(1)]; +} + +export function lowercase(string) { + return string.toLowerCase(); +} + +export function uppercase(string) { + return string.toUpperCase(); +} + +export function less_than(a, b) { + return a < b; +} + +export function add(a, b) { + return a + b; +} + +export function split(xs, pattern) { + return List.fromArray(xs.split(pattern)); +} + +export function concat(xs) { + let result = ""; + for (const x of xs) { + result = result + x; + } + return result; +} + +export function length(data) { + return data.length; +} + +export function string_byte_slice(string, index, length) { + return string.slice(index, index + length); +} + +export function string_grapheme_slice(string, idx, len) { + if (len <= 0 || idx >= string.length) { + return ""; + } + + const iterator = graphemes_iterator(string); + if (iterator) { + while (idx-- > 0) { + iterator.next(); + } + + let result = ""; + + while (len-- > 0) { + const v = iterator.next().value; + if (v === undefined) { + break; + } + + result += v.segment; + } + + return result; + } else { + return string + .match(/./gsu) + .slice(idx, idx + len) + .join(""); + } +} + +export function string_codeunit_slice(str, from, length) { + return str.slice(from, from + length); +} +export function crop_string(string, substring) { + return string.substring(string.indexOf(substring)); +} + +export function contains_string(haystack, needle) { + return haystack.indexOf(needle) >= 0; +} + +export function starts_with(haystack, needle) { + return haystack.startsWith(needle); +} + +export function ends_with(haystack, needle) { + return haystack.endsWith(needle); +} + +export function split_once(haystack, needle) { + const index = haystack.indexOf(needle); + if (index >= 0) { + const before = haystack.slice(0, index); + const after = haystack.slice(index + needle.length); + return new Ok([before, after]); + } else { + return new Error(Nil); + } +} + +const unicode_whitespaces = [ + "\u0020", // Space + "\u0009", // Horizontal tab + "\u000A", // Line feed + "\u000B", // Vertical tab + "\u000C", // Form feed + "\u000D", // Carriage return + "\u0085", // Next line + "\u2028", // Line separator + "\u2029", // Paragraph separator +].join(""); + +const trim_start_regex = /* @__PURE__ */ new RegExp( + `^[${unicode_whitespaces}]*`, +); +const trim_end_regex = /* @__PURE__ */ new RegExp(`[${unicode_whitespaces}]*$`); + +export function trim_start(string) { + return string.replace(trim_start_regex, ""); +} + +export function trim_end(string) { + return string.replace(trim_end_regex, ""); +} + +export function bit_array_from_string(string) { + return toBitArray([stringBits(string)]); +} + +export function bit_array_bit_size(bit_array) { + return bit_array.bitSize; +} + +export function bit_array_byte_size(bit_array) { + return bit_array.byteSize; +} + +export function bit_array_pad_to_bytes(bit_array) { + const trailingBitsCount = bit_array.bitSize % 8; + + // If the bit array is a whole number of bytes it can be returned unchanged + if (trailingBitsCount === 0) { + return bit_array; + } + + const finalByte = bit_array.byteAt(bit_array.byteSize - 1); + + // The required final byte has its unused trailing bits set to zero + const unusedBitsCount = 8 - trailingBitsCount; + const correctFinalByte = (finalByte >> unusedBitsCount) << unusedBitsCount; + + // If the unused bits in the final byte are already set to zero then the + // existing buffer can be re-used, avoiding a copy + if (finalByte === correctFinalByte) { + return new BitArray( + bit_array.rawBuffer, + bit_array.byteSize * 8, + bit_array.bitOffset, + ); + } + + // Copy the bit array into a new aligned buffer and set the correct final byte + const buffer = new Uint8Array(bit_array.byteSize); + for (let i = 0; i < buffer.length - 1; i++) { + buffer[i] = bit_array.byteAt(i); + } + buffer[buffer.length - 1] = correctFinalByte; + + return new BitArray(buffer); +} + +export function bit_array_concat(bit_arrays) { + return toBitArray(bit_arrays.toArray()); +} + +export function console_log(term) { + console.log(term); +} + +export function console_error(term) { + console.error(term); +} + +export function crash(message) { + throw new globalThis.Error(message); +} + +export function bit_array_to_string(bit_array) { + // If the bit array isn't a whole number of bytes then return an error + if (bit_array.bitSize % 8 !== 0) { + return new Error(Nil); + } + + try { + const decoder = new TextDecoder("utf-8", { fatal: true }); + + if (bit_array.bitOffset === 0) { + return new Ok(decoder.decode(bit_array.rawBuffer)); + } else { + // The input data isn't aligned, so copy it into a new aligned buffer so + // that TextDecoder can be used + const buffer = new Uint8Array(bit_array.byteSize); + for (let i = 0; i < buffer.length; i++) { + buffer[i] = bit_array.byteAt(i); + } + return new Ok(decoder.decode(buffer)); + } + } catch { + return new Error(Nil); + } +} + +export function print(string) { + if (typeof process === "object" && process.stdout?.write) { + process.stdout.write(string); // We can write without a trailing newline + } else if (typeof Deno === "object") { + Deno.stdout.writeSync(new TextEncoder().encode(string)); // We can write without a trailing newline + } else { + console.log(string); // We're in a browser. Newlines are mandated + } +} + +export function print_error(string) { + if (typeof process === "object" && process.stderr?.write) { + process.stderr.write(string); // We can write without a trailing newline + } else if (typeof Deno === "object") { + Deno.stderr.writeSync(new TextEncoder().encode(string)); // We can write without a trailing newline + } else { + console.error(string); // We're in a browser. Newlines are mandated + } +} + +export function print_debug(string) { + if (typeof process === "object" && process.stderr?.write) { + process.stderr.write(string + "\n"); // If we're in Node.js, use `stderr` + } else if (typeof Deno === "object") { + Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); // If we're in Deno, use `stderr` + } else { + console.log(string); // Otherwise, use `console.log` (so that it doesn't look like an error) + } +} + +export function ceiling(float) { + return Math.ceil(float); +} + +export function floor(float) { + return Math.floor(float); +} + +export function round(float) { + return Math.round(float); +} + +export function truncate(float) { + return Math.trunc(float); +} + +export function power(base, exponent) { + // It is checked in Gleam that: + // - The base is non-negative and that the exponent is not fractional. + // - The base is non-zero and the exponent is non-negative (otherwise + // the result will essentially be division by zero). + // It can thus be assumed that valid input is passed to the Math.pow + // function and a NaN or Infinity value will not be produced. + return Math.pow(base, exponent); +} + +export function random_uniform() { + const random_uniform_result = Math.random(); + // With round-to-nearest-even behavior, the ranges claimed for the functions below + // (excluding the one for Math.random() itself) aren't exact. + // If extremely large bounds are chosen (2^53 or higher), + // it's possible in extremely rare cases to calculate the usually-excluded upper bound. + // Note that as numbers in JavaScript are IEEE 754 floating point numbers + // See: + // Because of this, we just loop 'until' we get a valid result where 0.0 <= x < 1.0: + if (random_uniform_result === 1.0) { + return random_uniform(); + } + return random_uniform_result; +} + +export function bit_array_slice(bits, position, length) { + const start = Math.min(position, position + length); + const end = Math.max(position, position + length); + + if (start < 0 || end * 8 > bits.bitSize) { + return new Error(Nil); + } + + return new Ok(bitArraySlice(bits, start * 8, end * 8)); +} + +export function codepoint(int) { + return new UtfCodepoint(int); +} + +export function string_to_codepoint_integer_list(string) { + return List.fromArray(Array.from(string).map((item) => item.codePointAt(0))); +} + +export function utf_codepoint_list_to_string(utf_codepoint_integer_list) { + return utf_codepoint_integer_list + .toArray() + .map((x) => String.fromCodePoint(x.value)) + .join(""); +} + +export function utf_codepoint_to_int(utf_codepoint) { + return utf_codepoint.value; +} + +export function new_map() { + return Dict.new(); +} + +export function map_size(map) { + return map.size; +} + +export function map_to_list(map) { + return List.fromArray(map.entries()); +} + +export function map_remove(key, map) { + return map.delete(key); +} + +export function map_get(map, key) { + const value = map.get(key, NOT_FOUND); + if (value === NOT_FOUND) { + return new Error(Nil); + } + return new Ok(value); +} + +export function map_insert(key, value, map) { + return map.set(key, value); +} + +function unsafe_percent_decode(string) { + return decodeURIComponent(string || ""); +} + +function unsafe_percent_decode_query(string) { + return decodeURIComponent((string || "").replace("+", " ")); +} + +export function percent_decode(string) { + try { + return new Ok(unsafe_percent_decode(string)); + } catch { + return new Error(Nil); + } +} + +export function percent_encode(string) { + return encodeURIComponent(string).replace("%2B", "+"); +} + +export function parse_query(query) { + try { + const pairs = []; + for (const section of query.split("&")) { + const [key, value] = section.split("="); + if (!key) continue; + + const decodedKey = unsafe_percent_decode_query(key); + const decodedValue = unsafe_percent_decode_query(value); + pairs.push([decodedKey, decodedValue]); + } + return new Ok(List.fromArray(pairs)); + } catch { + return new Error(Nil); + } +} + +const b64EncodeLookup = [ + 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, + 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, + 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, + 122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 43, 47, +]; + +let b64TextDecoder; + +// Implementation based on https://github.com/mitschabaude/fast-base64/blob/main/js.js +export function base64_encode(bit_array, padding) { + b64TextDecoder ??= new TextDecoder(); + + bit_array = bit_array_pad_to_bytes(bit_array); + + const m = bit_array.byteSize; + const k = m % 3; + const n = Math.floor(m / 3) * 4 + (k && k + 1); + const N = Math.ceil(m / 3) * 4; + const encoded = new Uint8Array(N); + + for (let i = 0, j = 0; j < m; i += 4, j += 3) { + const y = + (bit_array.byteAt(j) << 16) + + (bit_array.byteAt(j + 1) << 8) + + (bit_array.byteAt(j + 2) | 0); + + encoded[i] = b64EncodeLookup[y >> 18]; + encoded[i + 1] = b64EncodeLookup[(y >> 12) & 0x3f]; + encoded[i + 2] = b64EncodeLookup[(y >> 6) & 0x3f]; + encoded[i + 3] = b64EncodeLookup[y & 0x3f]; + } + + let base64 = b64TextDecoder.decode(new Uint8Array(encoded.buffer, 0, n)); + + if (padding) { + if (k === 1) { + base64 += "=="; + } else if (k === 2) { + base64 += "="; + } + } + + return base64; +} + +// From https://developer.mozilla.org/en-US/docs/Glossary/Base64 +export function base64_decode(sBase64) { + try { + const binString = atob(sBase64); + const length = binString.length; + const array = new Uint8Array(length); + for (let i = 0; i < length; i++) { + array[i] = binString.charCodeAt(i); + } + return new Ok(new BitArray(array)); + } catch { + return new Error(Nil); + } +} + +export function classify_dynamic(data) { + if (typeof data === "string") { + return "String"; + } else if (typeof data === "boolean") { + return "Bool"; + } else if (data instanceof Result) { + return "Result"; + } else if (data instanceof List) { + return "List"; + } else if (data instanceof BitArray) { + return "BitArray"; + } else if (data instanceof Dict) { + return "Dict"; + } else if (Number.isInteger(data)) { + return "Int"; + } else if (Array.isArray(data)) { + return `Array`; + } else if (typeof data === "number") { + return "Float"; + } else if (data === null) { + return "Nil"; + } else if (data === undefined) { + return "Nil"; + } else { + const type = typeof data; + return type.charAt(0).toUpperCase() + type.slice(1); + } +} + +export function byte_size(string) { + return new TextEncoder().encode(string).length; +} + +// In JavaScript bitwise operations convert numbers to a sequence of 32 bits +// while Erlang uses arbitrary precision. +// To get around this problem and get consistent results use BigInt and then +// downcast the value back to a Number value. + +export function bitwise_and(x, y) { + return Number(BigInt(x) & BigInt(y)); +} + +export function bitwise_not(x) { + return Number(~BigInt(x)); +} + +export function bitwise_or(x, y) { + return Number(BigInt(x) | BigInt(y)); +} + +export function bitwise_exclusive_or(x, y) { + return Number(BigInt(x) ^ BigInt(y)); +} + +export function bitwise_shift_left(x, y) { + return Number(BigInt(x) << BigInt(y)); +} + +export function bitwise_shift_right(x, y) { + return Number(BigInt(x) >> BigInt(y)); +} + +export function inspect(v) { + return new Inspector().inspect(v); +} + +export function float_to_string(float) { + const string = float.toString().replace("+", ""); + if (string.indexOf(".") >= 0) { + return string; + } else { + const index = string.indexOf("e"); + if (index >= 0) { + return string.slice(0, index) + ".0" + string.slice(index); + } else { + return string + ".0"; + } + } +} + +class Inspector { + #references = new Set(); + + inspect(v) { + const t = typeof v; + if (v === true) return "True"; + if (v === false) return "False"; + if (v === null) return "//js(null)"; + if (v === undefined) return "Nil"; + if (t === "string") return this.#string(v); + if (t === "bigint" || Number.isInteger(v)) return v.toString(); + if (t === "number") return float_to_string(v); + if (v instanceof UtfCodepoint) return this.#utfCodepoint(v); + if (v instanceof BitArray) return this.#bit_array(v); + if (v instanceof RegExp) return `//js(${v})`; + if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; + if (v instanceof globalThis.Error) return `//js(${v.toString()})`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) + args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; + } + + if (this.#references.size === this.#references.add(v).size) { + return "//js(circular reference)"; + } + + let printed; + if (Array.isArray(v)) { + printed = `#(${v.map((v) => this.inspect(v)).join(", ")})`; + } else if (v instanceof List) { + printed = this.#list(v); + } else if (v instanceof CustomType) { + printed = this.#customType(v); + } else if (v instanceof Dict) { + printed = this.#dict(v); + } else if (v instanceof Set) { + return `//js(Set(${[...v].map((v) => this.inspect(v)).join(", ")}))`; + } else { + printed = this.#object(v); + } + this.#references.delete(v); + return printed; + } + + #object(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${this.inspect(k)}: ${this.inspect(v[k])}`); + } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; + } + + #dict(map) { + let body = "dict.from_list(["; + let first = true; + map.forEach((value, key) => { + if (!first) body = body + ", "; + body = body + "#(" + this.inspect(key) + ", " + this.inspect(value) + ")"; + first = false; + }); + return body + "])"; + } + + #customType(record) { + const props = Object.keys(record) + .map((label) => { + const value = this.inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }) + .join(", "); + return props + ? `${record.constructor.name}(${props})` + : record.constructor.name; + } + + #list(list) { + if (list instanceof Empty) { + return "[]"; + } + + let char_out = 'charlist.from_string("'; + let list_out = "["; + + let current = list; + while (current instanceof NonEmpty) { + let element = current.head; + current = current.tail; + + if (list_out !== "[") { + list_out += ", "; + } + list_out += this.inspect(element); + + if (char_out) { + if (Number.isInteger(element) && element >= 32 && element <= 126) { + char_out += String.fromCharCode(element); + } else { + char_out = null; + } + } + } + + if (char_out) { + return char_out + '")'; + } else { + return list_out + "]"; + } + } + + #string(str) { + let new_str = '"'; + for (let i = 0; i < str.length; i++) { + const char = str[i]; + switch (char) { + case "\n": + new_str += "\\n"; + break; + case "\r": + new_str += "\\r"; + break; + case "\t": + new_str += "\\t"; + break; + case "\f": + new_str += "\\f"; + break; + case "\\": + new_str += "\\\\"; + break; + case '"': + new_str += '\\"'; + break; + default: + if (char < " " || (char > "~" && char < "\u{00A0}")) { + new_str += + "\\u{" + + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + + "}"; + } else { + new_str += char; + } + } + } + new_str += '"'; + return new_str; + } + + #utfCodepoint(codepoint) { + return `//utfcodepoint(${String.fromCodePoint(codepoint.value)})`; + } + + #bit_array(bits) { + if (bits.bitSize === 0) { + return "<<>>"; + } + + let acc = "<<"; + + for (let i = 0; i < bits.byteSize - 1; i++) { + acc += bits.byteAt(i).toString(); + acc += ", "; + } + + if (bits.byteSize * 8 === bits.bitSize) { + acc += bits.byteAt(bits.byteSize - 1).toString(); + } else { + const trailingBitsCount = bits.bitSize % 8; + acc += bits.byteAt(bits.byteSize - 1) >> (8 - trailingBitsCount); + acc += `:size(${trailingBitsCount})`; + } + + acc += ">>"; + return acc; + } +} + +export function base16_encode(bit_array) { + const trailingBitsCount = bit_array.bitSize % 8; + + let result = ""; + + for (let i = 0; i < bit_array.byteSize; i++) { + let byte = bit_array.byteAt(i); + + if (i === bit_array.byteSize - 1 && trailingBitsCount !== 0) { + const unusedBitsCount = 8 - trailingBitsCount; + byte = (byte >> unusedBitsCount) << unusedBitsCount; + } + + result += byte.toString(16).padStart(2, "0").toUpperCase(); + } + + return result; +} + +export function base16_decode(string) { + const bytes = new Uint8Array(string.length / 2); + for (let i = 0; i < string.length; i += 2) { + const a = parseInt(string[i], 16); + const b = parseInt(string[i + 1], 16); + if (isNaN(a) || isNaN(b)) return new Error(Nil); + bytes[i / 2] = a * 16 + b; + } + return new Ok(new BitArray(bytes)); +} + +export function bit_array_to_int_and_size(bits) { + const trailingBitsCount = bits.bitSize % 8; + const unusedBitsCount = trailingBitsCount === 0 ? 0 : 8 - trailingBitsCount; + + return [bits.byteAt(0) >> unusedBitsCount, bits.bitSize]; +} + +export function bit_array_starts_with(bits, prefix) { + if (prefix.bitSize > bits.bitSize) { + return false; + } + + // Check any whole bytes + const byteCount = Math.trunc(prefix.bitSize / 8); + for (let i = 0; i < byteCount; i++) { + if (bits.byteAt(i) !== prefix.byteAt(i)) { + return false; + } + } + + // Check any trailing bits at the end of the prefix + if (prefix.bitSize % 8 !== 0) { + const unusedBitsCount = 8 - (prefix.bitSize % 8); + if ( + bits.byteAt(byteCount) >> unusedBitsCount !== + prefix.byteAt(byteCount) >> unusedBitsCount + ) { + return false; + } + } + + return true; +} + +export function log(x) { + // It is checked in Gleam that: + // - The input is strictly positive (x > 0) + // - This ensures that Math.log will never return NaN or -Infinity + // The function can thus safely pass the input to Math.log + // and a valid finite float will always be produced. + return Math.log(x); +} + +export function exp(x) { + return Math.exp(x); +} + +export function list_to_array(list) { + let current = list; + let array = []; + while (current instanceof NonEmpty) { + array.push(current.head); + current = current.tail; + } + return array; +} + +export function index(data, key) { + // Dictionaries and dictionary-like objects can be indexed + if (data instanceof Dict || data instanceof WeakMap || data instanceof Map) { + const token = {}; + const entry = data.get(key, token); + if (entry === token) return new Ok(new None()); + return new Ok(new Some(entry)); + } + + const key_is_int = Number.isInteger(key); + + // Only elements 0-7 of lists can be indexed, negative indices are not allowed + if (key_is_int && key >= 0 && key < 8 && data instanceof List) { + let i = 0; + for (const value of data) { + if (i === key) return new Ok(new Some(value)); + i++; + } + return new Error("Indexable"); + } + + // Arrays and objects can be indexed + if ( + (key_is_int && Array.isArray(data)) || + (data && typeof data === "object") || + (data && Object.getPrototypeOf(data) === Object.prototype) + ) { + if (key in data) return new Ok(new Some(data[key])); + return new Ok(new None()); + } + + return new Error(key_is_int ? "Indexable" : "Dict"); +} + +export function list(data, decode, pushPath, index, emptyList) { + if (!(data instanceof List || Array.isArray(data))) { + const error = new DecodeError("List", classify(data), emptyList); + return [emptyList, List.fromArray([error])]; + } + + const decoded = []; + + for (const element of data) { + const layer = decode(element); + const [out, errors] = layer; + + if (errors instanceof NonEmpty) { + const [_, errors] = pushPath(layer, index.toString()); + return [emptyList, errors]; + } + decoded.push(out); + index++; + } + + return [List.fromArray(decoded), emptyList]; +} + +export function dict(data) { + if (data instanceof Dict) { + return new Ok(data); + } + if (data instanceof Map || data instanceof WeakMap) { + return new Ok(Dict.fromMap(data)); + } + if (data == null) { + return new Error("Dict"); + } + if (typeof data !== "object") { + return new Error("Dict"); + } + const proto = Object.getPrototypeOf(data); + if (proto === Object.prototype || proto === null) { + return new Ok(Dict.fromObject(data)); + } + return new Error("Dict"); +} + +export function bit_array(data) { + if (data instanceof BitArray) return new Ok(data); + if (data instanceof Uint8Array) return new Ok(new BitArray(data)); + return new Error(new BitArray(new Uint8Array())); +} + +export function float(data) { + if (typeof data === "number") return new Ok(data); + return new Error(0.0); +} + +export function int(data) { + if (Number.isInteger(data)) return new Ok(data); + return new Error(0); +} + +export function string(data) { + if (typeof data === "string") return new Ok(data); + return new Error(""); +} + +export function is_null(data) { + return data === null || data === undefined; +} diff --git a/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@calendar.cache b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@calendar.cache new file mode 100644 index 0000000..b3da925 Binary files /dev/null and b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@calendar.cache differ diff --git a/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@calendar.cache_inline b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@calendar.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@calendar.cache_inline differ diff --git a/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@calendar.cache_meta b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@calendar.cache_meta new file mode 100644 index 0000000..e65ddb6 Binary files /dev/null and b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@calendar.cache_meta differ diff --git a/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@duration.cache b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@duration.cache new file mode 100644 index 0000000..6600435 Binary files /dev/null and b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@duration.cache differ diff --git a/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@duration.cache_inline b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@duration.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@duration.cache_inline differ diff --git a/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@duration.cache_meta b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@duration.cache_meta new file mode 100644 index 0000000..63b7b08 Binary files /dev/null and b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@duration.cache_meta differ diff --git a/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@timestamp.cache b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@timestamp.cache new file mode 100644 index 0000000..cbc5fca Binary files /dev/null and b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@timestamp.cache differ diff --git a/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@timestamp.cache_inline b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@timestamp.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@timestamp.cache_inline differ diff --git a/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@timestamp.cache_meta b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@timestamp.cache_meta new file mode 100644 index 0000000..9212c72 Binary files /dev/null and b/build/dev/javascript/gleam_time/_gleam_artefacts/gleam@time@timestamp.cache_meta differ diff --git a/build/dev/javascript/gleam_time/gleam.mjs b/build/dev/javascript/gleam_time/gleam.mjs new file mode 100644 index 0000000..197cbbc --- /dev/null +++ b/build/dev/javascript/gleam_time/gleam.mjs @@ -0,0 +1 @@ +export * from "../prelude.mjs"; diff --git a/build/dev/javascript/gleam_time/gleam/time/calendar.mjs b/build/dev/javascript/gleam_time/gleam/time/calendar.mjs new file mode 100644 index 0000000..65048b8 --- /dev/null +++ b/build/dev/javascript/gleam_time/gleam/time/calendar.mjs @@ -0,0 +1,391 @@ +import * as $int from "../../../gleam_stdlib/gleam/int.mjs"; +import * as $order from "../../../gleam_stdlib/gleam/order.mjs"; +import { Ok, Error, CustomType as $CustomType } from "../../gleam.mjs"; +import * as $duration from "../../gleam/time/duration.mjs"; +import { local_time_offset_seconds } from "../../gleam_time_ffi.mjs"; + +export class Date extends $CustomType { + constructor(year, month, day) { + super(); + this.year = year; + this.month = month; + this.day = day; + } +} +export const Date$Date = (year, month, day) => new Date(year, month, day); +export const Date$isDate = (value) => value instanceof Date; +export const Date$Date$year = (value) => value.year; +export const Date$Date$0 = (value) => value.year; +export const Date$Date$month = (value) => value.month; +export const Date$Date$1 = (value) => value.month; +export const Date$Date$day = (value) => value.day; +export const Date$Date$2 = (value) => value.day; + +export class TimeOfDay extends $CustomType { + constructor(hours, minutes, seconds, nanoseconds) { + super(); + this.hours = hours; + this.minutes = minutes; + this.seconds = seconds; + this.nanoseconds = nanoseconds; + } +} +export const TimeOfDay$TimeOfDay = (hours, minutes, seconds, nanoseconds) => + new TimeOfDay(hours, minutes, seconds, nanoseconds); +export const TimeOfDay$isTimeOfDay = (value) => value instanceof TimeOfDay; +export const TimeOfDay$TimeOfDay$hours = (value) => value.hours; +export const TimeOfDay$TimeOfDay$0 = (value) => value.hours; +export const TimeOfDay$TimeOfDay$minutes = (value) => value.minutes; +export const TimeOfDay$TimeOfDay$1 = (value) => value.minutes; +export const TimeOfDay$TimeOfDay$seconds = (value) => value.seconds; +export const TimeOfDay$TimeOfDay$2 = (value) => value.seconds; +export const TimeOfDay$TimeOfDay$nanoseconds = (value) => value.nanoseconds; +export const TimeOfDay$TimeOfDay$3 = (value) => value.nanoseconds; + +export class January extends $CustomType {} +export const Month$January = () => new January(); +export const Month$isJanuary = (value) => value instanceof January; + +export class February extends $CustomType {} +export const Month$February = () => new February(); +export const Month$isFebruary = (value) => value instanceof February; + +export class March extends $CustomType {} +export const Month$March = () => new March(); +export const Month$isMarch = (value) => value instanceof March; + +export class April extends $CustomType {} +export const Month$April = () => new April(); +export const Month$isApril = (value) => value instanceof April; + +export class May extends $CustomType {} +export const Month$May = () => new May(); +export const Month$isMay = (value) => value instanceof May; + +export class June extends $CustomType {} +export const Month$June = () => new June(); +export const Month$isJune = (value) => value instanceof June; + +export class July extends $CustomType {} +export const Month$July = () => new July(); +export const Month$isJuly = (value) => value instanceof July; + +export class August extends $CustomType {} +export const Month$August = () => new August(); +export const Month$isAugust = (value) => value instanceof August; + +export class September extends $CustomType {} +export const Month$September = () => new September(); +export const Month$isSeptember = (value) => value instanceof September; + +export class October extends $CustomType {} +export const Month$October = () => new October(); +export const Month$isOctober = (value) => value instanceof October; + +export class November extends $CustomType {} +export const Month$November = () => new November(); +export const Month$isNovember = (value) => value instanceof November; + +export class December extends $CustomType {} +export const Month$December = () => new December(); +export const Month$isDecember = (value) => value instanceof December; + +/** + * Get the offset for the computer's currently configured time zone. + * + * Note this may not be the time zone that is correct to use for your user. + * For example, if you are making a web application that runs on a server you + * want _their_ computer's time zone, not yours. + * + * This is the _current local_ offset, not the current local time zone. This + * means that while it will result in the expected outcome for the current + * time, it may result in unexpected output if used with other timestamps. For + * example: a timestamp that would locally be during daylight savings time if + * is it not currently daylight savings time when this function is called. + */ +export function local_offset() { + return $duration.seconds(local_time_offset_seconds()); +} + +/** + * Returns the English name for a month. + * + * # Examples + * + * ```gleam + * month_to_string(April) + * // -> "April" + * ``` + */ +export function month_to_string(month) { + if (month instanceof January) { + return "January"; + } else if (month instanceof February) { + return "February"; + } else if (month instanceof March) { + return "March"; + } else if (month instanceof April) { + return "April"; + } else if (month instanceof May) { + return "May"; + } else if (month instanceof June) { + return "June"; + } else if (month instanceof July) { + return "July"; + } else if (month instanceof August) { + return "August"; + } else if (month instanceof September) { + return "September"; + } else if (month instanceof October) { + return "October"; + } else if (month instanceof November) { + return "November"; + } else { + return "December"; + } +} + +/** + * Returns the number for the month, where January is 1 and December is 12. + * + * # Examples + * + * ```gleam + * month_to_int(January) + * // -> 1 + * ``` + */ +export function month_to_int(month) { + if (month instanceof January) { + return 1; + } else if (month instanceof February) { + return 2; + } else if (month instanceof March) { + return 3; + } else if (month instanceof April) { + return 4; + } else if (month instanceof May) { + return 5; + } else if (month instanceof June) { + return 6; + } else if (month instanceof July) { + return 7; + } else if (month instanceof August) { + return 8; + } else if (month instanceof September) { + return 9; + } else if (month instanceof October) { + return 10; + } else if (month instanceof November) { + return 11; + } else { + return 12; + } +} + +/** + * Returns the month for a given number, where January is 1 and December is 12. + * + * # Examples + * + * ```gleam + * month_from_int(1) + * // -> Ok(January) + * ``` + */ +export function month_from_int(month) { + if (month === 1) { + return new Ok(new January()); + } else if (month === 2) { + return new Ok(new February()); + } else if (month === 3) { + return new Ok(new March()); + } else if (month === 4) { + return new Ok(new April()); + } else if (month === 5) { + return new Ok(new May()); + } else if (month === 6) { + return new Ok(new June()); + } else if (month === 7) { + return new Ok(new July()); + } else if (month === 8) { + return new Ok(new August()); + } else if (month === 9) { + return new Ok(new September()); + } else if (month === 10) { + return new Ok(new October()); + } else if (month === 11) { + return new Ok(new November()); + } else if (month === 12) { + return new Ok(new December()); + } else { + return new Error(undefined); + } +} + +/** + * Determines if a given year is a leap year. + * + * A leap year occurs every 4 years, except for years divisible by 100, + * unless they are also divisible by 400. + * + * # Examples + * + * ```gleam + * is_leap_year(2024) + * // -> True + * ``` + * + * ```gleam + * is_leap_year(2023) + * // -> False + * ``` + */ +export function is_leap_year(year) { + let $ = (year % 400) === 0; + if ($) { + return $; + } else { + let $1 = (year % 100) === 0; + if ($1) { + return false; + } else { + return (year % 4) === 0; + } + } +} + +/** + * Checks if a given date is valid. + * + * This function properly accounts for leap years when validating February days. + * A leap year occurs every 4 years, except for years divisible by 100, + * unless they are also divisible by 400. + * + * # Examples + * + * ```gleam + * is_valid_date(Date(2023, April, 15)) + * // -> True + * ``` + * + * ```gleam + * is_valid_date(Date(2023, April, 31)) + * // -> False + * ``` + * + * ```gleam + * is_valid_date(Date(2024, February, 29)) + * // -> True (2024 is a leap year) + * ``` + */ +export function is_valid_date(date) { + let year; + let month; + let day; + year = date.year; + month = date.month; + day = date.day; + let $ = day < 1; + if ($) { + return false; + } else { + if (month instanceof January) { + return day <= 31; + } else if (month instanceof February) { + let _block; + let $1 = is_leap_year(year); + if ($1) { + _block = 29; + } else { + _block = 28; + } + let max_february_days = _block; + return day <= max_february_days; + } else if (month instanceof March) { + return day <= 31; + } else if (month instanceof April) { + return day <= 30; + } else if (month instanceof May) { + return day <= 31; + } else if (month instanceof June) { + return day <= 30; + } else if (month instanceof July) { + return day <= 31; + } else if (month instanceof August) { + return day <= 31; + } else if (month instanceof September) { + return day <= 30; + } else if (month instanceof October) { + return day <= 31; + } else if (month instanceof November) { + return day <= 30; + } else { + return day <= 31; + } + } +} + +/** + * Checks if a time of day is valid. + * + * Validates that hours are 0-23, minutes are 0-59, seconds are 0-59, + * and nanoseconds are 0-999,999,999. + * + * # Examples + * + * ```gleam + * is_valid_time_of_day(TimeOfDay(12, 30, 45, 123456789)) + * // -> True + * ``` + */ +export function is_valid_time_of_day(time) { + let hours; + let minutes; + let seconds; + let nanoseconds; + hours = time.hours; + minutes = time.minutes; + seconds = time.seconds; + nanoseconds = time.nanoseconds; + return (((((((hours >= 0) && (hours <= 23)) && (minutes >= 0)) && (minutes <= 59)) && (seconds >= 0)) && (seconds <= 59)) && (nanoseconds >= 0)) && (nanoseconds <= 999_999_999); +} + +/** + * Naively compares two dates without any time zone information, returning an + * order. + * + * ## Correctness + * + * This function compares dates without any time zone information, only using + * the rules for the gregorian calendar. This is typically sufficient, but be + * aware that in reality some time zones will change their calendar date + * occasionally. This can result in days being skipped, out of order, or + * happening multiple times. + * + * If you need real-world correct time ordering then use the + * `gleam/time/timestamp` module instead. + */ +export function naive_date_compare(one, other) { + let _pipe = $int.compare(one.year, other.year); + let _pipe$1 = $order.lazy_break_tie( + _pipe, + () => { + return $int.compare(month_to_int(one.month), month_to_int(other.month)); + }, + ); + return $order.lazy_break_tie( + _pipe$1, + () => { return $int.compare(one.day, other.day); }, + ); +} + +/** + * The offset for the [Coordinated Universal Time (UTC)](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) + * time zone. + * + * The utc zone has no time adjustments, it is always zero. It never observes + * daylight-saving time and it never shifts around based on political + * restructuring. + */ +export const utc_offset = $duration.empty; diff --git a/build/dev/javascript/gleam_time/gleam/time/duration.mjs b/build/dev/javascript/gleam_time/gleam/time/duration.mjs new file mode 100644 index 0000000..53657d0 --- /dev/null +++ b/build/dev/javascript/gleam_time/gleam/time/duration.mjs @@ -0,0 +1,382 @@ +import * as $bool from "../../../gleam_stdlib/gleam/bool.mjs"; +import * as $int from "../../../gleam_stdlib/gleam/int.mjs"; +import * as $order from "../../../gleam_stdlib/gleam/order.mjs"; +import * as $string from "../../../gleam_stdlib/gleam/string.mjs"; +import { CustomType as $CustomType, remainderInt, divideInt, isEqual } from "../../gleam.mjs"; + +class Duration extends $CustomType { + constructor(seconds, nanoseconds) { + super(); + this.seconds = seconds; + this.nanoseconds = nanoseconds; + } +} + +export class Nanosecond extends $CustomType {} +export const Unit$Nanosecond = () => new Nanosecond(); +export const Unit$isNanosecond = (value) => value instanceof Nanosecond; + +export class Microsecond extends $CustomType {} +export const Unit$Microsecond = () => new Microsecond(); +export const Unit$isMicrosecond = (value) => value instanceof Microsecond; + +export class Millisecond extends $CustomType {} +export const Unit$Millisecond = () => new Millisecond(); +export const Unit$isMillisecond = (value) => value instanceof Millisecond; + +export class Second extends $CustomType {} +export const Unit$Second = () => new Second(); +export const Unit$isSecond = (value) => value instanceof Second; + +export class Minute extends $CustomType {} +export const Unit$Minute = () => new Minute(); +export const Unit$isMinute = (value) => value instanceof Minute; + +export class Hour extends $CustomType {} +export const Unit$Hour = () => new Hour(); +export const Unit$isHour = (value) => value instanceof Hour; + +export class Day extends $CustomType {} +export const Unit$Day = () => new Day(); +export const Unit$isDay = (value) => value instanceof Day; + +export class Week extends $CustomType {} +export const Unit$Week = () => new Week(); +export const Unit$isWeek = (value) => value instanceof Week; + +export class Month extends $CustomType {} +export const Unit$Month = () => new Month(); +export const Unit$isMonth = (value) => value instanceof Month; + +export class Year extends $CustomType {} +export const Unit$Year = () => new Year(); +export const Unit$isYear = (value) => value instanceof Year; + +/** + * Ensure the duration is represented with `nanoseconds` being positive and + * less than 1 second. + * + * This function does not change the amount of time that the duratoin refers + * to, it only adjusts the values used to represent the time. + * + * @ignore + */ +function normalise(duration) { + let multiplier = 1_000_000_000; + let nanoseconds$1 = remainderInt(duration.nanoseconds, multiplier); + let overflow = duration.nanoseconds - nanoseconds$1; + let seconds$1 = duration.seconds + (divideInt(overflow, multiplier)); + let $ = nanoseconds$1 >= 0; + if ($) { + return new Duration(seconds$1, nanoseconds$1); + } else { + return new Duration(seconds$1 - 1, multiplier + nanoseconds$1); + } +} + +/** + * Convert a duration to a number of the largest number of a unit, serving as + * a rough description of the duration that a human can understand. + * + * The size used for each unit are described in the documentation for the + * `Unit` type. + * + * ```gleam + * seconds(125) + * |> approximate + * // -> #(2, Minute) + * ``` + * + * This function rounds _towards zero_. This means that if a duration is just + * short of 2 days then it will approximate to 1 day. + * + * ```gleam + * hours(47) + * |> approximate + * // -> #(1, Day) + * ``` + */ +export function approximate(duration) { + let s; + let ns; + s = duration.seconds; + ns = duration.nanoseconds; + let minute = 60; + let hour = minute * 60; + let day = hour * 24; + let week = day * 7; + let year = day * 365 + hour * 6; + let month = globalThis.Math.trunc(year / 12); + let microsecond = 1000; + let millisecond = microsecond * 1000; + let $ = undefined; + if (s < 0) { + let _block; + let _pipe = new Duration(- s, - ns); + let _pipe$1 = normalise(_pipe); + _block = approximate(_pipe$1); + let $1 = _block; + let amount; + let unit; + amount = $1[0]; + unit = $1[1]; + return [- amount, unit]; + } else if (s >= year) { + return [divideInt(s, year), new Year()]; + } else if (s >= month) { + return [divideInt(s, month), new Month()]; + } else if (s >= week) { + return [divideInt(s, week), new Week()]; + } else if (s >= day) { + return [divideInt(s, day), new Day()]; + } else if (s >= hour) { + return [divideInt(s, hour), new Hour()]; + } else if (s >= minute) { + return [divideInt(s, minute), new Minute()]; + } else if (s > 0) { + return [s, new Second()]; + } else if (ns >= millisecond) { + return [divideInt(ns, millisecond), new Millisecond()]; + } else if (ns >= microsecond) { + return [divideInt(ns, microsecond), new Microsecond()]; + } else { + return [ns, new Nanosecond()]; + } +} + +/** + * Compare one duration to another, indicating whether the first spans a + * larger amount of time (and so is greater) or smaller amount of time (and so + * is lesser) than the second. + * + * # Examples + * + * ```gleam + * compare(seconds(1), seconds(2)) + * // -> order.Lt + * ``` + * + * Whether a duration is negative or positive doesn't matter for comparing + * them, only the amount of time spanned matters. + * + * ```gleam + * compare(seconds(-2), seconds(1)) + * // -> order.Gt + * ``` + */ +export function compare(left, right) { + let parts = (x) => { + let $ = x.seconds >= 0; + if ($) { + return [x.seconds, x.nanoseconds]; + } else { + return [x.seconds * -1 - 1, 1_000_000_000 - x.nanoseconds]; + } + }; + let $ = parts(left); + let ls; + let lns; + ls = $[0]; + lns = $[1]; + let $1 = parts(right); + let rs; + let rns; + rs = $1[0]; + rns = $1[1]; + let _pipe = $int.compare(ls, rs); + return $order.break_tie(_pipe, $int.compare(lns, rns)); +} + +/** + * Calculate the difference between two durations. + * + * This is effectively substracting the first duration from the second. + * + * # Examples + * + * ```gleam + * difference(seconds(1), seconds(5)) + * // -> seconds(4) + * ``` + */ +export function difference(left, right) { + let _pipe = new Duration( + right.seconds - left.seconds, + right.nanoseconds - left.nanoseconds, + ); + return normalise(_pipe); +} + +/** + * Add two durations together. + * + * # Examples + * + * ```gleam + * add(seconds(1), seconds(5)) + * // -> seconds(6) + * ``` + */ +export function add(left, right) { + let _pipe = new Duration( + left.seconds + right.seconds, + left.nanoseconds + right.nanoseconds, + ); + return normalise(_pipe); +} + +function nanosecond_digits(loop$n, loop$position, loop$acc) { + while (true) { + let n = loop$n; + let position = loop$position; + let acc = loop$acc; + if (position === 9) { + return acc; + } else if ((acc === "") && ((remainderInt(n, 10)) === 0)) { + loop$n = globalThis.Math.trunc(n / 10); + loop$position = position + 1; + loop$acc = acc; + } else { + let acc$1 = $int.to_string(n % 10) + acc; + loop$n = globalThis.Math.trunc(n / 10); + loop$position = position + 1; + loop$acc = acc$1; + } + } +} + +/** + * Create a duration of a number of seconds. + */ +export function seconds(amount) { + return new Duration(amount, 0); +} + +/** + * Create a duration of a number of minutes. + */ +export function minutes(amount) { + return seconds(amount * 60); +} + +/** + * Create a duration of a number of hours. + */ +export function hours(amount) { + return seconds(amount * 60 * 60); +} + +/** + * Create a duration of a number of milliseconds. + */ +export function milliseconds(amount) { + let remainder = amount % 1000; + let overflow = amount - remainder; + let nanoseconds$1 = remainder * 1_000_000; + let seconds$1 = globalThis.Math.trunc(overflow / 1000); + let _pipe = new Duration(seconds$1, nanoseconds$1); + return normalise(_pipe); +} + +/** + * Create a duration of a number of nanoseconds. + * + * # JavaScript int limitations + * + * Remember that JavaScript can only perfectly represent ints between positive + * and negative 9,007,199,254,740,991! If you use a single call to this + * function to create durations larger than that number of nanoseconds then + * you will likely not get exactly the value you expect. Use `seconds` and + * `milliseconds` as much as possible for large durations. + */ +export function nanoseconds(amount) { + let _pipe = new Duration(0, amount); + return normalise(_pipe); +} + +/** + * Convert the duration to a number of seconds. + * + * There may be some small loss of precision due to `Duration` being + * nanosecond accurate and `Float` not being able to represent this. + */ +export function to_seconds(duration) { + let seconds$1 = $int.to_float(duration.seconds); + let nanoseconds$1 = $int.to_float(duration.nanoseconds); + return seconds$1 + (nanoseconds$1 / 1_000_000_000.0); +} + +/** + * Convert the duration to a number of seconds and nanoseconds. There is no + * loss of precision with this conversion on any target. + */ +export function to_seconds_and_nanoseconds(duration) { + return [duration.seconds, duration.nanoseconds]; +} + +export const empty = /* @__PURE__ */ new Duration(0, 0); + +/** + * Convert the duration to an [ISO8601][1] formatted duration string. + * + * The ISO8601 duration format is ambiguous without context due to months and + * years having different lengths, and because of leap seconds. This function + * encodes the duration as days, hours, and seconds without any leap seconds. + * Be sure to take this into account when using the duration strings. + * + * [1]: https://en.wikipedia.org/wiki/ISO_8601#Durations + */ +export function to_iso8601_string(duration) { + return $bool.guard( + isEqual(duration, empty), + "PT0S", + () => { + let split = (total, limit) => { + let amount = remainderInt(total, limit); + let remainder = divideInt((total - amount), limit); + return [amount, remainder]; + }; + let $ = split(duration.seconds, 60); + let seconds$1; + let rest; + seconds$1 = $[0]; + rest = $[1]; + let $1 = split(rest, 60); + let minutes$1; + let rest$1; + minutes$1 = $1[0]; + rest$1 = $1[1]; + let $2 = split(rest$1, 24); + let hours$1; + let rest$2; + hours$1 = $2[0]; + rest$2 = $2[1]; + let days = rest$2; + let add$1 = (out, value, unit) => { + if (value === 0) { + return out; + } else { + return (out + $int.to_string(value)) + unit; + } + }; + let _block; + let _pipe = "P"; + let _pipe$1 = add$1(_pipe, days, "D"); + let _pipe$2 = $string.append(_pipe$1, "T"); + let _pipe$3 = add$1(_pipe$2, hours$1, "H"); + _block = add$1(_pipe$3, minutes$1, "M"); + let output = _block; + let $3 = duration.nanoseconds; + if ($3 === 0) { + if (seconds$1 === 0) { + return output; + } else { + return (output + $int.to_string(seconds$1)) + "S"; + } + } else { + let f = nanosecond_digits(duration.nanoseconds, 0, ""); + return (((output + $int.to_string(seconds$1)) + ".") + f) + "S"; + } + }, + ); +} diff --git a/build/dev/javascript/gleam_time/gleam/time/timestamp.mjs b/build/dev/javascript/gleam_time/gleam/time/timestamp.mjs new file mode 100644 index 0000000..2f3ec9b --- /dev/null +++ b/build/dev/javascript/gleam_time/gleam/time/timestamp.mjs @@ -0,0 +1,1215 @@ +import * as $bit_array from "../../../gleam_stdlib/gleam/bit_array.mjs"; +import * as $float from "../../../gleam_stdlib/gleam/float.mjs"; +import * as $int from "../../../gleam_stdlib/gleam/int.mjs"; +import * as $list from "../../../gleam_stdlib/gleam/list.mjs"; +import * as $order from "../../../gleam_stdlib/gleam/order.mjs"; +import * as $result from "../../../gleam_stdlib/gleam/result.mjs"; +import * as $string from "../../../gleam_stdlib/gleam/string.mjs"; +import { + Ok, + Error, + toList, + Empty as $Empty, + prepend as listPrepend, + CustomType as $CustomType, + remainderInt, + divideFloat, + divideInt, + toBitArray, + bitArraySlice, +} from "../../gleam.mjs"; +import * as $calendar from "../../gleam/time/calendar.mjs"; +import * as $duration from "../../gleam/time/duration.mjs"; +import { system_time as get_system_time } from "../../gleam_time_ffi.mjs"; + +class Timestamp extends $CustomType { + constructor(seconds, nanoseconds) { + super(); + this.seconds = seconds; + this.nanoseconds = nanoseconds; + } +} + +/** + * Ensure the time is represented with `nanoseconds` being positive and less + * than 1 second. + * + * This function does not change the time that the timestamp refers to, it + * only adjusts the values used to represent the time. + * + * @ignore + */ +function normalise(timestamp) { + let multiplier = 1_000_000_000; + let nanoseconds = remainderInt(timestamp.nanoseconds, multiplier); + let overflow = timestamp.nanoseconds - nanoseconds; + let seconds = timestamp.seconds + (divideInt(overflow, multiplier)); + let $ = nanoseconds >= 0; + if ($) { + return new Timestamp(seconds, nanoseconds); + } else { + return new Timestamp(seconds - 1, multiplier + nanoseconds); + } +} + +/** + * Compare one timestamp to another, indicating whether the first is further + * into the future (greater) or further into the past (lesser) than the + * second. + * + * # Examples + * + * ```gleam + * compare(from_unix_seconds(1), from_unix_seconds(2)) + * // -> order.Lt + * ``` + */ +export function compare(left, right) { + return $order.break_tie( + $int.compare(left.seconds, right.seconds), + $int.compare(left.nanoseconds, right.nanoseconds), + ); +} + +/** + * Get the current system time. + * + * Note this time is not unique or monotonic, it could change at any time or + * even go backwards! The exact behaviour will depend on the runtime used. See + * the module documentation for more information. + * + * On Erlang this uses [`erlang:system_time/1`][1]. On JavaScript this uses + * [`Date.now`][2]. + * + * [1]: https://www.erlang.org/doc/apps/erts/erlang#system_time/1 + * [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now + */ +export function system_time() { + let $ = get_system_time(); + let seconds; + let nanoseconds; + seconds = $[0]; + nanoseconds = $[1]; + return normalise(new Timestamp(seconds, nanoseconds)); +} + +/** + * Calculate the difference between two timestamps. + * + * This is effectively substracting the first timestamp from the second. + * + * # Examples + * + * ```gleam + * difference(from_unix_seconds(1), from_unix_seconds(5)) + * // -> duration.seconds(4) + * ``` + */ +export function difference(left, right) { + let seconds = $duration.seconds(right.seconds - left.seconds); + let nanoseconds = $duration.nanoseconds(right.nanoseconds - left.nanoseconds); + return $duration.add(seconds, nanoseconds); +} + +/** + * Add a duration to a timestamp. + * + * # Examples + * + * ```gleam + * add(from_unix_seconds(1000), duration.seconds(5)) + * // -> from_unix_seconds(1005) + * ``` + */ +export function add(timestamp, duration) { + let $ = $duration.to_seconds_and_nanoseconds(duration); + let seconds; + let nanoseconds; + seconds = $[0]; + nanoseconds = $[1]; + let _pipe = new Timestamp( + timestamp.seconds + seconds, + timestamp.nanoseconds + nanoseconds, + ); + return normalise(_pipe); +} + +function pad_digit(digit, desired_length) { + let _pipe = $int.to_string(digit); + return $string.pad_start(_pipe, desired_length, "0"); +} + +function duration_to_minutes(duration) { + return $float.round($duration.to_seconds(duration) / 60.0); +} + +function modulo(n, m) { + let $ = $int.modulo(n, m); + if ($ instanceof Ok) { + let n$1 = $[0]; + return n$1; + } else { + return 0; + } +} + +function floored_div(numerator, denominator) { + let n = divideFloat($int.to_float(numerator), denominator); + return $float.round($float.floor(n)); +} + +function to_civil(minutes) { + let raw_day = floored_div(minutes, (60.0 * 24.0)) + 719_468; + let _block; + let $ = raw_day >= 0; + if ($) { + _block = globalThis.Math.trunc(raw_day / 146_097); + } else { + _block = globalThis.Math.trunc((raw_day - 146_096) / 146_097); + } + let era = _block; + let day_of_era = raw_day - era * 146_097; + let year_of_era = globalThis.Math.trunc( + (((day_of_era - (globalThis.Math.trunc(day_of_era / 1460))) + (globalThis.Math.trunc( + day_of_era / 36_524 + ))) - (globalThis.Math.trunc(day_of_era / 146_096))) / 365 + ); + let year = year_of_era + era * 400; + let day_of_year = day_of_era - ((365 * year_of_era + (globalThis.Math.trunc( + year_of_era / 4 + ))) - (globalThis.Math.trunc(year_of_era / 100))); + let mp = globalThis.Math.trunc((5 * day_of_year + 2) / 153); + let _block$1; + let $1 = mp < 10; + if ($1) { + _block$1 = mp + 3; + } else { + _block$1 = mp - 9; + } + let month = _block$1; + let day = (day_of_year - (globalThis.Math.trunc((153 * mp + 2) / 5))) + 1; + let _block$2; + let $2 = month <= 2; + if ($2) { + _block$2 = year + 1; + } else { + _block$2 = year; + } + let year$1 = _block$2; + return [year$1, month, day]; +} + +function to_calendar_from_offset(timestamp, offset) { + let total = timestamp.seconds + offset * 60; + let seconds = modulo(total, 60); + let total_minutes = floored_div(total, 60.0); + let minutes = globalThis.Math.trunc(modulo(total, 60 * 60) / 60); + let hours = divideInt(modulo(total, 24 * 60 * 60), 60 * 60); + let $ = to_civil(total_minutes); + let year; + let month; + let day; + year = $[0]; + month = $[1]; + day = $[2]; + return [year, month, day, hours, minutes, seconds]; +} + +/** + * Convert a `Timestamp` to calendar time, suitable for presenting to a human + * to read. + * + * If you want a machine to use the time value then you should not use this + * function and should instead keep it as a timestamp. See the documentation + * for the `gleam/time/calendar` module for more information. + * + * # Examples + * + * ```gleam + * timestamp.from_unix_seconds(0) + * |> timestamp.to_calendar(calendar.utc_offset) + * // -> #(Date(1970, January, 1), TimeOfDay(0, 0, 0, 0)) + * ``` + */ +export function to_calendar(timestamp, offset) { + let offset$1 = duration_to_minutes(offset); + let $ = to_calendar_from_offset(timestamp, offset$1); + let year; + let month; + let day; + let hours; + let minutes; + let seconds; + year = $[0]; + month = $[1]; + day = $[2]; + hours = $[3]; + minutes = $[4]; + seconds = $[5]; + let _block; + if (month === 1) { + _block = new $calendar.January(); + } else if (month === 2) { + _block = new $calendar.February(); + } else if (month === 3) { + _block = new $calendar.March(); + } else if (month === 4) { + _block = new $calendar.April(); + } else if (month === 5) { + _block = new $calendar.May(); + } else if (month === 6) { + _block = new $calendar.June(); + } else if (month === 7) { + _block = new $calendar.July(); + } else if (month === 8) { + _block = new $calendar.August(); + } else if (month === 9) { + _block = new $calendar.September(); + } else if (month === 10) { + _block = new $calendar.October(); + } else if (month === 11) { + _block = new $calendar.November(); + } else { + _block = new $calendar.December(); + } + let month$1 = _block; + let nanoseconds = timestamp.nanoseconds; + let date = new $calendar.Date(year, month$1, day); + let time = new $calendar.TimeOfDay(hours, minutes, seconds, nanoseconds); + return [date, time]; +} + +function do_remove_trailing_zeros(loop$reversed_digits) { + while (true) { + let reversed_digits = loop$reversed_digits; + if (reversed_digits instanceof $Empty) { + return reversed_digits; + } else { + let digit = reversed_digits.head; + if (digit === 0) { + let digits = reversed_digits.tail; + loop$reversed_digits = digits; + } else { + let reversed_digits$1 = reversed_digits; + return $list.reverse(reversed_digits$1); + } + } + } +} + +/** + * Given a list of digits, return new list with any trailing zeros removed. + * + * @ignore + */ +function remove_trailing_zeros(digits) { + let reversed_digits = $list.reverse(digits); + return do_remove_trailing_zeros(reversed_digits); +} + +function do_get_zero_padded_digits(loop$number, loop$digits, loop$count) { + while (true) { + let number = loop$number; + let digits = loop$digits; + let count = loop$count; + let number$1 = number; + if ((number$1 <= 0) && (count >= 9)) { + return digits; + } else { + let number$2 = number; + if (number$2 <= 0) { + loop$number = number$2; + loop$digits = listPrepend(0, digits); + loop$count = count + 1; + } else { + let number$3 = number; + let digit = number$3 % 10; + let number$4 = floored_div(number$3, 10.0); + loop$number = number$4; + loop$digits = listPrepend(digit, digits); + loop$count = count + 1; + } + } + } +} + +/** + * Returns the list of digits of `number`. If the number of digits is less + * than 9, the result is zero-padded at the front. + * + * @ignore + */ +function get_zero_padded_digits(number) { + return do_get_zero_padded_digits(number, toList([]), 0); +} + +/** + * Converts nanoseconds into a `String` representation of fractional seconds. + * + * Assumes that `nanoseconds < 1_000_000_000`, which will be true for any + * normalised timestamp. + * + * @ignore + */ +function show_second_fraction(nanoseconds) { + let $ = $int.compare(nanoseconds, 0); + if ($ instanceof $order.Lt) { + return ""; + } else if ($ instanceof $order.Eq) { + return ""; + } else { + let _block; + let _pipe = nanoseconds; + let _pipe$1 = get_zero_padded_digits(_pipe); + let _pipe$2 = remove_trailing_zeros(_pipe$1); + let _pipe$3 = $list.map(_pipe$2, $int.to_string); + _block = $string.join(_pipe$3, ""); + let second_fraction_part = _block; + return "." + second_fraction_part; + } +} + +/** + * Convert a timestamp to a RFC 3339 formatted time string, with an offset + * supplied as an additional argument. + * + * The output of this function is also ISO 8601 compatible so long as the + * offset not negative. Offsets have at-most minute precision, so an offset + * with higher precision will be rounded to the nearest minute. + * + * If you are making an API such as a HTTP JSON API you are encouraged to use + * Unix timestamps instead of this format or ISO 8601. Unix timestamps are a + * better choice as they don't contain offset information. Consider: + * + * - UTC offsets are not time zones. This does not and cannot tell us the time + * zone in which the date was recorded. So what are we supposed to do with + * this information? + * - Users typically want dates formatted according to their local time zone. + * What if the provided UTC offset is different from the current user's time + * zone? What are we supposed to do with it then? + * - Despite it being useless (or worse, a source of bugs), the UTC offset + * creates a larger payload to transfer. + * + * They also uses more memory than a unix timestamp. The way they are better + * than Unix timestamp is that it is easier for a human to read them, but + * this is a hinderance that tooling can remedy, and APIs are not primarily + * for humans. + * + * # Examples + * + * ```gleam + * timestamp.from_unix_seconds_and_nanoseconds(1000, 123_000_000) + * |> to_rfc3339(calendar.utc_offset) + * // -> "1970-01-01T00:16:40.123Z" + * ``` + * + * ```gleam + * timestamp.from_unix_seconds(1000) + * |> to_rfc3339(duration.seconds(3600)) + * // -> "1970-01-01T01:16:40+01:00" + * ``` + */ +export function to_rfc3339(timestamp, offset) { + let offset$1 = duration_to_minutes(offset); + let $ = to_calendar_from_offset(timestamp, offset$1); + let years; + let months; + let days; + let hours; + let minutes; + let seconds; + years = $[0]; + months = $[1]; + days = $[2]; + hours = $[3]; + minutes = $[4]; + seconds = $[5]; + let offset_minutes = modulo(offset$1, 60); + let offset_hours = $int.absolute_value(floored_div(offset$1, 60.0)); + let n2 = (_capture) => { return pad_digit(_capture, 2); }; + let n4 = (_capture) => { return pad_digit(_capture, 4); }; + let out = ""; + let out$1 = ((((out + n4(years)) + "-") + n2(months)) + "-") + n2(days); + let out$2 = out$1 + "T"; + let out$3 = ((((out$2 + n2(hours)) + ":") + n2(minutes)) + ":") + n2(seconds); + let out$4 = out$3 + show_second_fraction(timestamp.nanoseconds); + let $1 = $int.compare(offset$1, 0); + if ($1 instanceof $order.Lt) { + return (((out$4 + "-") + n2(offset_hours)) + ":") + n2(offset_minutes); + } else if ($1 instanceof $order.Eq) { + return out$4 + "Z"; + } else { + return (((out$4 + "+") + n2(offset_hours)) + ":") + n2(offset_minutes); + } +} + +function is_leap_year(year) { + return ((year % 4) === 0) && (((year % 100) !== 0) || ((year % 400) === 0)); +} + +function parse_sign(bytes) { + if (bytes.bitSize >= 8) { + if (bytes.byteAt(0) === 43) { + if ((bytes.bitSize - 8) % 8 === 0) { + let remaining_bytes = bitArraySlice(bytes, 8); + return new Ok(["+", remaining_bytes]); + } else { + return new Error(undefined); + } + } else if (bytes.byteAt(0) === 45 && (bytes.bitSize - 8) % 8 === 0) { + let remaining_bytes = bitArraySlice(bytes, 8); + return new Ok(["-", remaining_bytes]); + } else { + return new Error(undefined); + } + } else { + return new Error(undefined); + } +} + +/** + * Accept the given value from `bytes` and move past it if found. + * + * @ignore + */ +function accept_byte(bytes, value) { + if (bytes.bitSize >= 8 && (bytes.bitSize - 8) % 8 === 0) { + let byte = bytes.byteAt(0); + if (byte === value) { + let remaining_bytes = bitArraySlice(bytes, 8); + return new Ok(remaining_bytes); + } else { + return new Error(undefined); + } + } else { + return new Error(undefined); + } +} + +function accept_empty(bytes) { + if (bytes.bitSize === 0) { + return new Ok(undefined); + } else { + return new Error(undefined); + } +} + +/** + * Note: It is the callers responsibility to ensure the inputs are valid. + * + * See https://www.tondering.dk/claus/cal/julperiod.php#formula + * + * @ignore + */ +function julian_day_from_ymd(year, month, day) { + let adjustment = globalThis.Math.trunc((14 - month) / 12); + let adjusted_year = (year + 4800) - adjustment; + let adjusted_month = (month + 12 * adjustment) - 3; + return (((((day + (globalThis.Math.trunc((153 * adjusted_month + 2) / 5))) + 365 * adjusted_year) + (globalThis.Math.trunc( + adjusted_year / 4 + ))) - (globalThis.Math.trunc(adjusted_year / 100))) + (globalThis.Math.trunc( + adjusted_year / 400 + ))) - 32_045; +} + +/** + * Create a timestamp from a number of seconds since 00:00:00 UTC on 1 January + * 1970. + */ +export function from_unix_seconds(seconds) { + return new Timestamp(seconds, 0); +} + +/** + * Create a timestamp from a number of seconds and nanoseconds since 00:00:00 + * UTC on 1 January 1970. + * + * # JavaScript int limitations + * + * Remember that JavaScript can only perfectly represent ints between positive + * and negative 9,007,199,254,740,991! If you only use the nanosecond field + * then you will almost certainly not get the date value you want due to this + * loss of precision. Always use seconds primarily and then use nanoseconds + * for the final sub-second adjustment. + */ +export function from_unix_seconds_and_nanoseconds(seconds, nanoseconds) { + let _pipe = new Timestamp(seconds, nanoseconds); + return normalise(_pipe); +} + +/** + * Convert the timestamp to a number of seconds since 00:00:00 UTC on 1 + * January 1970. + * + * There may be some small loss of precision due to `Timestamp` being + * nanosecond accurate and `Float` not being able to represent this. + */ +export function to_unix_seconds(timestamp) { + let seconds = $int.to_float(timestamp.seconds); + let nanoseconds = $int.to_float(timestamp.nanoseconds); + return seconds + (nanoseconds / 1_000_000_000.0); +} + +/** + * Convert the timestamp to a number of seconds and nanoseconds since 00:00:00 + * UTC on 1 January 1970. There is no loss of precision with this conversion + * on any target. + */ +export function to_unix_seconds_and_nanoseconds(timestamp) { + return [timestamp.seconds, timestamp.nanoseconds]; +} + +const seconds_per_day = 86_400; + +const seconds_per_hour = 3600; + +const seconds_per_minute = 60; + +function offset_to_seconds(sign, hours, minutes) { + let abs_seconds = hours * seconds_per_hour + minutes * seconds_per_minute; + if (sign === "-") { + return - abs_seconds; + } else { + return abs_seconds; + } +} + +/** + * `julian_seconds_from_parts(year, month, day, hours, minutes, seconds)` + * returns the number of Julian + * seconds represented by the given arguments. + * + * Note: It is the callers responsibility to ensure the inputs are valid. + * + * See https://www.tondering.dk/claus/cal/julperiod.php#formula + * + * @ignore + */ +function julian_seconds_from_parts(year, month, day, hours, minutes, seconds) { + let julian_day_seconds = julian_day_from_ymd(year, month, day) * seconds_per_day; + return ((julian_day_seconds + hours * seconds_per_hour) + minutes * seconds_per_minute) + seconds; +} + +const nanoseconds_per_second = 1_000_000_000; + +/** + * The `:` character as a byte + * + * @ignore + */ +const byte_colon = 0x3A; + +/** + * The `-` character as a byte + * + * @ignore + */ +const byte_minus = 0x2D; + +/** + * The `0` character as a byte + * + * @ignore + */ +const byte_zero = 0x30; + +/** + * The `9` character as a byte + * + * @ignore + */ +const byte_nine = 0x39; + +function do_parse_second_fraction_as_nanoseconds( + loop$bytes, + loop$acc, + loop$power +) { + while (true) { + let bytes = loop$bytes; + let acc = loop$acc; + let power = loop$power; + let power$1 = globalThis.Math.trunc(power / 10); + if (bytes.bitSize >= 8 && (bytes.bitSize - 8) % 8 === 0) { + let byte = bytes.byteAt(0); + if (((0x30 <= byte) && (byte <= 0x39)) && (power$1 < 1)) { + let remaining_bytes = bitArraySlice(bytes, 8); + loop$bytes = remaining_bytes; + loop$acc = acc; + loop$power = power$1; + } else { + let byte$1 = bytes.byteAt(0); + if ((0x30 <= byte$1) && (byte$1 <= 0x39)) { + let remaining_bytes = bitArraySlice(bytes, 8); + let digit = byte$1 - 0x30; + loop$bytes = remaining_bytes; + loop$acc = acc + digit * power$1; + loop$power = power$1; + } else { + return new Ok([acc, bytes]); + } + } + } else { + return new Ok([acc, bytes]); + } + } +} + +function parse_second_fraction_as_nanoseconds(bytes) { + if (bytes.bitSize >= 8 && bytes.byteAt(0) === 46) { + if (bytes.bitSize >= 16 && (bytes.bitSize - 16) % 8 === 0) { + let byte = bytes.byteAt(1); + if ((0x30 <= byte) && (byte <= 0x39)) { + let remaining_bytes = bitArraySlice(bytes, 16); + return do_parse_second_fraction_as_nanoseconds( + toBitArray([byte, remaining_bytes]), + 0, + nanoseconds_per_second, + ); + } else if ((bytes.bitSize - 8) % 8 === 0) { + return new Error(undefined); + } else { + return new Ok([0, bytes]); + } + } else if ((bytes.bitSize - 8) % 8 === 0) { + return new Error(undefined); + } else { + return new Ok([0, bytes]); + } + } else { + return new Ok([0, bytes]); + } +} + +function do_parse_digits(loop$bytes, loop$count, loop$acc, loop$k) { + while (true) { + let bytes = loop$bytes; + let count = loop$count; + let acc = loop$acc; + let k = loop$k; + if (k >= count) { + return new Ok([acc, bytes]); + } else if (bytes.bitSize >= 8 && (bytes.bitSize - 8) % 8 === 0) { + let byte = bytes.byteAt(0); + if ((0x30 <= byte) && (byte <= 0x39)) { + let remaining_bytes = bitArraySlice(bytes, 8); + loop$bytes = remaining_bytes; + loop$count = count; + loop$acc = acc * 10 + (byte - 0x30); + loop$k = k + 1; + } else { + return new Error(undefined); + } + } else { + return new Error(undefined); + } + } +} + +/** + * Parse and return the given number of digits from the given bytes. + * + * @ignore + */ +function parse_digits(bytes, count) { + return do_parse_digits(bytes, count, 0, 0); +} + +function parse_year(bytes) { + return parse_digits(bytes, 4); +} + +function parse_month(bytes) { + return $result.try$( + parse_digits(bytes, 2), + (_use0) => { + let month; + let bytes$1; + month = _use0[0]; + bytes$1 = _use0[1]; + let $ = (1 <= month) && (month <= 12); + if ($) { + return new Ok([month, bytes$1]); + } else { + return new Error(undefined); + } + }, + ); +} + +function parse_day(bytes, year, month) { + return $result.try$( + parse_digits(bytes, 2), + (_use0) => { + let day; + let bytes$1; + day = _use0[0]; + bytes$1 = _use0[1]; + return $result.try$( + (() => { + if (month === 1) { + return new Ok(31); + } else if (month === 3) { + return new Ok(31); + } else if (month === 5) { + return new Ok(31); + } else if (month === 7) { + return new Ok(31); + } else if (month === 8) { + return new Ok(31); + } else if (month === 10) { + return new Ok(31); + } else if (month === 12) { + return new Ok(31); + } else if (month === 4) { + return new Ok(30); + } else if (month === 6) { + return new Ok(30); + } else if (month === 9) { + return new Ok(30); + } else if (month === 11) { + return new Ok(30); + } else if (month === 2) { + let $ = is_leap_year(year); + if ($) { + return new Ok(29); + } else { + return new Ok(28); + } + } else { + return new Error(undefined); + } + })(), + (max_day) => { + let $ = (1 <= day) && (day <= max_day); + if ($) { + return new Ok([day, bytes$1]); + } else { + return new Error(undefined); + } + }, + ); + }, + ); +} + +function parse_hours(bytes) { + return $result.try$( + parse_digits(bytes, 2), + (_use0) => { + let hours; + let bytes$1; + hours = _use0[0]; + bytes$1 = _use0[1]; + let $ = (0 <= hours) && (hours <= 23); + if ($) { + return new Ok([hours, bytes$1]); + } else { + return new Error(undefined); + } + }, + ); +} + +function parse_minutes(bytes) { + return $result.try$( + parse_digits(bytes, 2), + (_use0) => { + let minutes; + let bytes$1; + minutes = _use0[0]; + bytes$1 = _use0[1]; + let $ = (0 <= minutes) && (minutes <= 59); + if ($) { + return new Ok([minutes, bytes$1]); + } else { + return new Error(undefined); + } + }, + ); +} + +function parse_seconds(bytes) { + return $result.try$( + parse_digits(bytes, 2), + (_use0) => { + let seconds; + let bytes$1; + seconds = _use0[0]; + bytes$1 = _use0[1]; + let $ = (0 <= seconds) && (seconds <= 60); + if ($) { + return new Ok([seconds, bytes$1]); + } else { + return new Error(undefined); + } + }, + ); +} + +function parse_numeric_offset(bytes) { + return $result.try$( + parse_sign(bytes), + (_use0) => { + let sign; + let bytes$1; + sign = _use0[0]; + bytes$1 = _use0[1]; + return $result.try$( + parse_hours(bytes$1), + (_use0) => { + let hours; + let bytes$2; + hours = _use0[0]; + bytes$2 = _use0[1]; + return $result.try$( + accept_byte(bytes$2, byte_colon), + (bytes) => { + return $result.try$( + parse_minutes(bytes), + (_use0) => { + let minutes; + let bytes$1; + minutes = _use0[0]; + bytes$1 = _use0[1]; + let offset_seconds = offset_to_seconds(sign, hours, minutes); + return new Ok([offset_seconds, bytes$1]); + }, + ); + }, + ); + }, + ); + }, + ); +} + +function parse_offset(bytes) { + if (bytes.bitSize >= 8) { + if (bytes.byteAt(0) === 90) { + if ((bytes.bitSize - 8) % 8 === 0) { + let remaining_bytes = bitArraySlice(bytes, 8); + return new Ok([0, remaining_bytes]); + } else { + return parse_numeric_offset(bytes); + } + } else if (bytes.byteAt(0) === 122 && (bytes.bitSize - 8) % 8 === 0) { + let remaining_bytes = bitArraySlice(bytes, 8); + return new Ok([0, remaining_bytes]); + } else { + return parse_numeric_offset(bytes); + } + } else { + return parse_numeric_offset(bytes); + } +} + +/** + * The `t` character as a byte + * + * @ignore + */ +const byte_t_lowercase = 0x74; + +/** + * The `T` character as a byte + * + * @ignore + */ +const byte_t_uppercase = 0x54; + +/** + * The `T` character as a byte + * + * @ignore + */ +const byte_space = 0x20; + +function accept_date_time_separator(bytes) { + if (bytes.bitSize >= 8 && (bytes.bitSize - 8) % 8 === 0) { + let byte = bytes.byteAt(0); + if (((byte === 0x54) || (byte === 0x74)) || (byte === 0x20)) { + let remaining_bytes = bitArraySlice(bytes, 8); + return new Ok(remaining_bytes); + } else { + return new Error(undefined); + } + } else { + return new Error(undefined); + } +} + +/** + * The Julian seconds of the UNIX epoch (Julian day is 2_440_588) + * + * @ignore + */ +const julian_seconds_unix_epoch = 210_866_803_200; + +/** + * Note: The caller of this function must ensure that all inputs are valid. + * + * @ignore + */ +function from_date_time( + year, + month, + day, + hours, + minutes, + seconds, + second_fraction_as_nanoseconds, + offset_seconds +) { + let julian_seconds = julian_seconds_from_parts( + year, + month, + day, + hours, + minutes, + seconds, + ); + let julian_seconds_since_epoch = julian_seconds - julian_seconds_unix_epoch; + let _pipe = new Timestamp( + julian_seconds_since_epoch - offset_seconds, + second_fraction_as_nanoseconds, + ); + return normalise(_pipe); +} + +/** + * Create a `Timestamp` from a human-readable calendar time. + * + * # Examples + * + * ```gleam + * timestamp.from_calendar( + * date: calendar.Date(2024, calendar.December, 25), + * time: calendar.TimeOfDay(12, 30, 50, 0), + * offset: calendar.utc_offset, + * ) + * |> timestamp.to_rfc3339(calendar.utc_offset) + * // -> "2024-12-25T12:30:50Z" + * ``` + */ +export function from_calendar(date, time, offset) { + let _block; + let $ = date.month; + if ($ instanceof $calendar.January) { + _block = 1; + } else if ($ instanceof $calendar.February) { + _block = 2; + } else if ($ instanceof $calendar.March) { + _block = 3; + } else if ($ instanceof $calendar.April) { + _block = 4; + } else if ($ instanceof $calendar.May) { + _block = 5; + } else if ($ instanceof $calendar.June) { + _block = 6; + } else if ($ instanceof $calendar.July) { + _block = 7; + } else if ($ instanceof $calendar.August) { + _block = 8; + } else if ($ instanceof $calendar.September) { + _block = 9; + } else if ($ instanceof $calendar.October) { + _block = 10; + } else if ($ instanceof $calendar.November) { + _block = 11; + } else { + _block = 12; + } + let month = _block; + return from_date_time( + date.year, + month, + date.day, + time.hours, + time.minutes, + time.seconds, + time.nanoseconds, + $float.round($duration.to_seconds(offset)), + ); +} + +/** + * Parses an [RFC 3339 formatted time string][spec] into a `Timestamp`. + * + * [spec]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6 + * + * # Examples + * + * ```gleam + * let assert Ok(ts) = timestamp.parse_rfc3339("1970-01-01T00:00:01Z") + * timestamp.to_unix_seconds_and_nanoseconds(ts) + * // -> #(1, 0) + * ``` + * + * Parsing an invalid timestamp returns an error. + * + * ```gleam + * let assert Error(Nil) = timestamp.parse_rfc3339("1995-10-31") + * ``` + * + * ## Time zones + * + * It may at first seem that the RFC 3339 format includes timezone + * information, as it can specify an offset such as `Z` or `+3`, so why does + * this function not return calendar time with a time zone? There are multiple + * reasons: + * + * - RFC 3339's timestamp format is based on calendar time, but it is + * unambigous, so it can be converted into epoch time when being parsed. It + * is always better to internally use epoch time to represent unambiguous + * points in time, so we perform that conversion as a convenience and to + * ensure that programmers with less time experience don't accidentally use + * a less suitable time representation. + * + * - RFC 3339's contains _calendar time offset_ information, not time zone + * information. This is enough to convert it to an unambiguous timestamp, + * but it is not enough information to reliably work with calendar time. + * Without the time zone and the time zone database it's not possible to + * know what time period that offset is valid for, so it cannot be used + * without risk of bugs. + * + * ## Behaviour details + * + * - Follows the grammar specified in section 5.6 Internet Date/Time Format of + * RFC 3339 . + * - The `T` and `Z` characters may alternatively be lower case `t` or `z`, + * respectively. + * - Full dates and full times must be separated by `T` or `t`. A space is also + * permitted. + * - Leap seconds rules are not considered. That is, any timestamp may + * specify digts `00` - `60` for the seconds. + * - Any part of a fractional second that cannot be represented in the + * nanosecond precision is tructated. That is, for the time string, + * `"1970-01-01T00:00:00.1234567899Z"`, the fractional second `.1234567899` + * will be represented as `123_456_789` in the `Timestamp`. + */ +export function parse_rfc3339(input) { + let bytes = $bit_array.from_string(input); + return $result.try$( + parse_year(bytes), + (_use0) => { + let year; + let bytes$1; + year = _use0[0]; + bytes$1 = _use0[1]; + return $result.try$( + accept_byte(bytes$1, byte_minus), + (bytes) => { + return $result.try$( + parse_month(bytes), + (_use0) => { + let month; + let bytes$1; + month = _use0[0]; + bytes$1 = _use0[1]; + return $result.try$( + accept_byte(bytes$1, byte_minus), + (bytes) => { + return $result.try$( + parse_day(bytes, year, month), + (_use0) => { + let day; + let bytes$1; + day = _use0[0]; + bytes$1 = _use0[1]; + return $result.try$( + accept_date_time_separator(bytes$1), + (bytes) => { + return $result.try$( + parse_hours(bytes), + (_use0) => { + let hours; + let bytes$1; + hours = _use0[0]; + bytes$1 = _use0[1]; + return $result.try$( + accept_byte(bytes$1, byte_colon), + (bytes) => { + return $result.try$( + parse_minutes(bytes), + (_use0) => { + let minutes; + let bytes$1; + minutes = _use0[0]; + bytes$1 = _use0[1]; + return $result.try$( + accept_byte(bytes$1, byte_colon), + (bytes) => { + return $result.try$( + parse_seconds(bytes), + (_use0) => { + let seconds; + let bytes$1; + seconds = _use0[0]; + bytes$1 = _use0[1]; + return $result.try$( + parse_second_fraction_as_nanoseconds( + bytes$1, + ), + (_use0) => { + let second_fraction_as_nanoseconds; + let bytes$2; + second_fraction_as_nanoseconds = _use0[0]; + bytes$2 = _use0[1]; + return $result.try$( + parse_offset(bytes$2), + (_use0) => { + let offset_seconds; + let bytes$3; + offset_seconds = _use0[0]; + bytes$3 = _use0[1]; + return $result.try$( + accept_empty(bytes$3), + (_use0) => { + + return new Ok( + from_date_time( + year, + month, + day, + hours, + minutes, + seconds, + second_fraction_as_nanoseconds, + offset_seconds, + ), + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); + }, + ); +} + +/** + * The epoch of Unix time, which is 00:00:00 UTC on 1 January 1970. + */ +export const unix_epoch = /* @__PURE__ */ new Timestamp(0, 0); diff --git a/build/dev/javascript/gleam_time/gleam@time@calendar.erl b/build/dev/javascript/gleam_time/gleam@time@calendar.erl new file mode 100644 index 0000000..e295d09 --- /dev/null +++ b/build/dev/javascript/gleam_time/gleam@time@calendar.erl @@ -0,0 +1,468 @@ +-module(gleam@time@calendar). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/time/calendar.gleam"). +-export([local_offset/0, month_to_string/1, month_to_int/1, month_from_int/1, is_leap_year/1, is_valid_date/1, is_valid_time_of_day/1, naive_date_compare/2]). +-export_type([date/0, time_of_day/0, month/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " This module is for working with the Gregorian calendar, established by\n" + " Pope Gregory XIII in 1582!\n" + "\n" + " ## When should you use this module?\n" + "\n" + " > **tldr:** You probably want to use [`gleam/time/timestamp`](./timestamp.html)\n" + " > instead!\n" + "\n" + " Calendar time is difficult to work with programmatically, it is the source\n" + " of most time-related bugs in software. Compared to _epoch time_, which the\n" + " `gleam/time/timestamp` module uses, there are many disadvantages to\n" + " calendar time:\n" + "\n" + " - They are ambiguous if you don't know what time-zone is being used.\n" + "\n" + " - A time-zone database is required to understand calendar time even when\n" + " you have the time zone. These are large and your program has to\n" + " continously be updated as new versions of the database are published.\n" + "\n" + " - The type permits invalid states. e.g. `days` could be set to the number\n" + " 32, but this should not be possible!\n" + "\n" + " - There is not a single unique canonical value for each point in time,\n" + " thanks to time zones. Two different `Date` + `TimeOfDay` value pairs\n" + " could represent the same point in time. This means that you can't check\n" + " for time equality with `==` when using calendar types.\n" + "\n" + " - They are computationally complex, using a more memory to represent and\n" + " requiring a lot more CPU time to manipulate.\n" + "\n" + " There are also advantages to calendar time:\n" + "\n" + " - Calendar time is how human's talk about time, so if you want to show a\n" + " time or take a time from a human user then calendar time will make it\n" + " easier for them.\n" + "\n" + " - They can represent more abstract time periods such as \"New Year's Day\".\n" + " This may seem like an exact window of time at first, but really the\n" + " definition of \"New Year's Day\" is more fuzzy than that. When it starts\n" + " and ends will depend where in the world you are, so if you want to refer\n" + " to a day as a global concept instead of a fixed window of time for that\n" + " day in a specific location, then calendar time can represent that.\n" + "\n" + " So when should you use calendar time? These are our recommendations:\n" + "\n" + " - Default to `gleam/time/timestamp`, which is epoch time. It is\n" + " unambiguous, efficient, and significantly less likely to result in logic\n" + " bugs.\n" + "\n" + " - When writing time to a database or other data storage use epoch time,\n" + " using whatever epoch format it supports. For example, PostgreSQL\n" + " `timestamp` and `timestampz` are both epoch time, and `timestamp` is\n" + " preferred as it is more straightforward to use as your application is\n" + " also using epoch time.\n" + "\n" + " - When communicating with other computer systems continue to use epoch\n" + " time. For example, when sending times to another program you could\n" + " encode time as UNIX timestamps (seconds since 00:00:00 UTC on 1 January\n" + " 1970).\n" + "\n" + " - When communicating with humans use epoch time internally, and convert\n" + " to-and-from calendar time at the last moment, when iteracting with the\n" + " human user. It may also help the users to also show the time as a fuzzy\n" + " duration from the present time, such as \"about 4 days ago\".\n" + "\n" + " - When representing \"fuzzy\" human time concepts that don't exact periods\n" + " in time, such as \"one month\" (varies depending on which month, which\n" + " year, and in which time zone) and \"Christmas Day\" (varies depending on\n" + " which year and time zone) then use calendar time.\n" + "\n" + " Any time you do use calendar time you should be extra careful! It is very\n" + " easy to make mistake with. Avoid it where possible.\n" + "\n" + " ## Time zone offsets\n" + "\n" + " This package includes the `utc_offset` value and the `local_offset`\n" + " function, which are the offset for the UTC time zone and get the time\n" + " offset the computer running the program is configured to respectively.\n" + "\n" + " If you need to use other offsets in your program then you will need to get\n" + " them from somewhere else, such as from a package which loads the\n" + " [IANA Time Zone Database](https://www.iana.org/time-zones), or from the\n" + " website visitor's web browser, which your frontend can send for you.\n" + "\n" + " ## Use in APIs\n" + "\n" + " If you are making an API such as a HTTP JSON API you are encouraged to use\n" + " Unix timestamps instead of calendar times.\n" +). + +-type date() :: {date, integer(), month(), integer()}. + +-type time_of_day() :: {time_of_day, integer(), integer(), integer(), integer()}. + +-type month() :: january | + february | + march | + april | + may | + june | + july | + august | + september | + october | + november | + december. + +-file("src/gleam/time/calendar.gleam", 147). +?DOC( + " Get the offset for the computer's currently configured time zone.\n" + "\n" + " Note this may not be the time zone that is correct to use for your user.\n" + " For example, if you are making a web application that runs on a server you\n" + " want _their_ computer's time zone, not yours.\n" + "\n" + " This is the _current local_ offset, not the current local time zone. This\n" + " means that while it will result in the expected outcome for the current\n" + " time, it may result in unexpected output if used with other timestamps. For\n" + " example: a timestamp that would locally be during daylight savings time if\n" + " is it not currently daylight savings time when this function is called.\n" +). +-spec local_offset() -> gleam@time@duration:duration(). +local_offset() -> + gleam@time@duration:seconds(gleam_time_ffi:local_time_offset_seconds()). + +-file("src/gleam/time/calendar.gleam", 163). +?DOC( + " Returns the English name for a month.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " month_to_string(April)\n" + " // -> \"April\"\n" + " ```\n" +). +-spec month_to_string(month()) -> binary(). +month_to_string(Month) -> + case Month of + january -> + <<"January"/utf8>>; + + february -> + <<"February"/utf8>>; + + march -> + <<"March"/utf8>>; + + april -> + <<"April"/utf8>>; + + may -> + <<"May"/utf8>>; + + june -> + <<"June"/utf8>>; + + july -> + <<"July"/utf8>>; + + august -> + <<"August"/utf8>>; + + september -> + <<"September"/utf8>>; + + october -> + <<"October"/utf8>>; + + november -> + <<"November"/utf8>>; + + december -> + <<"December"/utf8>> + end. + +-file("src/gleam/time/calendar.gleam", 188). +?DOC( + " Returns the number for the month, where January is 1 and December is 12.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " month_to_int(January)\n" + " // -> 1\n" + " ```\n" +). +-spec month_to_int(month()) -> integer(). +month_to_int(Month) -> + case Month of + january -> + 1; + + february -> + 2; + + march -> + 3; + + april -> + 4; + + may -> + 5; + + june -> + 6; + + july -> + 7; + + august -> + 8; + + september -> + 9; + + october -> + 10; + + november -> + 11; + + december -> + 12 + end. + +-file("src/gleam/time/calendar.gleam", 213). +?DOC( + " Returns the month for a given number, where January is 1 and December is 12.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " month_from_int(1)\n" + " // -> Ok(January)\n" + " ```\n" +). +-spec month_from_int(integer()) -> {ok, month()} | {error, nil}. +month_from_int(Month) -> + case Month of + 1 -> + {ok, january}; + + 2 -> + {ok, february}; + + 3 -> + {ok, march}; + + 4 -> + {ok, april}; + + 5 -> + {ok, may}; + + 6 -> + {ok, june}; + + 7 -> + {ok, july}; + + 8 -> + {ok, august}; + + 9 -> + {ok, september}; + + 10 -> + {ok, october}; + + 11 -> + {ok, november}; + + 12 -> + {ok, december}; + + _ -> + {error, nil} + end. + +-file("src/gleam/time/calendar.gleam", 290). +?DOC( + " Determines if a given year is a leap year.\n" + "\n" + " A leap year occurs every 4 years, except for years divisible by 100,\n" + " unless they are also divisible by 400.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " is_leap_year(2024)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_leap_year(2023)\n" + " // -> False\n" + " ```\n" +). +-spec is_leap_year(integer()) -> boolean(). +is_leap_year(Year) -> + case (Year rem 400) =:= 0 of + true -> + true; + + false -> + case (Year rem 100) =:= 0 of + true -> + false; + + false -> + (Year rem 4) =:= 0 + end + end. + +-file("src/gleam/time/calendar.gleam", 254). +?DOC( + " Checks if a given date is valid.\n" + "\n" + " This function properly accounts for leap years when validating February days.\n" + " A leap year occurs every 4 years, except for years divisible by 100,\n" + " unless they are also divisible by 400.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " is_valid_date(Date(2023, April, 15))\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_valid_date(Date(2023, April, 31))\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " is_valid_date(Date(2024, February, 29))\n" + " // -> True (2024 is a leap year)\n" + " ```\n" +). +-spec is_valid_date(date()) -> boolean(). +is_valid_date(Date) -> + {date, Year, Month, Day} = Date, + case Day < 1 of + true -> + false; + + false -> + case Month of + january -> + Day =< 31; + + march -> + Day =< 31; + + may -> + Day =< 31; + + july -> + Day =< 31; + + august -> + Day =< 31; + + october -> + Day =< 31; + + december -> + Day =< 31; + + april -> + Day =< 30; + + june -> + Day =< 30; + + september -> + Day =< 30; + + november -> + Day =< 30; + + february -> + Max_february_days = case is_leap_year(Year) of + true -> + 29; + + false -> + 28 + end, + Day =< Max_february_days + end + end. + +-file("src/gleam/time/calendar.gleam", 313). +?DOC( + " Checks if a time of day is valid.\n" + "\n" + " Validates that hours are 0-23, minutes are 0-59, seconds are 0-59,\n" + " and nanoseconds are 0-999,999,999.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " is_valid_time_of_day(TimeOfDay(12, 30, 45, 123456789))\n" + " // -> True\n" + " ```\n" +). +-spec is_valid_time_of_day(time_of_day()) -> boolean(). +is_valid_time_of_day(Time) -> + {time_of_day, Hours, Minutes, Seconds, Nanoseconds} = Time, + (((((((Hours >= 0) andalso (Hours =< 23)) andalso (Minutes >= 0)) andalso (Minutes + =< 59)) + andalso (Seconds >= 0)) + andalso (Seconds =< 59)) + andalso (Nanoseconds >= 0)) + andalso (Nanoseconds =< 999999999). + +-file("src/gleam/time/calendar.gleam", 340). +?DOC( + " Naively compares two dates without any time zone information, returning an\n" + " order.\n" + "\n" + " ## Correctness\n" + "\n" + " This function compares dates without any time zone information, only using\n" + " the rules for the gregorian calendar. This is typically sufficient, but be\n" + " aware that in reality some time zones will change their calendar date\n" + " occasionally. This can result in days being skipped, out of order, or\n" + " happening multiple times.\n" + "\n" + " If you need real-world correct time ordering then use the\n" + " `gleam/time/timestamp` module instead.\n" +). +-spec naive_date_compare(date(), date()) -> gleam@order:order(). +naive_date_compare(One, Other) -> + _pipe = gleam@int:compare(erlang:element(2, One), erlang:element(2, Other)), + _pipe@1 = gleam@order:lazy_break_tie( + _pipe, + fun() -> + gleam@int:compare( + month_to_int(erlang:element(3, One)), + month_to_int(erlang:element(3, Other)) + ) + end + ), + gleam@order:lazy_break_tie( + _pipe@1, + fun() -> + gleam@int:compare(erlang:element(4, One), erlang:element(4, Other)) + end + ). diff --git a/build/dev/javascript/gleam_time/gleam@time@duration.erl b/build/dev/javascript/gleam_time/gleam@time@duration.erl new file mode 100644 index 0000000..7ba7ad2 --- /dev/null +++ b/build/dev/javascript/gleam_time/gleam@time@duration.erl @@ -0,0 +1,381 @@ +-module(gleam@time@duration). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/time/duration.gleam"). +-export([approximate/1, compare/2, difference/2, add/2, seconds/1, minutes/1, hours/1, milliseconds/1, nanoseconds/1, to_seconds/1, to_seconds_and_nanoseconds/1, to_iso8601_string/1]). +-export_type([duration/0, unit/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-opaque duration() :: {duration, integer(), integer()}. + +-type unit() :: nanosecond | + microsecond | + millisecond | + second | + minute | + hour | + day | + week | + month | + year. + +-file("src/gleam/time/duration.gleam", 110). +?DOC( + " Ensure the duration is represented with `nanoseconds` being positive and\n" + " less than 1 second.\n" + "\n" + " This function does not change the amount of time that the duratoin refers\n" + " to, it only adjusts the values used to represent the time.\n" +). +-spec normalise(duration()) -> duration(). +normalise(Duration) -> + Multiplier = 1000000000, + Nanoseconds = case Multiplier of + 0 -> 0; + Gleam@denominator -> erlang:element(3, Duration) rem Gleam@denominator + end, + Overflow = erlang:element(3, Duration) - Nanoseconds, + Seconds = erlang:element(2, Duration) + (case Multiplier of + 0 -> 0; + Gleam@denominator@1 -> Overflow div Gleam@denominator@1 + end), + case Nanoseconds >= 0 of + true -> + {duration, Seconds, Nanoseconds}; + + false -> + {duration, Seconds - 1, Multiplier + Nanoseconds} + end. + +-file("src/gleam/time/duration.gleam", 76). +?DOC( + " Convert a duration to a number of the largest number of a unit, serving as\n" + " a rough description of the duration that a human can understand.\n" + "\n" + " The size used for each unit are described in the documentation for the\n" + " `Unit` type.\n" + "\n" + " ```gleam\n" + " seconds(125)\n" + " |> approximate\n" + " // -> #(2, Minute)\n" + " ```\n" + "\n" + " This function rounds _towards zero_. This means that if a duration is just\n" + " short of 2 days then it will approximate to 1 day.\n" + "\n" + " ```gleam\n" + " hours(47)\n" + " |> approximate\n" + " // -> #(1, Day)\n" + " ```\n" +). +-spec approximate(duration()) -> {integer(), unit()}. +approximate(Duration) -> + {duration, S, Ns} = Duration, + Minute = 60, + Hour = Minute * 60, + Day = Hour * 24, + Week = Day * 7, + Year = (Day * 365) + (Hour * 6), + Month = Year div 12, + Microsecond = 1000, + Millisecond = Microsecond * 1000, + case nil of + _ when S < 0 -> + {Amount, Unit} = begin + _pipe = {duration, - S, - Ns}, + _pipe@1 = normalise(_pipe), + approximate(_pipe@1) + end, + {- Amount, Unit}; + + _ when S >= Year -> + {case Year of + 0 -> 0; + Gleam@denominator -> S div Gleam@denominator + end, year}; + + _ when S >= Month -> + {case Month of + 0 -> 0; + Gleam@denominator@1 -> S div Gleam@denominator@1 + end, month}; + + _ when S >= Week -> + {case Week of + 0 -> 0; + Gleam@denominator@2 -> S div Gleam@denominator@2 + end, week}; + + _ when S >= Day -> + {case Day of + 0 -> 0; + Gleam@denominator@3 -> S div Gleam@denominator@3 + end, day}; + + _ when S >= Hour -> + {case Hour of + 0 -> 0; + Gleam@denominator@4 -> S div Gleam@denominator@4 + end, hour}; + + _ when S >= Minute -> + {case Minute of + 0 -> 0; + Gleam@denominator@5 -> S div Gleam@denominator@5 + end, minute}; + + _ when S > 0 -> + {S, second}; + + _ when Ns >= Millisecond -> + {case Millisecond of + 0 -> 0; + Gleam@denominator@6 -> Ns div Gleam@denominator@6 + end, millisecond}; + + _ when Ns >= Microsecond -> + {case Microsecond of + 0 -> 0; + Gleam@denominator@7 -> Ns div Gleam@denominator@7 + end, microsecond}; + + _ -> + {Ns, nanosecond} + end. + +-file("src/gleam/time/duration.gleam", 140). +?DOC( + " Compare one duration to another, indicating whether the first spans a\n" + " larger amount of time (and so is greater) or smaller amount of time (and so\n" + " is lesser) than the second.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " compare(seconds(1), seconds(2))\n" + " // -> order.Lt\n" + " ```\n" + "\n" + " Whether a duration is negative or positive doesn't matter for comparing\n" + " them, only the amount of time spanned matters.\n" + "\n" + " ```gleam\n" + " compare(seconds(-2), seconds(1))\n" + " // -> order.Gt\n" + " ```\n" +). +-spec compare(duration(), duration()) -> gleam@order:order(). +compare(Left, Right) -> + Parts = fun(X) -> case erlang:element(2, X) >= 0 of + true -> + {erlang:element(2, X), erlang:element(3, X)}; + + false -> + {(erlang:element(2, X) * -1) - 1, + 1000000000 - erlang:element(3, X)} + end end, + {Ls, Lns} = Parts(Left), + {Rs, Rns} = Parts(Right), + _pipe = gleam@int:compare(Ls, Rs), + gleam@order:break_tie(_pipe, gleam@int:compare(Lns, Rns)). + +-file("src/gleam/time/duration.gleam", 164). +?DOC( + " Calculate the difference between two durations.\n" + "\n" + " This is effectively substracting the first duration from the second.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " difference(seconds(1), seconds(5))\n" + " // -> seconds(4)\n" + " ```\n" +). +-spec difference(duration(), duration()) -> duration(). +difference(Left, Right) -> + _pipe = {duration, + erlang:element(2, Right) - erlang:element(2, Left), + erlang:element(3, Right) - erlang:element(3, Left)}, + normalise(_pipe). + +-file("src/gleam/time/duration.gleam", 178). +?DOC( + " Add two durations together.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " add(seconds(1), seconds(5))\n" + " // -> seconds(6)\n" + " ```\n" +). +-spec add(duration(), duration()) -> duration(). +add(Left, Right) -> + _pipe = {duration, + erlang:element(2, Left) + erlang:element(2, Right), + erlang:element(3, Left) + erlang:element(3, Right)}, + normalise(_pipe). + +-file("src/gleam/time/duration.gleam", 225). +-spec nanosecond_digits(integer(), integer(), binary()) -> binary(). +nanosecond_digits(N, Position, Acc) -> + case Position of + 9 -> + Acc; + + _ when (Acc =:= <<""/utf8>>) andalso ((N rem 10) =:= 0) -> + nanosecond_digits(N div 10, Position + 1, Acc); + + _ -> + Acc@1 = <<(erlang:integer_to_binary(N rem 10))/binary, Acc/binary>>, + nanosecond_digits(N div 10, Position + 1, Acc@1) + end. + +-file("src/gleam/time/duration.gleam", 239). +?DOC(" Create a duration of a number of seconds.\n"). +-spec seconds(integer()) -> duration(). +seconds(Amount) -> + {duration, Amount, 0}. + +-file("src/gleam/time/duration.gleam", 244). +?DOC(" Create a duration of a number of minutes.\n"). +-spec minutes(integer()) -> duration(). +minutes(Amount) -> + seconds(Amount * 60). + +-file("src/gleam/time/duration.gleam", 249). +?DOC(" Create a duration of a number of hours.\n"). +-spec hours(integer()) -> duration(). +hours(Amount) -> + seconds((Amount * 60) * 60). + +-file("src/gleam/time/duration.gleam", 254). +?DOC(" Create a duration of a number of milliseconds.\n"). +-spec milliseconds(integer()) -> duration(). +milliseconds(Amount) -> + Remainder = Amount rem 1000, + Overflow = Amount - Remainder, + Nanoseconds = Remainder * 1000000, + Seconds = Overflow div 1000, + _pipe = {duration, Seconds, Nanoseconds}, + normalise(_pipe). + +-file("src/gleam/time/duration.gleam", 273). +?DOC( + " Create a duration of a number of nanoseconds.\n" + "\n" + " # JavaScript int limitations\n" + "\n" + " Remember that JavaScript can only perfectly represent ints between positive\n" + " and negative 9,007,199,254,740,991! If you use a single call to this\n" + " function to create durations larger than that number of nanoseconds then\n" + " you will likely not get exactly the value you expect. Use `seconds` and\n" + " `milliseconds` as much as possible for large durations.\n" +). +-spec nanoseconds(integer()) -> duration(). +nanoseconds(Amount) -> + _pipe = {duration, 0, Amount}, + normalise(_pipe). + +-file("src/gleam/time/duration.gleam", 283). +?DOC( + " Convert the duration to a number of seconds.\n" + "\n" + " There may be some small loss of precision due to `Duration` being\n" + " nanosecond accurate and `Float` not being able to represent this.\n" +). +-spec to_seconds(duration()) -> float(). +to_seconds(Duration) -> + Seconds = erlang:float(erlang:element(2, Duration)), + Nanoseconds = erlang:float(erlang:element(3, Duration)), + Seconds + (Nanoseconds / 1000000000.0). + +-file("src/gleam/time/duration.gleam", 292). +?DOC( + " Convert the duration to a number of seconds and nanoseconds. There is no\n" + " loss of precision with this conversion on any target.\n" +). +-spec to_seconds_and_nanoseconds(duration()) -> {integer(), integer()}. +to_seconds_and_nanoseconds(Duration) -> + {erlang:element(2, Duration), erlang:element(3, Duration)}. + +-file("src/gleam/time/duration.gleam", 192). +?DOC( + " Convert the duration to an [ISO8601][1] formatted duration string.\n" + "\n" + " The ISO8601 duration format is ambiguous without context due to months and\n" + " years having different lengths, and because of leap seconds. This function\n" + " encodes the duration as days, hours, and seconds without any leap seconds.\n" + " Be sure to take this into account when using the duration strings.\n" + "\n" + " [1]: https://en.wikipedia.org/wiki/ISO_8601#Durations\n" +). +-spec to_iso8601_string(duration()) -> binary(). +to_iso8601_string(Duration) -> + gleam@bool:guard( + Duration =:= {duration, 0, 0}, + <<"PT0S"/utf8>>, + fun() -> + Split = fun(Total, Limit) -> + Amount = case Limit of + 0 -> 0; + Gleam@denominator -> Total rem Gleam@denominator + end, + Remainder = case Limit of + 0 -> 0; + Gleam@denominator@1 -> (Total - Amount) div Gleam@denominator@1 + end, + {Amount, Remainder} + end, + {Seconds, Rest} = Split(erlang:element(2, Duration), 60), + {Minutes, Rest@1} = Split(Rest, 60), + {Hours, Rest@2} = Split(Rest@1, 24), + Days = Rest@2, + Add = fun(Out, Value, Unit) -> case Value of + 0 -> + Out; + + _ -> + <<<>/binary, + Unit/binary>> + end end, + Output = begin + _pipe = <<"P"/utf8>>, + _pipe@1 = Add(_pipe, Days, <<"D"/utf8>>), + _pipe@2 = gleam@string:append(_pipe@1, <<"T"/utf8>>), + _pipe@3 = Add(_pipe@2, Hours, <<"H"/utf8>>), + Add(_pipe@3, Minutes, <<"M"/utf8>>) + end, + case {Seconds, erlang:element(3, Duration)} of + {0, 0} -> + Output; + + {_, 0} -> + <<<>/binary, + "S"/utf8>>; + + {_, _} -> + F = nanosecond_digits( + erlang:element(3, Duration), + 0, + <<""/utf8>> + ), + <<<<<<<>/binary, + "."/utf8>>/binary, + F/binary>>/binary, + "S"/utf8>> + end + end + ). diff --git a/build/dev/javascript/gleam_time/gleam@time@timestamp.erl b/build/dev/javascript/gleam_time/gleam@time@timestamp.erl new file mode 100644 index 0000000..0d7413a --- /dev/null +++ b/build/dev/javascript/gleam_time/gleam@time@timestamp.erl @@ -0,0 +1,1188 @@ +-module(gleam@time@timestamp). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/time/timestamp.gleam"). +-export([compare/2, system_time/0, difference/2, add/2, to_calendar/2, to_rfc3339/2, from_unix_seconds/1, from_unix_seconds_and_nanoseconds/2, to_unix_seconds/1, to_unix_seconds_and_nanoseconds/1, from_calendar/3, parse_rfc3339/1]). +-export_type([timestamp/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Welcome to the timestamp module! This module and its `Timestamp` type are\n" + " what you will be using most commonly when working with time in Gleam.\n" + "\n" + " A timestamp represents a moment in time, represented as an amount of time\n" + " since the calendar time 00:00:00 UTC on 1 January 1970, also known as the\n" + " _Unix epoch_.\n" + "\n" + " # Wall clock time and monotonicity\n" + "\n" + " Time is very complicated, especially on computers! While they generally do\n" + " a good job of keeping track of what the time is, computers can get\n" + " out-of-sync and start to report a time that is too late or too early. Most\n" + " computers use \"network time protocol\" to tell each other what they think\n" + " the time is, and computers that realise they are running too fast or too\n" + " slow will adjust their clock to correct it. When this happens it can seem\n" + " to your program that the current time has changed, and it may have even\n" + " jumped backwards in time!\n" + "\n" + " This measure of time is called _wall clock time_, and it is what people\n" + " commonly think of when they think of time. It is important to be aware that\n" + " it can go backwards, and your program must not rely on it only ever going\n" + " forwards at a steady rate. For example, for tracking what order events happen\n" + " in. \n" + "\n" + " This module uses wall clock time. If your program needs time values to always\n" + " increase you will need a _monotonic_ time instead. It's uncommon that you\n" + " would need monotonic time, one example might be if you're making a\n" + " benchmarking framework.\n" + "\n" + " The exact way that time works will depend on what runtime you use. The\n" + " Erlang documentation on time has a lot of detail about time generally as well\n" + " as how it works on the BEAM, it is worth reading.\n" + " .\n" + "\n" + " # Converting to local calendar time\n" + "\n" + " Timestamps don't take into account time zones, so a moment in time will\n" + " have the same timestamp value regardless of where you are in the world. To\n" + " convert them to local time you will need to know the offset for the time\n" + " zone you wish to use, likely from a time zone database. See the\n" + " `gleam/time/calendar` module for more information.\n" + "\n" +). + +-opaque timestamp() :: {timestamp, integer(), integer()}. + +-file("src/gleam/time/timestamp.gleam", 119). +?DOC( + " Ensure the time is represented with `nanoseconds` being positive and less\n" + " than 1 second.\n" + "\n" + " This function does not change the time that the timestamp refers to, it\n" + " only adjusts the values used to represent the time.\n" +). +-spec normalise(timestamp()) -> timestamp(). +normalise(Timestamp) -> + Multiplier = 1000000000, + Nanoseconds = case Multiplier of + 0 -> 0; + Gleam@denominator -> erlang:element(3, Timestamp) rem Gleam@denominator + end, + Overflow = erlang:element(3, Timestamp) - Nanoseconds, + Seconds = erlang:element(2, Timestamp) + (case Multiplier of + 0 -> 0; + Gleam@denominator@1 -> Overflow div Gleam@denominator@1 + end), + case Nanoseconds >= 0 of + true -> + {timestamp, Seconds, Nanoseconds}; + + false -> + {timestamp, Seconds - 1, Multiplier + Nanoseconds} + end. + +-file("src/gleam/time/timestamp.gleam", 141). +?DOC( + " Compare one timestamp to another, indicating whether the first is further\n" + " into the future (greater) or further into the past (lesser) than the\n" + " second.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " compare(from_unix_seconds(1), from_unix_seconds(2))\n" + " // -> order.Lt\n" + " ```\n" +). +-spec compare(timestamp(), timestamp()) -> gleam@order:order(). +compare(Left, Right) -> + gleam@order:break_tie( + gleam@int:compare(erlang:element(2, Left), erlang:element(2, Right)), + gleam@int:compare(erlang:element(3, Left), erlang:element(3, Right)) + ). + +-file("src/gleam/time/timestamp.gleam", 160). +?DOC( + " Get the current system time.\n" + "\n" + " Note this time is not unique or monotonic, it could change at any time or\n" + " even go backwards! The exact behaviour will depend on the runtime used. See\n" + " the module documentation for more information.\n" + "\n" + " On Erlang this uses [`erlang:system_time/1`][1]. On JavaScript this uses\n" + " [`Date.now`][2].\n" + "\n" + " [1]: https://www.erlang.org/doc/apps/erts/erlang#system_time/1\n" + " [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now\n" +). +-spec system_time() -> timestamp(). +system_time() -> + {Seconds, Nanoseconds} = gleam_time_ffi:system_time(), + normalise({timestamp, Seconds, Nanoseconds}). + +-file("src/gleam/time/timestamp.gleam", 180). +?DOC( + " Calculate the difference between two timestamps.\n" + "\n" + " This is effectively substracting the first timestamp from the second.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " difference(from_unix_seconds(1), from_unix_seconds(5))\n" + " // -> duration.seconds(4)\n" + " ```\n" +). +-spec difference(timestamp(), timestamp()) -> gleam@time@duration:duration(). +difference(Left, Right) -> + Seconds = gleam@time@duration:seconds( + erlang:element(2, Right) - erlang:element(2, Left) + ), + Nanoseconds = gleam@time@duration:nanoseconds( + erlang:element(3, Right) - erlang:element(3, Left) + ), + gleam@time@duration:add(Seconds, Nanoseconds). + +-file("src/gleam/time/timestamp.gleam", 195). +?DOC( + " Add a duration to a timestamp.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " add(from_unix_seconds(1000), duration.seconds(5))\n" + " // -> from_unix_seconds(1005)\n" + " ```\n" +). +-spec add(timestamp(), gleam@time@duration:duration()) -> timestamp(). +add(Timestamp, Duration) -> + {Seconds, Nanoseconds} = gleam@time@duration:to_seconds_and_nanoseconds( + Duration + ), + _pipe = {timestamp, + erlang:element(2, Timestamp) + Seconds, + erlang:element(3, Timestamp) + Nanoseconds}, + normalise(_pipe). + +-file("src/gleam/time/timestamp.gleam", 262). +-spec pad_digit(integer(), integer()) -> binary(). +pad_digit(Digit, Desired_length) -> + _pipe = erlang:integer_to_binary(Digit), + gleam@string:pad_start(_pipe, Desired_length, <<"0"/utf8>>). + +-file("src/gleam/time/timestamp.gleam", 308). +-spec duration_to_minutes(gleam@time@duration:duration()) -> integer(). +duration_to_minutes(Duration) -> + erlang:round(gleam@time@duration:to_seconds(Duration) / 60.0). + +-file("src/gleam/time/timestamp.gleam", 370). +-spec modulo(integer(), integer()) -> integer(). +modulo(N, M) -> + case gleam@int:modulo(N, M) of + {ok, N@1} -> + N@1; + + {error, _} -> + 0 + end. + +-file("src/gleam/time/timestamp.gleam", 377). +-spec floored_div(integer(), float()) -> integer(). +floored_div(Numerator, Denominator) -> + N = case Denominator of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> erlang:float(Numerator) / Gleam@denominator + end, + erlang:round(math:floor(N)). + +-file("src/gleam/time/timestamp.gleam", 383). +-spec to_civil(integer()) -> {integer(), integer(), integer()}. +to_civil(Minutes) -> + Raw_day = floored_div(Minutes, (60.0 * 24.0)) + 719468, + Era = case Raw_day >= 0 of + true -> + Raw_day div 146097; + + false -> + (Raw_day - 146096) div 146097 + end, + Day_of_era = Raw_day - (Era * 146097), + Year_of_era = (((Day_of_era - (Day_of_era div 1460)) + (Day_of_era div 36524)) + - (Day_of_era div 146096)) + div 365, + Year = Year_of_era + (Era * 400), + Day_of_year = Day_of_era - (((365 * Year_of_era) + (Year_of_era div 4)) - (Year_of_era + div 100)), + Mp = ((5 * Day_of_year) + 2) div 153, + Month = case Mp < 10 of + true -> + Mp + 3; + + false -> + Mp - 9 + end, + Day = (Day_of_year - (((153 * Mp) + 2) div 5)) + 1, + Year@1 = case Month =< 2 of + true -> + Year + 1; + + false -> + Year + end, + {Year@1, Month, Day}. + +-file("src/gleam/time/timestamp.gleam", 312). +-spec to_calendar_from_offset(timestamp(), integer()) -> {integer(), + integer(), + integer(), + integer(), + integer(), + integer()}. +to_calendar_from_offset(Timestamp, Offset) -> + Total = erlang:element(2, Timestamp) + (Offset * 60), + Seconds = modulo(Total, 60), + Total_minutes = floored_div(Total, 60.0), + Minutes = modulo(Total, 60 * 60) div 60, + Hours = case (60 * 60) of + 0 -> 0; + Gleam@denominator -> modulo(Total, (24 * 60) * 60) div Gleam@denominator + end, + {Year, Month, Day} = to_civil(Total_minutes), + {Year, Month, Day, Hours, Minutes, Seconds}. + +-file("src/gleam/time/timestamp.gleam", 281). +?DOC( + " Convert a `Timestamp` to calendar time, suitable for presenting to a human\n" + " to read.\n" + "\n" + " If you want a machine to use the time value then you should not use this\n" + " function and should instead keep it as a timestamp. See the documentation\n" + " for the `gleam/time/calendar` module for more information.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " timestamp.from_unix_seconds(0)\n" + " |> timestamp.to_calendar(calendar.utc_offset)\n" + " // -> #(Date(1970, January, 1), TimeOfDay(0, 0, 0, 0))\n" + " ```\n" +). +-spec to_calendar(timestamp(), gleam@time@duration:duration()) -> {gleam@time@calendar:date(), + gleam@time@calendar:time_of_day()}. +to_calendar(Timestamp, Offset) -> + Offset@1 = duration_to_minutes(Offset), + {Year, Month, Day, Hours, Minutes, Seconds} = to_calendar_from_offset( + Timestamp, + Offset@1 + ), + Month@1 = case Month of + 1 -> + january; + + 2 -> + february; + + 3 -> + march; + + 4 -> + april; + + 5 -> + may; + + 6 -> + june; + + 7 -> + july; + + 8 -> + august; + + 9 -> + september; + + 10 -> + october; + + 11 -> + november; + + _ -> + december + end, + Nanoseconds = erlang:element(3, Timestamp), + Date = {date, Year, Month@1, Day}, + Time = {time_of_day, Hours, Minutes, Seconds, Nanoseconds}, + {Date, Time}. + +-file("src/gleam/time/timestamp.gleam", 446). +-spec do_remove_trailing_zeros(list(integer())) -> list(integer()). +do_remove_trailing_zeros(Reversed_digits) -> + case Reversed_digits of + [] -> + []; + + [Digit | Digits] when Digit =:= 0 -> + do_remove_trailing_zeros(Digits); + + Reversed_digits@1 -> + lists:reverse(Reversed_digits@1) + end. + +-file("src/gleam/time/timestamp.gleam", 440). +?DOC(" Given a list of digits, return new list with any trailing zeros removed.\n"). +-spec remove_trailing_zeros(list(integer())) -> list(integer()). +remove_trailing_zeros(Digits) -> + Reversed_digits = lists:reverse(Digits), + do_remove_trailing_zeros(Reversed_digits). + +-file("src/gleam/time/timestamp.gleam", 461). +-spec do_get_zero_padded_digits(integer(), list(integer()), integer()) -> list(integer()). +do_get_zero_padded_digits(Number, Digits, Count) -> + case Number of + Number@1 when (Number@1 =< 0) andalso (Count >= 9) -> + Digits; + + Number@2 when Number@2 =< 0 -> + do_get_zero_padded_digits(Number@2, [0 | Digits], Count + 1); + + Number@3 -> + Digit = Number@3 rem 10, + Number@4 = floored_div(Number@3, 10.0), + do_get_zero_padded_digits(Number@4, [Digit | Digits], Count + 1) + end. + +-file("src/gleam/time/timestamp.gleam", 457). +?DOC( + " Returns the list of digits of `number`. If the number of digits is less \n" + " than 9, the result is zero-padded at the front.\n" +). +-spec get_zero_padded_digits(integer()) -> list(integer()). +get_zero_padded_digits(Number) -> + do_get_zero_padded_digits(Number, [], 0). + +-file("src/gleam/time/timestamp.gleam", 420). +?DOC( + " Converts nanoseconds into a `String` representation of fractional seconds.\n" + " \n" + " Assumes that `nanoseconds < 1_000_000_000`, which will be true for any \n" + " normalised timestamp.\n" +). +-spec show_second_fraction(integer()) -> binary(). +show_second_fraction(Nanoseconds) -> + case gleam@int:compare(Nanoseconds, 0) of + lt -> + <<""/utf8>>; + + eq -> + <<""/utf8>>; + + gt -> + Second_fraction_part = begin + _pipe = Nanoseconds, + _pipe@1 = get_zero_padded_digits(_pipe), + _pipe@2 = remove_trailing_zeros(_pipe@1), + _pipe@3 = gleam@list:map( + _pipe@2, + fun erlang:integer_to_binary/1 + ), + gleam@string:join(_pipe@3, <<""/utf8>>) + end, + <<"."/utf8, Second_fraction_part/binary>> + end. + +-file("src/gleam/time/timestamp.gleam", 240). +?DOC( + " Convert a timestamp to a RFC 3339 formatted time string, with an offset\n" + " supplied as an additional argument.\n" + "\n" + " The output of this function is also ISO 8601 compatible so long as the\n" + " offset not negative. Offsets have at-most minute precision, so an offset\n" + " with higher precision will be rounded to the nearest minute.\n" + "\n" + " If you are making an API such as a HTTP JSON API you are encouraged to use\n" + " Unix timestamps instead of this format or ISO 8601. Unix timestamps are a\n" + " better choice as they don't contain offset information. Consider:\n" + "\n" + " - UTC offsets are not time zones. This does not and cannot tell us the time\n" + " zone in which the date was recorded. So what are we supposed to do with\n" + " this information?\n" + " - Users typically want dates formatted according to their local time zone.\n" + " What if the provided UTC offset is different from the current user's time\n" + " zone? What are we supposed to do with it then?\n" + " - Despite it being useless (or worse, a source of bugs), the UTC offset\n" + " creates a larger payload to transfer.\n" + "\n" + " They also uses more memory than a unix timestamp. The way they are better\n" + " than Unix timestamp is that it is easier for a human to read them, but\n" + " this is a hinderance that tooling can remedy, and APIs are not primarily\n" + " for humans.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " timestamp.from_unix_seconds_and_nanoseconds(1000, 123_000_000)\n" + " |> to_rfc3339(calendar.utc_offset)\n" + " // -> \"1970-01-01T00:16:40.123Z\"\n" + " ```\n" + "\n" + " ```gleam\n" + " timestamp.from_unix_seconds(1000)\n" + " |> to_rfc3339(duration.seconds(3600))\n" + " // -> \"1970-01-01T01:16:40+01:00\"\n" + " ```\n" +). +-spec to_rfc3339(timestamp(), gleam@time@duration:duration()) -> binary(). +to_rfc3339(Timestamp, Offset) -> + Offset@1 = duration_to_minutes(Offset), + {Years, Months, Days, Hours, Minutes, Seconds} = to_calendar_from_offset( + Timestamp, + Offset@1 + ), + Offset_minutes = modulo(Offset@1, 60), + Offset_hours = gleam@int:absolute_value(floored_div(Offset@1, 60.0)), + N2 = fun(_capture) -> pad_digit(_capture, 2) end, + N4 = fun(_capture@1) -> pad_digit(_capture@1, 4) end, + Out = <<""/utf8>>, + Out@1 = <<<<<<<<<>/binary, "-"/utf8>>/binary, + (N2(Months))/binary>>/binary, + "-"/utf8>>/binary, + (N2(Days))/binary>>, + Out@2 = <>, + Out@3 = <<<<<<<<<>/binary, ":"/utf8>>/binary, + (N2(Minutes))/binary>>/binary, + ":"/utf8>>/binary, + (N2(Seconds))/binary>>, + Out@4 = <>, + case gleam@int:compare(Offset@1, 0) of + eq -> + <>; + + gt -> + <<<<<<<>/binary, (N2(Offset_hours))/binary>>/binary, + ":"/utf8>>/binary, + (N2(Offset_minutes))/binary>>; + + lt -> + <<<<<<<>/binary, (N2(Offset_hours))/binary>>/binary, + ":"/utf8>>/binary, + (N2(Offset_minutes))/binary>> + end. + +-file("src/gleam/time/timestamp.gleam", 611). +-spec is_leap_year(integer()) -> boolean(). +is_leap_year(Year) -> + ((Year rem 4) =:= 0) andalso (((Year rem 100) /= 0) orelse ((Year rem 400) + =:= 0)). + +-file("src/gleam/time/timestamp.gleam", 715). +-spec parse_sign(bitstring()) -> {ok, {binary(), bitstring()}} | {error, nil}. +parse_sign(Bytes) -> + case Bytes of + <<"+"/utf8, Remaining_bytes/binary>> -> + {ok, {<<"+"/utf8>>, Remaining_bytes}}; + + <<"-"/utf8, Remaining_bytes@1/binary>> -> + {ok, {<<"-"/utf8>>, Remaining_bytes@1}}; + + _ -> + {error, nil} + end. + +-file("src/gleam/time/timestamp.gleam", 762). +?DOC(" Accept the given value from `bytes` and move past it if found.\n"). +-spec accept_byte(bitstring(), integer()) -> {ok, bitstring()} | {error, nil}. +accept_byte(Bytes, Value) -> + case Bytes of + <> when Byte =:= Value -> + {ok, Remaining_bytes}; + + _ -> + {error, nil} + end. + +-file("src/gleam/time/timestamp.gleam", 780). +-spec accept_empty(bitstring()) -> {ok, nil} | {error, nil}. +accept_empty(Bytes) -> + case Bytes of + <<>> -> + {ok, nil}; + + _ -> + {error, nil} + end. + +-file("src/gleam/time/timestamp.gleam", 840). +?DOC( + " Note: It is the callers responsibility to ensure the inputs are valid.\n" + " \n" + " See https://www.tondering.dk/claus/cal/julperiod.php#formula\n" +). +-spec julian_day_from_ymd(integer(), integer(), integer()) -> integer(). +julian_day_from_ymd(Year, Month, Day) -> + Adjustment = (14 - Month) div 12, + Adjusted_year = (Year + 4800) - Adjustment, + Adjusted_month = (Month + (12 * Adjustment)) - 3, + (((((Day + (((153 * Adjusted_month) + 2) div 5)) + (365 * Adjusted_year)) + (Adjusted_year + div 4)) + - (Adjusted_year div 100)) + + (Adjusted_year div 400)) + - 32045. + +-file("src/gleam/time/timestamp.gleam", 859). +?DOC( + " Create a timestamp from a number of seconds since 00:00:00 UTC on 1 January\n" + " 1970.\n" +). +-spec from_unix_seconds(integer()) -> timestamp(). +from_unix_seconds(Seconds) -> + {timestamp, Seconds, 0}. + +-file("src/gleam/time/timestamp.gleam", 874). +?DOC( + " Create a timestamp from a number of seconds and nanoseconds since 00:00:00\n" + " UTC on 1 January 1970.\n" + "\n" + " # JavaScript int limitations\n" + "\n" + " Remember that JavaScript can only perfectly represent ints between positive\n" + " and negative 9,007,199,254,740,991! If you only use the nanosecond field\n" + " then you will almost certainly not get the date value you want due to this\n" + " loss of precision. Always use seconds primarily and then use nanoseconds\n" + " for the final sub-second adjustment.\n" +). +-spec from_unix_seconds_and_nanoseconds(integer(), integer()) -> timestamp(). +from_unix_seconds_and_nanoseconds(Seconds, Nanoseconds) -> + _pipe = {timestamp, Seconds, Nanoseconds}, + normalise(_pipe). + +-file("src/gleam/time/timestamp.gleam", 888). +?DOC( + " Convert the timestamp to a number of seconds since 00:00:00 UTC on 1\n" + " January 1970.\n" + "\n" + " There may be some small loss of precision due to `Timestamp` being\n" + " nanosecond accurate and `Float` not being able to represent this.\n" +). +-spec to_unix_seconds(timestamp()) -> float(). +to_unix_seconds(Timestamp) -> + Seconds = erlang:float(erlang:element(2, Timestamp)), + Nanoseconds = erlang:float(erlang:element(3, Timestamp)), + Seconds + (Nanoseconds / 1000000000.0). + +-file("src/gleam/time/timestamp.gleam", 897). +?DOC( + " Convert the timestamp to a number of seconds and nanoseconds since 00:00:00\n" + " UTC on 1 January 1970. There is no loss of precision with this conversion\n" + " on any target.\n" +). +-spec to_unix_seconds_and_nanoseconds(timestamp()) -> {integer(), integer()}. +to_unix_seconds_and_nanoseconds(Timestamp) -> + {erlang:element(2, Timestamp), erlang:element(3, Timestamp)}. + +-file("src/gleam/time/timestamp.gleam", 723). +-spec offset_to_seconds(binary(), integer(), integer()) -> integer(). +offset_to_seconds(Sign, Hours, Minutes) -> + Abs_seconds = (Hours * 3600) + (Minutes * 60), + case Sign of + <<"-"/utf8>> -> + - Abs_seconds; + + _ -> + Abs_seconds + end. + +-file("src/gleam/time/timestamp.gleam", 819). +?DOC( + " `julian_seconds_from_parts(year, month, day, hours, minutes, seconds)` \n" + " returns the number of Julian \n" + " seconds represented by the given arguments.\n" + " \n" + " Note: It is the callers responsibility to ensure the inputs are valid.\n" + " \n" + " See https://www.tondering.dk/claus/cal/julperiod.php#formula\n" +). +-spec julian_seconds_from_parts( + integer(), + integer(), + integer(), + integer(), + integer(), + integer() +) -> integer(). +julian_seconds_from_parts(Year, Month, Day, Hours, Minutes, Seconds) -> + Julian_day_seconds = julian_day_from_ymd(Year, Month, Day) * 86400, + ((Julian_day_seconds + (Hours * 3600)) + (Minutes * 60)) + Seconds. + +-file("src/gleam/time/timestamp.gleam", 662). +-spec do_parse_second_fraction_as_nanoseconds(bitstring(), integer(), integer()) -> {ok, + {integer(), bitstring()}} | + {error, any()}. +do_parse_second_fraction_as_nanoseconds(Bytes, Acc, Power) -> + Power@1 = Power div 10, + case Bytes of + <> when ((16#30 =< Byte) andalso (Byte =< 16#39)) andalso (Power@1 < 1) -> + do_parse_second_fraction_as_nanoseconds( + Remaining_bytes, + Acc, + Power@1 + ); + + <> when (16#30 =< Byte@1) andalso (Byte@1 =< 16#39) -> + Digit = Byte@1 - 16#30, + do_parse_second_fraction_as_nanoseconds( + Remaining_bytes@1, + Acc + (Digit * Power@1), + Power@1 + ); + + _ -> + {ok, {Acc, Bytes}} + end. + +-file("src/gleam/time/timestamp.gleam", 642). +-spec parse_second_fraction_as_nanoseconds(bitstring()) -> {ok, + {integer(), bitstring()}} | + {error, nil}. +parse_second_fraction_as_nanoseconds(Bytes) -> + case Bytes of + <<"."/utf8, Byte, Remaining_bytes/binary>> when (16#30 =< Byte) andalso (Byte =< 16#39) -> + do_parse_second_fraction_as_nanoseconds( + <>, + 0, + 1000000000 + ); + + <<"."/utf8, _/binary>> -> + {error, nil}; + + _ -> + {ok, {0, Bytes}} + end. + +-file("src/gleam/time/timestamp.gleam", 741). +-spec do_parse_digits(bitstring(), integer(), integer(), integer()) -> {ok, + {integer(), bitstring()}} | + {error, nil}. +do_parse_digits(Bytes, Count, Acc, K) -> + case Bytes of + _ when K >= Count -> + {ok, {Acc, Bytes}}; + + <> when (16#30 =< Byte) andalso (Byte =< 16#39) -> + do_parse_digits( + Remaining_bytes, + Count, + (Acc * 10) + (Byte - 16#30), + K + 1 + ); + + _ -> + {error, nil} + end. + +-file("src/gleam/time/timestamp.gleam", 734). +?DOC(" Parse and return the given number of digits from the given bytes.\n"). +-spec parse_digits(bitstring(), integer()) -> {ok, {integer(), bitstring()}} | + {error, nil}. +parse_digits(Bytes, Count) -> + do_parse_digits(Bytes, Count, 0, 0). + +-file("src/gleam/time/timestamp.gleam", 573). +-spec parse_year(bitstring()) -> {ok, {integer(), bitstring()}} | {error, nil}. +parse_year(Bytes) -> + parse_digits(Bytes, 4). + +-file("src/gleam/time/timestamp.gleam", 577). +-spec parse_month(bitstring()) -> {ok, {integer(), bitstring()}} | {error, nil}. +parse_month(Bytes) -> + gleam@result:'try'( + parse_digits(Bytes, 2), + fun(_use0) -> + {Month, Bytes@1} = _use0, + case (1 =< Month) andalso (Month =< 12) of + true -> + {ok, {Month, Bytes@1}}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/time/timestamp.gleam", 585). +-spec parse_day(bitstring(), integer(), integer()) -> {ok, + {integer(), bitstring()}} | + {error, nil}. +parse_day(Bytes, Year, Month) -> + gleam@result:'try'( + parse_digits(Bytes, 2), + fun(_use0) -> + {Day, Bytes@1} = _use0, + gleam@result:'try'(case Month of + 1 -> + {ok, 31}; + + 3 -> + {ok, 31}; + + 5 -> + {ok, 31}; + + 7 -> + {ok, 31}; + + 8 -> + {ok, 31}; + + 10 -> + {ok, 31}; + + 12 -> + {ok, 31}; + + 4 -> + {ok, 30}; + + 6 -> + {ok, 30}; + + 9 -> + {ok, 30}; + + 11 -> + {ok, 30}; + + 2 -> + case is_leap_year(Year) of + true -> + {ok, 29}; + + false -> + {ok, 28} + end; + + _ -> + {error, nil} + end, fun(Max_day) -> case (1 =< Day) andalso (Day =< Max_day) of + true -> + {ok, {Day, Bytes@1}}; + + false -> + {error, nil} + end end) + end + ). + +-file("src/gleam/time/timestamp.gleam", 615). +-spec parse_hours(bitstring()) -> {ok, {integer(), bitstring()}} | {error, nil}. +parse_hours(Bytes) -> + gleam@result:'try'( + parse_digits(Bytes, 2), + fun(_use0) -> + {Hours, Bytes@1} = _use0, + case (0 =< Hours) andalso (Hours =< 23) of + true -> + {ok, {Hours, Bytes@1}}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/time/timestamp.gleam", 623). +-spec parse_minutes(bitstring()) -> {ok, {integer(), bitstring()}} | + {error, nil}. +parse_minutes(Bytes) -> + gleam@result:'try'( + parse_digits(Bytes, 2), + fun(_use0) -> + {Minutes, Bytes@1} = _use0, + case (0 =< Minutes) andalso (Minutes =< 59) of + true -> + {ok, {Minutes, Bytes@1}}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/time/timestamp.gleam", 631). +-spec parse_seconds(bitstring()) -> {ok, {integer(), bitstring()}} | + {error, nil}. +parse_seconds(Bytes) -> + gleam@result:'try'( + parse_digits(Bytes, 2), + fun(_use0) -> + {Seconds, Bytes@1} = _use0, + case (0 =< Seconds) andalso (Seconds =< 60) of + true -> + {ok, {Seconds, Bytes@1}}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/time/timestamp.gleam", 704). +-spec parse_numeric_offset(bitstring()) -> {ok, {integer(), bitstring()}} | + {error, nil}. +parse_numeric_offset(Bytes) -> + gleam@result:'try'( + parse_sign(Bytes), + fun(_use0) -> + {Sign, Bytes@1} = _use0, + gleam@result:'try'( + parse_hours(Bytes@1), + fun(_use0@1) -> + {Hours, Bytes@2} = _use0@1, + gleam@result:'try'( + accept_byte(Bytes@2, 16#3A), + fun(Bytes@3) -> + gleam@result:'try'( + parse_minutes(Bytes@3), + fun(_use0@2) -> + {Minutes, Bytes@4} = _use0@2, + Offset_seconds = offset_to_seconds( + Sign, + Hours, + Minutes + ), + {ok, {Offset_seconds, Bytes@4}} + end + ) + end + ) + end + ) + end + ). + +-file("src/gleam/time/timestamp.gleam", 696). +-spec parse_offset(bitstring()) -> {ok, {integer(), bitstring()}} | {error, nil}. +parse_offset(Bytes) -> + case Bytes of + <<"Z"/utf8, Remaining_bytes/binary>> -> + {ok, {0, Remaining_bytes}}; + + <<"z"/utf8, Remaining_bytes/binary>> -> + {ok, {0, Remaining_bytes}}; + + _ -> + parse_numeric_offset(Bytes) + end. + +-file("src/gleam/time/timestamp.gleam", 769). +-spec accept_date_time_separator(bitstring()) -> {ok, bitstring()} | + {error, nil}. +accept_date_time_separator(Bytes) -> + case Bytes of + <> when ((Byte =:= 16#54) orelse (Byte =:= 16#74)) orelse (Byte =:= 16#20) -> + {ok, Remaining_bytes}; + + _ -> + {error, nil} + end. + +-file("src/gleam/time/timestamp.gleam", 789). +?DOC(" Note: The caller of this function must ensure that all inputs are valid.\n"). +-spec from_date_time( + integer(), + integer(), + integer(), + integer(), + integer(), + integer(), + integer(), + integer() +) -> timestamp(). +from_date_time( + Year, + Month, + Day, + Hours, + Minutes, + Seconds, + Second_fraction_as_nanoseconds, + Offset_seconds +) -> + Julian_seconds = julian_seconds_from_parts( + Year, + Month, + Day, + Hours, + Minutes, + Seconds + ), + Julian_seconds_since_epoch = Julian_seconds - 210866803200, + _pipe = {timestamp, + Julian_seconds_since_epoch - Offset_seconds, + Second_fraction_as_nanoseconds}, + normalise(_pipe). + +-file("src/gleam/time/timestamp.gleam", 339). +?DOC( + " Create a `Timestamp` from a human-readable calendar time.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " timestamp.from_calendar(\n" + " date: calendar.Date(2024, calendar.December, 25),\n" + " time: calendar.TimeOfDay(12, 30, 50, 0),\n" + " offset: calendar.utc_offset,\n" + " )\n" + " |> timestamp.to_rfc3339(calendar.utc_offset)\n" + " // -> \"2024-12-25T12:30:50Z\"\n" + " ```\n" +). +-spec from_calendar( + gleam@time@calendar:date(), + gleam@time@calendar:time_of_day(), + gleam@time@duration:duration() +) -> timestamp(). +from_calendar(Date, Time, Offset) -> + Month = case erlang:element(3, Date) of + january -> + 1; + + february -> + 2; + + march -> + 3; + + april -> + 4; + + may -> + 5; + + june -> + 6; + + july -> + 7; + + august -> + 8; + + september -> + 9; + + october -> + 10; + + november -> + 11; + + december -> + 12 + end, + from_date_time( + erlang:element(2, Date), + Month, + erlang:element(4, Date), + erlang:element(2, Time), + erlang:element(3, Time), + erlang:element(4, Time), + erlang:element(5, Time), + erlang:round(gleam@time@duration:to_seconds(Offset)) + ). + +-file("src/gleam/time/timestamp.gleam", 533). +?DOC( + " Parses an [RFC 3339 formatted time string][spec] into a `Timestamp`.\n" + "\n" + " [spec]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6\n" + " \n" + " # Examples\n" + "\n" + " ```gleam\n" + " let assert Ok(ts) = timestamp.parse_rfc3339(\"1970-01-01T00:00:01Z\")\n" + " timestamp.to_unix_seconds_and_nanoseconds(ts)\n" + " // -> #(1, 0)\n" + " ```\n" + " \n" + " Parsing an invalid timestamp returns an error.\n" + " \n" + " ```gleam\n" + " let assert Error(Nil) = timestamp.parse_rfc3339(\"1995-10-31\")\n" + " ```\n" + "\n" + " ## Time zones\n" + "\n" + " It may at first seem that the RFC 3339 format includes timezone\n" + " information, as it can specify an offset such as `Z` or `+3`, so why does\n" + " this function not return calendar time with a time zone? There are multiple\n" + " reasons:\n" + "\n" + " - RFC 3339's timestamp format is based on calendar time, but it is\n" + " unambigous, so it can be converted into epoch time when being parsed. It\n" + " is always better to internally use epoch time to represent unambiguous\n" + " points in time, so we perform that conversion as a convenience and to\n" + " ensure that programmers with less time experience don't accidentally use\n" + " a less suitable time representation.\n" + "\n" + " - RFC 3339's contains _calendar time offset_ information, not time zone\n" + " information. This is enough to convert it to an unambiguous timestamp,\n" + " but it is not enough information to reliably work with calendar time.\n" + " Without the time zone and the time zone database it's not possible to\n" + " know what time period that offset is valid for, so it cannot be used\n" + " without risk of bugs.\n" + "\n" + " ## Behaviour details\n" + " \n" + " - Follows the grammar specified in section 5.6 Internet Date/Time Format of \n" + " RFC 3339 .\n" + " - The `T` and `Z` characters may alternatively be lower case `t` or `z`, \n" + " respectively.\n" + " - Full dates and full times must be separated by `T` or `t`. A space is also \n" + " permitted.\n" + " - Leap seconds rules are not considered. That is, any timestamp may \n" + " specify digts `00` - `60` for the seconds.\n" + " - Any part of a fractional second that cannot be represented in the \n" + " nanosecond precision is tructated. That is, for the time string, \n" + " `\"1970-01-01T00:00:00.1234567899Z\"`, the fractional second `.1234567899` \n" + " will be represented as `123_456_789` in the `Timestamp`.\n" +). +-spec parse_rfc3339(binary()) -> {ok, timestamp()} | {error, nil}. +parse_rfc3339(Input) -> + Bytes = gleam_stdlib:identity(Input), + gleam@result:'try'( + parse_year(Bytes), + fun(_use0) -> + {Year, Bytes@1} = _use0, + gleam@result:'try'( + accept_byte(Bytes@1, 16#2D), + fun(Bytes@2) -> + gleam@result:'try'( + parse_month(Bytes@2), + fun(_use0@1) -> + {Month, Bytes@3} = _use0@1, + gleam@result:'try'( + accept_byte(Bytes@3, 16#2D), + fun(Bytes@4) -> + gleam@result:'try'( + parse_day(Bytes@4, Year, Month), + fun(_use0@2) -> + {Day, Bytes@5} = _use0@2, + gleam@result:'try'( + accept_date_time_separator( + Bytes@5 + ), + fun(Bytes@6) -> + gleam@result:'try'( + parse_hours(Bytes@6), + fun(_use0@3) -> + {Hours, Bytes@7} = _use0@3, + gleam@result:'try'( + accept_byte( + Bytes@7, + 16#3A + ), + fun(Bytes@8) -> + gleam@result:'try'( + parse_minutes( + Bytes@8 + ), + fun( + _use0@4 + ) -> + {Minutes, + Bytes@9} = _use0@4, + gleam@result:'try'( + accept_byte( + Bytes@9, + 16#3A + ), + fun( + Bytes@10 + ) -> + gleam@result:'try'( + parse_seconds( + Bytes@10 + ), + fun( + _use0@5 + ) -> + {Seconds, + Bytes@11} = _use0@5, + gleam@result:'try'( + parse_second_fraction_as_nanoseconds( + Bytes@11 + ), + fun( + _use0@6 + ) -> + {Second_fraction_as_nanoseconds, + Bytes@12} = _use0@6, + gleam@result:'try'( + parse_offset( + Bytes@12 + ), + fun( + _use0@7 + ) -> + {Offset_seconds, + Bytes@13} = _use0@7, + gleam@result:'try'( + accept_empty( + Bytes@13 + ), + fun( + _use0@8 + ) -> + nil = _use0@8, + {ok, + from_date_time( + Year, + Month, + Day, + Hours, + Minutes, + Seconds, + Second_fraction_as_nanoseconds, + Offset_seconds + )} + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ). diff --git a/build/dev/javascript/gleam_time/gleam_time_ffi.erl b/build/dev/javascript/gleam_time/gleam_time_ffi.erl new file mode 100644 index 0000000..34d8c88 --- /dev/null +++ b/build/dev/javascript/gleam_time/gleam_time_ffi.erl @@ -0,0 +1,12 @@ +-module(gleam_time_ffi). +-export([system_time/0, local_time_offset_seconds/0]). + +system_time() -> + {0, erlang:system_time(nanosecond)}. + +local_time_offset_seconds() -> + Utc = calendar:universal_time(), + Local = calendar:local_time(), + UtcSeconds = calendar:datetime_to_gregorian_seconds(Utc), + LocalSeconds = calendar:datetime_to_gregorian_seconds(Local), + LocalSeconds - UtcSeconds. diff --git a/build/dev/javascript/gleam_time/gleam_time_ffi.mjs b/build/dev/javascript/gleam_time/gleam_time_ffi.mjs new file mode 100644 index 0000000..27d09aa --- /dev/null +++ b/build/dev/javascript/gleam_time/gleam_time_ffi.mjs @@ -0,0 +1,11 @@ +export function system_time() { + const now = Date.now(); + const milliseconds = now % 1_000; + const nanoseconds = milliseconds * 1000_000; + const seconds = (now - milliseconds) / 1_000; + return [seconds, nanoseconds]; +} + +export function local_time_offset_seconds() { + return new Date().getTimezoneOffset() * -60; +} diff --git a/build/dev/javascript/gleam_version b/build/dev/javascript/gleam_version new file mode 100644 index 0000000..f88cf52 --- /dev/null +++ b/build/dev/javascript/gleam_version @@ -0,0 +1 @@ +1.13.0 \ No newline at end of file diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit.cache b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit.cache new file mode 100644 index 0000000..b226414 Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit.cache differ diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit.cache_inline b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit.cache_inline differ diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit.cache_meta b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit.cache_meta new file mode 100644 index 0000000..5c75cc9 Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit.cache_meta differ diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@gleam_panic.cache b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@gleam_panic.cache new file mode 100644 index 0000000..1f02d43 Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@gleam_panic.cache differ diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@gleam_panic.cache_inline b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@gleam_panic.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@gleam_panic.cache_inline differ diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@gleam_panic.cache_meta b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@gleam_panic.cache_meta new file mode 100644 index 0000000..8e2ed2a Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@gleam_panic.cache_meta differ diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@reporting.cache b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@reporting.cache new file mode 100644 index 0000000..7f17018 Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@reporting.cache differ diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@reporting.cache_inline b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@reporting.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@reporting.cache_inline differ diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@reporting.cache_meta b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@reporting.cache_meta new file mode 100644 index 0000000..34ab986 Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@internal@reporting.cache_meta differ diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@should.cache b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@should.cache new file mode 100644 index 0000000..8c8ecf7 Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@should.cache differ diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@should.cache_inline b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@should.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@should.cache_inline differ diff --git a/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@should.cache_meta b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@should.cache_meta new file mode 100644 index 0000000..4989b1e Binary files /dev/null and b/build/dev/javascript/gleeunit/_gleam_artefacts/gleeunit@should.cache_meta differ diff --git a/build/dev/javascript/gleeunit/gleam.mjs b/build/dev/javascript/gleeunit/gleam.mjs new file mode 100644 index 0000000..197cbbc --- /dev/null +++ b/build/dev/javascript/gleeunit/gleam.mjs @@ -0,0 +1 @@ +export * from "../prelude.mjs"; diff --git a/build/dev/javascript/gleeunit/gleeunit.erl b/build/dev/javascript/gleeunit/gleeunit.erl new file mode 100644 index 0000000..bcb58b6 --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit.erl @@ -0,0 +1,89 @@ +-module(gleeunit). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleeunit.gleam"). +-export([main/0]). +-export_type([atom_/0, encoding/0, report_module_name/0, gleeunit_progress_option/0, eunit_option/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type atom_() :: any(). + +-type encoding() :: utf8. + +-type report_module_name() :: gleeunit_progress. + +-type gleeunit_progress_option() :: {colored, boolean()}. + +-type eunit_option() :: verbose | + no_tty | + {report, {report_module_name(), list(gleeunit_progress_option())}} | + {scale_timeouts, integer()}. + +-file("src/gleeunit.gleam", 42). +-spec gleam_to_erlang_module_name(binary()) -> binary(). +gleam_to_erlang_module_name(Path) -> + case gleam_stdlib:string_ends_with(Path, <<".gleam"/utf8>>) of + true -> + _pipe = Path, + _pipe@1 = gleam@string:replace( + _pipe, + <<".gleam"/utf8>>, + <<""/utf8>> + ), + gleam@string:replace(_pipe@1, <<"/"/utf8>>, <<"@"/utf8>>); + + false -> + _pipe@2 = Path, + _pipe@3 = gleam@string:split(_pipe@2, <<"/"/utf8>>), + _pipe@4 = gleam@list:last(_pipe@3), + _pipe@5 = gleam@result:unwrap(_pipe@4, Path), + gleam@string:replace(_pipe@5, <<".erl"/utf8>>, <<""/utf8>>) + end. + +-file("src/gleeunit.gleam", 18). +-spec do_main() -> nil. +do_main() -> + Options = [verbose, + no_tty, + {report, {gleeunit_progress, [{colored, true}]}}, + {scale_timeouts, 10}], + Result = begin + _pipe = gleeunit_ffi:find_files( + <<"**/*.{erl,gleam}"/utf8>>, + <<"test"/utf8>> + ), + _pipe@1 = gleam@list:map(_pipe, fun gleam_to_erlang_module_name/1), + _pipe@2 = gleam@list:map( + _pipe@1, + fun(_capture) -> erlang:binary_to_atom(_capture, utf8) end + ), + gleeunit_ffi:run_eunit(_pipe@2, Options) + end, + Code = case Result of + {ok, _} -> + 0; + + {error, _} -> + 1 + end, + erlang:halt(Code). + +-file("src/gleeunit.gleam", 13). +?DOC( + " Find and run all test functions for the current project using Erlang's EUnit\n" + " test framework, or a custom JavaScript test runner.\n" + "\n" + " Any Erlang or Gleam function in the `test` directory with a name ending in\n" + " `_test` is considered a test function and will be run.\n" + "\n" + " A test that panics is considered a failure.\n" +). +-spec main() -> nil. +main() -> + do_main(). diff --git a/build/dev/javascript/gleeunit/gleeunit.mjs b/build/dev/javascript/gleeunit/gleeunit.mjs new file mode 100644 index 0000000..83018dd --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit.mjs @@ -0,0 +1,62 @@ +import * as $list from "../gleam_stdlib/gleam/list.mjs"; +import * as $result from "../gleam_stdlib/gleam/result.mjs"; +import * as $string from "../gleam_stdlib/gleam/string.mjs"; +import { CustomType as $CustomType } from "./gleam.mjs"; +import { main as do_main } from "./gleeunit_ffi.mjs"; + +class Utf8 extends $CustomType {} + +class GleeunitProgress extends $CustomType {} + +class Colored extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} + +class Verbose extends $CustomType {} + +class NoTty extends $CustomType {} + +class Report extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} + +class ScaleTimeouts extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} + +function gleam_to_erlang_module_name(path) { + let $ = $string.ends_with(path, ".gleam"); + if ($) { + let _pipe = path; + let _pipe$1 = $string.replace(_pipe, ".gleam", ""); + return $string.replace(_pipe$1, "/", "@"); + } else { + let _pipe = path; + let _pipe$1 = $string.split(_pipe, "/"); + let _pipe$2 = $list.last(_pipe$1); + let _pipe$3 = $result.unwrap(_pipe$2, path); + return $string.replace(_pipe$3, ".erl", ""); + } +} + +/** + * Find and run all test functions for the current project using Erlang's EUnit + * test framework, or a custom JavaScript test runner. + * + * Any Erlang or Gleam function in the `test` directory with a name ending in + * `_test` is considered a test function and will be run. + * + * A test that panics is considered a failure. + */ +export function main() { + return do_main(); +} diff --git a/build/dev/javascript/gleeunit/gleeunit/internal/gleam_panic.mjs b/build/dev/javascript/gleeunit/gleeunit/internal/gleam_panic.mjs new file mode 100644 index 0000000..f6af89d --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit/internal/gleam_panic.mjs @@ -0,0 +1,180 @@ +import * as $dynamic from "../../../gleam_stdlib/gleam/dynamic.mjs"; +import { CustomType as $CustomType } from "../../gleam.mjs"; +import { from_dynamic } from "./gleeunit_gleam_panic_ffi.mjs"; + +export { from_dynamic }; + +export class GleamPanic extends $CustomType { + constructor(message, file, module, function$, line, kind) { + super(); + this.message = message; + this.file = file; + this.module = module; + this.function = function$; + this.line = line; + this.kind = kind; + } +} +export const GleamPanic$GleamPanic = (message, file, module, function$, line, kind) => + new GleamPanic(message, file, module, function$, line, kind); +export const GleamPanic$isGleamPanic = (value) => value instanceof GleamPanic; +export const GleamPanic$GleamPanic$message = (value) => value.message; +export const GleamPanic$GleamPanic$0 = (value) => value.message; +export const GleamPanic$GleamPanic$file = (value) => value.file; +export const GleamPanic$GleamPanic$1 = (value) => value.file; +export const GleamPanic$GleamPanic$module = (value) => value.module; +export const GleamPanic$GleamPanic$2 = (value) => value.module; +export const GleamPanic$GleamPanic$function = (value) => value.function; +export const GleamPanic$GleamPanic$3 = (value) => value.function; +export const GleamPanic$GleamPanic$line = (value) => value.line; +export const GleamPanic$GleamPanic$4 = (value) => value.line; +export const GleamPanic$GleamPanic$kind = (value) => value.kind; +export const GleamPanic$GleamPanic$5 = (value) => value.kind; + +export class Todo extends $CustomType {} +export const PanicKind$Todo = () => new Todo(); +export const PanicKind$isTodo = (value) => value instanceof Todo; + +export class Panic extends $CustomType {} +export const PanicKind$Panic = () => new Panic(); +export const PanicKind$isPanic = (value) => value instanceof Panic; + +export class LetAssert extends $CustomType { + constructor(start, end, pattern_start, pattern_end, value) { + super(); + this.start = start; + this.end = end; + this.pattern_start = pattern_start; + this.pattern_end = pattern_end; + this.value = value; + } +} +export const PanicKind$LetAssert = (start, end, pattern_start, pattern_end, value) => + new LetAssert(start, end, pattern_start, pattern_end, value); +export const PanicKind$isLetAssert = (value) => value instanceof LetAssert; +export const PanicKind$LetAssert$start = (value) => value.start; +export const PanicKind$LetAssert$0 = (value) => value.start; +export const PanicKind$LetAssert$end = (value) => value.end; +export const PanicKind$LetAssert$1 = (value) => value.end; +export const PanicKind$LetAssert$pattern_start = (value) => value.pattern_start; +export const PanicKind$LetAssert$2 = (value) => value.pattern_start; +export const PanicKind$LetAssert$pattern_end = (value) => value.pattern_end; +export const PanicKind$LetAssert$3 = (value) => value.pattern_end; +export const PanicKind$LetAssert$value = (value) => value.value; +export const PanicKind$LetAssert$4 = (value) => value.value; + +export class Assert extends $CustomType { + constructor(start, end, expression_start, kind) { + super(); + this.start = start; + this.end = end; + this.expression_start = expression_start; + this.kind = kind; + } +} +export const PanicKind$Assert = (start, end, expression_start, kind) => + new Assert(start, end, expression_start, kind); +export const PanicKind$isAssert = (value) => value instanceof Assert; +export const PanicKind$Assert$start = (value) => value.start; +export const PanicKind$Assert$0 = (value) => value.start; +export const PanicKind$Assert$end = (value) => value.end; +export const PanicKind$Assert$1 = (value) => value.end; +export const PanicKind$Assert$expression_start = (value) => + value.expression_start; +export const PanicKind$Assert$2 = (value) => value.expression_start; +export const PanicKind$Assert$kind = (value) => value.kind; +export const PanicKind$Assert$3 = (value) => value.kind; + +export class BinaryOperator extends $CustomType { + constructor(operator, left, right) { + super(); + this.operator = operator; + this.left = left; + this.right = right; + } +} +export const AssertKind$BinaryOperator = (operator, left, right) => + new BinaryOperator(operator, left, right); +export const AssertKind$isBinaryOperator = (value) => + value instanceof BinaryOperator; +export const AssertKind$BinaryOperator$operator = (value) => value.operator; +export const AssertKind$BinaryOperator$0 = (value) => value.operator; +export const AssertKind$BinaryOperator$left = (value) => value.left; +export const AssertKind$BinaryOperator$1 = (value) => value.left; +export const AssertKind$BinaryOperator$right = (value) => value.right; +export const AssertKind$BinaryOperator$2 = (value) => value.right; + +export class FunctionCall extends $CustomType { + constructor(arguments$) { + super(); + this.arguments = arguments$; + } +} +export const AssertKind$FunctionCall = (arguments$) => + new FunctionCall(arguments$); +export const AssertKind$isFunctionCall = (value) => + value instanceof FunctionCall; +export const AssertKind$FunctionCall$arguments = (value) => value.arguments; +export const AssertKind$FunctionCall$0 = (value) => value.arguments; + +export class OtherExpression extends $CustomType { + constructor(expression) { + super(); + this.expression = expression; + } +} +export const AssertKind$OtherExpression = (expression) => + new OtherExpression(expression); +export const AssertKind$isOtherExpression = (value) => + value instanceof OtherExpression; +export const AssertKind$OtherExpression$expression = (value) => + value.expression; +export const AssertKind$OtherExpression$0 = (value) => value.expression; + +export class AssertedExpression extends $CustomType { + constructor(start, end, kind) { + super(); + this.start = start; + this.end = end; + this.kind = kind; + } +} +export const AssertedExpression$AssertedExpression = (start, end, kind) => + new AssertedExpression(start, end, kind); +export const AssertedExpression$isAssertedExpression = (value) => + value instanceof AssertedExpression; +export const AssertedExpression$AssertedExpression$start = (value) => + value.start; +export const AssertedExpression$AssertedExpression$0 = (value) => value.start; +export const AssertedExpression$AssertedExpression$end = (value) => value.end; +export const AssertedExpression$AssertedExpression$1 = (value) => value.end; +export const AssertedExpression$AssertedExpression$kind = (value) => value.kind; +export const AssertedExpression$AssertedExpression$2 = (value) => value.kind; + +export class Literal extends $CustomType { + constructor(value) { + super(); + this.value = value; + } +} +export const ExpressionKind$Literal = (value) => new Literal(value); +export const ExpressionKind$isLiteral = (value) => value instanceof Literal; +export const ExpressionKind$Literal$value = (value) => value.value; +export const ExpressionKind$Literal$0 = (value) => value.value; + +export class Expression extends $CustomType { + constructor(value) { + super(); + this.value = value; + } +} +export const ExpressionKind$Expression = (value) => new Expression(value); +export const ExpressionKind$isExpression = (value) => + value instanceof Expression; +export const ExpressionKind$Expression$value = (value) => value.value; +export const ExpressionKind$Expression$0 = (value) => value.value; + +export class Unevaluated extends $CustomType {} +export const ExpressionKind$Unevaluated = () => new Unevaluated(); +export const ExpressionKind$isUnevaluated = (value) => + value instanceof Unevaluated; diff --git a/build/dev/javascript/gleeunit/gleeunit/internal/gleeunit_gleam_panic_ffi.erl b/build/dev/javascript/gleeunit/gleeunit/internal/gleeunit_gleam_panic_ffi.erl new file mode 100644 index 0000000..d78f5e5 --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit/internal/gleeunit_gleam_panic_ffi.erl @@ -0,0 +1,49 @@ +-module(gleeunit_gleam_panic_ffi). +-export([from_dynamic/1]). + +from_dynamic(#{ + gleam_error := assert, + start := Start, + 'end' := End, + expression_start := EStart +} = E) -> + wrap(E, {assert, Start, End, EStart, assert_kind(E)}); +from_dynamic(#{ + gleam_error := let_assert, + start := Start, + 'end' := End, + pattern_start := PStart, + pattern_end := PEnd, + value := Value +} = E) -> + wrap(E, {let_assert, Start, End, PStart, PEnd, Value}); +from_dynamic(#{gleam_error := panic} = E) -> + wrap(E, panic); +from_dynamic(#{gleam_error := todo} = E) -> + wrap(E, todo); +from_dynamic(_) -> + {error, nil}. + +assert_kind(#{kind := binary_operator, left := L, right := R, operator := O}) -> + {binary_operator, atom_to_binary(O), expression(L), expression(R)}; +assert_kind(#{kind := function_call, arguments := Arguments}) -> + {function_call, lists:map(fun expression/1, Arguments)}; +assert_kind(#{kind := expression, expression := Expression}) -> + {other_expression, expression(Expression)}. + +expression(#{start := S, 'end' := E, kind := literal, value := Value}) -> + {asserted_expression, S, E, {literal, Value}}; +expression(#{start := S, 'end' := E, kind := expression, value := Value}) -> + {asserted_expression, S, E, {expression, Value}}; +expression(#{start := S, 'end' := E, kind := unevaluated}) -> + {asserted_expression, S, E, unevaluated}. + +wrap(#{ + gleam_error := _, + file := File, + message := Message, + module := Module, + function := Function, + line := Line +}, Kind) -> + {ok, {gleam_panic, Message, File, Module, Function, Line, Kind}}. diff --git a/build/dev/javascript/gleeunit/gleeunit/internal/gleeunit_gleam_panic_ffi.mjs b/build/dev/javascript/gleeunit/gleeunit/internal/gleeunit_gleam_panic_ffi.mjs new file mode 100644 index 0000000..03f6025 --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit/internal/gleeunit_gleam_panic_ffi.mjs @@ -0,0 +1,91 @@ +import { Result$Ok, Result$Error, List$Empty, List$NonEmpty } from "../../gleam.mjs"; +import { + GleamPanic$GleamPanic, + PanicKind$Todo, + PanicKind$Panic, + PanicKind$LetAssert, + PanicKind$Assert, + AssertKind$BinaryOperator, + AssertKind$FunctionCall, + AssertKind$OtherExpression, + AssertedExpression$AssertedExpression, + ExpressionKind$Literal, + ExpressionKind$Expression, + ExpressionKind$Unevaluated, +} from "./gleam_panic.mjs"; + +export function from_dynamic(error) { + if (!(error instanceof globalThis.Error) || !error.gleam_error) { + return Result$Error(undefined); + } + + if (error.gleam_error === "todo") { + return wrap(error, PanicKind$Todo()); + } + + if (error.gleam_error === "panic") { + return wrap(error, PanicKind$Panic()); + } + + if (error.gleam_error === "let_assert") { + let kind = PanicKind$LetAssert( + error.start, + error.end, + error.pattern_start, + error.pattern_end, + error.value, + ); + return wrap(error, kind); + } + + if (error.gleam_error === "assert") { + let kind = PanicKind$Assert( + error.start, + error.end, + error.expression_start, + assert_kind(error), + ); + return wrap(error, kind); + } + + return Result$Error(undefined); +} + +function assert_kind(error) { + if (error.kind == "binary_operator") { + return AssertKind$BinaryOperator( + error.operator, + expression(error.left), + expression(error.right), + ); + } + + if (error.kind == "function_call") { + let list = List$Empty(); + let i = error.arguments.length; + while (i--) { + list = List$NonEmpty(expression(error.arguments[i]), list); + } + return AssertKind$FunctionCall(list); + } + + return AssertKind$OtherExpression(expression(error.expression)); +} + +function expression(data) { + const expression = AssertedExpression$AssertedExpression(data.start, data.end, undefined); + if (data.kind == "literal") { + expression.kind = ExpressionKind$Literal(data.value); + } else if (data.kind == "expression") { + expression.kind = ExpressionKind$Expression(data.value); + } else { + expression.kind = ExpressionKind$Unevaluated(); + } + return expression; +} + +function wrap(e, kind) { + return Result$Ok( + GleamPanic$GleamPanic(e.message, e.file, e.module, e.function, e.line, kind), + ); +} diff --git a/build/dev/javascript/gleeunit/gleeunit/internal/reporting.mjs b/build/dev/javascript/gleeunit/gleeunit/internal/reporting.mjs new file mode 100644 index 0000000..8ebf9a2 --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit/internal/reporting.mjs @@ -0,0 +1,256 @@ +import * as $bit_array from "../../../gleam_stdlib/gleam/bit_array.mjs"; +import * as $dynamic from "../../../gleam_stdlib/gleam/dynamic.mjs"; +import * as $int from "../../../gleam_stdlib/gleam/int.mjs"; +import * as $io from "../../../gleam_stdlib/gleam/io.mjs"; +import * as $list from "../../../gleam_stdlib/gleam/list.mjs"; +import * as $option from "../../../gleam_stdlib/gleam/option.mjs"; +import * as $result from "../../../gleam_stdlib/gleam/result.mjs"; +import * as $string from "../../../gleam_stdlib/gleam/string.mjs"; +import { Ok, Error, toList, CustomType as $CustomType } from "../../gleam.mjs"; +import * as $gleam_panic from "../../gleeunit/internal/gleam_panic.mjs"; +import { read_file as read_file_text } from "../../gleeunit_ffi.mjs"; + +export class State extends $CustomType { + constructor(passed, failed, skipped) { + super(); + this.passed = passed; + this.failed = failed; + this.skipped = skipped; + } +} +export const State$State = (passed, failed, skipped) => + new State(passed, failed, skipped); +export const State$isState = (value) => value instanceof State; +export const State$State$passed = (value) => value.passed; +export const State$State$0 = (value) => value.passed; +export const State$State$failed = (value) => value.failed; +export const State$State$1 = (value) => value.failed; +export const State$State$skipped = (value) => value.skipped; +export const State$State$2 = (value) => value.skipped; + +export function new_state() { + return new State(0, 0, 0); +} + +function bold(text) { + return ("\u{001b}[1m" + text) + "\u{001b}[22m"; +} + +function cyan(text) { + return ("\u{001b}[36m" + text) + "\u{001b}[39m"; +} + +function code_snippet(src, start, end) { + let _pipe = $result.try$( + $option.to_result(src, undefined), + (src) => { + return $result.try$( + $bit_array.slice(src, start, end - start), + (snippet) => { + return $result.try$( + $bit_array.to_string(snippet), + (snippet) => { + let snippet$1 = ((cyan(" code") + ": ") + snippet) + "\n"; + return new Ok(snippet$1); + }, + ); + }, + ); + }, + ); + return $result.unwrap(_pipe, ""); +} + +function yellow(text) { + return ("\u{001b}[33m" + text) + "\u{001b}[39m"; +} + +export function test_skipped(state, module, function$) { + $io.print(((("\n" + module) + ".") + function$) + yellow(" skipped")); + return new State(state.passed, state.failed, state.skipped + 1); +} + +function green(text) { + return ("\u{001b}[32m" + text) + "\u{001b}[39m"; +} + +export function test_passed(state) { + $io.print(green(".")); + return new State(state.passed + 1, state.failed, state.skipped); +} + +function red(text) { + return ("\u{001b}[31m" + text) + "\u{001b}[39m"; +} + +export function finished(state) { + let $ = state.failed; + if ($ === 0) { + let $1 = state.skipped; + if ($1 === 0) { + let $2 = state.passed; + if ($2 === 0) { + $io.println("\nNo tests found!"); + return 1; + } else { + let message = ("\n" + $int.to_string(state.passed)) + " passed, no failures"; + $io.println(green(message)); + return 0; + } + } else { + let message = ((("\n" + $int.to_string(state.passed)) + " passed, 0 failures, ") + $int.to_string( + state.skipped, + )) + " skipped"; + $io.println(yellow(message)); + return 1; + } + } else { + let $1 = state.skipped; + if ($1 === 0) { + let message = ((("\n" + $int.to_string(state.passed)) + " passed, ") + $int.to_string( + state.failed, + )) + " failures"; + $io.println(red(message)); + return 1; + } else { + let message = ((((("\n" + $int.to_string(state.passed)) + " passed, ") + $int.to_string( + state.failed, + )) + " failures, ") + $int.to_string(state.skipped)) + " skipped"; + $io.println(red(message)); + return 1; + } + } +} + +export function eunit_missing() { + let message = bold(red("Error")) + ": EUnit libraries not found.\n\nYour Erlang installation seems to be incomplete. If you installed Erlang using\na package manager ensure that you have installed the full Erlang\ndistribution instead of a stripped-down version.\n"; + $io.print_error(message); + return new Error(undefined); +} + +function grey(text) { + return ("\u{001b}[90m" + text) + "\u{001b}[39m"; +} + +function format_unknown(module, function$, error) { + return $string.concat( + toList([ + grey((module + ".") + function$) + "\n", + "An unexpected error occurred:\n", + "\n", + (" " + $string.inspect(error)) + "\n", + ]), + ); +} + +function inspect_value(value) { + let $ = value.kind; + if ($ instanceof $gleam_panic.Literal) { + return grey("literal"); + } else if ($ instanceof $gleam_panic.Expression) { + let value$1 = $.value; + return $string.inspect(value$1); + } else { + return grey("unevaluated"); + } +} + +function assert_value(name, value) { + return ((cyan(name) + ": ") + inspect_value(value)) + "\n"; +} + +function assert_info(kind) { + if (kind instanceof $gleam_panic.BinaryOperator) { + let left = kind.left; + let right = kind.right; + return $string.concat( + toList([assert_value(" left", left), assert_value("right", right)]), + ); + } else if (kind instanceof $gleam_panic.FunctionCall) { + let arguments$ = kind.arguments; + let _pipe = arguments$; + let _pipe$1 = $list.index_map( + _pipe, + (e, i) => { + let number = $string.pad_start($int.to_string(i), 5, " "); + return assert_value(number, e); + }, + ); + return $string.concat(_pipe$1); + } else { + return ""; + } +} + +function format_gleam_error(error, module, function$, src) { + let location = grey((error.file + ":") + $int.to_string(error.line)); + let $ = error.kind; + if ($ instanceof $gleam_panic.Todo) { + return $string.concat( + toList([ + ((bold(yellow("todo")) + " ") + location) + "\n", + ((((cyan(" test") + ": ") + module) + ".") + function$) + "\n", + ((cyan(" info") + ": ") + error.message) + "\n", + ]), + ); + } else if ($ instanceof $gleam_panic.Panic) { + return $string.concat( + toList([ + ((bold(red("panic")) + " ") + location) + "\n", + ((((cyan(" test") + ": ") + module) + ".") + function$) + "\n", + ((cyan(" info") + ": ") + error.message) + "\n", + ]), + ); + } else if ($ instanceof $gleam_panic.LetAssert) { + let start = $.start; + let end = $.end; + let value = $.value; + return $string.concat( + toList([ + ((bold(red("let assert")) + " ") + location) + "\n", + ((((cyan(" test") + ": ") + module) + ".") + function$) + "\n", + code_snippet(src, start, end), + ((cyan("value") + ": ") + $string.inspect(value)) + "\n", + ((cyan(" info") + ": ") + error.message) + "\n", + ]), + ); + } else { + let start = $.start; + let end = $.end; + let kind = $.kind; + return $string.concat( + toList([ + ((bold(red("assert")) + " ") + location) + "\n", + ((((cyan(" test") + ": ") + module) + ".") + function$) + "\n", + code_snippet(src, start, end), + assert_info(kind), + ((cyan(" info") + ": ") + error.message) + "\n", + ]), + ); + } +} + +function read_file(path) { + let $ = read_file_text(path); + if ($ instanceof Ok) { + let text = $[0]; + return new Ok($bit_array.from_string(text)); + } else { + return $; + } +} + +export function test_failed(state, module, function$, error) { + let _block; + let $ = $gleam_panic.from_dynamic(error); + if ($ instanceof Ok) { + let error$1 = $[0]; + let src = $option.from_result(read_file(error$1.file)); + _block = format_gleam_error(error$1, module, function$, src); + } else { + _block = format_unknown(module, function$, error); + } + let message = _block; + $io.print("\n" + message); + return new State(state.passed, state.failed + 1, state.skipped); +} diff --git a/build/dev/javascript/gleeunit/gleeunit/should.mjs b/build/dev/javascript/gleeunit/gleeunit/should.mjs new file mode 100644 index 0000000..c34c700 --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit/should.mjs @@ -0,0 +1,135 @@ +import * as $option from "../../gleam_stdlib/gleam/option.mjs"; +import { None, Some } from "../../gleam_stdlib/gleam/option.mjs"; +import * as $string from "../../gleam_stdlib/gleam/string.mjs"; +import { Ok, Error, toList, makeError, isEqual } from "../gleam.mjs"; + +const FILEPATH = "src/gleeunit/should.gleam"; + +export function equal(a, b) { + let $ = isEqual(a, b); + if ($) { + return undefined; + } else { + throw makeError( + "panic", + FILEPATH, + "gleeunit/should", + 10, + "equal", + $string.concat( + toList([ + "\n", + $string.inspect(a), + "\nshould equal\n", + $string.inspect(b), + ]), + ), + {} + ) + } +} + +export function not_equal(a, b) { + let $ = !isEqual(a, b); + if ($) { + return undefined; + } else { + throw makeError( + "panic", + FILEPATH, + "gleeunit/should", + 23, + "not_equal", + $string.concat( + toList([ + "\n", + $string.inspect(a), + "\nshould not equal\n", + $string.inspect(b), + ]), + ), + {} + ) + } +} + +export function be_ok(a) { + if (a instanceof Ok) { + let value = a[0]; + return value; + } else { + throw makeError( + "panic", + FILEPATH, + "gleeunit/should", + 35, + "be_ok", + $string.concat(toList(["\n", $string.inspect(a), "\nshould be ok"])), + {} + ) + } +} + +export function be_error(a) { + if (a instanceof Error) { + let error = a[0]; + return error; + } else { + throw makeError( + "panic", + FILEPATH, + "gleeunit/should", + 42, + "be_error", + $string.concat(toList(["\n", $string.inspect(a), "\nshould be error"])), + {} + ) + } +} + +export function be_some(a) { + if (a instanceof Some) { + let value = a[0]; + return value; + } else { + throw makeError( + "panic", + FILEPATH, + "gleeunit/should", + 49, + "be_some", + $string.concat(toList(["\n", $string.inspect(a), "\nshould be some"])), + {} + ) + } +} + +export function be_none(a) { + if (a instanceof None) { + return undefined; + } else { + throw makeError( + "panic", + FILEPATH, + "gleeunit/should", + 56, + "be_none", + $string.concat(toList(["\n", $string.inspect(a), "\nshould be none"])), + {} + ) + } +} + +export function be_true(actual) { + let _pipe = actual; + return equal(_pipe, true); +} + +export function be_false(actual) { + let _pipe = actual; + return equal(_pipe, false); +} + +export function fail() { + return be_true(false); +} diff --git a/build/dev/javascript/gleeunit/gleeunit@internal@gleam_panic.erl b/build/dev/javascript/gleeunit/gleeunit@internal@gleam_panic.erl new file mode 100644 index 0000000..398ea7d --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit@internal@gleam_panic.erl @@ -0,0 +1,56 @@ +-module(gleeunit@internal@gleam_panic). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleeunit/internal/gleam_panic.gleam"). +-export([from_dynamic/1]). +-export_type([gleam_panic/0, panic_kind/0, assert_kind/0, asserted_expression/0, expression_kind/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC(false). + +-type gleam_panic() :: {gleam_panic, + binary(), + binary(), + binary(), + binary(), + integer(), + panic_kind()}. + +-type panic_kind() :: todo | + panic | + {let_assert, + integer(), + integer(), + integer(), + integer(), + gleam@dynamic:dynamic_()} | + {assert, integer(), integer(), integer(), assert_kind()}. + +-type assert_kind() :: {binary_operator, + binary(), + asserted_expression(), + asserted_expression()} | + {function_call, list(asserted_expression())} | + {other_expression, asserted_expression()}. + +-type asserted_expression() :: {asserted_expression, + integer(), + integer(), + expression_kind()}. + +-type expression_kind() :: {literal, gleam@dynamic:dynamic_()} | + {expression, gleam@dynamic:dynamic_()} | + unevaluated. + +-file("src/gleeunit/internal/gleam_panic.gleam", 49). +?DOC(false). +-spec from_dynamic(gleam@dynamic:dynamic_()) -> {ok, gleam_panic()} | + {error, nil}. +from_dynamic(Data) -> + gleeunit_gleam_panic_ffi:from_dynamic(Data). diff --git a/build/dev/javascript/gleeunit/gleeunit@internal@reporting.erl b/build/dev/javascript/gleeunit/gleeunit@internal@reporting.erl new file mode 100644 index 0000000..8c37c79 --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit@internal@reporting.erl @@ -0,0 +1,343 @@ +-module(gleeunit@internal@reporting). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleeunit/internal/reporting.gleam"). +-export([new_state/0, test_skipped/3, test_passed/1, finished/1, eunit_missing/0, test_failed/4]). +-export_type([state/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC(false). + +-type state() :: {state, integer(), integer(), integer()}. + +-file("src/gleeunit/internal/reporting.gleam", 15). +?DOC(false). +-spec new_state() -> state(). +new_state() -> + {state, 0, 0, 0}. + +-file("src/gleeunit/internal/reporting.gleam", 207). +?DOC(false). +-spec bold(binary()) -> binary(). +bold(Text) -> + <<<<"\x{001b}[1m"/utf8, Text/binary>>/binary, "\x{001b}[22m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 211). +?DOC(false). +-spec cyan(binary()) -> binary(). +cyan(Text) -> + <<<<"\x{001b}[36m"/utf8, Text/binary>>/binary, "\x{001b}[39m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 191). +?DOC(false). +-spec code_snippet(gleam@option:option(bitstring()), integer(), integer()) -> binary(). +code_snippet(Src, Start, End) -> + _pipe = begin + gleam@result:'try'( + gleam@option:to_result(Src, nil), + fun(Src@1) -> + gleam@result:'try'( + gleam_stdlib:bit_array_slice(Src@1, Start, End - Start), + fun(Snippet) -> + gleam@result:'try'( + gleam@bit_array:to_string(Snippet), + fun(Snippet@1) -> + Snippet@2 = <<<<<<(cyan(<<" code"/utf8>>))/binary, + ": "/utf8>>/binary, + Snippet@1/binary>>/binary, + "\n"/utf8>>, + {ok, Snippet@2} + end + ) + end + ) + end + ) + end, + gleam@result:unwrap(_pipe, <<""/utf8>>). + +-file("src/gleeunit/internal/reporting.gleam", 215). +?DOC(false). +-spec yellow(binary()) -> binary(). +yellow(Text) -> + <<<<"\x{001b}[33m"/utf8, Text/binary>>/binary, "\x{001b}[39m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 202). +?DOC(false). +-spec test_skipped(state(), binary(), binary()) -> state(). +test_skipped(State, Module, Function) -> + gleam_stdlib:print( + <<<<<<<<"\n"/utf8, Module/binary>>/binary, "."/utf8>>/binary, + Function/binary>>/binary, + (yellow(<<" skipped"/utf8>>))/binary>> + ), + {state, + erlang:element(2, State), + erlang:element(3, State), + erlang:element(4, State) + 1}. + +-file("src/gleeunit/internal/reporting.gleam", 219). +?DOC(false). +-spec green(binary()) -> binary(). +green(Text) -> + <<<<"\x{001b}[32m"/utf8, Text/binary>>/binary, "\x{001b}[39m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 66). +?DOC(false). +-spec test_passed(state()) -> state(). +test_passed(State) -> + gleam_stdlib:print(green(<<"."/utf8>>)), + {state, + erlang:element(2, State) + 1, + erlang:element(3, State), + erlang:element(4, State)}. + +-file("src/gleeunit/internal/reporting.gleam", 223). +?DOC(false). +-spec red(binary()) -> binary(). +red(Text) -> + <<<<"\x{001b}[31m"/utf8, Text/binary>>/binary, "\x{001b}[39m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 19). +?DOC(false). +-spec finished(state()) -> integer(). +finished(State) -> + case State of + {state, 0, 0, 0} -> + gleam_stdlib:println(<<"\nNo tests found!"/utf8>>), + 1; + + {state, _, 0, 0} -> + Message = <<<<"\n"/utf8, + (erlang:integer_to_binary(erlang:element(2, State)))/binary>>/binary, + " passed, no failures"/utf8>>, + gleam_stdlib:println(green(Message)), + 0; + + {state, _, _, 0} -> + Message@1 = <<<<<<<<"\n"/utf8, + (erlang:integer_to_binary(erlang:element(2, State)))/binary>>/binary, + " passed, "/utf8>>/binary, + (erlang:integer_to_binary(erlang:element(3, State)))/binary>>/binary, + " failures"/utf8>>, + gleam_stdlib:println(red(Message@1)), + 1; + + {state, _, 0, _} -> + Message@2 = <<<<<<<<"\n"/utf8, + (erlang:integer_to_binary(erlang:element(2, State)))/binary>>/binary, + " passed, 0 failures, "/utf8>>/binary, + (erlang:integer_to_binary(erlang:element(4, State)))/binary>>/binary, + " skipped"/utf8>>, + gleam_stdlib:println(yellow(Message@2)), + 1; + + {state, _, _, _} -> + Message@3 = <<<<<<<<<<<<"\n"/utf8, + (erlang:integer_to_binary( + erlang:element(2, State) + ))/binary>>/binary, + " passed, "/utf8>>/binary, + (erlang:integer_to_binary(erlang:element(3, State)))/binary>>/binary, + " failures, "/utf8>>/binary, + (erlang:integer_to_binary(erlang:element(4, State)))/binary>>/binary, + " skipped"/utf8>>, + gleam_stdlib:println(red(Message@3)), + 1 + end. + +-file("src/gleeunit/internal/reporting.gleam", 89). +?DOC(false). +-spec eunit_missing() -> {ok, any()} | {error, nil}. +eunit_missing() -> + Message = <<(bold(red(<<"Error"/utf8>>)))/binary, + ": EUnit libraries not found. + +Your Erlang installation seems to be incomplete. If you installed Erlang using +a package manager ensure that you have installed the full Erlang +distribution instead of a stripped-down version. +"/utf8>>, + gleam_stdlib:print_error(Message), + {error, nil}. + +-file("src/gleeunit/internal/reporting.gleam", 227). +?DOC(false). +-spec grey(binary()) -> binary(). +grey(Text) -> + <<<<"\x{001b}[90m"/utf8, Text/binary>>/binary, "\x{001b}[39m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 100). +?DOC(false). +-spec format_unknown(binary(), binary(), gleam@dynamic:dynamic_()) -> binary(). +format_unknown(Module, Function, Error) -> + erlang:list_to_binary( + [<<(grey(<<<>/binary, Function/binary>>))/binary, + "\n"/utf8>>, + <<"An unexpected error occurred:\n"/utf8>>, + <<"\n"/utf8>>, + <<<<" "/utf8, (gleam@string:inspect(Error))/binary>>/binary, + "\n"/utf8>>] + ). + +-file("src/gleeunit/internal/reporting.gleam", 183). +?DOC(false). +-spec inspect_value(gleeunit@internal@gleam_panic:asserted_expression()) -> binary(). +inspect_value(Value) -> + case erlang:element(4, Value) of + unevaluated -> + grey(<<"unevaluated"/utf8>>); + + {literal, _} -> + grey(<<"literal"/utf8>>); + + {expression, Value@1} -> + gleam@string:inspect(Value@1) + end. + +-file("src/gleeunit/internal/reporting.gleam", 179). +?DOC(false). +-spec assert_value( + binary(), + gleeunit@internal@gleam_panic:asserted_expression() +) -> binary(). +assert_value(Name, Value) -> + <<<<<<(cyan(Name))/binary, ": "/utf8>>/binary, + (inspect_value(Value))/binary>>/binary, + "\n"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 160). +?DOC(false). +-spec assert_info(gleeunit@internal@gleam_panic:assert_kind()) -> binary(). +assert_info(Kind) -> + case Kind of + {binary_operator, _, Left, Right} -> + erlang:list_to_binary( + [assert_value(<<" left"/utf8>>, Left), + assert_value(<<"right"/utf8>>, Right)] + ); + + {function_call, Arguments} -> + _pipe = Arguments, + _pipe@1 = gleam@list:index_map( + _pipe, + fun(E, I) -> + Number = gleam@string:pad_start( + erlang:integer_to_binary(I), + 5, + <<" "/utf8>> + ), + assert_value(Number, E) + end + ), + erlang:list_to_binary(_pipe@1); + + {other_expression, _} -> + <<""/utf8>> + end. + +-file("src/gleeunit/internal/reporting.gleam", 113). +?DOC(false). +-spec format_gleam_error( + gleeunit@internal@gleam_panic:gleam_panic(), + binary(), + binary(), + gleam@option:option(bitstring()) +) -> binary(). +format_gleam_error(Error, Module, Function, Src) -> + Location = grey( + <<<<(erlang:element(3, Error))/binary, ":"/utf8>>/binary, + (erlang:integer_to_binary(erlang:element(6, Error)))/binary>> + ), + case erlang:element(7, Error) of + panic -> + erlang:list_to_binary( + [<<<<<<(bold(red(<<"panic"/utf8>>)))/binary, " "/utf8>>/binary, + Location/binary>>/binary, + "\n"/utf8>>, + <<<<<<<<<<(cyan(<<" test"/utf8>>))/binary, ": "/utf8>>/binary, + Module/binary>>/binary, + "."/utf8>>/binary, + Function/binary>>/binary, + "\n"/utf8>>, + <<<<<<(cyan(<<" info"/utf8>>))/binary, ": "/utf8>>/binary, + (erlang:element(2, Error))/binary>>/binary, + "\n"/utf8>>] + ); + + todo -> + erlang:list_to_binary( + [<<<<<<(bold(yellow(<<"todo"/utf8>>)))/binary, " "/utf8>>/binary, + Location/binary>>/binary, + "\n"/utf8>>, + <<<<<<<<<<(cyan(<<" test"/utf8>>))/binary, ": "/utf8>>/binary, + Module/binary>>/binary, + "."/utf8>>/binary, + Function/binary>>/binary, + "\n"/utf8>>, + <<<<<<(cyan(<<" info"/utf8>>))/binary, ": "/utf8>>/binary, + (erlang:element(2, Error))/binary>>/binary, + "\n"/utf8>>] + ); + + {assert, Start, End, _, Kind} -> + erlang:list_to_binary( + [<<<<<<(bold(red(<<"assert"/utf8>>)))/binary, " "/utf8>>/binary, + Location/binary>>/binary, + "\n"/utf8>>, + <<<<<<<<<<(cyan(<<" test"/utf8>>))/binary, ": "/utf8>>/binary, + Module/binary>>/binary, + "."/utf8>>/binary, + Function/binary>>/binary, + "\n"/utf8>>, + code_snippet(Src, Start, End), + assert_info(Kind), + <<<<<<(cyan(<<" info"/utf8>>))/binary, ": "/utf8>>/binary, + (erlang:element(2, Error))/binary>>/binary, + "\n"/utf8>>] + ); + + {let_assert, Start@1, End@1, _, _, Value} -> + erlang:list_to_binary( + [<<<<<<(bold(red(<<"let assert"/utf8>>)))/binary, " "/utf8>>/binary, + Location/binary>>/binary, + "\n"/utf8>>, + <<<<<<<<<<(cyan(<<" test"/utf8>>))/binary, ": "/utf8>>/binary, + Module/binary>>/binary, + "."/utf8>>/binary, + Function/binary>>/binary, + "\n"/utf8>>, + code_snippet(Src, Start@1, End@1), + <<<<<<(cyan(<<"value"/utf8>>))/binary, ": "/utf8>>/binary, + (gleam@string:inspect(Value))/binary>>/binary, + "\n"/utf8>>, + <<<<<<(cyan(<<" info"/utf8>>))/binary, ": "/utf8>>/binary, + (erlang:element(2, Error))/binary>>/binary, + "\n"/utf8>>] + ) + end. + +-file("src/gleeunit/internal/reporting.gleam", 71). +?DOC(false). +-spec test_failed(state(), binary(), binary(), gleam@dynamic:dynamic_()) -> state(). +test_failed(State, Module, Function, Error) -> + Message = case gleeunit_gleam_panic_ffi:from_dynamic(Error) of + {ok, Error@1} -> + Src = gleam@option:from_result( + file:read_file(erlang:element(3, Error@1)) + ), + format_gleam_error(Error@1, Module, Function, Src); + + {error, _} -> + format_unknown(Module, Function, Error) + end, + gleam_stdlib:print(<<"\n"/utf8, Message/binary>>), + {state, + erlang:element(2, State), + erlang:element(3, State) + 1, + erlang:element(4, State)}. diff --git a/build/dev/javascript/gleeunit/gleeunit@should.erl b/build/dev/javascript/gleeunit/gleeunit@should.erl new file mode 100644 index 0000000..81048de --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit@should.erl @@ -0,0 +1,153 @@ +-module(gleeunit@should). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleeunit/should.gleam"). +-export([equal/2, not_equal/2, be_ok/1, be_error/1, be_some/1, be_none/1, be_true/1, be_false/1, fail/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC(" Use the `assert` keyword instead of this module.\n"). + +-file("src/gleeunit/should.gleam", 6). +-spec equal(DOF, DOF) -> nil. +equal(A, B) -> + case A =:= B of + true -> + nil; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould equal\n"/utf8>>, + gleam@string:inspect(B)] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"equal"/utf8>>, + line => 10}) + end. + +-file("src/gleeunit/should.gleam", 19). +-spec not_equal(DOG, DOG) -> nil. +not_equal(A, B) -> + case A /= B of + true -> + nil; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould not equal\n"/utf8>>, + gleam@string:inspect(B)] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"not_equal"/utf8>>, + line => 23}) + end. + +-file("src/gleeunit/should.gleam", 32). +-spec be_ok({ok, DOH} | {error, any()}) -> DOH. +be_ok(A) -> + case A of + {ok, Value} -> + Value; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould be ok"/utf8>>] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"be_ok"/utf8>>, + line => 35}) + end. + +-file("src/gleeunit/should.gleam", 39). +-spec be_error({ok, any()} | {error, DOM}) -> DOM. +be_error(A) -> + case A of + {error, Error} -> + Error; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould be error"/utf8>>] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"be_error"/utf8>>, + line => 42}) + end. + +-file("src/gleeunit/should.gleam", 46). +-spec be_some(gleam@option:option(DOP)) -> DOP. +be_some(A) -> + case A of + {some, Value} -> + Value; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould be some"/utf8>>] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"be_some"/utf8>>, + line => 49}) + end. + +-file("src/gleeunit/should.gleam", 53). +-spec be_none(gleam@option:option(any())) -> nil. +be_none(A) -> + case A of + none -> + nil; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould be none"/utf8>>] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"be_none"/utf8>>, + line => 56}) + end. + +-file("src/gleeunit/should.gleam", 60). +-spec be_true(boolean()) -> nil. +be_true(Actual) -> + _pipe = Actual, + equal(_pipe, true). + +-file("src/gleeunit/should.gleam", 65). +-spec be_false(boolean()) -> nil. +be_false(Actual) -> + _pipe = Actual, + equal(_pipe, false). + +-file("src/gleeunit/should.gleam", 70). +-spec fail() -> nil. +fail() -> + be_true(false). diff --git a/build/dev/javascript/gleeunit/gleeunit_ffi.erl b/build/dev/javascript/gleeunit/gleeunit_ffi.erl new file mode 100644 index 0000000..05c7490 --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit_ffi.erl @@ -0,0 +1,21 @@ +-module(gleeunit_ffi). + +-export([find_files/2, run_eunit/2]). + +find_files(Pattern, In) -> + Results = filelib:wildcard(binary_to_list(Pattern), binary_to_list(In)), + lists:map(fun list_to_binary/1, Results). + +run_eunit(Tests, Options) -> + case code:which(eunit) of + non_existing -> + gleeunit@internal@reporting:eunit_missing(); + + _ -> + case eunit:test(Tests, Options) of + ok -> {ok, nil}; + error -> {error, nil}; + {error, Term} -> {error, Term} + end + end. + diff --git a/build/dev/javascript/gleeunit/gleeunit_ffi.mjs b/build/dev/javascript/gleeunit/gleeunit_ffi.mjs new file mode 100644 index 0000000..7bdc071 --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit_ffi.mjs @@ -0,0 +1,100 @@ +import { readFileSync } from "node:fs"; +import { Result$Ok, Result$Error } from "./gleam.mjs"; +import * as reporting from "./gleeunit/internal/reporting.mjs"; + +export function read_file(path) { + try { + return Result$Ok(readFileSync(path)); + } catch { + return Result$Error(undefined); + } +} + +async function* gleamFiles(directory) { + for (let entry of await read_dir(directory)) { + let path = join_path(directory, entry); + if (path.endsWith(".gleam")) { + yield path; + } else { + try { + yield* gleamFiles(path); + } catch (error) { + // Could not read directory, assume it's a file + } + } + } +} + +async function readRootPackageName() { + let toml = await async_read_file("gleam.toml", "utf-8"); + for (let line of toml.split("\n")) { + let matches = line.match(/\s*name\s*=\s*"([a-z][a-z0-9_]*)"/); // Match regexp in compiler-cli/src/new.rs in validate_name() + if (matches) return matches[1]; + } + throw new Error("Could not determine package name from gleam.toml"); +} + +export async function main() { + let state = reporting.new_state(); + + let packageName = await readRootPackageName(); + let dist = `../${packageName}/`; + + for await (let path of await gleamFiles("test")) { + let js_path = path.slice("test/".length).replace(".gleam", ".mjs"); + let module = await import(join_path(dist, js_path)); + for (let fnName of Object.keys(module)) { + if (!fnName.endsWith("_test")) continue; + try { + await module[fnName](); + state = reporting.test_passed(state); + } catch (error) { + let moduleName = js_path.slice(0, -4); + state = reporting.test_failed(state, moduleName, fnName, error); + } + } + } + + const status = reporting.finished(state); + exit(status); +} + +export function crash(message) { + throw new Error(message); +} + +function exit(code) { + if (globalThis.Deno) { + Deno.exit(code); + } else { + process.exit(code); + } +} + +async function read_dir(path) { + if (globalThis.Deno) { + let items = []; + for await (let item of Deno.readDir(path, { withFileTypes: true })) { + items.push(item.name); + } + return items; + } else { + let { readdir } = await import("node:fs/promises"); + return readdir(path); + } +} + +function join_path(a, b) { + if (a.endsWith("/")) return a + b; + return a + "/" + b; +} + +async function async_read_file(path) { + if (globalThis.Deno) { + return Deno.readTextFile(path); + } else { + let { readFile } = await import("node:fs/promises"); + let contents = await readFile(path); + return contents.toString(); + } +} diff --git a/build/dev/javascript/gleeunit/gleeunit_progress.erl b/build/dev/javascript/gleeunit/gleeunit_progress.erl new file mode 100644 index 0000000..e6576a5 --- /dev/null +++ b/build/dev/javascript/gleeunit/gleeunit_progress.erl @@ -0,0 +1,72 @@ +%% A formatter adapted from Sean Cribb's https://github.com/seancribbs/eunit_formatters + +-module(gleeunit_progress). +-define(NOTEST, true). + +%% eunit_listener callbacks +-export([ + init/1, handle_begin/3, handle_end/3, handle_cancel/3, terminate/2, + start/0, start/1 +]). + +-define(reporting, gleeunit@internal@reporting). + +start() -> + start([]). + +start(Options) -> + eunit_listener:start(?MODULE, Options). + +init(_Options) -> + ?reporting:new_state(). + +handle_begin(_test_or_group, _data, State) -> + State. + +handle_end(group, _data, State) -> + State; +handle_end(test, Data, State) -> + {AtomModule, AtomFunction, _Arity} = proplists:get_value(source, Data), + Module = erlang:atom_to_binary(AtomModule), + Function = erlang:atom_to_binary(AtomFunction), + + % EUnit swallows stdout, so print it to make debugging easier. + case proplists:get_value(output, Data) of + undefined -> ok; + <<>> -> ok; + Out -> gleam@io:print(Out) + end, + + case proplists:get_value(status, Data) of + ok -> + ?reporting:test_passed(State); + {skipped, _Reason} -> + ?reporting:test_skipped(State, Module, Function); + {error, {_, Exception, _Stack}} -> + ?reporting:test_failed(State, Module, Function, Exception) + end. + + +handle_cancel(_test_or_group, Data, State) -> + ?reporting:test_failed(State, <<"gleeunit">>, <<"main">>, Data). + +terminate({ok, _Data}, State) -> + ?reporting:finished(State), + ok; +terminate({error, Reason}, State) -> + ?reporting:finished(State), + io:fwrite(" +Eunit failed: + +~80p + +This is probably a bug in gleeunit. Please report it. +", [Reason]), + sync_end(error). + +sync_end(Result) -> + receive + {stop, Reference, ReplyTo} -> + ReplyTo ! {result, Reference, Result}, + ok + end. diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint.cache b/build/dev/javascript/paint/_gleam_artefacts/paint.cache new file mode 100644 index 0000000..1855215 Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint.cache differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint.cache_inline b/build/dev/javascript/paint/_gleam_artefacts/paint.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint.cache_inline differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint.cache_meta b/build/dev/javascript/paint/_gleam_artefacts/paint.cache_meta new file mode 100644 index 0000000..10507bd Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint.cache_meta differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@canvas.cache b/build/dev/javascript/paint/_gleam_artefacts/paint@canvas.cache new file mode 100644 index 0000000..315922b Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@canvas.cache differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@canvas.cache_inline b/build/dev/javascript/paint/_gleam_artefacts/paint@canvas.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@canvas.cache_inline differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@canvas.cache_meta b/build/dev/javascript/paint/_gleam_artefacts/paint@canvas.cache_meta new file mode 100644 index 0000000..25fa8d6 Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@canvas.cache_meta differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@encode.cache b/build/dev/javascript/paint/_gleam_artefacts/paint@encode.cache new file mode 100644 index 0000000..ab7aec2 Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@encode.cache differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@encode.cache_inline b/build/dev/javascript/paint/_gleam_artefacts/paint@encode.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@encode.cache_inline differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@encode.cache_meta b/build/dev/javascript/paint/_gleam_artefacts/paint@encode.cache_meta new file mode 100644 index 0000000..a0d8583 Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@encode.cache_meta differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@event.cache b/build/dev/javascript/paint/_gleam_artefacts/paint@event.cache new file mode 100644 index 0000000..787d70d Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@event.cache differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@event.cache_inline b/build/dev/javascript/paint/_gleam_artefacts/paint@event.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@event.cache_inline differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@event.cache_meta b/build/dev/javascript/paint/_gleam_artefacts/paint@event.cache_meta new file mode 100644 index 0000000..6aaec3a Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@event.cache_meta differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@internal@impl_canvas.cache b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@impl_canvas.cache new file mode 100644 index 0000000..f7d3a72 Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@impl_canvas.cache differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@internal@impl_canvas.cache_inline b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@impl_canvas.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@impl_canvas.cache_inline differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@internal@impl_canvas.cache_meta b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@impl_canvas.cache_meta new file mode 100644 index 0000000..db2fae1 Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@impl_canvas.cache_meta differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@internal@types.cache b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@types.cache new file mode 100644 index 0000000..72e9025 Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@types.cache differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@internal@types.cache_inline b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@types.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@types.cache_inline differ diff --git a/build/dev/javascript/paint/_gleam_artefacts/paint@internal@types.cache_meta b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@types.cache_meta new file mode 100644 index 0000000..b521eae Binary files /dev/null and b/build/dev/javascript/paint/_gleam_artefacts/paint@internal@types.cache_meta differ diff --git a/build/dev/javascript/paint/gleam.mjs b/build/dev/javascript/paint/gleam.mjs new file mode 100644 index 0000000..197cbbc --- /dev/null +++ b/build/dev/javascript/paint/gleam.mjs @@ -0,0 +1 @@ +export * from "../prelude.mjs"; diff --git a/build/dev/javascript/paint/impl_canvas_bindings.mjs b/build/dev/javascript/paint/impl_canvas_bindings.mjs new file mode 100644 index 0000000..5fed16a --- /dev/null +++ b/build/dev/javascript/paint/impl_canvas_bindings.mjs @@ -0,0 +1,271 @@ +import { Ok, Error } from "./gleam.mjs"; + +class PaintCanvas extends HTMLElement { + // Open an issue if you are in need of any other attributes :) + static observedAttributes = ["width", "height", "style", "picture"]; + + constructor() { + super(); + // Create a canvas + this.canvas = document.createElement("canvas"); + const style = document.createElement("style"); + style.textContent = ` + :host { + display: inline-block; + } + `; + this.shadow = this.attachShadow({ mode: "open" }); + this.shadow.appendChild(style); + this.shadow.appendChild(this.canvas); + this.ctx = this.canvas.getContext("2d"); + } + + attributeChangedCallback(name, _oldValue, newValue) { + if (name === "picture") { + this.picture = newValue; + return; + } else if (name === "width") { + this.width = newValue; + } else if (name === "height") { + this.height = newValue; + } + } + + drawPicture() { + if (!this.pictureString) { + return; + } + + this.ctx.reset(); + const display = + window.PAINT_STATE[ + "display_on_rendering_context_with_default_drawing_state" + ]; + + display(this.pictureString, this.ctx); + } + + set picture(value) { + this.pictureString = value; + this.drawPicture(); + } + + set width(value) { + this.canvas.width = value; + this.drawPicture(); + } + + set height(value) { + this.canvas.height = value; + this.drawPicture(); + } + + get width() { + return this.canvas.width; + } + + get height() { + return this.canvas.height; + } +} + +export function define_web_component() { + window.customElements.define("paint-canvas", PaintCanvas); +} + +export function get_rendering_context(selector) { + // TODO: Handle the case where the canvas element is not found. + return document.querySelector(selector).getContext("2d"); +} + +export function setup_request_animation_frame(callback) { + window.requestAnimationFrame((time) => { + callback(time); + }); +} + +export function setup_input_handler(event_name, callback) { + window.addEventListener(event_name, callback); +} + +export function get_key_code(event) { + return event.keyCode; +} + +export function set_global(state, id) { + if (typeof window.PAINT_STATE == "undefined") { + window.PAINT_STATE = {}; + } + window.PAINT_STATE[id] = state; +} + +export function get_global(id) { + if (!window.PAINT_STATE) { + return new Error(undefined); + } + if (!(id in window.PAINT_STATE)) { + return new Error(undefined); + } + return new Ok(window.PAINT_STATE[id]); +} + +export function get_width(ctx) { + return ctx.canvas.clientWidth; +} + +export function get_height(ctx) { + return ctx.canvas.clientHeight; +} + +// Based on https://stackoverflow.com/questions/17130395/real-mouse-position-in-canvas +export function mouse_pos(ctx, event) { + // Calculate the scaling of the canvas vs its content + const rect = ctx.canvas.getBoundingClientRect(); + const scaleX = ctx.canvas.width / rect.width; + const scaleY = ctx.canvas.height / rect.height; + + return [ + (event.clientX - rect.left) * scaleX, + (event.clientY - rect.top) * scaleY, + ]; +} + +// if check_pressed is true, the function will return true if the button was pressed +// if check_pressed is false, the function will return true if the button was released +export function check_mouse_button( + event, + previous_event, + button_index, + check_pressed, +) { + let previous_buttons = previous_event?.buttons ?? 0; + let current_buttons = event.buttons; + + // ~001 && + // 011 + // ----- + // 010 found the newly pressed! + // + // 011 && + // ~001 + // ----- + // 010 found the newly released! + if (check_pressed) { + previous_buttons = ~previous_buttons; + } else { + current_buttons = ~current_buttons; + } + + let button = previous_buttons & current_buttons & (1 << button_index); + return !!button; +} + +export function reset(ctx) { + ctx.reset(); +} + +export function arc(ctx, radius, start, end, fill, stroke) { + ctx.beginPath(); + ctx.arc(0, 0, radius, start, end); + if (fill) { + ctx.fill(); + } + if (stroke) { + ctx.stroke(); + } +} + +export function polygon(ctx, points, closed, fill, stroke) { + ctx.beginPath(); + ctx.moveTo(0, 0); + let started = false; + for (const point of points) { + let x = point[0]; + let y = point[1]; + if (started) { + ctx.lineTo(x, y); + } else { + ctx.moveTo(x, y); + started = true; + } + } + + if (closed) { + ctx.closePath(); + } + + if (fill && closed) { + ctx.fill(); + } + + if (stroke) { + ctx.stroke(); + } +} + +export function text(ctx, text, style) { + ctx.font = style; + ctx.fillText(text, 0, 0); +} + +export function save(ctx) { + ctx.save(); +} + +export function restore(ctx) { + ctx.restore(); +} + +export function set_fill_colour(ctx, css_colour) { + ctx.fillStyle = css_colour; +} + +export function set_stroke_color(ctx, css_color) { + ctx.strokeStyle = css_color; +} + +export function set_line_width(ctx, width) { + ctx.lineWidth = width; +} + +export function translate(ctx, x, y) { + ctx.translate(x, y); +} + +export function scale(ctx, x, y) { + ctx.scale(x, y); +} + +export function rotate(ctx, radians) { + ctx.rotate(radians); +} + +export function reset_transform(ctx) { + ctx.resetTransform(); +} + +export function draw_image(ctx, image, width_px, height_px) { + ctx.drawImage(image, 0, 0, width_px, height_px); +} + +export function image_from_query(selector) { + return document.querySelector(selector); +} + +export function image_from_src(src) { + const image = new Image(); + image.src = src; + return image; +} + +export function on_image_load(image, callback) { + if (image.complete) { + callback(); + } else { + image.addEventListener("load", callback); + } +} + +export function set_image_smoothing_enabled(ctx, value) { + ctx.imageSmoothingEnabled = value; +} diff --git a/build/dev/javascript/paint/numbers_ffi.mjs b/build/dev/javascript/paint/numbers_ffi.mjs new file mode 100644 index 0000000..0e6e6d3 --- /dev/null +++ b/build/dev/javascript/paint/numbers_ffi.mjs @@ -0,0 +1,3 @@ +export function pi() { + return Math.PI; +} diff --git a/build/dev/javascript/paint/paint.mjs b/build/dev/javascript/paint/paint.mjs new file mode 100644 index 0000000..ba06945 --- /dev/null +++ b/build/dev/javascript/paint/paint.mjs @@ -0,0 +1,246 @@ +import * as $colour from "../gleam_community_colour/gleam_community/colour.mjs"; +import * as $result from "../gleam_stdlib/gleam/result.mjs"; +import { toList, makeError } from "./gleam.mjs"; +import { pi } from "./numbers_ffi.mjs"; +import * as $internal_implementation from "./paint/internal/types.mjs"; + +const FILEPATH = "src/paint.gleam"; + +/** + * Create an angle expressed in radians + */ +export function angle_rad(radians) { + return new $internal_implementation.Radians(radians); +} + +/** + * A utility around [colour.from_rgb_hex_string](https://hexdocs.pm/gleam_community_colour/gleam_community/colour.html#from_rgb_hex_string) + * (from `gleam_community/colour`) that **panics** on an invalid hex code. + */ +export function colour_hex(string) { + return $result.lazy_unwrap( + $colour.from_rgb_hex_string(string), + () => { + throw makeError( + "panic", + FILEPATH, + "paint", + 47, + "colour_hex", + "Failed to parse hex code", + {} + ) + }, + ); +} + +/** + * A utility around [colour.from_rgb255](https://hexdocs.pm/gleam_community_colour/gleam_community/colour.html#from_rgb255) + * (from `gleam_community/colour`) that **panics** if the values are outside of the allowed range. + */ +export function colour_rgb(red, green, blue) { + return $result.lazy_unwrap( + $colour.from_rgb255(red, green, blue), + () => { + throw makeError( + "panic", + FILEPATH, + "paint", + 55, + "colour_rgb", + "The value was not inside of the valid range [0-255]", + {} + ) + }, + ); +} + +/** + * A blank picture + */ +export function blank() { + return new $internal_implementation.Blank(); +} + +/** + * An arc with some radius going from some + * starting angle to some other angle in clock-wise direction + */ +export function arc(radius, start, end) { + return new $internal_implementation.Arc(radius, start, end); +} + +/** + * A polygon consisting of a list of 2d points + */ +export function polygon(points) { + return new $internal_implementation.Polygon(points, true); +} + +/** + * Lines (same as a polygon but not a closed shape) + */ +export function lines(points) { + return new $internal_implementation.Polygon(points, false); +} + +/** + * A rectangle with some given width and height + */ +export function rectangle(width, height) { + return polygon( + toList([[0.0, 0.0], [width, 0.0], [width, height], [0.0, height]]), + ); +} + +/** + * A square + */ +export function square(length) { + return rectangle(length, length); +} + +/** + * Draw an image such as a PNG, JPEG or an SVG. See the `canvas` back-end for more details on how to load images. + */ +export function image(image, width_px, height_px) { + return new $internal_implementation.ImageRef(image, width_px, height_px); +} + +/** + * Set image scaling to be smooth (this is the default behaviour) + */ +export function image_scaling_smooth(picture) { + return new $internal_implementation.ImageScalingBehaviour( + picture, + new $internal_implementation.ScalingSmooth(), + ); +} + +/** + * Disable smooth image scaling, suitable for pixel art. + */ +export function image_scaling_pixelated(picture) { + return new $internal_implementation.ImageScalingBehaviour( + picture, + new $internal_implementation.ScalingPixelated(), + ); +} + +/** + * Text with some given font size + */ +export function text(text, font_size) { + return new $internal_implementation.Text( + text, + new $internal_implementation.FontProperties(font_size, "sans-serif"), + ); +} + +/** + * Translate a picture in horizontal and vertical direction + */ +export function translate_xy(picture, x, y) { + return new $internal_implementation.Translate(picture, [x, y]); +} + +/** + * Translate a picture in the horizontal direction + */ +export function translate_x(picture, x) { + return translate_xy(picture, x, 0.0); +} + +/** + * Translate a picture in the vertical direction + */ +export function translate_y(picture, y) { + return translate_xy(picture, 0.0, y); +} + +/** + * Scale the picture in the horizontal direction + */ +export function scale_x(picture, factor) { + return new $internal_implementation.Scale(picture, [factor, 1.0]); +} + +/** + * Scale the picture in the vertical direction + */ +export function scale_y(picture, factor) { + return new $internal_implementation.Scale(picture, [1.0, factor]); +} + +/** + * Scale the picture uniformly in horizontal and vertical direction + */ +export function scale_uniform(picture, factor) { + return new $internal_implementation.Scale(picture, [factor, factor]); +} + +/** + * Rotate the picture in a clock-wise direction + */ +export function rotate(picture, angle) { + return new $internal_implementation.Rotate(picture, angle); +} + +/** + * Fill a picture with some given colour, see `Colour`. + */ +export function fill(picture, colour) { + return new $internal_implementation.Fill(picture, colour); +} + +/** + * Set a solid stroke with some given colour and width + */ +export function stroke(picture, colour, width) { + return new $internal_implementation.Stroke( + picture, + new $internal_implementation.SolidStroke(colour, width), + ); +} + +/** + * Remove the stroke of the given picture + */ +export function stroke_none(picture) { + return new $internal_implementation.Stroke( + picture, + new $internal_implementation.NoStroke(), + ); +} + +/** + * Combine multiple pictures into one + */ +export function combine(pictures) { + return new $internal_implementation.Combine(pictures); +} + +/** + * Concatenate two pictures + */ +export function concat(picture, another_picture) { + return combine(toList([picture, another_picture])); +} + +/** + * Create an angle expressed in degrees + */ +export function angle_deg(degrees) { + return new $internal_implementation.Radians(((degrees * pi())) / 180.0); +} + +/** + * A circle with some given radius + */ +export function circle(radius) { + return new $internal_implementation.Arc( + radius, + new $internal_implementation.Radians(0.0), + new $internal_implementation.Radians(2.0 * pi()), + ); +} diff --git a/build/dev/javascript/paint/paint/canvas.mjs b/build/dev/javascript/paint/paint/canvas.mjs new file mode 100644 index 0000000..1bb61c7 --- /dev/null +++ b/build/dev/javascript/paint/paint/canvas.mjs @@ -0,0 +1,655 @@ +import * as $colour from "../../gleam_community_colour/gleam_community/colour.mjs"; +import * as $int from "../../gleam_stdlib/gleam/int.mjs"; +import * as $option from "../../gleam_stdlib/gleam/option.mjs"; +import { None, Some } from "../../gleam_stdlib/gleam/option.mjs"; +import { Ok, Empty as $Empty, CustomType as $CustomType, makeError } from "../gleam.mjs"; +import * as $paint from "../paint.mjs"; +import { translate_xy } from "../paint.mjs"; +import * as $encode from "../paint/encode.mjs"; +import * as $event from "../paint/event.mjs"; +import * as $impl_canvas from "../paint/internal/impl_canvas.mjs"; +import * as $types from "../paint/internal/types.mjs"; +import { + Arc, + Blank, + Combine, + Fill, + FontProperties, + Image, + NoStroke, + Polygon, + Radians, + Rotate, + Scale, + SolidStroke, + Stroke, + Text, + Translate, +} from "../paint/internal/types.mjs"; + +const FILEPATH = "src/paint/canvas.gleam"; + +export class Config extends $CustomType { + constructor(width, height) { + super(); + this.width = width; + this.height = height; + } +} +export const Config$Config = (width, height) => new Config(width, height); +export const Config$isConfig = (value) => value instanceof Config; +export const Config$Config$width = (value) => value.width; +export const Config$Config$0 = (value) => value.width; +export const Config$Config$height = (value) => value.height; +export const Config$Config$1 = (value) => value.height; + +class DrawingState extends $CustomType { + constructor(fill, stroke) { + super(); + this.fill = fill; + this.stroke = stroke; + } +} + +/** + * Create a reference to an image using a CSS query selector. For example: + * ``` + * fn kitten() { + * canvas.image_from_query("#kitten") + * } + * // In the HTML file: + * // + * ``` + * + * > [!WARNING] + * > **Important**: Make sure the image has loaded before trying to draw a pictures referencing it. + * > You can do this using `canvas.wait_until_loaded` function. + */ +export function image_from_query(selector) { + let id = "image-selector-" + selector; + let $ = $impl_canvas.get_global(id); + if ($ instanceof Ok) { + undefined + } else { + let image = $impl_canvas.image_from_query(selector); + $impl_canvas.set_global(image, id) + } + return new Image(id); +} + +/** + * Create a reference to an image using a source path. + * ``` + * fn my_logo_image() { + * canvas.image_from_src("./priv/static/logo.svg") + * } + * ``` + * + * > [!WARNING] + * > **Important**: Make sure the image has loaded before trying to draw a pictures referencing it. + * > You can do this using `canvas.wait_until_loaded` function. + */ +export function image_from_src(src) { + let id = "image-src-" + src; + let $ = $impl_canvas.get_global(id); + if ($ instanceof Ok) { + undefined + } else { + let image = $impl_canvas.image_from_src(src); + $impl_canvas.set_global(image, id) + } + return new Image(id); +} + +/** + * Wait until a list of images have all been loaded, for example: + * ``` + * fn lucy() { + * canvas.image_from_query("#lucy") + * } + * + * fn cat() { + * canvas.image_from_src("./path/to/kitten.png") + * } + * + * pub fn main() { + * use <- canvas.wait_until_loaded([lucy(), kitten()]) + * // It is now safe to draw Pictures containing the images lucy and kitten :) + * } + * ``` + */ +export function wait_until_loaded(images, on_loaded) { + if (images instanceof $Empty) { + return on_loaded(); + } else { + let image = images.head; + let rest = images.tail; + let id; + id = image.id; + let $ = $impl_canvas.get_global(id); + let js_image; + if ($ instanceof Ok) { + js_image = $[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "paint/canvas", + 101, + "wait_until_loaded", + "Pattern match failed, no pattern matched the value.", + { + value: $, + start: 3025, + end: 3077, + pattern_start: 3036, + pattern_end: 3048 + } + ) + } + return $impl_canvas.on_image_load( + js_image, + () => { return wait_until_loaded(rest, on_loaded); }, + ); + } +} + +function display_on_rendering_context(loop$picture, loop$ctx, loop$state) { + while (true) { + let picture = loop$picture; + let ctx = loop$ctx; + let state = loop$state; + if (picture instanceof Blank) { + return undefined; + } else if (picture instanceof Polygon) { + let points = picture[0]; + let closed = picture.closed; + return $impl_canvas.polygon(ctx, points, closed, state.fill, state.stroke); + } else if (picture instanceof Arc) { + let radius = picture.radius; + let start = picture.start; + let end = picture.end; + let start_radians; + start_radians = start[0]; + let end_radians; + end_radians = end[0]; + return $impl_canvas.arc( + ctx, + radius, + start_radians, + end_radians, + state.fill, + state.stroke, + ); + } else if (picture instanceof Text) { + let text = picture.text; + let properties = picture.style; + let size_px; + let font_family; + size_px = properties.size_px; + font_family = properties.font_family; + $impl_canvas.save(ctx); + $impl_canvas.text( + ctx, + text, + ($int.to_string(size_px) + "px ") + font_family, + ); + return $impl_canvas.restore(ctx); + } else if (picture instanceof $types.ImageRef) { + let width_px = picture.width_px; + let height_px = picture.height_px; + let id = picture[0].id; + let $ = $impl_canvas.get_global(id); + let image; + if ($ instanceof Ok) { + image = $[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "paint/canvas", + 231, + "display_on_rendering_context", + "Pattern match failed, no pattern matched the value.", + { + value: $, + start: 6608, + end: 6657, + pattern_start: 6619, + pattern_end: 6628 + } + ) + } + return $impl_canvas.draw_image(ctx, image, width_px, height_px); + } else if (picture instanceof Fill) { + let p = picture[0]; + let colour = picture[1]; + $impl_canvas.save(ctx); + $impl_canvas.set_fill_colour(ctx, $colour.to_css_rgba_string(colour)); + display_on_rendering_context(p, ctx, new DrawingState(true, state.stroke)); + return $impl_canvas.restore(ctx); + } else if (picture instanceof Stroke) { + let p = picture[0]; + let stroke = picture[1]; + if (stroke instanceof NoStroke) { + loop$picture = p; + loop$ctx = ctx; + loop$state = new DrawingState(state.fill, false); + } else { + let color = stroke[0]; + let width = stroke[1]; + $impl_canvas.save(ctx); + $impl_canvas.set_stroke_color(ctx, $colour.to_css_rgba_string(color)); + $impl_canvas.set_line_width(ctx, width); + display_on_rendering_context(p, ctx, new DrawingState(state.fill, true)); + return $impl_canvas.restore(ctx); + } + } else if (picture instanceof $types.ImageScalingBehaviour) { + let p = picture[0]; + let behaviour = picture[1]; + $impl_canvas.save(ctx); + $impl_canvas.set_image_smoothing_enabled( + ctx, + (() => { + if (behaviour instanceof $types.ScalingSmooth) { + return true; + } else { + return false; + } + })(), + ); + display_on_rendering_context(p, ctx, state); + return $impl_canvas.restore(ctx); + } else if (picture instanceof Translate) { + let p = picture[0]; + let vec = picture[1]; + let x; + let y; + x = vec[0]; + y = vec[1]; + $impl_canvas.save(ctx); + $impl_canvas.translate(ctx, x, y); + display_on_rendering_context(p, ctx, state); + return $impl_canvas.restore(ctx); + } else if (picture instanceof Scale) { + let p = picture[0]; + let vec = picture[1]; + let x; + let y; + x = vec[0]; + y = vec[1]; + $impl_canvas.save(ctx); + $impl_canvas.scale(ctx, x, y); + display_on_rendering_context(p, ctx, state); + return $impl_canvas.restore(ctx); + } else if (picture instanceof Rotate) { + let p = picture[0]; + let angle = picture[1]; + let rad; + rad = angle[0]; + $impl_canvas.save(ctx); + $impl_canvas.rotate(ctx, rad); + display_on_rendering_context(p, ctx, state); + return $impl_canvas.restore(ctx); + } else { + let pictures = picture[0]; + if (pictures instanceof $Empty) { + return undefined; + } else { + let p = pictures.head; + let ps = pictures.tail; + display_on_rendering_context(p, ctx, state); + loop$picture = new Combine(ps); + loop$ctx = ctx; + loop$state = state; + } + } + } +} + +function parse_key_code(key_code) { + if (key_code === 32) { + return new Some(new $event.KeySpace()); + } else if (key_code === 37) { + return new Some(new $event.KeyLeftArrow()); + } else if (key_code === 38) { + return new Some(new $event.KeyUpArrow()); + } else if (key_code === 39) { + return new Some(new $event.KeyRightArrow()); + } else if (key_code === 40) { + return new Some(new $event.KeyDownArrow()); + } else if (key_code === 87) { + return new Some(new $event.KeyW()); + } else if (key_code === 65) { + return new Some(new $event.KeyA()); + } else if (key_code === 83) { + return new Some(new $event.KeyS()); + } else if (key_code === 68) { + return new Some(new $event.KeyD()); + } else if (key_code === 90) { + return new Some(new $event.KeyZ()); + } else if (key_code === 88) { + return new Some(new $event.KeyX()); + } else if (key_code === 67) { + return new Some(new $event.KeyC()); + } else if (key_code === 18) { + return new Some(new $event.KeyEnter()); + } else if (key_code === 27) { + return new Some(new $event.KeyEscape()); + } else if (key_code === 8) { + return new Some(new $event.KeyBackspace()); + } else { + return new None(); + } +} + +/** + * Utility to set the origin in the center of the canvas + */ +export function center(picture) { + return (config) => { + let width; + let height; + width = config.width; + height = config.height; + let _pipe = picture; + return translate_xy(_pipe, width * 0.5, height * 0.5); + }; +} + +const default_drawing_state = /* @__PURE__ */ new DrawingState(false, true); + +/** + * Display a picture on a HTML canvas element + * (specified by some [CSS Selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors)). + * ``` + * canvas.display(fn (_: canvas.Config) { circle(50.0) }, "#mycanvas") + * ``` + */ +export function display(init, selector) { + let ctx = $impl_canvas.get_rendering_context(selector); + $impl_canvas.reset(ctx); + let picture = init( + new Config($impl_canvas.get_width(ctx), $impl_canvas.get_height(ctx)), + ); + return display_on_rendering_context(picture, ctx, default_drawing_state); +} + +function get_tick_func(ctx, view, update, selector) { + return (time) => { + let $ = $impl_canvas.get_global(selector); + let current_state; + if ($ instanceof Ok) { + current_state = $[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "paint/canvas", + 399, + "get_tick_func", + "Pattern match failed, no pattern matched the value.", + { + value: $, + start: 11581, + end: 11644, + pattern_start: 11592, + pattern_end: 11609 + } + ) + } + let new_state = update(current_state, new $event.Tick(time)); + $impl_canvas.set_global(new_state, selector); + let picture = view(new_state); + $impl_canvas.reset(ctx); + display_on_rendering_context(picture, ctx, default_drawing_state); + return $impl_canvas.setup_request_animation_frame( + get_tick_func(ctx, view, update, selector), + ); + }; +} + +/** + * Animations, interactive applications and tiny games can be built using the + * `interact` function. It roughly follows the [Elm architecture](https://guide.elm-lang.org/architecture/). + * Here is a short example: + * ``` + * type State = + * Int + * + * fn init(_: canvas.Config) -> State { + * 0 + * } + * + * fn update(state: State, event: event.Event) -> State { + * case event { + * event.Tick(_) -> state + 1 + * _ -> state + * } + * } + * + * fn view(state: State) -> Picture { + * paint.circle(int.to_float(state)) + * } + * + * fn main() { + * interact(init, update, view, "#mycanvas") + * } + * ``` + */ +export function interact(init, update, view, selector) { + let ctx = $impl_canvas.get_rendering_context(selector); + let initial_state = init( + new Config($impl_canvas.get_width(ctx), $impl_canvas.get_height(ctx)), + ); + $impl_canvas.set_global(initial_state, selector); + let create_key_handler = (event_name, constructor) => { + return $impl_canvas.setup_input_handler( + event_name, + (event) => { + let key = parse_key_code($impl_canvas.get_key_code(event)); + if (key instanceof Some) { + let key$1 = key[0]; + let $ = $impl_canvas.get_global(selector); + let old_state; + if ($ instanceof Ok) { + old_state = $[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "paint/canvas", + 292, + "interact", + "Pattern match failed, no pattern matched the value.", + { + value: $, + start: 8332, + end: 8391, + pattern_start: 8343, + pattern_end: 8356 + } + ) + } + let new_state = update(old_state, constructor(key$1)); + return $impl_canvas.set_global(new_state, selector); + } else { + return undefined; + } + }, + ); + }; + create_key_handler( + "keydown", + (var0) => { return new $event.KeyboardPressed(var0); }, + ); + create_key_handler( + "keyup", + (var0) => { return new $event.KeyboardRelased(var0); }, + ); + $impl_canvas.setup_input_handler( + "mousemove", + (event) => { + let $ = $impl_canvas.mouse_pos(ctx, event); + let x; + let y; + x = $[0]; + y = $[1]; + let $1 = $impl_canvas.get_global(selector); + let old_state; + if ($1 instanceof Ok) { + old_state = $1[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "paint/canvas", + 309, + "interact", + "Pattern match failed, no pattern matched the value.", + { + value: $1, + start: 8863, + end: 8922, + pattern_start: 8874, + pattern_end: 8887 + } + ) + } + let new_state = update(old_state, new $event.MouseMoved(x, y)); + $impl_canvas.set_global(new_state, selector); + return undefined; + }, + ); + let create_mouse_button_handler = (event_name, constructor, check_pressed) => { + return $impl_canvas.setup_input_handler( + event_name, + (event) => { + let previous_event_id = "PAINT_PREVIOUS_MOUSE_INPUT_FOR_" + selector; + let previous_event = $impl_canvas.get_global(previous_event_id); + $impl_canvas.set_global(event, previous_event_id); + let check_button = (i) => { + return $impl_canvas.check_mouse_button( + event, + previous_event, + i, + check_pressed, + ); + }; + let trigger_update = (button) => { + let $ = $impl_canvas.get_global(selector); + let old_state; + if ($ instanceof Ok) { + old_state = $[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "paint/canvas", + 338, + "interact", + "Pattern match failed, no pattern matched the value.", + { + value: $, + start: 9856, + end: 9915, + pattern_start: 9867, + pattern_end: 9880 + } + ) + } + let new_state = update(old_state, constructor(button)); + return $impl_canvas.set_global(new_state, selector); + }; + let $ = check_button(0); + if ($) { + trigger_update(new $event.MouseButtonLeft()) + } else { + undefined + } + let $1 = check_button(1); + if ($1) { + trigger_update(new $event.MouseButtonRight()) + } else { + undefined + } + let $2 = check_button(2); + if ($2) { + trigger_update(new $event.MouseButtonMiddle()) + } else { + undefined + } + return undefined; + }, + ); + }; + create_mouse_button_handler( + "mousedown", + (var0) => { return new $event.MousePressed(var0); }, + true, + ); + create_mouse_button_handler( + "mouseup", + (var0) => { return new $event.MouseReleased(var0); }, + false, + ); + return $impl_canvas.setup_request_animation_frame( + get_tick_func(ctx, view, update, selector), + ); +} + +/** + * If you are using [Lustre](https://github.com/lustre-labs/lustre) or some other framework to build + * your web application you may prefer to use the [web components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) API + * and the `define_web_component` function. + * ``` + * // Call this function once to register a custom HTML element + * canvas.define_web_component() + * // You can then display your picture by setting the "picture" + * // property or attribute on the element. + * + * // In Lustre it would look something like this: + * fn canvas(picture: paint.Picture, attributes: List(attribute.Attribute(a))) { + * element.element( + * "paint-canvas", + * [attribute.attribute("picture", encode.to_string(picture)), ..attributes], + * [], + * ) + *} + * ``` + * A more detailed example for using this API can be found in the `demos/with_lustre` directory. + */ +export function define_web_component() { + $impl_canvas.define_web_component(); + return $impl_canvas.set_global( + (encoded_picture, ctx) => { + let $ = $encode.from_string(encoded_picture); + let picture; + if ($ instanceof Ok) { + picture = $[0]; + } else { + throw makeError( + "let_assert", + FILEPATH, + "paint/canvas", + 447, + "define_web_component", + "Invalid picture provided to web component", + { + value: $, + start: 13602, + end: 13662, + pattern_start: 13613, + pattern_end: 13624 + } + ) + } + return display_on_rendering_context(picture, ctx, default_drawing_state); + }, + "display_on_rendering_context_with_default_drawing_state", + ); +} diff --git a/build/dev/javascript/paint/paint/encode.mjs b/build/dev/javascript/paint/paint/encode.mjs new file mode 100644 index 0000000..f9b88f3 --- /dev/null +++ b/build/dev/javascript/paint/paint/encode.mjs @@ -0,0 +1,506 @@ +import * as $colour from "../../gleam_community_colour/gleam_community/colour.mjs"; +import * as $json from "../../gleam_json/gleam/json.mjs"; +import * as $decode from "../../gleam_stdlib/gleam/dynamic/decode.mjs"; +import { toList } from "../gleam.mjs"; +import * as $paint from "../paint.mjs"; +import * as $types from "../paint/internal/types.mjs"; +import { FontProperties, NoStroke, Radians, SolidStroke } from "../paint/internal/types.mjs"; + +function decode_angle() { + return $decode.field( + "radians", + $decode.float, + (radians) => { return $decode.success(new Radians(radians)); }, + ); +} + +function decode_font() { + return $decode.field( + "sizePx", + $decode.int, + (size_px) => { + return $decode.field( + "fontFamily", + $decode.string, + (font_family) => { + return $decode.success(new FontProperties(size_px, font_family)); + }, + ); + }, + ); +} + +function decode_stroke() { + return $decode.field( + "type", + $decode.string, + (stroke_type) => { + if (stroke_type === "noStroke") { + return $decode.success(new NoStroke()); + } else if (stroke_type === "solidStroke") { + return $decode.field( + "colour", + $colour.decoder(), + (colour) => { + return $decode.field( + "thickness", + $decode.float, + (thickness) => { + return $decode.success(new SolidStroke(colour, thickness)); + }, + ); + }, + ); + } else { + return $decode.failure(new NoStroke(), "StrokeProperties"); + } + }, + ); +} + +function decode_vec2() { + return $decode.field( + "x", + $decode.float, + (x) => { + return $decode.field( + "y", + $decode.float, + (y) => { return $decode.success([x, y]); }, + ); + }, + ); +} + +function decode_picture() { + return $decode.recursive( + () => { + return $decode.field( + "type", + $decode.string, + (ty) => { + if (ty === "arc") { + return $decode.field( + "radius", + $decode.float, + (radius) => { + return $decode.field( + "start", + decode_angle(), + (start) => { + return $decode.field( + "end", + decode_angle(), + (end) => { + return $decode.success( + new $types.Arc(radius, start, end), + ); + }, + ); + }, + ); + }, + ); + } else if (ty === "blank") { + return $decode.success(new $types.Blank()); + } else if (ty === "combine") { + return $decode.field( + "pictures", + $decode.list(decode_picture()), + (pictures) => { + return $decode.success(new $types.Combine(pictures)); + }, + ); + } else if (ty === "fill") { + return $decode.field( + "picture", + decode_picture(), + (picture) => { + return $decode.field( + "colour", + $colour.decoder(), + (colour) => { + return $decode.success(new $types.Fill(picture, colour)); + }, + ); + }, + ); + } else if (ty === "polygon") { + return $decode.field( + "points", + $decode.list(decode_vec2()), + (points) => { + return $decode.field( + "closed", + $decode.bool, + (closed) => { + return $decode.success(new $types.Polygon(points, closed)); + }, + ); + }, + ); + } else if (ty === "rotate") { + return $decode.field( + "angle", + decode_angle(), + (angle) => { + return $decode.field( + "picture", + decode_picture(), + (picture) => { + return $decode.success(new $types.Rotate(picture, angle)); + }, + ); + }, + ); + } else if (ty === "scale") { + return $decode.field( + "x", + $decode.float, + (x) => { + return $decode.field( + "y", + $decode.float, + (y) => { + return $decode.field( + "picture", + decode_picture(), + (picture) => { + return $decode.success( + new $types.Scale(picture, [x, y]), + ); + }, + ); + }, + ); + }, + ); + } else if (ty === "stroke") { + return $decode.field( + "stroke", + decode_stroke(), + (stroke) => { + return $decode.field( + "picture", + decode_picture(), + (picture) => { + return $decode.success(new $types.Stroke(picture, stroke)); + }, + ); + }, + ); + } else if (ty === "text") { + return $decode.field( + "text", + $decode.string, + (text) => { + return $decode.field( + "style", + decode_font(), + (style) => { + return $decode.success(new $types.Text(text, style)); + }, + ); + }, + ); + } else if (ty === "translate") { + return $decode.field( + "x", + $decode.float, + (x) => { + return $decode.field( + "y", + $decode.float, + (y) => { + return $decode.field( + "picture", + decode_picture(), + (picture) => { + return $decode.success( + new $types.Translate(picture, [x, y]), + ); + }, + ); + }, + ); + }, + ); + } else if (ty === "image") { + return $decode.field( + "id", + $decode.string, + (id) => { + return $decode.field( + "width_px", + $decode.int, + (width_px) => { + return $decode.field( + "height_px", + $decode.int, + (height_px) => { + return $decode.success( + new $types.ImageRef( + new $types.Image(id), + width_px, + height_px, + ), + ); + }, + ); + }, + ); + }, + ); + } else if (ty === "image_scaling_behaviour") { + return $decode.field( + "behaviour", + $decode.string, + (behaviour) => { + return $decode.field( + "picture", + decode_picture(), + (picture) => { + if (behaviour === "smooth") { + return $decode.success( + new $types.ImageScalingBehaviour( + picture, + new $types.ScalingSmooth(), + ), + ); + } else if (behaviour === "pixelated") { + return $decode.success( + new $types.ImageScalingBehaviour( + picture, + new $types.ScalingPixelated(), + ), + ); + } else { + return $decode.failure(new $types.Blank(), "Picture"); + } + }, + ); + }, + ); + } else { + return $decode.failure(new $types.Blank(), "Picture"); + } + }, + ); + }, + ); +} + +/** + * Attempt to deserialize a `Picture` + */ +export function from_string(string) { + let decoder = $decode.field( + "picture", + decode_picture(), + (picture) => { return $decode.success(picture); }, + ); + return $json.parse(string, decoder); +} + +function font_to_json(font) { + let size_px; + let font_family; + size_px = font.size_px; + font_family = font.font_family; + return $json.object( + toList([ + ["sizePx", $json.int(size_px)], + ["fontFamily", $json.string(font_family)], + ]), + ); +} + +function stroke_to_json(stroke) { + if (stroke instanceof NoStroke) { + return $json.object(toList([["type", $json.string("noStroke")]])); + } else { + let colour = stroke[0]; + let thickness = stroke[1]; + return $json.object( + toList([ + ["type", $json.string("solidStroke")], + ["colour", $colour.encode(colour)], + ["thickness", $json.float(thickness)], + ]), + ); + } +} + +function angle_to_json(angle) { + let rad; + rad = angle[0]; + return $json.object(toList([["radians", $json.float(rad)]])); +} + +function picture_to_json(picture) { + if (picture instanceof $types.Blank) { + return $json.object(toList([["type", $json.string("blank")]])); + } else if (picture instanceof $types.Polygon) { + let points = picture[0]; + let closed = picture.closed; + return $json.object( + toList([ + ["type", $json.string("polygon")], + [ + "points", + $json.array( + points, + (point) => { + let x; + let y; + x = point[0]; + y = point[1]; + return $json.object( + toList([["x", $json.float(x)], ["y", $json.float(y)]]), + ); + }, + ), + ], + ["closed", $json.bool(closed)], + ]), + ); + } else if (picture instanceof $types.Arc) { + let radius = picture.radius; + let start = picture.start; + let end = picture.end; + return $json.object( + toList([ + ["type", $json.string("arc")], + ["radius", $json.float(radius)], + ["start", angle_to_json(start)], + ["end", angle_to_json(end)], + ]), + ); + } else if (picture instanceof $types.Text) { + let text = picture.text; + let style = picture.style; + return $json.object( + toList([ + ["type", $json.string("text")], + ["text", $json.string(text)], + ["style", font_to_json(style)], + ]), + ); + } else if (picture instanceof $types.ImageRef) { + let width_px = picture.width_px; + let height_px = picture.height_px; + let id = picture[0].id; + return $json.object( + toList([ + ["type", $json.string("image")], + ["id", $json.string(id)], + ["width_px", $json.int(width_px)], + ["height_px", $json.int(height_px)], + ]), + ); + } else if (picture instanceof $types.Fill) { + let picture$1 = picture[0]; + let colour = picture[1]; + return $json.object( + toList([ + ["type", $json.string("fill")], + ["colour", $colour.encode(colour)], + ["picture", picture_to_json(picture$1)], + ]), + ); + } else if (picture instanceof $types.Stroke) { + let picture$1 = picture[0]; + let stroke = picture[1]; + return $json.object( + toList([ + ["type", $json.string("stroke")], + ["stroke", stroke_to_json(stroke)], + ["picture", picture_to_json(picture$1)], + ]), + ); + } else if (picture instanceof $types.ImageScalingBehaviour) { + let picture$1 = picture[0]; + let behaviour = picture[1]; + return $json.object( + toList([ + ["type", $json.string("image_scaling_behaviour")], + [ + "behaviour", + $json.string( + (() => { + if (behaviour instanceof $types.ScalingSmooth) { + return "smooth"; + } else { + return "pixelated"; + } + })(), + ), + ], + ["picture", picture_to_json(picture$1)], + ]), + ); + } else if (picture instanceof $types.Translate) { + let picture$1 = picture[0]; + let x = picture[1][0]; + let y = picture[1][1]; + return $json.object( + toList([ + ["type", $json.string("translate")], + ["x", $json.float(x)], + ["y", $json.float(y)], + ["picture", picture_to_json(picture$1)], + ]), + ); + } else if (picture instanceof $types.Scale) { + let picture$1 = picture[0]; + let x = picture[1][0]; + let y = picture[1][1]; + return $json.object( + toList([ + ["type", $json.string("scale")], + ["x", $json.float(x)], + ["y", $json.float(y)], + ["picture", picture_to_json(picture$1)], + ]), + ); + } else if (picture instanceof $types.Rotate) { + let picture$1 = picture[0]; + let angle = picture[1]; + return $json.object( + toList([ + ["type", $json.string("rotate")], + ["angle", angle_to_json(angle)], + ["picture", picture_to_json(picture$1)], + ]), + ); + } else { + let from = picture[0]; + return $json.object( + toList([ + ["type", $json.string("combine")], + ["pictures", $json.array(from, picture_to_json)], + ]), + ); + } +} + +/** + * Serialize a `Picture` to a string. + * + * Note, serializing an `Image` texture will only store an ID referencing the image. This means that if you deserialize a Picture containing + * references to images, you are responsible for making sure all images are loaded before drawing the picture. + * More advanced APIs to support use cases such as these are planned for a future release. + * + * Also, if you wish to store the serialized data, remember that the library currently makes no stability guarantee that + * the data can be deserialized by *future* versions of the library. + */ +export function to_string(picture) { + let version = "paint:unstable"; + let _pipe = $json.object( + toList([ + ["version", $json.string(version)], + ["picture", picture_to_json(picture)], + ]), + ); + return $json.to_string(_pipe); +} diff --git a/build/dev/javascript/paint/paint/event.mjs b/build/dev/javascript/paint/paint/event.mjs new file mode 100644 index 0000000..86faa42 --- /dev/null +++ b/build/dev/javascript/paint/paint/event.mjs @@ -0,0 +1,163 @@ +import { CustomType as $CustomType } from "../gleam.mjs"; + +/** + * Triggered before drawing. Contains the number of milliseconds elapsed. + */ +export class Tick extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const Event$Tick = ($0) => new Tick($0); +export const Event$isTick = (value) => value instanceof Tick; +export const Event$Tick$0 = (value) => value[0]; + +/** + * Triggered when a key is pressed + */ +export class KeyboardPressed extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const Event$KeyboardPressed = ($0) => new KeyboardPressed($0); +export const Event$isKeyboardPressed = (value) => + value instanceof KeyboardPressed; +export const Event$KeyboardPressed$0 = (value) => value[0]; + +/** + * Triggered when a key is released + */ +export class KeyboardRelased extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const Event$KeyboardRelased = ($0) => new KeyboardRelased($0); +export const Event$isKeyboardRelased = (value) => + value instanceof KeyboardRelased; +export const Event$KeyboardRelased$0 = (value) => value[0]; + +/** + * Triggered when the mouse is moved. Contains + * the `x` and `y` value for the mouse position. + */ +export class MouseMoved extends $CustomType { + constructor($0, $1) { + super(); + this[0] = $0; + this[1] = $1; + } +} +export const Event$MouseMoved = ($0, $1) => new MouseMoved($0, $1); +export const Event$isMouseMoved = (value) => value instanceof MouseMoved; +export const Event$MouseMoved$0 = (value) => value[0]; +export const Event$MouseMoved$1 = (value) => value[1]; + +/** + * Triggered when a mouse button is pressed + */ +export class MousePressed extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const Event$MousePressed = ($0) => new MousePressed($0); +export const Event$isMousePressed = (value) => value instanceof MousePressed; +export const Event$MousePressed$0 = (value) => value[0]; + +/** + * Triggered when a mouse button is released. + * + * Note, on the web you might encounter issues where the + * release event for the right mouse button is not triggered + * because of the context menu. + */ +export class MouseReleased extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const Event$MouseReleased = ($0) => new MouseReleased($0); +export const Event$isMouseReleased = (value) => value instanceof MouseReleased; +export const Event$MouseReleased$0 = (value) => value[0]; + +export class KeyLeftArrow extends $CustomType {} +export const Key$KeyLeftArrow = () => new KeyLeftArrow(); +export const Key$isKeyLeftArrow = (value) => value instanceof KeyLeftArrow; + +export class KeyRightArrow extends $CustomType {} +export const Key$KeyRightArrow = () => new KeyRightArrow(); +export const Key$isKeyRightArrow = (value) => value instanceof KeyRightArrow; + +export class KeyUpArrow extends $CustomType {} +export const Key$KeyUpArrow = () => new KeyUpArrow(); +export const Key$isKeyUpArrow = (value) => value instanceof KeyUpArrow; + +export class KeyDownArrow extends $CustomType {} +export const Key$KeyDownArrow = () => new KeyDownArrow(); +export const Key$isKeyDownArrow = (value) => value instanceof KeyDownArrow; + +export class KeySpace extends $CustomType {} +export const Key$KeySpace = () => new KeySpace(); +export const Key$isKeySpace = (value) => value instanceof KeySpace; + +export class KeyW extends $CustomType {} +export const Key$KeyW = () => new KeyW(); +export const Key$isKeyW = (value) => value instanceof KeyW; + +export class KeyA extends $CustomType {} +export const Key$KeyA = () => new KeyA(); +export const Key$isKeyA = (value) => value instanceof KeyA; + +export class KeyS extends $CustomType {} +export const Key$KeyS = () => new KeyS(); +export const Key$isKeyS = (value) => value instanceof KeyS; + +export class KeyD extends $CustomType {} +export const Key$KeyD = () => new KeyD(); +export const Key$isKeyD = (value) => value instanceof KeyD; + +export class KeyZ extends $CustomType {} +export const Key$KeyZ = () => new KeyZ(); +export const Key$isKeyZ = (value) => value instanceof KeyZ; + +export class KeyX extends $CustomType {} +export const Key$KeyX = () => new KeyX(); +export const Key$isKeyX = (value) => value instanceof KeyX; + +export class KeyC extends $CustomType {} +export const Key$KeyC = () => new KeyC(); +export const Key$isKeyC = (value) => value instanceof KeyC; + +export class KeyEnter extends $CustomType {} +export const Key$KeyEnter = () => new KeyEnter(); +export const Key$isKeyEnter = (value) => value instanceof KeyEnter; + +export class KeyEscape extends $CustomType {} +export const Key$KeyEscape = () => new KeyEscape(); +export const Key$isKeyEscape = (value) => value instanceof KeyEscape; + +export class KeyBackspace extends $CustomType {} +export const Key$KeyBackspace = () => new KeyBackspace(); +export const Key$isKeyBackspace = (value) => value instanceof KeyBackspace; + +export class MouseButtonLeft extends $CustomType {} +export const MouseButton$MouseButtonLeft = () => new MouseButtonLeft(); +export const MouseButton$isMouseButtonLeft = (value) => + value instanceof MouseButtonLeft; + +export class MouseButtonRight extends $CustomType {} +export const MouseButton$MouseButtonRight = () => new MouseButtonRight(); +export const MouseButton$isMouseButtonRight = (value) => + value instanceof MouseButtonRight; + +export class MouseButtonMiddle extends $CustomType {} +export const MouseButton$MouseButtonMiddle = () => new MouseButtonMiddle(); +export const MouseButton$isMouseButtonMiddle = (value) => + value instanceof MouseButtonMiddle; diff --git a/build/dev/javascript/paint/paint/internal/impl_canvas.mjs b/build/dev/javascript/paint/paint/internal/impl_canvas.mjs new file mode 100644 index 0000000..10ddff7 --- /dev/null +++ b/build/dev/javascript/paint/paint/internal/impl_canvas.mjs @@ -0,0 +1,65 @@ +import { + setup_input_handler, + get_width, + get_height, + set_global, + get_global, + reset, + save, + restore, + translate, + scale, + rotate, + reset_transform, + set_fill_colour, + set_stroke_color, + set_line_width, + set_image_smoothing_enabled, + arc, + polygon, + text, + draw_image, + image_from_query, + image_from_src, + on_image_load, +} from "../../impl_canvas_bindings.mjs"; +import { + define_web_component, + setup_request_animation_frame, + get_rendering_context, + get_key_code, + mouse_pos, + check_mouse_button, +} from "./../../impl_canvas_bindings.mjs"; + +export { + arc, + check_mouse_button, + define_web_component, + draw_image, + get_global, + get_height, + get_key_code, + get_rendering_context, + get_width, + image_from_query, + image_from_src, + mouse_pos, + on_image_load, + polygon, + reset, + reset_transform, + restore, + rotate, + save, + scale, + set_fill_colour, + set_global, + set_image_smoothing_enabled, + set_line_width, + set_stroke_color, + setup_input_handler, + setup_request_animation_frame, + text, + translate, +}; diff --git a/build/dev/javascript/paint/paint/internal/types.mjs b/build/dev/javascript/paint/paint/internal/types.mjs new file mode 100644 index 0000000..7e53029 --- /dev/null +++ b/build/dev/javascript/paint/paint/internal/types.mjs @@ -0,0 +1,217 @@ +import * as $colour from "../../../gleam_community_colour/gleam_community/colour.mjs"; +import { CustomType as $CustomType } from "../../gleam.mjs"; + +export class Blank extends $CustomType {} +export const Picture$Blank = () => new Blank(); +export const Picture$isBlank = (value) => value instanceof Blank; + +export class Polygon extends $CustomType { + constructor($0, closed) { + super(); + this[0] = $0; + this.closed = closed; + } +} +export const Picture$Polygon = ($0, closed) => new Polygon($0, closed); +export const Picture$isPolygon = (value) => value instanceof Polygon; +export const Picture$Polygon$0 = (value) => value[0]; +export const Picture$Polygon$closed = (value) => value.closed; +export const Picture$Polygon$1 = (value) => value.closed; + +export class Arc extends $CustomType { + constructor(radius, start, end) { + super(); + this.radius = radius; + this.start = start; + this.end = end; + } +} +export const Picture$Arc = (radius, start, end) => new Arc(radius, start, end); +export const Picture$isArc = (value) => value instanceof Arc; +export const Picture$Arc$radius = (value) => value.radius; +export const Picture$Arc$0 = (value) => value.radius; +export const Picture$Arc$start = (value) => value.start; +export const Picture$Arc$1 = (value) => value.start; +export const Picture$Arc$end = (value) => value.end; +export const Picture$Arc$2 = (value) => value.end; + +export class Text extends $CustomType { + constructor(text, style) { + super(); + this.text = text; + this.style = style; + } +} +export const Picture$Text = (text, style) => new Text(text, style); +export const Picture$isText = (value) => value instanceof Text; +export const Picture$Text$text = (value) => value.text; +export const Picture$Text$0 = (value) => value.text; +export const Picture$Text$style = (value) => value.style; +export const Picture$Text$1 = (value) => value.style; + +export class ImageRef extends $CustomType { + constructor($0, width_px, height_px) { + super(); + this[0] = $0; + this.width_px = width_px; + this.height_px = height_px; + } +} +export const Picture$ImageRef = ($0, width_px, height_px) => + new ImageRef($0, width_px, height_px); +export const Picture$isImageRef = (value) => value instanceof ImageRef; +export const Picture$ImageRef$0 = (value) => value[0]; +export const Picture$ImageRef$width_px = (value) => value.width_px; +export const Picture$ImageRef$1 = (value) => value.width_px; +export const Picture$ImageRef$height_px = (value) => value.height_px; +export const Picture$ImageRef$2 = (value) => value.height_px; + +export class Fill extends $CustomType { + constructor($0, $1) { + super(); + this[0] = $0; + this[1] = $1; + } +} +export const Picture$Fill = ($0, $1) => new Fill($0, $1); +export const Picture$isFill = (value) => value instanceof Fill; +export const Picture$Fill$0 = (value) => value[0]; +export const Picture$Fill$1 = (value) => value[1]; + +export class Stroke extends $CustomType { + constructor($0, $1) { + super(); + this[0] = $0; + this[1] = $1; + } +} +export const Picture$Stroke = ($0, $1) => new Stroke($0, $1); +export const Picture$isStroke = (value) => value instanceof Stroke; +export const Picture$Stroke$0 = (value) => value[0]; +export const Picture$Stroke$1 = (value) => value[1]; + +export class ImageScalingBehaviour extends $CustomType { + constructor($0, $1) { + super(); + this[0] = $0; + this[1] = $1; + } +} +export const Picture$ImageScalingBehaviour = ($0, $1) => + new ImageScalingBehaviour($0, $1); +export const Picture$isImageScalingBehaviour = (value) => + value instanceof ImageScalingBehaviour; +export const Picture$ImageScalingBehaviour$0 = (value) => value[0]; +export const Picture$ImageScalingBehaviour$1 = (value) => value[1]; + +export class Translate extends $CustomType { + constructor($0, $1) { + super(); + this[0] = $0; + this[1] = $1; + } +} +export const Picture$Translate = ($0, $1) => new Translate($0, $1); +export const Picture$isTranslate = (value) => value instanceof Translate; +export const Picture$Translate$0 = (value) => value[0]; +export const Picture$Translate$1 = (value) => value[1]; + +export class Scale extends $CustomType { + constructor($0, $1) { + super(); + this[0] = $0; + this[1] = $1; + } +} +export const Picture$Scale = ($0, $1) => new Scale($0, $1); +export const Picture$isScale = (value) => value instanceof Scale; +export const Picture$Scale$0 = (value) => value[0]; +export const Picture$Scale$1 = (value) => value[1]; + +export class Rotate extends $CustomType { + constructor($0, $1) { + super(); + this[0] = $0; + this[1] = $1; + } +} +export const Picture$Rotate = ($0, $1) => new Rotate($0, $1); +export const Picture$isRotate = (value) => value instanceof Rotate; +export const Picture$Rotate$0 = (value) => value[0]; +export const Picture$Rotate$1 = (value) => value[1]; + +export class Combine extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const Picture$Combine = ($0) => new Combine($0); +export const Picture$isCombine = (value) => value instanceof Combine; +export const Picture$Combine$0 = (value) => value[0]; + +export class Image extends $CustomType { + constructor(id) { + super(); + this.id = id; + } +} +export const Image$Image = (id) => new Image(id); +export const Image$isImage = (value) => value instanceof Image; +export const Image$Image$id = (value) => value.id; +export const Image$Image$0 = (value) => value.id; + +export class ScalingSmooth extends $CustomType {} +export const ImageScalingBehaviour$ScalingSmooth = () => new ScalingSmooth(); +export const ImageScalingBehaviour$isScalingSmooth = (value) => + value instanceof ScalingSmooth; + +export class ScalingPixelated extends $CustomType {} +export const ImageScalingBehaviour$ScalingPixelated = () => + new ScalingPixelated(); +export const ImageScalingBehaviour$isScalingPixelated = (value) => + value instanceof ScalingPixelated; + +export class NoStroke extends $CustomType {} +export const StrokeProperties$NoStroke = () => new NoStroke(); +export const StrokeProperties$isNoStroke = (value) => value instanceof NoStroke; + +export class SolidStroke extends $CustomType { + constructor($0, $1) { + super(); + this[0] = $0; + this[1] = $1; + } +} +export const StrokeProperties$SolidStroke = ($0, $1) => new SolidStroke($0, $1); +export const StrokeProperties$isSolidStroke = (value) => + value instanceof SolidStroke; +export const StrokeProperties$SolidStroke$0 = (value) => value[0]; +export const StrokeProperties$SolidStroke$1 = (value) => value[1]; + +export class FontProperties extends $CustomType { + constructor(size_px, font_family) { + super(); + this.size_px = size_px; + this.font_family = font_family; + } +} +export const FontProperties$FontProperties = (size_px, font_family) => + new FontProperties(size_px, font_family); +export const FontProperties$isFontProperties = (value) => + value instanceof FontProperties; +export const FontProperties$FontProperties$size_px = (value) => value.size_px; +export const FontProperties$FontProperties$0 = (value) => value.size_px; +export const FontProperties$FontProperties$font_family = (value) => + value.font_family; +export const FontProperties$FontProperties$1 = (value) => value.font_family; + +export class Radians extends $CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +export const Angle$Radians = ($0) => new Radians($0); +export const Angle$isRadians = (value) => value instanceof Radians; +export const Angle$Radians$0 = (value) => value[0]; diff --git a/build/dev/javascript/prelude.mjs b/build/dev/javascript/prelude.mjs new file mode 100644 index 0000000..6acd225 --- /dev/null +++ b/build/dev/javascript/prelude.mjs @@ -0,0 +1,1575 @@ +export class CustomType { + withFields(fields) { + let properties = Object.keys(this).map((label) => + label in fields ? fields[label] : this[label], + ); + return new this.constructor(...properties); + } +} + +export class List { + static fromArray(array, tail) { + let t = tail || new Empty(); + for (let i = array.length - 1; i >= 0; --i) { + t = new NonEmpty(array[i], t); + } + return t; + } + + [Symbol.iterator]() { + return new ListIterator(this); + } + + toArray() { + return [...this]; + } + + atLeastLength(desired) { + let current = this; + while (desired-- > 0 && current) current = current.tail; + return current !== undefined; + } + + hasLength(desired) { + let current = this; + while (desired-- > 0 && current) current = current.tail; + return desired === -1 && current instanceof Empty; + } + + countLength() { + let current = this; + let length = 0; + while (current) { + current = current.tail; + length++; + } + return length - 1; + } +} + +export function prepend(element, tail) { + return new NonEmpty(element, tail); +} + +export function toList(elements, tail) { + return List.fromArray(elements, tail); +} + +class ListIterator { + #current; + + constructor(current) { + this.#current = current; + } + + next() { + if (this.#current instanceof Empty) { + return { done: true }; + } else { + let { head, tail } = this.#current; + this.#current = tail; + return { value: head, done: false }; + } + } +} + +export class Empty extends List {} +export const List$Empty = () => new Empty(); +export const List$isEmpty = (value) => value instanceof Empty; + +export class NonEmpty extends List { + constructor(head, tail) { + super(); + this.head = head; + this.tail = tail; + } +} +export const List$NonEmpty = (head, tail) => new NonEmpty(head, tail); +export const List$isNonEmpty = (value) => value instanceof NonEmpty; + +export const List$NonEmpty$first = (value) => value.head; +export const List$NonEmpty$rest = (value) => value.tail; + +/** + * A bit array is a contiguous sequence of bits similar to Erlang's Binary type. + */ +export class BitArray { + /** + * The size in bits of this bit array's data. + * + * @type {number} + */ + bitSize; + + /** + * The size in bytes of this bit array's data. If this bit array doesn't store + * a whole number of bytes then this value is rounded up. + * + * @type {number} + */ + byteSize; + + /** + * The number of unused high bits in the first byte of this bit array's + * buffer prior to the start of its data. The value of any unused high bits is + * undefined. + * + * The bit offset will be in the range 0-7. + * + * @type {number} + */ + bitOffset; + + /** + * The raw bytes that hold this bit array's data. + * + * If `bitOffset` is not zero then there are unused high bits in the first + * byte of this buffer. + * + * If `bitOffset + bitSize` is not a multiple of 8 then there are unused low + * bits in the last byte of this buffer. + * + * @type {Uint8Array} + */ + rawBuffer; + + /** + * Constructs a new bit array from a `Uint8Array`, an optional size in + * bits, and an optional bit offset. + * + * If no bit size is specified it is taken as `buffer.length * 8`, i.e. all + * bytes in the buffer make up the new bit array's data. + * + * If no bit offset is specified it defaults to zero, i.e. there are no unused + * high bits in the first byte of the buffer. + * + * @param {Uint8Array} buffer + * @param {number} [bitSize] + * @param {number} [bitOffset] + */ + constructor(buffer, bitSize, bitOffset) { + if (!(buffer instanceof Uint8Array)) { + throw globalThis.Error( + "BitArray can only be constructed from a Uint8Array", + ); + } + + this.bitSize = bitSize ?? buffer.length * 8; + this.byteSize = Math.trunc((this.bitSize + 7) / 8); + this.bitOffset = bitOffset ?? 0; + + // Validate the bit size + if (this.bitSize < 0) { + throw globalThis.Error(`BitArray bit size is invalid: ${this.bitSize}`); + } + + // Validate the bit offset + if (this.bitOffset < 0 || this.bitOffset > 7) { + throw globalThis.Error( + `BitArray bit offset is invalid: ${this.bitOffset}`, + ); + } + + // Validate the length of the buffer + if (buffer.length !== Math.trunc((this.bitOffset + this.bitSize + 7) / 8)) { + throw globalThis.Error("BitArray buffer length is invalid"); + } + + this.rawBuffer = buffer; + } + + /** + * Returns a specific byte in this bit array. If the byte index is out of + * range then `undefined` is returned. + * + * When returning the final byte of a bit array with a bit size that's not a + * multiple of 8, the content of the unused low bits are undefined. + * + * @param {number} index + * @returns {number | undefined} + */ + byteAt(index) { + if (index < 0 || index >= this.byteSize) { + return undefined; + } + + return bitArrayByteAt(this.rawBuffer, this.bitOffset, index); + } + + equals(other) { + if (this.bitSize !== other.bitSize) { + return false; + } + + const wholeByteCount = Math.trunc(this.bitSize / 8); + + // If both bit offsets are zero do a byte-aligned equality check which is + // faster + if (this.bitOffset === 0 && other.bitOffset === 0) { + // Compare any whole bytes + for (let i = 0; i < wholeByteCount; i++) { + if (this.rawBuffer[i] !== other.rawBuffer[i]) { + return false; + } + } + + // Compare any trailing bits, excluding unused low bits + const trailingBitsCount = this.bitSize % 8; + if (trailingBitsCount) { + const unusedLowBitCount = 8 - trailingBitsCount; + if ( + this.rawBuffer[wholeByteCount] >> unusedLowBitCount !== + other.rawBuffer[wholeByteCount] >> unusedLowBitCount + ) { + return false; + } + } + } else { + // Compare any whole bytes + for (let i = 0; i < wholeByteCount; i++) { + const a = bitArrayByteAt(this.rawBuffer, this.bitOffset, i); + const b = bitArrayByteAt(other.rawBuffer, other.bitOffset, i); + + if (a !== b) { + return false; + } + } + + // Compare any trailing bits + const trailingBitsCount = this.bitSize % 8; + if (trailingBitsCount) { + const a = bitArrayByteAt( + this.rawBuffer, + this.bitOffset, + wholeByteCount, + ); + const b = bitArrayByteAt( + other.rawBuffer, + other.bitOffset, + wholeByteCount, + ); + + const unusedLowBitCount = 8 - trailingBitsCount; + if (a >> unusedLowBitCount !== b >> unusedLowBitCount) { + return false; + } + } + } + + return true; + } + + /** + * Returns this bit array's internal buffer. + * + * @deprecated Use `BitArray.byteAt()` or `BitArray.rawBuffer` instead. + * + * @returns {Uint8Array} + */ + get buffer() { + bitArrayPrintDeprecationWarning( + "buffer", + "Use BitArray.byteAt() or BitArray.rawBuffer instead", + ); + + if (this.bitOffset !== 0 || this.bitSize % 8 !== 0) { + throw new globalThis.Error( + "BitArray.buffer does not support unaligned bit arrays", + ); + } + + return this.rawBuffer; + } + + /** + * Returns the length in bytes of this bit array's internal buffer. + * + * @deprecated Use `BitArray.bitSize` or `BitArray.byteSize` instead. + * + * @returns {number} + */ + get length() { + bitArrayPrintDeprecationWarning( + "length", + "Use BitArray.bitSize or BitArray.byteSize instead", + ); + + if (this.bitOffset !== 0 || this.bitSize % 8 !== 0) { + throw new globalThis.Error( + "BitArray.length does not support unaligned bit arrays", + ); + } + + return this.rawBuffer.length; + } +} + +export const BitArray$BitArray = (buffer, bitSize, bitOffset) => + new BitArray(buffer, bitSize, bitOffset); + +/** + * Returns the nth byte in the given buffer, after applying the specified bit + * offset. If the index is out of bounds then zero is returned. + * + * @param {Uint8Array} buffer + * @param {number} bitOffset + * @param {number} index + * @returns {number} + */ +function bitArrayByteAt(buffer, bitOffset, index) { + if (bitOffset === 0) { + return buffer[index] ?? 0; + } else { + const a = (buffer[index] << bitOffset) & 0xff; + const b = buffer[index + 1] >> (8 - bitOffset); + + return a | b; + } +} + +export class UtfCodepoint { + constructor(value) { + this.value = value; + } +} + +const isBitArrayDeprecationMessagePrinted = {}; +function bitArrayPrintDeprecationWarning(name, message) { + if (isBitArrayDeprecationMessagePrinted[name]) { + return; + } + + console.warn( + `Deprecated BitArray.${name} property used in JavaScript FFI code. ${message}.`, + ); + + isBitArrayDeprecationMessagePrinted[name] = true; +} + +/** + * Slices a bit array to produce a new bit array. If `end` is not supplied then + * all bits from `start` onward are returned. + * + * If the slice is out of bounds then an exception is thrown. + * + * @param {BitArray} bitArray + * @param {number} start + * @param {number} [end] + * @returns {BitArray} + */ +export function bitArraySlice(bitArray, start, end) { + end ??= bitArray.bitSize; + + bitArrayValidateRange(bitArray, start, end); + + // Handle zero-length slices + if (start === end) { + return new BitArray(new Uint8Array()); + } + + // Early return for slices that cover the whole bit array + if (start === 0 && end === bitArray.bitSize) { + return bitArray; + } + + start += bitArray.bitOffset; + end += bitArray.bitOffset; + + const startByteIndex = Math.trunc(start / 8); + const endByteIndex = Math.trunc((end + 7) / 8); + const byteLength = endByteIndex - startByteIndex; + + // Avoid creating a new Uint8Array if the view of the underlying ArrayBuffer + // is the same. This can occur when slicing off just the first or last bit of + // a bit array, i.e. when only the bit offset or bit size need to be updated. + let buffer; + if (startByteIndex === 0 && byteLength === bitArray.rawBuffer.byteLength) { + buffer = bitArray.rawBuffer; + } else { + buffer = new Uint8Array( + bitArray.rawBuffer.buffer, + bitArray.rawBuffer.byteOffset + startByteIndex, + byteLength, + ); + } + + return new BitArray(buffer, end - start, start % 8); +} + +/** + * Interprets a slice of this bit array as a floating point number, either + * 32-bit or 64-bit, with the specified endianness. + * + * The value of `end - start` must be exactly 32 or 64, otherwise an exception + * will be thrown. + * + * @param {BitArray} bitArray + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @returns {number} + */ +export function bitArraySliceToFloat(bitArray, start, end, isBigEndian) { + bitArrayValidateRange(bitArray, start, end); + + const floatSize = end - start; + + // Check size is valid + if (floatSize !== 16 && floatSize !== 32 && floatSize !== 64) { + const msg = + `Sized floats must be 16-bit, 32-bit or 64-bit, got size of ` + + `${floatSize} bits`; + throw new globalThis.Error(msg); + } + + start += bitArray.bitOffset; + + const isStartByteAligned = start % 8 === 0; + + // If the bit range is byte aligned then the float can be read directly out + // of the existing buffer + if (isStartByteAligned) { + const view = new DataView( + bitArray.rawBuffer.buffer, + bitArray.rawBuffer.byteOffset + start / 8, + ); + + if (floatSize === 64) { + return view.getFloat64(0, !isBigEndian); + } else if (floatSize === 32) { + return view.getFloat32(0, !isBigEndian); + } else if (floatSize === 16) { + return fp16UintToNumber(view.getUint16(0, !isBigEndian)); + } + } + + // Copy the unaligned bytes into an aligned array so a DataView can be used + const alignedBytes = new Uint8Array(floatSize / 8); + const byteOffset = Math.trunc(start / 8); + for (let i = 0; i < alignedBytes.length; i++) { + alignedBytes[i] = bitArrayByteAt( + bitArray.rawBuffer, + start % 8, + byteOffset + i, + ); + } + + // Read the float out of the aligned buffer + const view = new DataView(alignedBytes.buffer); + if (floatSize === 64) { + return view.getFloat64(0, !isBigEndian); + } else if (floatSize === 32) { + return view.getFloat32(0, !isBigEndian); + } else { + return fp16UintToNumber(view.getUint16(0, !isBigEndian)); + } +} + +/** + * Interprets a slice of this bit array as a signed or unsigned integer with the + * specified endianness. + * + * @param {BitArray} bitArray + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ +export function bitArraySliceToInt( + bitArray, + start, + end, + isBigEndian, + isSigned, +) { + bitArrayValidateRange(bitArray, start, end); + + if (start === end) { + return 0; + } + + start += bitArray.bitOffset; + end += bitArray.bitOffset; + + const isStartByteAligned = start % 8 === 0; + const isEndByteAligned = end % 8 === 0; + + // If the slice is byte-aligned then there is no need to handle unaligned + // slices, meaning a simpler and faster implementation can be used instead + if (isStartByteAligned && isEndByteAligned) { + return intFromAlignedSlice( + bitArray, + start / 8, + end / 8, + isBigEndian, + isSigned, + ); + } + + const size = end - start; + + const startByteIndex = Math.trunc(start / 8); + const endByteIndex = Math.trunc((end - 1) / 8); + + // Handle the case of the slice being completely contained in a single byte + if (startByteIndex == endByteIndex) { + const mask = 0xff >> start % 8; + const unusedLowBitCount = (8 - (end % 8)) % 8; + + let value = + (bitArray.rawBuffer[startByteIndex] & mask) >> unusedLowBitCount; + + // For signed integers, if the high bit is set reinterpret as two's + // complement + if (isSigned) { + const highBit = 2 ** (size - 1); + if (value >= highBit) { + value -= highBit * 2; + } + } + + return value; + } + + // The integer value to be read is not aligned and crosses at least one byte + // boundary in the input array + + if (size <= 53) { + return intFromUnalignedSliceUsingNumber( + bitArray.rawBuffer, + start, + end, + isBigEndian, + isSigned, + ); + } else { + return intFromUnalignedSliceUsingBigInt( + bitArray.rawBuffer, + start, + end, + isBigEndian, + isSigned, + ); + } +} + +/** + * Joins the given segments into a new bit array, tightly packing them together. + * Each segment must be one of the following types: + * + * - A `number`: A single byte value in the range 0-255. Values outside this + * range will be wrapped. + * - A `Uint8Array`: A sequence of byte values of any length. + * - A `BitArray`: A sequence of bits of any length, which may not be byte + * aligned. + * + * The bit size of the returned bit array will be the sum of the size in bits + * of the input segments. + * + * @param {(number | Uint8Array | BitArray)[]} segments + * @returns {BitArray} + */ +export function toBitArray(segments) { + if (segments.length === 0) { + return new BitArray(new Uint8Array()); + } + + if (segments.length === 1) { + const segment = segments[0]; + + // When there is a single BitArray segment it can be returned as-is + if (segment instanceof BitArray) { + return segment; + } + + // When there is a single Uint8Array segment, pass it directly to the bit + // array constructor to avoid a copy + if (segment instanceof Uint8Array) { + return new BitArray(segment); + } + + return new BitArray(new Uint8Array(/** @type {number[]} */ (segments))); + } + + // Count the total number of bits and check if all segments are numbers, i.e. + // single bytes + let bitSize = 0; + let areAllSegmentsNumbers = true; + for (const segment of segments) { + if (segment instanceof BitArray) { + bitSize += segment.bitSize; + areAllSegmentsNumbers = false; + } else if (segment instanceof Uint8Array) { + bitSize += segment.byteLength * 8; + areAllSegmentsNumbers = false; + } else { + bitSize += 8; + } + } + + // If all segments are numbers then pass the segments array directly to the + // Uint8Array constructor + if (areAllSegmentsNumbers) { + return new BitArray(new Uint8Array(/** @type {number[]} */ (segments))); + } + + // Pack the segments into a Uint8Array + const buffer = new Uint8Array(Math.trunc((bitSize + 7) / 8)); + + // The current write position in bits into the above array. Byte-aligned + // segments, i.e. when the cursor is a multiple of 8, are able to be processed + // faster due to being able to copy bytes directly. + let cursor = 0; + + for (let segment of segments) { + const isCursorByteAligned = cursor % 8 === 0; + + if (segment instanceof BitArray) { + if (isCursorByteAligned && segment.bitOffset === 0) { + buffer.set(segment.rawBuffer, cursor / 8); + cursor += segment.bitSize; + + // Zero any unused bits in the last byte of the buffer. Their content is + // undefined and shouldn't be included in the output. + const trailingBitsCount = segment.bitSize % 8; + if (trailingBitsCount !== 0) { + const lastByteIndex = Math.trunc(cursor / 8); + buffer[lastByteIndex] >>= 8 - trailingBitsCount; + buffer[lastByteIndex] <<= 8 - trailingBitsCount; + } + } else { + appendUnalignedBits( + segment.rawBuffer, + segment.bitSize, + segment.bitOffset, + ); + } + } else if (segment instanceof Uint8Array) { + if (isCursorByteAligned) { + buffer.set(segment, cursor / 8); + cursor += segment.byteLength * 8; + } else { + appendUnalignedBits(segment, segment.byteLength * 8, 0); + } + } else { + if (isCursorByteAligned) { + buffer[cursor / 8] = segment; + cursor += 8; + } else { + appendUnalignedBits(new Uint8Array([segment]), 8, 0); + } + } + } + + function appendUnalignedBits(unalignedBits, size, offset) { + if (size === 0) { + return; + } + + const byteSize = Math.trunc(size + 7 / 8); + + const highBitsCount = cursor % 8; + const lowBitsCount = 8 - highBitsCount; + + let byteIndex = Math.trunc(cursor / 8); + + for (let i = 0; i < byteSize; i++) { + let byte = bitArrayByteAt(unalignedBits, offset, i); + + // If this is a partial byte then zero out the trailing bits as their + // content is undefined and shouldn't be included in the output + if (size < 8) { + byte >>= 8 - size; + byte <<= 8 - size; + } + + // Copy the high bits of the input byte to the low bits of the current + // output byte + buffer[byteIndex] |= byte >> highBitsCount; + + let appendedBitsCount = size - Math.max(0, size - lowBitsCount); + size -= appendedBitsCount; + cursor += appendedBitsCount; + + if (size === 0) { + break; + } + + // Copy the low bits of the input byte to the high bits of the next output + // byte + buffer[++byteIndex] = byte << lowBitsCount; + appendedBitsCount = size - Math.max(0, size - highBitsCount); + size -= appendedBitsCount; + cursor += appendedBitsCount; + } + } + + return new BitArray(buffer, bitSize); +} + +/** + * Encodes a floating point value into a `Uint8Array`. This is used to create + * float segments that are part of bit array expressions. + * + * @param {number} value + * @param {number} size + * @param {boolean} isBigEndian + * @returns {Uint8Array} + */ +export function sizedFloat(value, size, isBigEndian) { + if (size !== 16 && size !== 32 && size !== 64) { + const msg = `Sized floats must be 16-bit, 32-bit or 64-bit, got size of ${size} bits`; + throw new globalThis.Error(msg); + } + + if (size === 16) { + return numberToFp16Uint(value, isBigEndian); + } + + const buffer = new Uint8Array(size / 8); + + const view = new DataView(buffer.buffer); + + if (size == 64) { + view.setFloat64(0, value, !isBigEndian); + } else { + view.setFloat32(0, value, !isBigEndian); + } + + return buffer; +} + +/** + * Encodes an integer value into a `Uint8Array`, or a `BitArray` if the size in + * bits is not a multiple of 8. This is used to create integer segments used in + * bit array expressions. + * + * @param {number} value + * @param {number} size + * @param {boolean} isBigEndian + * @returns {Uint8Array | BitArray} + */ +export function sizedInt(value, size, isBigEndian) { + if (size <= 0) { + return new Uint8Array(); + } + + // Fast path when size is 8 bits. This relies on the rounding behavior of the + // Uint8Array constructor. + if (size === 8) { + return new Uint8Array([value]); + } + + // Fast path when size is less than 8 bits: shift the value up to the high + // bits + if (size < 8) { + value <<= 8 - size; + return new BitArray(new Uint8Array([value]), size); + } + + // Allocate output buffer + const buffer = new Uint8Array(Math.trunc((size + 7) / 8)); + + // The number of trailing bits in the final byte. Will be zero if the size is + // an exact number of bytes. + const trailingBitsCount = size % 8; + + // The number of unused bits in the final byte of the buffer + const unusedBitsCount = 8 - trailingBitsCount; + + // For output sizes not exceeding 32 bits the number type is used. For larger + // output sizes the BigInt type is needed. + // + // The code in each of these two paths must be kept in sync. + if (size <= 32) { + if (isBigEndian) { + let i = buffer.length - 1; + + // Set the trailing bits at the end of the output buffer + if (trailingBitsCount) { + buffer[i--] = (value << unusedBitsCount) & 0xff; + value >>= trailingBitsCount; + } + + for (; i >= 0; i--) { + buffer[i] = value; + value >>= 8; + } + } else { + let i = 0; + + const wholeByteCount = Math.trunc(size / 8); + for (; i < wholeByteCount; i++) { + buffer[i] = value; + value >>= 8; + } + + // Set the trailing bits at the end of the output buffer + if (trailingBitsCount) { + buffer[i] = value << unusedBitsCount; + } + } + } else { + const bigTrailingBitsCount = BigInt(trailingBitsCount); + const bigUnusedBitsCount = BigInt(unusedBitsCount); + + let bigValue = BigInt(value); + + if (isBigEndian) { + let i = buffer.length - 1; + + // Set the trailing bits at the end of the output buffer + if (trailingBitsCount) { + buffer[i--] = Number(bigValue << bigUnusedBitsCount); + bigValue >>= bigTrailingBitsCount; + } + + for (; i >= 0; i--) { + buffer[i] = Number(bigValue); + bigValue >>= 8n; + } + } else { + let i = 0; + + const wholeByteCount = Math.trunc(size / 8); + for (; i < wholeByteCount; i++) { + buffer[i] = Number(bigValue); + bigValue >>= 8n; + } + + // Set the trailing bits at the end of the output buffer + if (trailingBitsCount) { + buffer[i] = Number(bigValue << bigUnusedBitsCount); + } + } + } + + // Integers that aren't a whole number of bytes are returned as a BitArray so + // their size in bits is tracked + if (trailingBitsCount) { + return new BitArray(buffer, size); + } + + return buffer; +} + +/** + * Reads an aligned slice of any size as an integer. + * + * @param {BitArray} bitArray + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ +function intFromAlignedSlice(bitArray, start, end, isBigEndian, isSigned) { + const byteSize = end - start; + + if (byteSize <= 6) { + return intFromAlignedSliceUsingNumber( + bitArray.rawBuffer, + start, + end, + isBigEndian, + isSigned, + ); + } else { + return intFromAlignedSliceUsingBigInt( + bitArray.rawBuffer, + start, + end, + isBigEndian, + isSigned, + ); + } +} + +/** + * Reads an aligned slice up to 48 bits in size as an integer. Uses the + * JavaScript `number` type internally. + * + * @param {Uint8Array} buffer + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ +function intFromAlignedSliceUsingNumber( + buffer, + start, + end, + isBigEndian, + isSigned, +) { + const byteSize = end - start; + + let value = 0; + + // Read bytes as an unsigned integer + if (isBigEndian) { + for (let i = start; i < end; i++) { + value *= 256; + value += buffer[i]; + } + } else { + for (let i = end - 1; i >= start; i--) { + value *= 256; + value += buffer[i]; + } + } + + // For signed integers, if the high bit is set reinterpret as two's + // complement + if (isSigned) { + const highBit = 2 ** (byteSize * 8 - 1); + if (value >= highBit) { + value -= highBit * 2; + } + } + + return value; +} + +/** + * Reads an aligned slice of any size as an integer. Uses the JavaScript + * `BigInt` type internally. + * + * @param {Uint8Array} buffer + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ +function intFromAlignedSliceUsingBigInt( + buffer, + start, + end, + isBigEndian, + isSigned, +) { + const byteSize = end - start; + + let value = 0n; + + // Read bytes as an unsigned integer value + if (isBigEndian) { + for (let i = start; i < end; i++) { + value *= 256n; + value += BigInt(buffer[i]); + } + } else { + for (let i = end - 1; i >= start; i--) { + value *= 256n; + value += BigInt(buffer[i]); + } + } + + // For signed integers, if the high bit is set reinterpret as two's + // complement + if (isSigned) { + const highBit = 1n << BigInt(byteSize * 8 - 1); + if (value >= highBit) { + value -= highBit * 2n; + } + } + + // Convert the result into a JS number. This may cause quantizing/error on + // values outside JavaScript's safe integer range. + return Number(value); +} + +/** + * Reads an unaligned slice up to 53 bits in size as an integer. Uses the + * JavaScript `number` type internally. + * + * This function assumes that the slice crosses at least one byte boundary in + * the input. + * + * @param {Uint8Array} buffer + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ +function intFromUnalignedSliceUsingNumber( + buffer, + start, + end, + isBigEndian, + isSigned, +) { + const isStartByteAligned = start % 8 === 0; + + let size = end - start; + let byteIndex = Math.trunc(start / 8); + + let value = 0; + + if (isBigEndian) { + // Read any leading bits + if (!isStartByteAligned) { + const leadingBitsCount = 8 - (start % 8); + value = buffer[byteIndex++] & ((1 << leadingBitsCount) - 1); + size -= leadingBitsCount; + } + + // Read any whole bytes + while (size >= 8) { + value *= 256; + value += buffer[byteIndex++]; + size -= 8; + } + + // Read any trailing bits + if (size > 0) { + value *= 2 ** size; + value += buffer[byteIndex] >> (8 - size); + } + } else { + // For little endian, if the start is aligned then whole bytes can be read + // directly out of the input array, with the trailing bits handled at the + // end + if (isStartByteAligned) { + let size = end - start; + let scale = 1; + + // Read whole bytes + while (size >= 8) { + value += buffer[byteIndex++] * scale; + scale *= 256; + size -= 8; + } + + // Read trailing bits + value += (buffer[byteIndex] >> (8 - size)) * scale; + } else { + // Read little endian data where the start is not byte-aligned. This is + // done by reading whole bytes that cross a byte boundary in the input + // data, then reading any trailing bits. + + const highBitsCount = start % 8; + const lowBitsCount = 8 - highBitsCount; + + let size = end - start; + let scale = 1; + + // Extract whole bytes + while (size >= 8) { + const byte = + (buffer[byteIndex] << highBitsCount) | + (buffer[byteIndex + 1] >> lowBitsCount); + + value += (byte & 0xff) * scale; + + scale *= 256; + size -= 8; + byteIndex++; + } + + // Read any trailing bits. These trailing bits may cross a byte boundary + // in the input buffer. + if (size > 0) { + const lowBitsUsed = size - Math.max(0, size - lowBitsCount); + + let trailingByte = + (buffer[byteIndex] & ((1 << lowBitsCount) - 1)) >> + (lowBitsCount - lowBitsUsed); + + size -= lowBitsUsed; + + if (size > 0) { + trailingByte *= 2 ** size; + trailingByte += buffer[byteIndex + 1] >> (8 - size); + } + + value += trailingByte * scale; + } + } + } + + // For signed integers, if the high bit is set reinterpret as two's + // complement + if (isSigned) { + const highBit = 2 ** (end - start - 1); + if (value >= highBit) { + value -= highBit * 2; + } + } + + return value; +} + +/** + * Reads an unaligned slice of any size as an integer. Uses the JavaScript + * `BigInt` type internally. + * + * This function assumes that the slice crosses at least one byte boundary in + * the input. + * + * @param {Uint8Array} buffer + * @param {number} start + * @param {number} end + * @param {boolean} isBigEndian + * @param {boolean} isSigned + * @returns {number} + */ +function intFromUnalignedSliceUsingBigInt( + buffer, + start, + end, + isBigEndian, + isSigned, +) { + const isStartByteAligned = start % 8 === 0; + + let size = end - start; + let byteIndex = Math.trunc(start / 8); + + let value = 0n; + + if (isBigEndian) { + // Read any leading bits + if (!isStartByteAligned) { + const leadingBitsCount = 8 - (start % 8); + value = BigInt(buffer[byteIndex++] & ((1 << leadingBitsCount) - 1)); + size -= leadingBitsCount; + } + + // Read any whole bytes + while (size >= 8) { + value *= 256n; + value += BigInt(buffer[byteIndex++]); + size -= 8; + } + + // Read any trailing bits + if (size > 0) { + value <<= BigInt(size); + value += BigInt(buffer[byteIndex] >> (8 - size)); + } + } else { + // For little endian, if the start is aligned then whole bytes can be read + // directly out of the input array, with the trailing bits handled at the + // end + if (isStartByteAligned) { + let size = end - start; + let shift = 0n; + + // Read whole bytes + while (size >= 8) { + value += BigInt(buffer[byteIndex++]) << shift; + shift += 8n; + size -= 8; + } + + // Read trailing bits + value += BigInt(buffer[byteIndex] >> (8 - size)) << shift; + } else { + // Read little endian data where the start is not byte-aligned. This is + // done by reading whole bytes that cross a byte boundary in the input + // data, then reading any trailing bits. + + const highBitsCount = start % 8; + const lowBitsCount = 8 - highBitsCount; + + let size = end - start; + let shift = 0n; + + // Extract whole bytes + while (size >= 8) { + const byte = + (buffer[byteIndex] << highBitsCount) | + (buffer[byteIndex + 1] >> lowBitsCount); + + value += BigInt(byte & 0xff) << shift; + + shift += 8n; + size -= 8; + byteIndex++; + } + + // Read any trailing bits. These trailing bits may cross a byte boundary + // in the input buffer. + if (size > 0) { + const lowBitsUsed = size - Math.max(0, size - lowBitsCount); + + let trailingByte = + (buffer[byteIndex] & ((1 << lowBitsCount) - 1)) >> + (lowBitsCount - lowBitsUsed); + + size -= lowBitsUsed; + + if (size > 0) { + trailingByte <<= size; + trailingByte += buffer[byteIndex + 1] >> (8 - size); + } + + value += BigInt(trailingByte) << shift; + } + } + } + + // For signed integers, if the high bit is set reinterpret as two's + // complement + if (isSigned) { + const highBit = 2n ** BigInt(end - start - 1); + if (value >= highBit) { + value -= highBit * 2n; + } + } + + // Convert the result into a JS number. This may cause quantizing/error on + // values outside JavaScript's safe integer range. + return Number(value); +} + +/** + * Interprets a 16-bit unsigned integer value as a 16-bit floating point value. + * + * @param {number} intValue + * @returns {number} + */ +function fp16UintToNumber(intValue) { + const sign = intValue >= 0x8000 ? -1 : 1; + const exponent = (intValue & 0x7c00) >> 10; + const fraction = intValue & 0x03ff; + + let value; + if (exponent === 0) { + value = 6.103515625e-5 * (fraction / 0x400); + } else if (exponent === 0x1f) { + value = fraction === 0 ? Infinity : NaN; + } else { + value = Math.pow(2, exponent - 15) * (1 + fraction / 0x400); + } + + return sign * value; +} + +/** + * Converts a floating point number to bytes for a 16-bit floating point value. + * + * @param {number} intValue + * @param {boolean} isBigEndian + * @returns {Uint8Array} + */ +function numberToFp16Uint(value, isBigEndian) { + const buffer = new Uint8Array(2); + + if (isNaN(value)) { + buffer[1] = 0x7e; + } else if (value === Infinity) { + buffer[1] = 0x7c; + } else if (value === -Infinity) { + buffer[1] = 0xfc; + } else if (value === 0) { + // Both values are already zero + } else { + const sign = value < 0 ? 1 : 0; + value = Math.abs(value); + + let exponent = Math.floor(Math.log2(value)); + let fraction = value / Math.pow(2, exponent) - 1; + + exponent += 15; + + if (exponent <= 0) { + exponent = 0; + fraction = value / Math.pow(2, -14); + } else if (exponent >= 31) { + exponent = 31; + fraction = 0; + } + + fraction = Math.round(fraction * 1024); + + buffer[1] = + (sign << 7) | ((exponent & 0x1f) << 2) | ((fraction >> 8) & 0x03); + buffer[0] = fraction & 0xff; + } + + if (isBigEndian) { + const a = buffer[0]; + buffer[0] = buffer[1]; + buffer[1] = a; + } + + return buffer; +} + +/** + * Throws an exception if the given start and end values are out of bounds for + * a bit array. + * + * @param {BitArray} bitArray + * @param {number} start + * @param {number} end + */ +function bitArrayValidateRange(bitArray, start, end) { + if ( + start < 0 || + start > bitArray.bitSize || + end < start || + end > bitArray.bitSize + ) { + const msg = + `Invalid bit array slice: start = ${start}, end = ${end}, ` + + `bit size = ${bitArray.bitSize}`; + throw new globalThis.Error(msg); + } +} + +/** @type {TextEncoder | undefined} */ +let utf8Encoder; + +/** + * Returns the UTF-8 bytes for a string. + * + * @param {string} string + * @returns {Uint8Array} + */ +export function stringBits(string) { + utf8Encoder ??= new TextEncoder(); + return utf8Encoder.encode(string); +} + +/** + * Returns the UTF-8 bytes for a single UTF codepoint. + * + * @param {UtfCodepoint} codepoint + * @returns {Uint8Array} + */ +export function codepointBits(codepoint) { + return stringBits(String.fromCodePoint(codepoint.value)); +} + +/** + * Returns the UTF-16 bytes for a string. + * + * @param {string} string + * @param {boolean} isBigEndian + * @returns {Uint8Array} + */ +export function stringToUtf16(string, isBigEndian) { + const buffer = new ArrayBuffer(string.length * 2); + const bufferView = new DataView(buffer); + + for (let i = 0; i < string.length; i++) { + bufferView.setUint16(i * 2, string.charCodeAt(i), !isBigEndian); + } + + return new Uint8Array(buffer); +} + +/** + * Returns the UTF-16 bytes for a single UTF codepoint. + * + * @param {UtfCodepoint} codepoint + * @param {boolean} isBigEndian + * @returns {Uint8Array} + */ +export function codepointToUtf16(codepoint, isBigEndian) { + return stringToUtf16(String.fromCodePoint(codepoint.value), isBigEndian); +} + +/** + * Returns the UTF-32 bytes for a string. + * + * @param {string} string + * @param {boolean} isBigEndian + * @returns {Uint8Array} + */ +export function stringToUtf32(string, isBigEndian) { + const buffer = new ArrayBuffer(string.length * 4); + const bufferView = new DataView(buffer); + let length = 0; + + for (let i = 0; i < string.length; i++) { + const codepoint = string.codePointAt(i); + + bufferView.setUint32(length * 4, codepoint, !isBigEndian); + length++; + + if (codepoint > 0xffff) { + i++; + } + } + + return new Uint8Array(buffer.slice(0, length * 4)); +} + +/** + * Returns the UTF-32 bytes for a single UTF codepoint. + * + * @param {UtfCodepoint} codepoint + * @param {boolean} isBigEndian + * @returns {Uint8Array} + */ +export function codepointToUtf32(codepoint, isBigEndian) { + return stringToUtf32(String.fromCodePoint(codepoint.value), isBigEndian); +} + +export class Result extends CustomType { + static isResult(data) { + return data instanceof Result; + } +} + +export class Ok extends Result { + constructor(value) { + super(); + this[0] = value; + } + + isOk() { + return true; + } +} +export const Result$Ok = (value) => new Ok(value); +export const Result$isOk = (value) => value instanceof Ok; +export const Result$Ok$0 = (value) => value[0]; + +export class Error extends Result { + constructor(detail) { + super(); + this[0] = detail; + } + + isOk() { + return false; + } +} +export const Result$Error = (detail) => new Error(detail); +export const Result$isError = (value) => value instanceof Error; +export const Result$Error$0 = (value) => value[0]; + +export function isEqual(x, y) { + let values = [x, y]; + + while (values.length) { + let a = values.pop(); + let b = values.pop(); + if (a === b) continue; + + if (!isObject(a) || !isObject(b)) return false; + let unequal = + !structurallyCompatibleObjects(a, b) || + unequalDates(a, b) || + unequalBuffers(a, b) || + unequalArrays(a, b) || + unequalMaps(a, b) || + unequalSets(a, b) || + unequalRegExps(a, b); + if (unequal) return false; + + const proto = Object.getPrototypeOf(a); + if (proto !== null && typeof proto.equals === "function") { + try { + if (a.equals(b)) continue; + else return false; + } catch {} + } + + let [keys, get] = getters(a); + const ka = keys(a); + const kb = keys(b); + if (ka.length !== kb.length) return false; + for (let k of ka) { + values.push(get(a, k), get(b, k)); + } + } + + return true; +} + +function getters(object) { + if (object instanceof Map) { + return [(x) => x.keys(), (x, y) => x.get(y)]; + } else { + let extra = object instanceof globalThis.Error ? ["message"] : []; + return [(x) => [...extra, ...Object.keys(x)], (x, y) => x[y]]; + } +} + +function unequalDates(a, b) { + return a instanceof Date && (a > b || a < b); +} + +function unequalBuffers(a, b) { + return ( + !(a instanceof BitArray) && + a.buffer instanceof ArrayBuffer && + a.BYTES_PER_ELEMENT && + !(a.byteLength === b.byteLength && a.every((n, i) => n === b[i])) + ); +} + +function unequalArrays(a, b) { + return Array.isArray(a) && a.length !== b.length; +} + +function unequalMaps(a, b) { + return a instanceof Map && a.size !== b.size; +} + +function unequalSets(a, b) { + return ( + a instanceof Set && (a.size != b.size || [...a].some((e) => !b.has(e))) + ); +} + +function unequalRegExps(a, b) { + return a instanceof RegExp && (a.source !== b.source || a.flags !== b.flags); +} + +function isObject(a) { + return typeof a === "object" && a !== null; +} + +function structurallyCompatibleObjects(a, b) { + if (typeof a !== "object" && typeof b !== "object" && (!a || !b)) + return false; + + let nonstructural = [Promise, WeakSet, WeakMap, Function]; + if (nonstructural.some((c) => a instanceof c)) return false; + + return a.constructor === b.constructor; +} + +export function remainderInt(a, b) { + if (b === 0) { + return 0; + } else { + return a % b; + } +} + +export function divideInt(a, b) { + return Math.trunc(divideFloat(a, b)); +} + +export function divideFloat(a, b) { + if (b === 0) { + return 0; + } else { + return a / b; + } +} + +export function makeError(variant, file, module, line, fn, message, extra) { + let error = new globalThis.Error(message); + error.gleam_error = variant; + error.file = file; + error.module = module; + error.line = line; + error.function = fn; + // TODO: Remove this with Gleam v2.0.0 + error.fn = fn; + for (let k in extra) error[k] = extra[k]; + return error; +} diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache b/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache new file mode 100644 index 0000000..40230af Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache_inline b/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache_inline differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache_meta b/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache_meta new file mode 100644 index 0000000..6cd0de3 Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache_meta differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache_warnings b/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache_warnings new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/game.cache_warnings differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache b/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache new file mode 100644 index 0000000..7e8f299 Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache_inline b/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache_inline differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache_meta b/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache_meta new file mode 100644 index 0000000..0ed27b2 Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache_meta differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache_warnings b/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache_warnings new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/hud.cache_warnings differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache b/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache new file mode 100644 index 0000000..cb4cbb8 Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache_inline b/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache_inline differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache_meta b/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache_meta new file mode 100644 index 0000000..e0a618d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache_meta differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache_warnings b/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache_warnings new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/math@math.cache_warnings differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache b/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache new file mode 100644 index 0000000..7879101 Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache_inline b/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache_inline differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache_meta b/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache_meta new file mode 100644 index 0000000..32f8c5f Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache_meta differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache_warnings b/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache_warnings new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/physics.cache_warnings differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache b/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache new file mode 100644 index 0000000..876474c Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache_inline b/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache_inline differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache_meta b/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache_meta new file mode 100644 index 0000000..3cec355 Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache_meta differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache_warnings b/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache_warnings new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/rendering.cache_warnings differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache b/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache new file mode 100644 index 0000000..fc7937e Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache_inline b/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache_inline differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache_meta b/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache_meta new file mode 100644 index 0000000..7ebe7d5 Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache_meta differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache_warnings b/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache_warnings new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/target.cache_warnings differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache b/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache new file mode 100644 index 0000000..989e47c Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache_inline b/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache_inline new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache_inline differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache_meta b/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache_meta new file mode 100644 index 0000000..63d7ea9 Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache_meta differ diff --git a/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache_warnings b/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache_warnings new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/build/dev/javascript/stellar_prune/_gleam_artefacts/types.cache_warnings differ diff --git a/build/dev/javascript/stellar_prune/browser_ffi.mjs b/build/dev/javascript/stellar_prune/browser_ffi.mjs new file mode 100644 index 0000000..ef62a4c --- /dev/null +++ b/build/dev/javascript/stellar_prune/browser_ffi.mjs @@ -0,0 +1,40 @@ +let mouseX = 0; +let mouseY = 0; +let mousePressed = false; + +if (typeof window !== 'undefined') { + const canvas = document.getElementById('canvas'); + + window.addEventListener('mousemove', (e) => { + if (canvas) { + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + mouseX = (e.clientX - rect.left) * scaleX; + mouseY = (e.clientY - rect.top) * scaleY; + } else { + mouseX = e.clientX; + mouseY = e.clientY; + } + }); + + window.addEventListener('mousedown', () => { + mousePressed = true; + }); + + window.addEventListener('mouseup', () => { + mousePressed = false; + }); +} + +export function request_animation_frame(callback) { + requestAnimationFrame(() => callback()); +} + +export function get_mouse_position() { + return { x: mouseX, y: mouseY }; +} + +export function is_mouse_pressed() { + return mousePressed; +} diff --git a/build/dev/javascript/stellar_prune/game.mjs b/build/dev/javascript/stellar_prune/game.mjs new file mode 100644 index 0000000..7410e69 --- /dev/null +++ b/build/dev/javascript/stellar_prune/game.mjs @@ -0,0 +1,325 @@ +import * as $list from "../gleam_stdlib/gleam/list.mjs"; +import * as $paint from "../paint/paint.mjs"; +import { request_animation_frame, get_mouse_position, is_mouse_pressed } from "./browser_ffi.mjs"; +import { Ok, toList, prepend as listPrepend, CustomType as $CustomType, isEqual } from "./gleam.mjs"; +import * as $hud from "./hud.mjs"; +import { CrossHair, Health, Hud, Score } from "./hud.mjs"; +import * as $physics from "./physics.mjs"; +import { Gravity, Physics, Vec2 } from "./physics.mjs"; +import * as $rendering from "./rendering.mjs"; +import * as $target from "./target.mjs"; +import { Diamond, Lucy, Rocket } from "./target.mjs"; +import * as $types from "./types.mjs"; +import { GameOver, MainMenu, Playing, PlayingState } from "./types.mjs"; + +export { is_mouse_pressed, request_animation_frame }; + +export class TargetClickEffect extends $CustomType { + constructor(score_change, health_change) { + super(); + this.score_change = score_change; + this.health_change = health_change; + } +} +export const TargetClickEffect$TargetClickEffect = (score_change, health_change) => + new TargetClickEffect(score_change, health_change); +export const TargetClickEffect$isTargetClickEffect = (value) => + value instanceof TargetClickEffect; +export const TargetClickEffect$TargetClickEffect$score_change = (value) => + value.score_change; +export const TargetClickEffect$TargetClickEffect$0 = (value) => + value.score_change; +export const TargetClickEffect$TargetClickEffect$health_change = (value) => + value.health_change; +export const TargetClickEffect$TargetClickEffect$1 = (value) => + value.health_change; + +export class TargetFilterResult extends $CustomType { + constructor(remaining_targets, health_loss) { + super(); + this.remaining_targets = remaining_targets; + this.health_loss = health_loss; + } +} +export const TargetFilterResult$TargetFilterResult = (remaining_targets, health_loss) => + new TargetFilterResult(remaining_targets, health_loss); +export const TargetFilterResult$isTargetFilterResult = (value) => + value instanceof TargetFilterResult; +export const TargetFilterResult$TargetFilterResult$remaining_targets = (value) => + value.remaining_targets; +export const TargetFilterResult$TargetFilterResult$0 = (value) => + value.remaining_targets; +export const TargetFilterResult$TargetFilterResult$health_loss = (value) => + value.health_loss; +export const TargetFilterResult$TargetFilterResult$1 = (value) => + value.health_loss; + +export class SpawnResult extends $CustomType { + constructor(targets, spawn_timer) { + super(); + this.targets = targets; + this.spawn_timer = spawn_timer; + } +} +export const SpawnResult$SpawnResult = (targets, spawn_timer) => + new SpawnResult(targets, spawn_timer); +export const SpawnResult$isSpawnResult = (value) => + value instanceof SpawnResult; +export const SpawnResult$SpawnResult$targets = (value) => value.targets; +export const SpawnResult$SpawnResult$0 = (value) => value.targets; +export const SpawnResult$SpawnResult$spawn_timer = (value) => value.spawn_timer; +export const SpawnResult$SpawnResult$1 = (value) => value.spawn_timer; + +function init_playing_state() { + return new PlayingState( + toList([]), + new Physics(new Gravity(new Vec2(0.0, 0.08))), + new Hud( + new Score( + new Vec2(1050.0, 70.0), + new $target.Colorable($paint.colour_hex("#ffffff")), + 0, + ), + new CrossHair( + new Vec2(0.0, 0.0), + new $target.Colorable($paint.colour_hex("#ffffff")), + 5.0, + ), + new Health(new Vec2(30.0, 30.0), 120.0, 3, 3), + ), + new Vec2(0.0, 0.0), + 0.0, + ); +} + +function get_target_click_effects(kind) { + if (kind instanceof Lucy) { + return new TargetClickEffect(1, 0); + } else if (kind instanceof Diamond) { + let points_value = kind.points_value; + return new TargetClickEffect(points_value, 0); + } else { + return new TargetClickEffect(0, -1); + } +} + +function filter_out_of_bounds_targets(targets) { + return $list.fold( + targets, + new TargetFilterResult(toList([]), 0), + (acc, target) => { + let $ = target.position.y > 900.0; + if ($) { + let $1 = target.kind; + if ($1 instanceof Rocket) { + return acc; + } else { + return new TargetFilterResult( + acc.remaining_targets, + acc.health_loss + 1, + ); + } + } else { + return new TargetFilterResult( + listPrepend(target, acc.remaining_targets), + acc.health_loss, + ); + } + }, + ); +} + +function should_spawn_new_target(spawn_timer, active_targets, score) { + let spawn_interval = 60.0; + let max_targets = 1 + (globalThis.Math.trunc(score / 5)); + return (spawn_timer >= spawn_interval) && ($list.length(active_targets) < max_targets); +} + +function handle_target_click(playing_state, clicked_target) { + let new_targets = $list.filter( + playing_state.targets, + (target) => { return !isEqual(target, clicked_target); }, + ); + let effect = get_target_click_effects(clicked_target.kind); + return new PlayingState( + new_targets, + playing_state.physics, + (() => { + let _record = playing_state.hud; + return new Hud( + (() => { + let _record$1 = playing_state.hud.score; + return new Score( + _record$1.position, + _record$1.colorable, + playing_state.hud.score.score + effect.score_change, + ); + })(), + _record.cross_hair, + (() => { + let _record$1 = playing_state.hud.health; + return new Health( + _record$1.position, + _record$1.size, + playing_state.hud.health.amount + effect.health_change, + _record$1.max_health, + ); + })(), + ); + })(), + playing_state.mouse, + playing_state.spawn_timer, + ); +} + +function update_playing_state(playing_state) { + let $ = playing_state.physics; + let gravity; + gravity = $.gravity; + let updated_targets = $list.map( + playing_state.targets, + (target) => { return $target.update_target(target, gravity.acceleration); }, + ); + let filter_result = filter_out_of_bounds_targets(updated_targets); + let new_spawn_timer = playing_state.spawn_timer + 1.0; + let _block; + let $1 = should_spawn_new_target( + new_spawn_timer, + filter_result.remaining_targets, + playing_state.hud.score.score, + ); + if ($1) { + _block = new SpawnResult( + listPrepend($target.create_target(), filter_result.remaining_targets), + 0.0, + ); + } else { + _block = new SpawnResult(filter_result.remaining_targets, new_spawn_timer); + } + let spawn_result = _block; + let new_mouse = get_mouse_position(); + return new PlayingState( + spawn_result.targets, + playing_state.physics, + (() => { + let _record = playing_state.hud; + return new Hud( + _record.score, + (() => { + let _record$1 = playing_state.hud.cross_hair; + return new CrossHair(new_mouse, _record$1.colorable, _record$1.size); + })(), + (() => { + let _record$1 = playing_state.hud.health; + return new Health( + _record$1.position, + _record$1.size, + playing_state.hud.health.amount - filter_result.health_loss, + _record$1.max_health, + ); + })(), + ); + })(), + new_mouse, + spawn_result.spawn_timer, + ); +} + +function update_state(state) { + if (state instanceof MainMenu) { + return state; + } else if (state instanceof Playing) { + let playing_state = state.state; + let prev_mouse = state.previous_mouse_pressed; + return new Playing(update_playing_state(playing_state), prev_mouse); + } else { + return state; + } +} + +function find_clicked_target(mouse, targets) { + return $list.find( + targets, + (target) => { return $target.is_point_in_target(mouse, target); }, + ); +} + +function handle_playing_click(playing_state) { + let $ = find_clicked_target(playing_state.mouse, playing_state.targets); + if ($ instanceof Ok) { + let clicked_target = $[0]; + return handle_target_click(playing_state, clicked_target); + } else { + return playing_state; + } +} + +function handle_main_menu_click(mouse_pressed) { + if (mouse_pressed) { + return new Playing(init_playing_state(), false); + } else { + return new MainMenu(); + } +} + +function handle_playing_state(playing_state, mouse_pressed) { + let _block; + if (mouse_pressed) { + _block = handle_playing_click(playing_state); + } else { + _block = playing_state; + } + let new_state = _block; + let $ = new_state.hud.health.amount <= 0; + if ($) { + return new GameOver(new_state.hud.score.score, mouse_pressed); + } else { + return new Playing(new_state, mouse_pressed); + } +} + +function handle_game_over_click( + final_score, + mouse_pressed, + previous_mouse_pressed +) { + let just_clicked = mouse_pressed && !previous_mouse_pressed; + if (just_clicked) { + return new Playing(init_playing_state(), false); + } else { + return new GameOver(final_score, mouse_pressed); + } +} + +function handle_events(state) { + let current_mouse_pressed = is_mouse_pressed(); + if (state instanceof MainMenu) { + return handle_main_menu_click(current_mouse_pressed); + } else if (state instanceof Playing) { + let playing_state = state.state; + return handle_playing_state(playing_state, current_mouse_pressed); + } else { + let final_score = state.final_score; + let previous_mouse_pressed = state.previous_mouse_pressed; + return handle_game_over_click( + final_score, + current_mouse_pressed, + previous_mouse_pressed, + ); + } +} + +function game_loop(state) { + $rendering.render(state); + return request_animation_frame( + () => { + let _pipe = handle_events(state); + let _pipe$1 = update_state(_pipe); + return game_loop(_pipe$1); + }, + ); +} + +export function main() { + return game_loop(new MainMenu()); +} diff --git a/build/dev/javascript/stellar_prune/gleam.mjs b/build/dev/javascript/stellar_prune/gleam.mjs new file mode 100644 index 0000000..197cbbc --- /dev/null +++ b/build/dev/javascript/stellar_prune/gleam.mjs @@ -0,0 +1 @@ +export * from "../prelude.mjs"; diff --git a/build/dev/javascript/stellar_prune/hud.mjs b/build/dev/javascript/stellar_prune/hud.mjs new file mode 100644 index 0000000..fafec79 --- /dev/null +++ b/build/dev/javascript/stellar_prune/hud.mjs @@ -0,0 +1,153 @@ +import * as $float from "../gleam_stdlib/gleam/float.mjs"; +import * as $int from "../gleam_stdlib/gleam/int.mjs"; +import * as $paint from "../paint/paint.mjs"; +import * as $canvas from "../paint/paint/canvas.mjs"; +import { toList, CustomType as $CustomType } from "./gleam.mjs"; +import * as $physics from "./physics.mjs"; +import { Vec2 } from "./physics.mjs"; +import * as $target from "./target.mjs"; +import { Colorable } from "./target.mjs"; + +export class Score extends $CustomType { + constructor(position, colorable, score) { + super(); + this.position = position; + this.colorable = colorable; + this.score = score; + } +} +export const Score$Score = (position, colorable, score) => + new Score(position, colorable, score); +export const Score$isScore = (value) => value instanceof Score; +export const Score$Score$position = (value) => value.position; +export const Score$Score$0 = (value) => value.position; +export const Score$Score$colorable = (value) => value.colorable; +export const Score$Score$1 = (value) => value.colorable; +export const Score$Score$score = (value) => value.score; +export const Score$Score$2 = (value) => value.score; + +export class CrossHair extends $CustomType { + constructor(position, colorable, size) { + super(); + this.position = position; + this.colorable = colorable; + this.size = size; + } +} +export const CrossHair$CrossHair = (position, colorable, size) => + new CrossHair(position, colorable, size); +export const CrossHair$isCrossHair = (value) => value instanceof CrossHair; +export const CrossHair$CrossHair$position = (value) => value.position; +export const CrossHair$CrossHair$0 = (value) => value.position; +export const CrossHair$CrossHair$colorable = (value) => value.colorable; +export const CrossHair$CrossHair$1 = (value) => value.colorable; +export const CrossHair$CrossHair$size = (value) => value.size; +export const CrossHair$CrossHair$2 = (value) => value.size; + +export class Health extends $CustomType { + constructor(position, size, amount, max_health) { + super(); + this.position = position; + this.size = size; + this.amount = amount; + this.max_health = max_health; + } +} +export const Health$Health = (position, size, amount, max_health) => + new Health(position, size, amount, max_health); +export const Health$isHealth = (value) => value instanceof Health; +export const Health$Health$position = (value) => value.position; +export const Health$Health$0 = (value) => value.position; +export const Health$Health$size = (value) => value.size; +export const Health$Health$1 = (value) => value.size; +export const Health$Health$amount = (value) => value.amount; +export const Health$Health$2 = (value) => value.amount; +export const Health$Health$max_health = (value) => value.max_health; +export const Health$Health$3 = (value) => value.max_health; + +export class Hud extends $CustomType { + constructor(score, cross_hair, health) { + super(); + this.score = score; + this.cross_hair = cross_hair; + this.health = health; + } +} +export const Hud$Hud = (score, cross_hair, health) => + new Hud(score, cross_hair, health); +export const Hud$isHud = (value) => value instanceof Hud; +export const Hud$Hud$score = (value) => value.score; +export const Hud$Hud$0 = (value) => value.score; +export const Hud$Hud$cross_hair = (value) => value.cross_hair; +export const Hud$Hud$1 = (value) => value.cross_hair; +export const Hud$Hud$health = (value) => value.health; +export const Hud$Hud$2 = (value) => value.health; + +export function score_to_picture(score) { + let position; + let color; + let score$1; + position = score.position; + score$1 = score.score; + color = score.colorable.color; + let _pipe = $paint.text("Score: " + $int.to_string(score$1), 32); + let _pipe$1 = $paint.translate_xy(_pipe, position.x, position.y); + return $paint.fill(_pipe$1, color); +} + +export function cross_hair_to_picture(cross_hair) { + let x; + let y; + let color; + let size; + size = cross_hair.size; + x = cross_hair.position.x; + y = cross_hair.position.y; + color = cross_hair.colorable.color; + let _pipe = $paint.circle(size); + let _pipe$1 = $paint.stroke(_pipe, color, 5.0); + return $paint.translate_xy(_pipe$1, x, y); +} + +function health_to_picture_helper(loop$health, loop$acc, loop$count) { + while (true) { + let health = loop$health; + let acc = loop$acc; + let count = loop$count; + let $ = count < health.max_health; + if ($) { + let _block; + let $1 = count < health.amount; + if ($1) { + _block = $canvas.image_from_src("../res/heart.svg"); + } else { + _block = $canvas.image_from_src("../res/no-heart.svg"); + } + let heart_image = _block; + let size = $float.round(health.size); + loop$health = health; + loop$acc = $paint.combine( + toList([ + acc, + (() => { + let _pipe = $paint.image(heart_image, size, size); + let _pipe$1 = $paint.image_scaling_pixelated(_pipe); + return $paint.translate_xy( + _pipe$1, + $int.to_float(count) * (health.size + 10.0), + 0.0, + ); + })(), + ]), + ); + loop$count = count + 1; + } else { + return acc; + } + } +} + +export function health_to_picture(health) { + let _pipe = health_to_picture_helper(health, $paint.blank(), 0); + return $paint.translate_xy(_pipe, health.position.x, health.position.y); +} diff --git a/build/dev/javascript/stellar_prune/math/math.mjs b/build/dev/javascript/stellar_prune/math/math.mjs new file mode 100644 index 0000000..cbe2232 --- /dev/null +++ b/build/dev/javascript/stellar_prune/math/math.mjs @@ -0,0 +1,3 @@ +import { sin, cos, tan, atan2, floor, ceil, round, random, pi } from "./math_ffi.mjs"; + +export { atan2, ceil, cos, floor, pi, random, round, sin, tan }; diff --git a/build/dev/javascript/stellar_prune/math/math_ffi.mjs b/build/dev/javascript/stellar_prune/math/math_ffi.mjs new file mode 100644 index 0000000..8736bc2 --- /dev/null +++ b/build/dev/javascript/stellar_prune/math/math_ffi.mjs @@ -0,0 +1,35 @@ +export function sin(x) { + return Math.sin(x); +} + +export function cos(x) { + return Math.cos(x); +} + +export function tan(x) { + return Math.tan(x); +} + +export function atan2(y, x) { + return Math.atan2(y, x); +} + +export function floor(x) { + return Math.floor(x); +} + +export function ceil(x) { + return Math.ceil(x); +} + +export function round(x) { + return Math.round(x); +} + +export function random() { + return Math.random(); +} + +export function pi() { + return Math.PI; +} diff --git a/build/dev/javascript/stellar_prune/physics.mjs b/build/dev/javascript/stellar_prune/physics.mjs new file mode 100644 index 0000000..d0eb5a4 --- /dev/null +++ b/build/dev/javascript/stellar_prune/physics.mjs @@ -0,0 +1,63 @@ +import { CustomType as $CustomType } from "./gleam.mjs"; + +export class Vec2 extends $CustomType { + constructor(x, y) { + super(); + this.x = x; + this.y = y; + } +} +export const Vec2$Vec2 = (x, y) => new Vec2(x, y); +export const Vec2$isVec2 = (value) => value instanceof Vec2; +export const Vec2$Vec2$x = (value) => value.x; +export const Vec2$Vec2$0 = (value) => value.x; +export const Vec2$Vec2$y = (value) => value.y; +export const Vec2$Vec2$1 = (value) => value.y; + +export class RigidBody extends $CustomType { + constructor(velocity, acceleration, rotation, rotation_velocity) { + super(); + this.velocity = velocity; + this.acceleration = acceleration; + this.rotation = rotation; + this.rotation_velocity = rotation_velocity; + } +} +export const RigidBody$RigidBody = (velocity, acceleration, rotation, rotation_velocity) => + new RigidBody(velocity, acceleration, rotation, rotation_velocity); +export const RigidBody$isRigidBody = (value) => value instanceof RigidBody; +export const RigidBody$RigidBody$velocity = (value) => value.velocity; +export const RigidBody$RigidBody$0 = (value) => value.velocity; +export const RigidBody$RigidBody$acceleration = (value) => value.acceleration; +export const RigidBody$RigidBody$1 = (value) => value.acceleration; +export const RigidBody$RigidBody$rotation = (value) => value.rotation; +export const RigidBody$RigidBody$2 = (value) => value.rotation; +export const RigidBody$RigidBody$rotation_velocity = (value) => + value.rotation_velocity; +export const RigidBody$RigidBody$3 = (value) => value.rotation_velocity; + +export class Gravity extends $CustomType { + constructor(acceleration) { + super(); + this.acceleration = acceleration; + } +} +export const Gravity$Gravity = (acceleration) => new Gravity(acceleration); +export const Gravity$isGravity = (value) => value instanceof Gravity; +export const Gravity$Gravity$acceleration = (value) => value.acceleration; +export const Gravity$Gravity$0 = (value) => value.acceleration; + +export class Physics extends $CustomType { + constructor(gravity) { + super(); + this.gravity = gravity; + } +} +export const Physics$Physics = (gravity) => new Physics(gravity); +export const Physics$isPhysics = (value) => value instanceof Physics; +export const Physics$Physics$gravity = (value) => value.gravity; +export const Physics$Physics$0 = (value) => value.gravity; + +export function add_vec2(v1, v2) { + return new Vec2(v1.x + v2.x, v1.y + v2.y); +} diff --git a/build/dev/javascript/stellar_prune/rendering.mjs b/build/dev/javascript/stellar_prune/rendering.mjs new file mode 100644 index 0000000..457b2f8 --- /dev/null +++ b/build/dev/javascript/stellar_prune/rendering.mjs @@ -0,0 +1,155 @@ +import * as $int from "../gleam_stdlib/gleam/int.mjs"; +import * as $list from "../gleam_stdlib/gleam/list.mjs"; +import * as $paint from "../paint/paint.mjs"; +import * as $canvas from "../paint/paint/canvas.mjs"; +import { toList, prepend as listPrepend } from "./gleam.mjs"; +import * as $hud from "./hud.mjs"; +import * as $target from "./target.mjs"; +import * as $types from "./types.mjs"; +import { GameOver, MainMenu, Playing } from "./types.mjs"; + +function draw_main_menu() { + let lucy_image = $canvas.image_from_src("../res/lucy.svg"); + let diamond_image = $canvas.image_from_src("../res/diamond.svg"); + let rocket_image = $canvas.image_from_src("../res/rocket.svg"); + return $paint.combine( + toList([ + (() => { + let _pipe = $paint.text("Stellar prune", 72); + let _pipe$1 = $paint.translate_xy(_pipe, 300.0, 100.0); + let _pipe$2 = $paint.fill(_pipe$1, $paint.colour_hex("#ffffff")); + return $paint.image_scaling_pixelated(_pipe$2); + })(), + (() => { + let _pipe = $paint.text("Click/swipe the targets to score points!", 28); + let _pipe$1 = $paint.translate_xy(_pipe, 320.0, 200.0); + let _pipe$2 = $paint.fill(_pipe$1, $paint.colour_hex("#aaaaaa")); + return $paint.image_scaling_pixelated(_pipe$2); + })(), + (() => { + let _pipe = $paint.text( + "Don't let them escape or you'll lose health!", + 28, + ); + let _pipe$1 = $paint.translate_xy(_pipe, 320.0, 240.0); + let _pipe$2 = $paint.fill(_pipe$1, $paint.colour_hex("#aaaaaa")); + return $paint.image_scaling_pixelated(_pipe$2); + })(), + (() => { + let _pipe = $paint.image(lucy_image, 80, 80); + let _pipe$1 = $paint.image_scaling_pixelated(_pipe); + return $paint.translate_xy(_pipe$1, 320.0, 320.0); + })(), + (() => { + let _pipe = $paint.text("Lucy - Click for 1 point.", 24); + let _pipe$1 = $paint.translate_xy(_pipe, 420.0, 370.0); + let _pipe$2 = $paint.fill(_pipe$1, $paint.colour_hex("#ffffff")); + return $paint.image_scaling_pixelated(_pipe$2); + })(), + (() => { + let _pipe = $paint.image(diamond_image, 80, 80); + let _pipe$1 = $paint.image_scaling_pixelated(_pipe); + return $paint.translate_xy(_pipe$1, 320.0, 420.0); + })(), + (() => { + let _pipe = $paint.text("Diamond - Click for 3 points!", 24); + let _pipe$1 = $paint.translate_xy(_pipe, 420.0, 460.0); + let _pipe$2 = $paint.fill(_pipe$1, $paint.colour_hex("#ffffff")); + return $paint.image_scaling_pixelated(_pipe$2); + })(), + (() => { + let _pipe = $paint.image(rocket_image, 80, 80); + let _pipe$1 = $paint.image_scaling_pixelated(_pipe); + return $paint.translate_xy(_pipe$1, 320.0, 520.0); + })(), + (() => { + let _pipe = $paint.text("Rocket - AVOID! Loses 1 health", 24); + let _pipe$1 = $paint.translate_xy(_pipe, 420.0, 560.0); + let _pipe$2 = $paint.fill(_pipe$1, $paint.colour_hex("#ff6666")); + return $paint.image_scaling_pixelated(_pipe$2); + })(), + (() => { + let _pipe = $paint.text("Click to play!", 48); + let _pipe$1 = $paint.translate_xy(_pipe, 420.0, 680.0); + let _pipe$2 = $paint.fill(_pipe$1, $paint.colour_hex("#ffaff3")); + return $paint.image_scaling_pixelated(_pipe$2); + })(), + ]), + ); +} + +function draw_game_over(final_score) { + return $paint.combine( + toList([ + (() => { + let _pipe = $paint.text("Well done!", 128); + let _pipe$1 = $paint.translate_xy(_pipe, 300.0, 175.0); + let _pipe$2 = $paint.fill(_pipe$1, $paint.colour_hex("#ffaff3")); + return $paint.image_scaling_pixelated(_pipe$2); + })(), + (() => { + let _pipe = $paint.text("Final Score: ", 48); + let _pipe$1 = $paint.translate_xy(_pipe, 440.0, 400.0); + let _pipe$2 = $paint.fill(_pipe$1, $paint.colour_hex("#ffffff")); + return $paint.image_scaling_pixelated(_pipe$2); + })(), + (() => { + let _pipe = $paint.text($int.to_string(final_score), 48); + let _pipe$1 = $paint.translate_xy(_pipe, 710.0, 400.0); + let _pipe$2 = $paint.fill(_pipe$1, $paint.colour_hex("#ffaff3")); + return $paint.image_scaling_pixelated(_pipe$2); + })(), + (() => { + let _pipe = $paint.text("Click to play again!", 32); + let _pipe$1 = $paint.translate_xy(_pipe, 455.0, 600.0); + let _pipe$2 = $paint.fill(_pipe$1, $paint.colour_hex("#ffaff3")); + return $paint.image_scaling_pixelated(_pipe$2); + })(), + ]), + ); +} + +function draw_playing(playing_state) { + let target_pictures = $list.map( + playing_state.targets, + (target) => { + let position; + let rigid_body; + let paintable; + position = target.position; + rigid_body = target.rigid_body; + paintable = target.paintable; + let _pipe = $target.paintable_to_picture(paintable); + let _pipe$1 = $paint.rotate(_pipe, $paint.angle_deg(rigid_body.rotation)); + return $paint.translate_xy(_pipe$1, position.x, position.y); + }, + ); + let hud_without_crosshair = $paint.combine( + toList([ + $hud.health_to_picture(playing_state.hud.health), + $hud.score_to_picture(playing_state.hud.score), + ]), + ); + let crosshair = $hud.cross_hair_to_picture(playing_state.hud.cross_hair); + return $paint.combine( + listPrepend( + hud_without_crosshair, + $list.append(target_pictures, toList([crosshair])), + ), + ); +} + +export function render(state) { + let _block; + if (state instanceof MainMenu) { + _block = draw_main_menu(); + } else if (state instanceof Playing) { + let playing_state = state.state; + _block = draw_playing(playing_state); + } else { + let final_score = state.final_score; + _block = draw_game_over(final_score); + } + let picture = _block; + return $canvas.display((_) => { return picture; }, "#canvas"); +} diff --git a/build/dev/javascript/stellar_prune/target.mjs b/build/dev/javascript/stellar_prune/target.mjs new file mode 100644 index 0000000..04732a2 --- /dev/null +++ b/build/dev/javascript/stellar_prune/target.mjs @@ -0,0 +1,194 @@ +import * as $int from "../gleam_stdlib/gleam/int.mjs"; +import * as $paint from "../paint/paint.mjs"; +import * as $canvas from "../paint/paint/canvas.mjs"; +import { CustomType as $CustomType } from "./gleam.mjs"; +import * as $math from "./math/math.mjs"; +import * as $physics from "./physics.mjs"; +import { RigidBody, Vec2, add_vec2 } from "./physics.mjs"; + +export class Colorable extends $CustomType { + constructor(color) { + super(); + this.color = color; + } +} +export const Colorable$Colorable = (color) => new Colorable(color); +export const Colorable$isColorable = (value) => value instanceof Colorable; +export const Colorable$Colorable$color = (value) => value.color; +export const Colorable$Colorable$0 = (value) => value.color; + +export class Size extends $CustomType { + constructor(width, height) { + super(); + this.width = width; + this.height = height; + } +} +export const Size$Size = (width, height) => new Size(width, height); +export const Size$isSize = (value) => value instanceof Size; +export const Size$Size$width = (value) => value.width; +export const Size$Size$0 = (value) => value.width; +export const Size$Size$height = (value) => value.height; +export const Size$Size$1 = (value) => value.height; + +export class Square extends $CustomType { + constructor(colorable, size) { + super(); + this.colorable = colorable; + this.size = size; + } +} +export const Paintable$Square = (colorable, size) => + new Square(colorable, size); +export const Paintable$isSquare = (value) => value instanceof Square; +export const Paintable$Square$colorable = (value) => value.colorable; +export const Paintable$Square$0 = (value) => value.colorable; +export const Paintable$Square$size = (value) => value.size; +export const Paintable$Square$1 = (value) => value.size; + +export class Sprite extends $CustomType { + constructor(image, size) { + super(); + this.image = image; + this.size = size; + } +} +export const Paintable$Sprite = (image, size) => new Sprite(image, size); +export const Paintable$isSprite = (value) => value instanceof Sprite; +export const Paintable$Sprite$image = (value) => value.image; +export const Paintable$Sprite$0 = (value) => value.image; +export const Paintable$Sprite$size = (value) => value.size; +export const Paintable$Sprite$1 = (value) => value.size; + +export class Lucy extends $CustomType {} +export const TargetKind$Lucy = () => new Lucy(); +export const TargetKind$isLucy = (value) => value instanceof Lucy; + +export class Diamond extends $CustomType { + constructor(points_value) { + super(); + this.points_value = points_value; + } +} +export const TargetKind$Diamond = (points_value) => new Diamond(points_value); +export const TargetKind$isDiamond = (value) => value instanceof Diamond; +export const TargetKind$Diamond$points_value = (value) => value.points_value; +export const TargetKind$Diamond$0 = (value) => value.points_value; + +export class Rocket extends $CustomType {} +export const TargetKind$Rocket = () => new Rocket(); +export const TargetKind$isRocket = (value) => value instanceof Rocket; + +export class Target extends $CustomType { + constructor(position, rigid_body, paintable, kind) { + super(); + this.position = position; + this.rigid_body = rigid_body; + this.paintable = paintable; + this.kind = kind; + } +} +export const Target$Target = (position, rigid_body, paintable, kind) => + new Target(position, rigid_body, paintable, kind); +export const Target$isTarget = (value) => value instanceof Target; +export const Target$Target$position = (value) => value.position; +export const Target$Target$0 = (value) => value.position; +export const Target$Target$rigid_body = (value) => value.rigid_body; +export const Target$Target$1 = (value) => value.rigid_body; +export const Target$Target$paintable = (value) => value.paintable; +export const Target$Target$2 = (value) => value.paintable; +export const Target$Target$kind = (value) => value.kind; +export const Target$Target$3 = (value) => value.kind; + +export function create_target() { + let vx = ($math.random() * 6.0) - 3.0; + let vy = ($math.random() * -4.0) - 8.0; + let rotation_vel = ($math.random() * 2.0) - 1.0; + let start_x = ($math.random() * 600.0) + 200.0; + let random_type = $math.random(); + let _block; + let r = random_type; + if (r < 0.7) { + _block = ["../res/lucy.svg", new Lucy()]; + } else { + let r$1 = random_type; + if (r$1 < 0.85) { + _block = ["../res/diamond.svg", new Diamond(3)]; + } else { + _block = ["../res/rocket.svg", new Rocket()]; + } + } + let $ = _block; + let sprite_path; + let target_kind; + sprite_path = $[0]; + target_kind = $[1]; + return new Target( + new Vec2(start_x, 900.0), + new RigidBody(new Vec2(vx, vy), new Vec2(0.0, 0.0), 0.0, rotation_vel), + new Sprite($canvas.image_from_src(sprite_path), new Size(120, 120)), + target_kind, + ); +} + +export function update_target(target, gravity) { + let $ = target.rigid_body; + let velocity; + let acceleration; + let rotation; + let rotation_velocity; + velocity = $.velocity; + acceleration = $.acceleration; + rotation = $.rotation; + rotation_velocity = $.rotation_velocity; + let total_acceleration = add_vec2(acceleration, gravity); + let new_velocity = add_vec2(velocity, total_acceleration); + let new_position = add_vec2(target.position, new_velocity); + let new_rigid_body = new RigidBody( + new_velocity, + acceleration, + rotation + rotation_velocity, + rotation_velocity, + ); + return new Target(new_position, new_rigid_body, target.paintable, target.kind); +} + +export function is_point_in_target(point, target) { + let dx = point.x - target.position.x; + let dy = point.y - target.position.y; + let angle_rad = ((target.rigid_body.rotation * $math.pi())) / 180.0; + let cos_angle = $math.cos(angle_rad); + let sin_angle = $math.sin(angle_rad); + let local_x = (dx * cos_angle) + (dy * sin_angle); + let local_y = (dy * cos_angle) - (dx * sin_angle); + let $ = target.paintable; + if ($ instanceof Square) { + let size = $.size; + let half_size = size / 2.0; + return (((local_x >= (0.0 - half_size)) && (local_x <= half_size)) && (local_y >= (0.0 - half_size))) && (local_y <= half_size); + } else { + let size = $.size; + let half_width = $int.to_float(size.width) / 2.0; + let half_height = $int.to_float(size.height) / 2.0; + return (((local_x >= (0.0 - half_width)) && (local_x <= half_width)) && (local_y >= (0.0 - half_height))) && (local_y <= half_height); + } +} + +export function paintable_to_picture(paintable) { + if (paintable instanceof Square) { + let colorable = paintable.colorable; + let size = paintable.size; + let _pipe = $paint.square(size); + return $paint.fill(_pipe, colorable.color); + } else { + let image = paintable.image; + let size = paintable.size; + let _pipe = $paint.image(image, size.width, size.height); + let _pipe$1 = $paint.image_scaling_pixelated(_pipe); + return $paint.translate_xy( + _pipe$1, + 0.0 - ($int.to_float(size.width) / 2.0), + 0.0 - ($int.to_float(size.height) / 2.0), + ); + } +} diff --git a/build/dev/javascript/stellar_prune/types.mjs b/build/dev/javascript/stellar_prune/types.mjs new file mode 100644 index 0000000..72fff8f --- /dev/null +++ b/build/dev/javascript/stellar_prune/types.mjs @@ -0,0 +1,66 @@ +import { CustomType as $CustomType } from "./gleam.mjs"; +import * as $hud from "./hud.mjs"; +import * as $physics from "./physics.mjs"; +import * as $target from "./target.mjs"; + +export class PlayingState extends $CustomType { + constructor(targets, physics, hud, mouse, spawn_timer) { + super(); + this.targets = targets; + this.physics = physics; + this.hud = hud; + this.mouse = mouse; + this.spawn_timer = spawn_timer; + } +} +export const PlayingState$PlayingState = (targets, physics, hud, mouse, spawn_timer) => + new PlayingState(targets, physics, hud, mouse, spawn_timer); +export const PlayingState$isPlayingState = (value) => + value instanceof PlayingState; +export const PlayingState$PlayingState$targets = (value) => value.targets; +export const PlayingState$PlayingState$0 = (value) => value.targets; +export const PlayingState$PlayingState$physics = (value) => value.physics; +export const PlayingState$PlayingState$1 = (value) => value.physics; +export const PlayingState$PlayingState$hud = (value) => value.hud; +export const PlayingState$PlayingState$2 = (value) => value.hud; +export const PlayingState$PlayingState$mouse = (value) => value.mouse; +export const PlayingState$PlayingState$3 = (value) => value.mouse; +export const PlayingState$PlayingState$spawn_timer = (value) => + value.spawn_timer; +export const PlayingState$PlayingState$4 = (value) => value.spawn_timer; + +export class MainMenu extends $CustomType {} +export const GameState$MainMenu = () => new MainMenu(); +export const GameState$isMainMenu = (value) => value instanceof MainMenu; + +export class Playing extends $CustomType { + constructor(state, previous_mouse_pressed) { + super(); + this.state = state; + this.previous_mouse_pressed = previous_mouse_pressed; + } +} +export const GameState$Playing = (state, previous_mouse_pressed) => + new Playing(state, previous_mouse_pressed); +export const GameState$isPlaying = (value) => value instanceof Playing; +export const GameState$Playing$state = (value) => value.state; +export const GameState$Playing$0 = (value) => value.state; +export const GameState$Playing$previous_mouse_pressed = (value) => + value.previous_mouse_pressed; +export const GameState$Playing$1 = (value) => value.previous_mouse_pressed; + +export class GameOver extends $CustomType { + constructor(final_score, previous_mouse_pressed) { + super(); + this.final_score = final_score; + this.previous_mouse_pressed = previous_mouse_pressed; + } +} +export const GameState$GameOver = (final_score, previous_mouse_pressed) => + new GameOver(final_score, previous_mouse_pressed); +export const GameState$isGameOver = (value) => value instanceof GameOver; +export const GameState$GameOver$final_score = (value) => value.final_score; +export const GameState$GameOver$0 = (value) => value.final_score; +export const GameState$GameOver$previous_mouse_pressed = (value) => + value.previous_mouse_pressed; +export const GameState$GameOver$1 = (value) => value.previous_mouse_pressed; diff --git a/build/gleam-dev-erlang.lock b/build/gleam-dev-erlang.lock new file mode 100644 index 0000000..e69de29 diff --git a/build/gleam-dev-javascript.lock b/build/gleam-dev-javascript.lock new file mode 100644 index 0000000..e69de29 diff --git a/build/gleam-lsp-erlang.lock b/build/gleam-lsp-erlang.lock new file mode 100644 index 0000000..e69de29 diff --git a/build/gleam-lsp-javascript.lock b/build/gleam-lsp-javascript.lock new file mode 100644 index 0000000..e69de29 diff --git a/build/gleam-prod-erlang.lock b/build/gleam-prod-erlang.lock new file mode 100644 index 0000000..e69de29 diff --git a/build/gleam-prod-javascript.lock b/build/gleam-prod-javascript.lock new file mode 100644 index 0000000..e69de29 diff --git a/build/packages/gleam.lock b/build/packages/gleam.lock new file mode 100644 index 0000000..e69de29 diff --git a/build/packages/gleam_community_colour/LICENCE b/build/packages/gleam_community_colour/LICENCE new file mode 100644 index 0000000..a84f0ec --- /dev/null +++ b/build/packages/gleam_community_colour/LICENCE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2023 Gleam Community Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/build/packages/gleam_community_colour/README.md b/build/packages/gleam_community_colour/README.md new file mode 100644 index 0000000..0eccdd7 --- /dev/null +++ b/build/packages/gleam_community_colour/README.md @@ -0,0 +1,36 @@ +# gleam-community/colour + +A package for a standard Colour type, conversions, and other utilities. + +[![Package Version](https://img.shields.io/hexpm/v/gleam_community_colour)](https://hex.pm/packages/gleam_community_colour) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gleam_community_colour/) + +✨ This project is written in pure Gleam so you can use it anywhere Gleam runs: Erlang, Elixir, Node, Deno, and the browser! + +--- + +## Quickstart + +```gleam +import gleam_community/colour +import gleam_community/colour/accessibility + +pub fn main() { + let foreground = colour.from_hsl(h: 0.858, s: 1.0, l: 0.843) + + let background_options = [colour.light_grey, colour.dark_grey] + + let background = accessibility.maximum_contrast(foreground, background_options) +} +``` + +## Installation + +`gleam_community` packages are published to [hex.pm](https://hex.pm/packages/gleam_community_colour) +with the prefix `gleam_community_`. You can add them to your Gleam projects directly: + +```sh +gleam add gleam_community_colour +``` + +The docs can be found over at [hexdocs.pm](https://hexdocs.pm/gleam_community_colour). diff --git a/build/packages/gleam_community_colour/gleam.toml b/build/packages/gleam_community_colour/gleam.toml new file mode 100644 index 0000000..3b7155c --- /dev/null +++ b/build/packages/gleam_community_colour/gleam.toml @@ -0,0 +1,13 @@ +name = "gleam_community_colour" +version = "2.0.2" +licences = ["Apache-2.0"] +description = "Colour types, conversions, and other utilities" +repository = { type = "github", user = "gleam-community", repo = "colour" } +gleam = ">= 1.4.0" + +[dependencies] +gleam_stdlib = ">= 0.50.0 and < 2.0.0" +gleam_json = ">= 2.2.0 and < 4.0.0" + +[dev-dependencies] +gleeunit = ">= 1.3.0 and < 2.0.0" diff --git a/build/packages/gleam_community_colour/include/gleam_community@colour_Hsla.hrl b/build/packages/gleam_community_colour/include/gleam_community@colour_Hsla.hrl new file mode 100644 index 0000000..06116df --- /dev/null +++ b/build/packages/gleam_community_colour/include/gleam_community@colour_Hsla.hrl @@ -0,0 +1 @@ +-record(hsla, {h :: float(), s :: float(), l :: float(), a :: float()}). diff --git a/build/packages/gleam_community_colour/include/gleam_community@colour_Rgba.hrl b/build/packages/gleam_community_colour/include/gleam_community@colour_Rgba.hrl new file mode 100644 index 0000000..fff139e --- /dev/null +++ b/build/packages/gleam_community_colour/include/gleam_community@colour_Rgba.hrl @@ -0,0 +1 @@ +-record(rgba, {r :: float(), g :: float(), b :: float(), a :: float()}). diff --git a/build/packages/gleam_community_colour/src/gleam_community/colour.gleam b/build/packages/gleam_community_colour/src/gleam_community/colour.gleam new file mode 100644 index 0000000..f394cff --- /dev/null +++ b/build/packages/gleam_community_colour/src/gleam_community/colour.gleam @@ -0,0 +1,1214 @@ +//// +//// - **Types** +//// - [`Colour`](#Colour) +//// - [`Color`](#Color) +//// - **Constructors** +//// - [`from_rgb255`](#from_rgb255) +//// - [`from_rgb`](#from_rgb) +//// - [`from_rgba`](#from_rgba) +//// - [`from_hsl`](#from_hsl) +//// - [`from_hsla`](#from_hsla) +//// - [`from_rgb_hex`](#from_rgb_hex) +//// - [`from_rgba_hex`](#from_rgba_hex) +//// - [`from_rgb_hex_string`](#from_rgb_hex_string) +//// - [`from_rgba_hex_string`](#from_rgba_hex_string) +//// - **Conversions** +//// - [`to_rgba`](#to_rgba) +//// - [`to_hsla`](#hsla) +//// - [`to_css_rgba_string`](#to_css_rgba_string) +//// - [`to_rgba_hex_string`](#to_rgba_hex_string) +//// - [`to_rgb_hex_string`](#to_rgb_hex_string) +//// - [`to_rgba_hex`](#to_rgba_hex) +//// - [`to_rgb_hex`](#to_rgb_hex) +//// - **JSON** +//// - [`encode`](#encode) +//// - [`decoder`](#decoder) +//// - **Colours** +//// - [`light_red`](#light_red) +//// - [`red`](#red) +//// - [`dark_red`](#dark_red) +//// - [`light_orange`](#light_orange) +//// - [`orange`](#orange) +//// - [`dark_orange`](#dark_orange) +//// - [`light_yellow`](#light_yellow) +//// - [`yellow`](#yellow) +//// - [`dark_yellow`](#dark_yellow) +//// - [`light_green`](#light_green) +//// - [`green`](#green) +//// - [`dark_green`](#dark_green) +//// - [`light_blue`](#light_blue) +//// - [`blue`](#blue) +//// - [`dark_blue`](#dark_blue) +//// - [`light_purple`](#light_purple) +//// - [`purple`](#purple) +//// - [`dark_purple`](#dark_purple) +//// - [`light_brown`](#light_brown) +//// - [`brown`](#brown) +//// - [`dark_brown`](#dark_brown) +//// - [`black`](#black) +//// - [`white`](#white) +//// - [`light_grey`](#light_grey) +//// - [`grey`](#grey) +//// - [`dark_grey`](#dark_grey) +//// - [`light_gray`](#light_gray) +//// - [`gray`](#gray) +//// - [`dark_gray`](#dark_gray) +//// - [`light_charcoal`](#light_charcoal) +//// - [`charcoal`](#charcoal) +//// - [`dark_charcoal`](#dark_charcoal) +//// - [`pink`](#pink) +//// +//// --- +//// +//// This package was heavily inspired by the `elm-color` module. +//// The original source code can be found +//// here. +//// +////
+//// The license of that package is produced below: +//// +//// +//// > MIT License +//// +//// > Copyright 2018 Aaron VonderHaar +//// +//// > Redistribution and use in source and binary forms, with or without modification, +//// are permitted provided that the following conditions are met: +//// +//// 1. Redistributions of source code must retain the above copyright notice, +//// this list of conditions and the following disclaimer. +//// +//// 2. Redistributions in binary form must reproduce the above copyright notice, +//// this list of conditions and the following disclaimer in the documentation +//// and/or other materials provided with the distribution. +//// +//// 3. Neither the name of the copyright holder nor the names of its contributors +//// may be used to endorse or promote products derived from this software without +//// specific prior written permission. +//// +//// > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +//// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +//// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +//// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +//// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +//// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +//// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +//// +//// > The above copyright notice and this permission notice shall be included in all +//// copies or substantial portions of the Software. +////
+//// + +// Just in case we decide in the future to no longer include the above reference +// and license, this package was initially a port of the `elm-color` module: +// +// https://github.com/avh4/elm-color/ +// + +// IMPORTS -------------------------------------------------------------------- + +import gleam/dynamic/decode +import gleam/float +import gleam/int +import gleam/json.{type Json} +import gleam/list +import gleam/result +import gleam/string + +// TYPES ---------------------------------------------------------------------- + +/// A representation of a colour that can be converted to RGBA or HSLA format. +/// +/// +///
+/// +pub opaque type Colour { + Rgba(r: Float, g: Float, b: Float, a: Float) + Hsla(h: Float, s: Float, l: Float, a: Float) +} + +/// Type alias for `Colour` +/// +/// +///
+/// +pub type Color = + Colour + +// UTILITY -------------------------------------------------------------------- + +fn valid_colour_value(c: Float) -> Result(Float, Nil) { + case c >. 1.0 || c <. 0.0 { + True -> Error(Nil) + False -> Ok(c) + } +} + +fn hue_to_rgb(hue: Float, m1: Float, m2: Float) -> Float { + let h = case hue { + _ if hue <. 0.0 -> hue +. 1.0 + _ if hue >. 1.0 -> hue -. 1.0 + _ -> hue + } + + let h_t_6 = h *. 6.0 + let h_t_2 = h *. 2.0 + let h_t_3 = h *. 3.0 + + case h { + _ if h_t_6 <. 1.0 -> m1 +. { m2 -. m1 } *. h *. 6.0 + _ if h_t_2 <. 1.0 -> m2 + _ if h_t_3 <. 2.0 -> m1 +. { m2 -. m1 } *. { 2.0 /. 3.0 -. h } *. 6.0 + _ -> m1 + } +} + +fn hex_string_to_int(hex_string: String) -> Result(Int, Nil) { + let hex = case hex_string { + "#" <> hex_number -> hex_number + "0x" <> hex_number -> hex_number + _ -> hex_string + } + + hex + |> string.lowercase() + |> string.to_graphemes() + |> list.reverse() + |> list.index_fold(Ok(0), fn(total, char, index) { + case total { + Error(Nil) -> Error(Nil) + Ok(v) -> { + use num <- result.try(case char { + "a" -> Ok(10) + "b" -> Ok(11) + "c" -> Ok(12) + "d" -> Ok(13) + "e" -> Ok(14) + "f" -> Ok(15) + _ -> int.parse(char) + }) + use base <- result.try(int.power(16, int.to_float(index))) + Ok(v + float.round(int.to_float(num) *. base)) + } + } + }) +} + +fn hsla_to_rgba( + h: Float, + s: Float, + l: Float, + a: Float, +) -> #(Float, Float, Float, Float) { + let m2 = case l <=. 0.5 { + True -> l *. { s +. 1.0 } + False -> l +. s -. l *. s + } + + let m1 = l *. 2.0 -. m2 + + let r = hue_to_rgb(h +. 1.0 /. 3.0, m1, m2) + let g = hue_to_rgb(h, m1, m2) + let b = hue_to_rgb(h -. 1.0 /. 3.0, m1, m2) + + #(r, g, b, a) +} + +fn rgba_to_hsla( + r: Float, + g: Float, + b: Float, + a: Float, +) -> #(Float, Float, Float, Float) { + let min_colour = float.min(r, float.min(g, b)) + + let max_colour = float.max(r, float.max(g, b)) + + let h1 = case True { + _ if max_colour == r -> float.divide(g -. b, max_colour -. min_colour) + _ if max_colour == g -> + float.divide(b -. r, max_colour -. min_colour) + |> result.try(fn(d) { Ok(2.0 +. d) }) + _ -> + float.divide(r -. g, max_colour -. min_colour) + |> result.try(fn(d) { Ok(4.0 +. d) }) + } + + let h2 = case h1 { + Ok(v) -> Ok(v *. { 1.0 /. 6.0 }) + _ -> h1 + } + + let h3 = case h2 { + Ok(v) if v <. 0.0 -> v +. 1.0 + Ok(v) -> v + _ -> 0.0 + } + + let l = { min_colour +. max_colour } /. 2.0 + + let s = case True { + _ if min_colour == max_colour -> 0.0 + _ if l <. 0.5 -> + { max_colour -. min_colour } /. { max_colour +. min_colour } + _ -> { max_colour -. min_colour } /. { 2.0 -. max_colour -. min_colour } + } + + #(h3, s, l, a) +} + +// CONSTRUCTORS --------------------------------------------------------------- + +/// Returns a `Result(Colour)` created from the given 8 bit RGB values. +/// +/// Returns `Error(Nil)` if the supplied RGB values are greater than 255 or less than 0. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_rgb255(255, 0, 0) +/// } +/// ``` +///
+/// +/// +/// +pub fn from_rgb255(r red: Int, g green: Int, b blue: Int) -> Result(Colour, Nil) { + use r <- result.try( + red + |> int.to_float() + |> float.divide(255.0) + |> result.try(valid_colour_value), + ) + + use g <- result.try( + green + |> int.to_float() + |> float.divide(255.0) + |> result.try(valid_colour_value), + ) + + use b <- result.try( + blue + |> int.to_float() + |> float.divide(255.0) + |> result.try(valid_colour_value), + ) + + Ok(Rgba(r: r, g: g, b: b, a: 1.0)) +} + +/// Returns `Result(Colour)` created from the given RGB values. +/// +/// If the supplied RGB values are greater than 1.0 or less than 0.0 returns `Error(Nil)` +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_rgb(1.0, 0.0, 0.0) +/// } +/// ``` +///
+/// +/// +/// +pub fn from_rgb( + r red: Float, + g green: Float, + b blue: Float, +) -> Result(Colour, Nil) { + use r <- result.try(valid_colour_value(red)) + use g <- result.try(valid_colour_value(green)) + use b <- result.try(valid_colour_value(blue)) + + Ok(Rgba(r: r, g: g, b: b, a: 1.0)) +} + +/// Returns `Result(Colour)` created from the given RGBA values. +/// +/// Returns `Error(Nil)` if the supplied RGBA values are greater than 1.0 or less than 0.0. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red_half_opacity) = from_rbga(1.0, 0.0, 0.0, 0.5) +/// } +/// ``` +///
+/// +/// +/// +pub fn from_rgba( + r red: Float, + g green: Float, + b blue: Float, + a alpha: Float, +) -> Result(Colour, Nil) { + use r <- result.try(valid_colour_value(red)) + use g <- result.try(valid_colour_value(green)) + use b <- result.try(valid_colour_value(blue)) + use a <- result.try(valid_colour_value(alpha)) + + Ok(Rgba(r: r, g: g, b: b, a: a)) +} + +/// Returns `Result(Colour)` created from the given HSLA values. +/// +/// Returns `Error(Nil)`f the supplied HSLA values are greater than 1.0 or less than 0.0. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red_half_opacity) = from_hsla(0.0, 1.0, 0.5, 0.5) +/// } +/// ``` +///
+/// +/// +/// +pub fn from_hsla( + h hue: Float, + s saturation: Float, + l lightness: Float, + a alpha: Float, +) -> Result(Colour, Nil) { + use h <- result.try(valid_colour_value(hue)) + use s <- result.try(valid_colour_value(saturation)) + use l <- result.try(valid_colour_value(lightness)) + use a <- result.try(valid_colour_value(alpha)) + + Ok(Hsla(h: h, s: s, l: l, a: a)) +} + +/// Returns `Result(Colour)` created from the given HSL values. +/// +/// Returns `Error(Nil)` if the supplied HSL values are greater than 1.0 or less than 0.0. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_hsla(0.0, 1.0, 0.5) +/// } +/// ``` +///
+/// +/// +/// +pub fn from_hsl( + h hue: Float, + s saturation: Float, + l lightness: Float, +) -> Result(Colour, Nil) { + from_hsla(hue, saturation, lightness, 1.0) +} + +/// Returns a `Result(Colour)` created from the given hex `Int`. +/// +/// Returns `Error(Nil)` if the supplied hex `Int is greater than 0xffffff or less than 0x0. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_rgb_hex(0xff0000) +/// } +/// ``` +///
+/// +/// +/// +pub fn from_rgb_hex(hex: Int) -> Result(Colour, Nil) { + case hex > 0xffffff || hex < 0 { + True -> Error(Nil) + False -> { + let r = + int.bitwise_shift_right(hex, 16) + |> int.bitwise_and(0xff) + let g = + int.bitwise_shift_right(hex, 8) + |> int.bitwise_and(0xff) + let b = int.bitwise_and(hex, 0xff) + from_rgb255(r, g, b) + } + } +} + +/// Returns a `Result(Colour)` created from the given RGB hex `String`. +/// +/// Returns `Error(Nil)` if the supplied hex `String` is invalid, or greater than `"#ffffff" or less than `"#0"` +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_rgb_hex_string("#ff0000") +/// } +/// ``` +///
+/// +/// +/// +pub fn from_rgb_hex_string(hex_string: String) -> Result(Colour, Nil) { + use hex_int <- result.try(hex_string_to_int(hex_string)) + + from_rgb_hex(hex_int) +} + +/// Returns a `Result(Colour)` created from the given RGBA hex `String`. +/// +/// Returns `Error(Nil)` if the supplied hex `String` is invalid, or greater than `"#ffffffff" or less than `"#0"` +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red_half_opacity) = from_rgba_hex_string("#ff00007f") +/// } +/// ``` +///
+/// +/// +/// +pub fn from_rgba_hex_string(hex_string: String) -> Result(Colour, Nil) { + use hex_int <- result.try(hex_string_to_int(hex_string)) + + from_rgba_hex(hex_int) +} + +/// Returns a `Result(Colour)` created from the given hex `Int`. +/// +/// Returns `Error(Nil)` if the supplied hex `Int is greater than 0xffffffff or less than 0x0. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red_half_opacity) = from_rgba_hex(0xff00007f) +/// } +/// ``` +///
+/// +/// +/// +pub fn from_rgba_hex(hex: Int) -> Result(Colour, Nil) { + case hex > 0xffffffff || hex < 0 { + True -> Error(Nil) + False -> { + // This won't fail because we are always dividing by 255.0 + let assert Ok(r) = + int.bitwise_shift_right(hex, 24) + |> int.bitwise_and(0xff) + |> int.to_float() + |> float.divide(255.0) + // This won't fail because we are always dividing by 255.0 + let assert Ok(g) = + int.bitwise_shift_right(hex, 16) + |> int.bitwise_and(0xff) + |> int.to_float() + |> float.divide(255.0) + // This won't fail because we are always dividing by 255.0 + let assert Ok(b) = + int.bitwise_shift_right(hex, 8) + |> int.bitwise_and(0xff) + |> int.to_float() + |> float.divide(255.0) + // This won't fail because we are always dividing by 255.0 + let assert Ok(a) = + int.bitwise_and(hex, 0xff) + |> int.to_float() + |> float.divide(255.0) + from_rgba(r, g, b, a) + } + } +} + +// CONVERSIONS ---------------------------------------------------------------- + +/// Returns `#(Float, Float, Float, Float)` representing the given `Colour`'s +/// R, G, B, and A values respectively. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_rgb255(255, 0, 0) +/// let #(r, g, b, a) = to_rgba(red) +/// } +/// ``` +///
+/// +/// +/// +pub fn to_rgba(colour: Colour) -> #(Float, Float, Float, Float) { + case colour { + Rgba(r, g, b, a) -> #(r, g, b, a) + Hsla(h, s, l, a) -> hsla_to_rgba(h, s, l, a) + } +} + +/// Returns `#(Float, Float, Float, Float)` representing the given `Colour`'s +/// H, S, L, and A values respectively. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_rgb255(255, 0, 0) +/// let #(h, s, l, a) = to_hsla(red) +/// } +/// ``` +///
+/// +/// +/// +pub fn to_hsla(colour: Colour) -> #(Float, Float, Float, Float) { + case colour { + Hsla(h, s, l, a) -> #(h, s, l, a) + Rgba(r, g, b, a) -> rgba_to_hsla(r, g, b, a) + } +} + +/// Returns an rgba formatted CSS `String` created from the given `Colour`. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_rgb255(255, 0, 0) +/// let css_red = to_css_rgba_string(red) +/// } +/// ``` +///
+/// +/// +/// +pub fn to_css_rgba_string(colour: Colour) -> String { + let #(r, g, b, a) = to_rgba(colour) + + let percent = fn(x: Float) -> Float { + // This won't fail because we are always dividing by 100.0 + let assert Ok(p) = + x + |> float.multiply(10_000.0) + |> float.round() + |> int.to_float() + |> float.divide(100.0) + + p + } + + let round_to = fn(x: Float) -> Float { + // This won't fail because we are always dividing by 1000.0 + let assert Ok(r) = + x + |> float.multiply(1000.0) + |> float.round() + |> int.to_float() + |> float.divide(1000.0) + + r + } + + string.join( + [ + "rgba(", + float.to_string(percent(r)) <> "%,", + float.to_string(percent(g)) <> "%,", + float.to_string(percent(b)) <> "%,", + float.to_string(round_to(a)), + ")", + ], + "", + ) +} + +/// Returns an rgba hex formatted `String` created from the given `Colour`. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_rgba(1.0, 0.0, 0.0, 1.0) +/// let red_hex = to_rgba_hex_string(red) +/// } +/// ``` +///
+/// +/// +/// +pub fn to_rgba_hex_string(colour: Colour) -> String { + let hex_string = + to_rgba_hex(colour) + |> int.to_base16() + + case string.length(hex_string) { + 8 -> hex_string + l -> string.repeat("0", 8 - l) <> hex_string + } +} + +/// Returns an rgb hex formatted `String` created from the given `Colour`. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_rgba(255, 0, 0) +/// let red_hex = to_rgb_hex_string(red) +/// } +/// ``` +///
+/// +/// +/// +pub fn to_rgb_hex_string(colour: Colour) -> String { + let hex_string = + to_rgb_hex(colour) + |> int.to_base16() + + case string.length(hex_string) { + 6 -> hex_string + l -> string.repeat("0", 6 - l) <> hex_string + } +} + +/// Returns an hex `Int` created from the given `Colour`. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_rgba(1.0, 0.0, 0.0, 1.0) +/// let red_hex_int = to_rgba_hex(red) +/// } +/// ``` +///
+/// +/// +/// +pub fn to_rgba_hex(colour: Colour) -> Int { + let #(r, g, b, a) = to_rgba(colour) + + let red = + r *. 255.0 + |> float.round() + |> int.bitwise_shift_left(24) + + let green = + g *. 255.0 + |> float.round() + |> int.bitwise_shift_left(16) + + let blue = + b *. 255.0 + |> float.round() + |> int.bitwise_shift_left(8) + + let alpha = + a *. 255.0 + |> float.round() + + red + green + blue + alpha +} + +/// Returns a rgb hex `Int` created from the given `Colour`. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// assert Ok(red) = from_rgba(255, 0, 0) +/// let red_hex_int = to_rgb_hex(red) +/// } +/// ``` +///
+/// +/// +/// +pub fn to_rgb_hex(colour: Colour) -> Int { + let #(r, g, b, _) = to_rgba(colour) + + let red = + r *. 255.0 + |> float.round() + |> int.bitwise_shift_left(16) + + let green = + g *. 255.0 + |> float.round() + |> int.bitwise_shift_left(8) + + let blue = + b *. 255.0 + |> float.round() + + red + green + blue +} + +// JSON ------------------------------------------------------------------------ + +/// Encodes a `Colour` value as a Gleam [`Json`](https://hexdocs.pm/gleam_json/gleam/json.html#Json) +/// value. You'll need this if you want to send a `Colour` value over the network +/// in a HTTP request or response, for example. +/// +/// +/// +pub fn encode(colour: Colour) -> Json { + case colour { + Rgba(r, g, b, a) -> encode_rgba(r, g, b, a) + Hsla(h, s, l, a) -> encode_hsla(h, s, l, a) + } +} + +fn encode_rgba(r: Float, g: Float, b: Float, a: Float) -> Json { + json.object([ + #("r", json.float(r)), + #("g", json.float(g)), + #("b", json.float(b)), + #("a", json.float(a)), + ]) +} + +fn encode_hsla(h: Float, s: Float, l: Float, a: Float) -> Json { + json.object([ + #("h", json.float(h)), + #("s", json.float(s)), + #("l", json.float(l)), + #("a", json.float(a)), + ]) +} + +/// Attempt to decode some [`Dynamic`](https://hexdocs.pm/gleam_stdlib/gleam/dynamic.html#Dynamic) +/// value into a `Colour`. Most often you'll use this to decode some JSON. +/// +/// +/// +pub fn decoder() -> decode.Decoder(Colour) { + decode.one_of(rgba_decoder(), or: [hsla_decoder()]) +} + +fn rgba_decoder() -> decode.Decoder(Colour) { + use r <- decode.field("r", decode.float) + use g <- decode.field("g", decode.float) + use b <- decode.field("b", decode.float) + use a <- decode.field("a", decode.float) + + decode.success(Rgba(r, g, b, a)) +} + +fn hsla_decoder() -> decode.Decoder(Colour) { + use h <- decode.field("h", decode.float) + use s <- decode.field("s", decode.float) + use l <- decode.field("l", decode.float) + use a <- decode.field("a", decode.float) + + decode.success(Hsla(h, s, l, a)) +} + +// COLOURS --------------------------------------------------------------------- + +/// A `Colour` reprsenting the colour RGBA(239, 41, 41, 1.0) +pub const light_red = Rgba( + r: 0.9372549019607843, + g: 0.1607843137254902, + b: 0.1607843137254902, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(204, 0, 0, 1.0) +pub const red = Rgba(r: 0.8, g: 0.0, b: 0.0, a: 1.0) + +/// A `Colour` reprsenting the colour RGBA(164, 0, 0, 1.0) +pub const dark_red = Rgba(r: 0.6431372549019608, g: 0.0, b: 0.0, a: 1.0) + +/// A `Colour` reprsenting the colour RGBA(252, 175, 62, 1.0) +pub const light_orange = Rgba( + r: 0.9882352941176471, + g: 0.6862745098039216, + b: 0.24313725490196078, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(245, 121, 0, 1.0) +pub const orange = Rgba( + r: 0.9607843137254902, + g: 0.4745098039215686, + b: 0.0, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(206, 92, 0, 1.0) +pub const dark_orange = Rgba( + r: 0.807843137254902, + g: 0.3607843137254902, + b: 0.0, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(255, 233, 79, 1.0) +pub const light_yellow = Rgba( + r: 1.0, + g: 0.9137254901960784, + b: 0.30980392156862746, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(237, 212, 0, 1.0) +pub const yellow = Rgba( + r: 0.9294117647058824, + g: 0.8313725490196079, + b: 0.0, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(196, 160, 0, 1.0) +pub const dark_yellow = Rgba( + r: 0.7686274509803922, + g: 0.6274509803921569, + b: 0.0, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(138, 226, 52, 1.0) +pub const light_green = Rgba( + r: 0.5411764705882353, + g: 0.8862745098039215, + b: 0.20392156862745098, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(115, 210, 22, 1.0) +pub const green = Rgba( + r: 0.45098039215686275, + g: 0.8235294117647058, + b: 0.08627450980392157, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(78, 154, 6, 1.0) +pub const dark_green = Rgba( + r: 0.3058823529411765, + g: 0.6039215686274509, + b: 0.023529411764705882, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(114, 159, 207, 1.0) +pub const light_blue = Rgba( + r: 0.4470588235294118, + g: 0.6235294117647059, + b: 0.8117647058823529, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(52, 101, 164, 1.0) +pub const blue = Rgba( + r: 0.20392156862745098, + g: 0.396078431372549, + b: 0.6431372549019608, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(32, 74, 135, 1.0) +pub const dark_blue = Rgba( + r: 0.12549019607843137, + g: 0.2901960784313726, + b: 0.5294117647058824, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(173, 127, 168, 1.0) +pub const light_purple = Rgba( + r: 0.6784313725490196, + g: 0.4980392156862745, + b: 0.6588235294117647, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(117, 80, 123, 1.0) +pub const purple = Rgba( + r: 0.4588235294117647, + g: 0.3137254901960784, + b: 0.4823529411764706, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(92, 53, 102, 1.0) +pub const dark_purple = Rgba( + r: 0.3607843137254902, + g: 0.20784313725490197, + b: 0.4, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(233, 185, 110, 1.0) +pub const light_brown = Rgba( + r: 0.9137254901960784, + g: 0.7254901960784313, + b: 0.43137254901960786, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(193, 125, 17, 1.0) +pub const brown = Rgba( + r: 0.7568627450980392, + g: 0.49019607843137253, + b: 0.06666666666666667, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(143, 89, 2, 1.0) +pub const dark_brown = Rgba( + r: 0.5607843137254902, + g: 0.34901960784313724, + b: 0.00784313725490196, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(0, 0, 0, 1.0) +pub const black = Rgba(r: 0.0, g: 0.0, b: 0.0, a: 1.0) + +/// A `Colour` reprsenting the colour RGBA(255, 255, 255, 1.0) +pub const white = Rgba(r: 1.0, g: 1.0, b: 1.0, a: 1.0) + +/// A `Colour` reprsenting the colour RGBA(238, 238, 236, 1.0) +pub const light_grey = Rgba( + r: 0.9333333333333333, + g: 0.9333333333333333, + b: 0.9254901960784314, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(211, 215, 207, 1.0) +pub const grey = Rgba( + r: 0.8274509803921568, + g: 0.8431372549019608, + b: 0.8117647058823529, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(186, 189, 182, 1.0) +pub const dark_grey = Rgba( + r: 0.7294117647058823, + g: 0.7411764705882353, + b: 0.7137254901960784, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(238, 238, 236, 1.0) +pub const light_gray = Rgba( + r: 0.9333333333333333, + g: 0.9333333333333333, + b: 0.9254901960784314, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(211, 215, 207, 1.0) +pub const gray = Rgba( + r: 0.8274509803921568, + g: 0.8431372549019608, + b: 0.8117647058823529, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(186, 189, 182, 1.0) +pub const dark_gray = Rgba( + r: 0.7294117647058823, + g: 0.7411764705882353, + b: 0.7137254901960784, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(136, 138, 133, 1.0) +pub const light_charcoal = Rgba( + r: 0.5333333333333333, + g: 0.5411764705882353, + b: 0.5215686274509804, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(85, 87, 83, 1.0) +pub const charcoal = Rgba( + r: 0.3333333333333333, + g: 0.3411764705882353, + b: 0.3254901960784314, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(46, 52, 54, 1.0) +pub const dark_charcoal = Rgba( + r: 0.1803921568627451, + g: 0.20392156862745098, + b: 0.21176470588235294, + a: 1.0, +) + +/// A `Colour` reprsenting the colour RGBA(255, 175, 243, 1.0) +pub const pink = Rgba( + r: 1.0, + g: 0.6862745098039216, + b: 0.9529411764705882, + a: 1.0, +) diff --git a/build/packages/gleam_community_colour/src/gleam_community/colour/accessibility.gleam b/build/packages/gleam_community_colour/src/gleam_community/colour/accessibility.gleam new file mode 100644 index 0000000..54f75e4 --- /dev/null +++ b/build/packages/gleam_community_colour/src/gleam_community/colour/accessibility.gleam @@ -0,0 +1,173 @@ +//// +//// - **Accessibility** +//// - [`luminance`](#luminance) +//// - [`contrast_ratio`](#contrast_ratio) +//// - [`maximum_contrast`](#maximum_contrast) +//// +//// --- +//// +//// This package was heavily inspired by the `elm-color-extra` module. +//// The original source code can be found +//// here. +//// +////
+//// The license of that package is produced below: +//// +//// +//// > MIT License +//// +//// > Copyright (c) 2016 Andreas Köberle +//// +//// > 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: +//// +//// > The above copyright notice and this permission notice shall be included in all +//// copies or substantial portions of the Software. +//// +//// > 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. +//// +////
+//// + +// Just in case we decide in the future to no longer include the above reference +// and license, this package was initially a port of the `elm-color-extra` module: +// +// https://github.com/noahzgordon/elm-color-extra +// + +// IMPORTS -------------------------------------------------------------------- + +import gleam/float +import gleam/list +import gleam_community/colour.{type Colour} + +// UTILITIES ------------------------------------------------------------------ + +fn intensity(colour_value: Float) -> Float { + // Calculation taken from https://www.w3.org/TR/WCAG20/#relativeluminancedef + case True { + _ if colour_value <=. 0.03928 -> colour_value /. 12.92 + _ -> { + // Is this guaranteed to be `OK`? + let assert Ok(i) = float.power({ colour_value +. 0.055 } /. 1.055, 2.4) + i + } + } +} + +// ACCESSIBILITY -------------------------------------------------------------- + +/// Returns the relative brightness of the given `Colour` as a `Float` between +/// 0.0, and 1.0 with 0.0 being the darkest possible colour and 1.0 being the lightest. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// luminance(colour.white) // 1.0 +/// } +/// ``` +///
+/// +/// +/// +pub fn luminance(colour: Colour) -> Float { + // Calculation taken from https://www.w3.org/TR/WCAG20/#relativeluminancedef + let #(r, g, b, _) = colour.to_rgba(colour) + + let r_intensity = intensity(r) + let g_intensity = intensity(g) + let b_intensity = intensity(b) + + 0.2126 *. r_intensity +. 0.7152 *. g_intensity +. 0.0722 *. b_intensity +} + +/// Returns the contrast between two `Colour` values as a `Float` between 1.0, +/// and 21.0 with 1.0 being no contrast and, 21.0 being the highest possible contrast. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// contrast_ratio(between: colour.white, and: colour.black) // 21.0 +/// } +/// ``` +///
+/// +/// +/// +pub fn contrast_ratio(between colour_a: Colour, and colour_b: Colour) -> Float { + // Calculation taken from https://www.w3.org/TR/WCAG20/#contrast-ratiodef + let luminance_a = luminance(colour_a) +. 0.05 + let luminance_b = luminance(colour_b) +. 0.05 + + case luminance_a >. luminance_b { + True -> luminance_a /. luminance_b + False -> luminance_b /. luminance_a + } +} + +/// Returns the `Colour` with the highest contrast between the base `Colour`, +/// and and the other provided `Colour` values. +/// +///
+/// Example: +/// +/// ```gleam +/// fn example() { +/// maximum_contrast( +/// colour.yellow, +/// [colour.white, colour.dark_blue, colour.green], +/// ) +/// } +/// ``` +///
+/// +/// +/// +pub fn maximum_contrast( + base: Colour, + colours: List(Colour), +) -> Result(Colour, Nil) { + colours + |> list.sort(fn(colour_a, colour_b) { + let contrast_a = contrast_ratio(base, colour_a) + let contrast_b = contrast_ratio(base, colour_b) + + float.compare(contrast_b, contrast_a) + }) + |> list.first() +} diff --git a/build/packages/gleam_community_colour/src/gleam_community@colour.erl b/build/packages/gleam_community_colour/src/gleam_community@colour.erl new file mode 100644 index 0000000..faee070 --- /dev/null +++ b/build/packages/gleam_community_colour/src/gleam_community@colour.erl @@ -0,0 +1,1199 @@ +-module(gleam_community@colour). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). +-define(FILEPATH, "src/gleam_community/colour.gleam"). +-export([from_rgb255/3, from_rgb/3, from_rgba/4, from_hsla/4, from_hsl/3, from_rgb_hex/1, from_rgb_hex_string/1, from_rgba_hex/1, from_rgba_hex_string/1, to_rgba/1, to_hsla/1, to_css_rgba_string/1, to_rgba_hex/1, to_rgba_hex_string/1, to_rgb_hex/1, to_rgb_hex_string/1, encode/1, decoder/0]). +-export_type([colour/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + "\n" + " - **Types**\n" + " - [`Colour`](#Colour)\n" + " - [`Color`](#Color)\n" + " - **Constructors**\n" + " - [`from_rgb255`](#from_rgb255)\n" + " - [`from_rgb`](#from_rgb)\n" + " - [`from_rgba`](#from_rgba)\n" + " - [`from_hsl`](#from_hsl)\n" + " - [`from_hsla`](#from_hsla)\n" + " - [`from_rgb_hex`](#from_rgb_hex)\n" + " - [`from_rgba_hex`](#from_rgba_hex)\n" + " - [`from_rgb_hex_string`](#from_rgb_hex_string)\n" + " - [`from_rgba_hex_string`](#from_rgba_hex_string)\n" + " - **Conversions**\n" + " - [`to_rgba`](#to_rgba)\n" + " - [`to_hsla`](#hsla)\n" + " - [`to_css_rgba_string`](#to_css_rgba_string)\n" + " - [`to_rgba_hex_string`](#to_rgba_hex_string)\n" + " - [`to_rgb_hex_string`](#to_rgb_hex_string)\n" + " - [`to_rgba_hex`](#to_rgba_hex)\n" + " - [`to_rgb_hex`](#to_rgb_hex)\n" + " - **JSON**\n" + " - [`encode`](#encode)\n" + " - [`decoder`](#decoder)\n" + " - **Colours**\n" + " - [`light_red`](#light_red)\n" + " - [`red`](#red)\n" + " - [`dark_red`](#dark_red)\n" + " - [`light_orange`](#light_orange)\n" + " - [`orange`](#orange)\n" + " - [`dark_orange`](#dark_orange)\n" + " - [`light_yellow`](#light_yellow)\n" + " - [`yellow`](#yellow)\n" + " - [`dark_yellow`](#dark_yellow)\n" + " - [`light_green`](#light_green)\n" + " - [`green`](#green)\n" + " - [`dark_green`](#dark_green)\n" + " - [`light_blue`](#light_blue)\n" + " - [`blue`](#blue)\n" + " - [`dark_blue`](#dark_blue)\n" + " - [`light_purple`](#light_purple)\n" + " - [`purple`](#purple)\n" + " - [`dark_purple`](#dark_purple)\n" + " - [`light_brown`](#light_brown)\n" + " - [`brown`](#brown)\n" + " - [`dark_brown`](#dark_brown)\n" + " - [`black`](#black)\n" + " - [`white`](#white)\n" + " - [`light_grey`](#light_grey)\n" + " - [`grey`](#grey)\n" + " - [`dark_grey`](#dark_grey)\n" + " - [`light_gray`](#light_gray)\n" + " - [`gray`](#gray)\n" + " - [`dark_gray`](#dark_gray)\n" + " - [`light_charcoal`](#light_charcoal)\n" + " - [`charcoal`](#charcoal)\n" + " - [`dark_charcoal`](#dark_charcoal)\n" + " - [`pink`](#pink)\n" + "\n" + " ---\n" + "\n" + " This package was heavily inspired by the `elm-color` module.\n" + " The original source code can be found\n" + " here.\n" + "\n" + "
\n" + " The license of that package is produced below:\n" + "\n" + "\n" + " > MIT License\n" + "\n" + " > Copyright 2018 Aaron VonderHaar\n" + "\n" + " > Redistribution and use in source and binary forms, with or without modification,\n" + " are permitted provided that the following conditions are met:\n" + "\n" + " 1. Redistributions of source code must retain the above copyright notice,\n" + " this list of conditions and the following disclaimer.\n" + "\n" + " 2. Redistributions in binary form must reproduce the above copyright notice,\n" + " this list of conditions and the following disclaimer in the documentation\n" + " and/or other materials provided with the distribution.\n" + "\n" + " 3. Neither the name of the copyright holder nor the names of its contributors\n" + " may be used to endorse or promote products derived from this software without\n" + " specific prior written permission.\n" + "\n" + " > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n" + " ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n" + " OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL\n" + " THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n" + " EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\n" + " OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n" + " THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n" + " ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + "\n" + " > The above copyright notice and this permission notice shall be included in all\n" + " copies or substantial portions of the Software.\n" + "
\n" + "\n" +). + +-opaque colour() :: {rgba, float(), float(), float(), float()} | + {hsla, float(), float(), float(), float()}. + +-file("src/gleam_community/colour.gleam", 155). +-spec valid_colour_value(float()) -> {ok, float()} | {error, nil}. +valid_colour_value(C) -> + case (C > 1.0) orelse (C < +0.0) of + true -> + {error, nil}; + + false -> + {ok, C} + end. + +-file("src/gleam_community/colour.gleam", 162). +-spec hue_to_rgb(float(), float(), float()) -> float(). +hue_to_rgb(Hue, M1, M2) -> + H = case Hue of + _ when Hue < +0.0 -> + Hue + 1.0; + + _ when Hue > 1.0 -> + Hue - 1.0; + + _ -> + Hue + end, + H_t_6 = H * 6.0, + H_t_2 = H * 2.0, + H_t_3 = H * 3.0, + case H of + _ when H_t_6 < 1.0 -> + M1 + (((M2 - M1) * H) * 6.0); + + _ when H_t_2 < 1.0 -> + M2; + + _ when H_t_3 < 2.0 -> + M1 + (((M2 - M1) * ((2.0 / 3.0) - H)) * 6.0); + + _ -> + M1 + end. + +-file("src/gleam_community/colour.gleam", 181). +-spec hex_string_to_int(binary()) -> {ok, integer()} | {error, nil}. +hex_string_to_int(Hex_string) -> + Hex = case Hex_string of + <<"#"/utf8, Hex_number/binary>> -> + Hex_number; + + <<"0x"/utf8, Hex_number@1/binary>> -> + Hex_number@1; + + _ -> + Hex_string + end, + _pipe = Hex, + _pipe@1 = string:lowercase(_pipe), + _pipe@2 = gleam@string:to_graphemes(_pipe@1), + _pipe@3 = lists:reverse(_pipe@2), + gleam@list:index_fold( + _pipe@3, + {ok, 0}, + fun(Total, Char, Index) -> case Total of + {error, nil} -> + {error, nil}; + + {ok, V} -> + gleam@result:'try'(case Char of + <<"a"/utf8>> -> + {ok, 10}; + + <<"b"/utf8>> -> + {ok, 11}; + + <<"c"/utf8>> -> + {ok, 12}; + + <<"d"/utf8>> -> + {ok, 13}; + + <<"e"/utf8>> -> + {ok, 14}; + + <<"f"/utf8>> -> + {ok, 15}; + + _ -> + gleam_stdlib:parse_int(Char) + end, fun(Num) -> + gleam@result:'try'( + gleam@int:power(16, erlang:float(Index)), + fun(Base) -> + {ok, + V + erlang:round( + erlang:float(Num) * Base + )} + end + ) + end) + end end + ). + +-file("src/gleam_community/colour.gleam", 212). +-spec hsla_to_rgba(float(), float(), float(), float()) -> {float(), + float(), + float(), + float()}. +hsla_to_rgba(H, S, L, A) -> + M2 = case L =< 0.5 of + true -> + L * (S + 1.0); + + false -> + (L + S) - (L * S) + end, + M1 = (L * 2.0) - M2, + R = hue_to_rgb(H + (1.0 / 3.0), M1, M2), + G = hue_to_rgb(H, M1, M2), + B = hue_to_rgb(H - (1.0 / 3.0), M1, M2), + {R, G, B, A}. + +-file("src/gleam_community/colour.gleam", 232). +-spec rgba_to_hsla(float(), float(), float(), float()) -> {float(), + float(), + float(), + float()}. +rgba_to_hsla(R, G, B, A) -> + Min_colour = gleam@float:min(R, gleam@float:min(G, B)), + Max_colour = gleam@float:max(R, gleam@float:max(G, B)), + H1 = case true of + _ when Max_colour =:= R -> + gleam@float:divide(G - B, Max_colour - Min_colour); + + _ when Max_colour =:= G -> + _pipe = gleam@float:divide(B - R, Max_colour - Min_colour), + gleam@result:'try'(_pipe, fun(D) -> {ok, 2.0 + D} end); + + _ -> + _pipe@1 = gleam@float:divide(R - G, Max_colour - Min_colour), + gleam@result:'try'(_pipe@1, fun(D@1) -> {ok, 4.0 + D@1} end) + end, + H2 = case H1 of + {ok, V} -> + {ok, V * (1.0 / 6.0)}; + + _ -> + H1 + end, + H3 = case H2 of + {ok, V@1} when V@1 < +0.0 -> + V@1 + 1.0; + + {ok, V@2} -> + V@2; + + _ -> + +0.0 + end, + L = (Min_colour + Max_colour) / 2.0, + S = case true of + _ when Min_colour =:= Max_colour -> + +0.0; + + _ when L < 0.5 -> + case (Max_colour + Min_colour) of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> (Max_colour - Min_colour) / Gleam@denominator + end; + + _ -> + case ((2.0 - Max_colour) - Min_colour) of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator@1 -> (Max_colour - Min_colour) / Gleam@denominator@1 + end + end, + {H3, S, L, A}. + +-file("src/gleam_community/colour.gleam", 300). +?DOC( + " Returns a `Result(Colour)` created from the given 8 bit RGB values.\n" + "\n" + " Returns `Error(Nil)` if the supplied RGB values are greater than 255 or less than 0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb255(255, 0, 0)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec from_rgb255(integer(), integer(), integer()) -> {ok, colour()} | + {error, nil}. +from_rgb255(Red, Green, Blue) -> + gleam@result:'try'( + begin + _pipe = Red, + _pipe@1 = erlang:float(_pipe), + _pipe@2 = gleam@float:divide(_pipe@1, 255.0), + gleam@result:'try'(_pipe@2, fun valid_colour_value/1) + end, + fun(R) -> + gleam@result:'try'( + begin + _pipe@3 = Green, + _pipe@4 = erlang:float(_pipe@3), + _pipe@5 = gleam@float:divide(_pipe@4, 255.0), + gleam@result:'try'(_pipe@5, fun valid_colour_value/1) + end, + fun(G) -> + gleam@result:'try'( + begin + _pipe@6 = Blue, + _pipe@7 = erlang:float(_pipe@6), + _pipe@8 = gleam@float:divide(_pipe@7, 255.0), + gleam@result:'try'( + _pipe@8, + fun valid_colour_value/1 + ) + end, + fun(B) -> {ok, {rgba, R, G, B, 1.0}} end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 348). +?DOC( + " Returns `Result(Colour)` created from the given RGB values.\n" + "\n" + " If the supplied RGB values are greater than 1.0 or less than 0.0 returns `Error(Nil)`\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb(1.0, 0.0, 0.0)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec from_rgb(float(), float(), float()) -> {ok, colour()} | {error, nil}. +from_rgb(Red, Green, Blue) -> + gleam@result:'try'( + valid_colour_value(Red), + fun(R) -> + gleam@result:'try'( + valid_colour_value(Green), + fun(G) -> + gleam@result:'try'( + valid_colour_value(Blue), + fun(B) -> {ok, {rgba, R, G, B, 1.0}} end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 383). +?DOC( + " Returns `Result(Colour)` created from the given RGBA values.\n" + "\n" + " Returns `Error(Nil)` if the supplied RGBA values are greater than 1.0 or less than 0.0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red_half_opacity) = from_rbga(1.0, 0.0, 0.0, 0.5)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec from_rgba(float(), float(), float(), float()) -> {ok, colour()} | + {error, nil}. +from_rgba(Red, Green, Blue, Alpha) -> + gleam@result:'try'( + valid_colour_value(Red), + fun(R) -> + gleam@result:'try'( + valid_colour_value(Green), + fun(G) -> + gleam@result:'try'( + valid_colour_value(Blue), + fun(B) -> + gleam@result:'try'( + valid_colour_value(Alpha), + fun(A) -> {ok, {rgba, R, G, B, A}} end + ) + end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 420). +?DOC( + " Returns `Result(Colour)` created from the given HSLA values.\n" + "\n" + " Returns `Error(Nil)`f the supplied HSLA values are greater than 1.0 or less than 0.0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red_half_opacity) = from_hsla(0.0, 1.0, 0.5, 0.5)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec from_hsla(float(), float(), float(), float()) -> {ok, colour()} | + {error, nil}. +from_hsla(Hue, Saturation, Lightness, Alpha) -> + gleam@result:'try'( + valid_colour_value(Hue), + fun(H) -> + gleam@result:'try'( + valid_colour_value(Saturation), + fun(S) -> + gleam@result:'try'( + valid_colour_value(Lightness), + fun(L) -> + gleam@result:'try'( + valid_colour_value(Alpha), + fun(A) -> {ok, {hsla, H, S, L, A}} end + ) + end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 457). +?DOC( + " Returns `Result(Colour)` created from the given HSL values.\n" + "\n" + " Returns `Error(Nil)` if the supplied HSL values are greater than 1.0 or less than 0.0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_hsla(0.0, 1.0, 0.5)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec from_hsl(float(), float(), float()) -> {ok, colour()} | {error, nil}. +from_hsl(Hue, Saturation, Lightness) -> + from_hsla(Hue, Saturation, Lightness, 1.0). + +-file("src/gleam_community/colour.gleam", 488). +?DOC( + " Returns a `Result(Colour)` created from the given hex `Int`.\n" + "\n" + " Returns `Error(Nil)` if the supplied hex `Int is greater than 0xffffff or less than 0x0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb_hex(0xff0000)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec from_rgb_hex(integer()) -> {ok, colour()} | {error, nil}. +from_rgb_hex(Hex) -> + case (Hex > 16#ffffff) orelse (Hex < 0) of + true -> + {error, nil}; + + false -> + R = begin + _pipe = erlang:'bsr'(Hex, 16), + erlang:'band'(_pipe, 16#ff) + end, + G = begin + _pipe@1 = erlang:'bsr'(Hex, 8), + erlang:'band'(_pipe@1, 16#ff) + end, + B = erlang:'band'(Hex, 16#ff), + from_rgb255(R, G, B) + end. + +-file("src/gleam_community/colour.gleam", 527). +?DOC( + " Returns a `Result(Colour)` created from the given RGB hex `String`.\n" + "\n" + " Returns `Error(Nil)` if the supplied hex `String` is invalid, or greater than `\"#ffffff\" or less than `\"#0\"`\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb_hex_string(\"#ff0000\")\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec from_rgb_hex_string(binary()) -> {ok, colour()} | {error, nil}. +from_rgb_hex_string(Hex_string) -> + gleam@result:'try'( + hex_string_to_int(Hex_string), + fun(Hex_int) -> from_rgb_hex(Hex_int) end + ). + +-file("src/gleam_community/colour.gleam", 585). +?DOC( + " Returns a `Result(Colour)` created from the given hex `Int`.\n" + "\n" + " Returns `Error(Nil)` if the supplied hex `Int is greater than 0xffffffff or less than 0x0.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red_half_opacity) = from_rgba_hex(0xff00007f)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec from_rgba_hex(integer()) -> {ok, colour()} | {error, nil}. +from_rgba_hex(Hex) -> + case (Hex > 16#ffffffff) orelse (Hex < 0) of + true -> + {error, nil}; + + false -> + R@1 = case begin + _pipe = erlang:'bsr'(Hex, 24), + _pipe@1 = erlang:'band'(_pipe, 16#ff), + _pipe@2 = erlang:float(_pipe@1), + gleam@float:divide(_pipe@2, 255.0) + end of + {ok, R} -> R; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"from_rgba_hex"/utf8>>, + line => 590, + value => _assert_fail, + start => 17111, + 'end' => 17260, + pattern_start => 17122, + pattern_end => 17127}) + end, + G@1 = case begin + _pipe@3 = erlang:'bsr'(Hex, 16), + _pipe@4 = erlang:'band'(_pipe@3, 16#ff), + _pipe@5 = erlang:float(_pipe@4), + gleam@float:divide(_pipe@5, 255.0) + end of + {ok, G} -> G; + _assert_fail@1 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"from_rgba_hex"/utf8>>, + line => 596, + value => _assert_fail@1, + start => 17332, + 'end' => 17481, + pattern_start => 17343, + pattern_end => 17348}) + end, + B@1 = case begin + _pipe@6 = erlang:'bsr'(Hex, 8), + _pipe@7 = erlang:'band'(_pipe@6, 16#ff), + _pipe@8 = erlang:float(_pipe@7), + gleam@float:divide(_pipe@8, 255.0) + end of + {ok, B} -> B; + _assert_fail@2 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"from_rgba_hex"/utf8>>, + line => 602, + value => _assert_fail@2, + start => 17553, + 'end' => 17701, + pattern_start => 17564, + pattern_end => 17569}) + end, + A@1 = case begin + _pipe@9 = erlang:'band'(Hex, 16#ff), + _pipe@10 = erlang:float(_pipe@9), + gleam@float:divide(_pipe@10, 255.0) + end of + {ok, A} -> A; + _assert_fail@3 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"from_rgba_hex"/utf8>>, + line => 608, + value => _assert_fail@3, + start => 17773, + 'end' => 17883, + pattern_start => 17784, + pattern_end => 17789}) + end, + from_rgba(R@1, G@1, B@1, A@1) + end. + +-file("src/gleam_community/colour.gleam", 556). +?DOC( + " Returns a `Result(Colour)` created from the given RGBA hex `String`.\n" + "\n" + " Returns `Error(Nil)` if the supplied hex `String` is invalid, or greater than `\"#ffffffff\" or less than `\"#0\"`\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red_half_opacity) = from_rgba_hex_string(\"#ff00007f\")\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec from_rgba_hex_string(binary()) -> {ok, colour()} | {error, nil}. +from_rgba_hex_string(Hex_string) -> + gleam@result:'try'( + hex_string_to_int(Hex_string), + fun(Hex_int) -> from_rgba_hex(Hex_int) end + ). + +-file("src/gleam_community/colour.gleam", 642). +?DOC( + " Returns `#(Float, Float, Float, Float)` representing the given `Colour`'s\n" + " R, G, B, and A values respectively.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb255(255, 0, 0)\n" + " let #(r, g, b, a) = to_rgba(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec to_rgba(colour()) -> {float(), float(), float(), float()}. +to_rgba(Colour) -> + case Colour of + {rgba, R, G, B, A} -> + {R, G, B, A}; + + {hsla, H, S, L, A@1} -> + hsla_to_rgba(H, S, L, A@1) + end. + +-file("src/gleam_community/colour.gleam", 672). +?DOC( + " Returns `#(Float, Float, Float, Float)` representing the given `Colour`'s\n" + " H, S, L, and A values respectively.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb255(255, 0, 0)\n" + " let #(h, s, l, a) = to_hsla(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec to_hsla(colour()) -> {float(), float(), float(), float()}. +to_hsla(Colour) -> + case Colour of + {hsla, H, S, L, A} -> + {H, S, L, A}; + + {rgba, R, G, B, A@1} -> + rgba_to_hsla(R, G, B, A@1) + end. + +-file("src/gleam_community/colour.gleam", 701). +?DOC( + " Returns an rgba formatted CSS `String` created from the given `Colour`.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgb255(255, 0, 0)\n" + " let css_red = to_css_rgba_string(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec to_css_rgba_string(colour()) -> binary(). +to_css_rgba_string(Colour) -> + {R, G, B, A} = to_rgba(Colour), + Percent = fun(X) -> + P@1 = case begin + _pipe = X, + _pipe@1 = gleam@float:multiply(_pipe, 10000.0), + _pipe@2 = erlang:round(_pipe@1), + _pipe@3 = erlang:float(_pipe@2), + gleam@float:divide(_pipe@3, 100.0) + end of + {ok, P} -> P; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"to_css_rgba_string"/utf8>>, + line => 706, + value => _assert_fail, + start => 20510, + 'end' => 20646, + pattern_start => 20521, + pattern_end => 20526}) + end, + P@1 + end, + Round_to = fun(X@1) -> + R@2 = case begin + _pipe@4 = X@1, + _pipe@5 = gleam@float:multiply(_pipe@4, 1000.0), + _pipe@6 = erlang:round(_pipe@5), + _pipe@7 = erlang:float(_pipe@6), + gleam@float:divide(_pipe@7, 1000.0) + end of + {ok, R@1} -> R@1; + _assert_fail@1 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour"/utf8>>, + function => <<"to_css_rgba_string"/utf8>>, + line => 718, + value => _assert_fail@1, + start => 20768, + 'end' => 20903, + pattern_start => 20779, + pattern_end => 20784}) + end, + R@2 + end, + gleam@string:join( + [<<"rgba("/utf8>>, + <<(gleam_stdlib:float_to_string(Percent(R)))/binary, "%,"/utf8>>, + <<(gleam_stdlib:float_to_string(Percent(G)))/binary, "%,"/utf8>>, + <<(gleam_stdlib:float_to_string(Percent(B)))/binary, "%,"/utf8>>, + gleam_stdlib:float_to_string(Round_to(A)), + <<")"/utf8>>], + <<""/utf8>> + ). + +-file("src/gleam_community/colour.gleam", 829). +?DOC( + " Returns an hex `Int` created from the given `Colour`.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgba(1.0, 0.0, 0.0, 1.0)\n" + " let red_hex_int = to_rgba_hex(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec to_rgba_hex(colour()) -> integer(). +to_rgba_hex(Colour) -> + {R, G, B, A} = to_rgba(Colour), + Red = begin + _pipe = R * 255.0, + _pipe@1 = erlang:round(_pipe), + erlang:'bsl'(_pipe@1, 24) + end, + Green = begin + _pipe@2 = G * 255.0, + _pipe@3 = erlang:round(_pipe@2), + erlang:'bsl'(_pipe@3, 16) + end, + Blue = begin + _pipe@4 = B * 255.0, + _pipe@5 = erlang:round(_pipe@4), + erlang:'bsl'(_pipe@5, 8) + end, + Alpha = begin + _pipe@6 = A * 255.0, + erlang:round(_pipe@6) + end, + ((Red + Green) + Blue) + Alpha. + +-file("src/gleam_community/colour.gleam", 763). +?DOC( + " Returns an rgba hex formatted `String` created from the given `Colour`.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgba(1.0, 0.0, 0.0, 1.0)\n" + " let red_hex = to_rgba_hex_string(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec to_rgba_hex_string(colour()) -> binary(). +to_rgba_hex_string(Colour) -> + Hex_string = begin + _pipe = to_rgba_hex(Colour), + gleam@int:to_base16(_pipe) + end, + case string:length(Hex_string) of + 8 -> + Hex_string; + + L -> + <<(gleam@string:repeat(<<"0"/utf8>>, 8 - L))/binary, + Hex_string/binary>> + end. + +-file("src/gleam_community/colour.gleam", 876). +?DOC( + " Returns a rgb hex `Int` created from the given `Colour`.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgba(255, 0, 0)\n" + " let red_hex_int = to_rgb_hex(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec to_rgb_hex(colour()) -> integer(). +to_rgb_hex(Colour) -> + {R, G, B, _} = to_rgba(Colour), + Red = begin + _pipe = R * 255.0, + _pipe@1 = erlang:round(_pipe), + erlang:'bsl'(_pipe@1, 16) + end, + Green = begin + _pipe@2 = G * 255.0, + _pipe@3 = erlang:round(_pipe@2), + erlang:'bsl'(_pipe@3, 8) + end, + Blue = begin + _pipe@4 = B * 255.0, + erlang:round(_pipe@4) + end, + (Red + Green) + Blue. + +-file("src/gleam_community/colour.gleam", 796). +?DOC( + " Returns an rgb hex formatted `String` created from the given `Colour`.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " assert Ok(red) = from_rgba(255, 0, 0)\n" + " let red_hex = to_rgb_hex_string(red)\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec to_rgb_hex_string(colour()) -> binary(). +to_rgb_hex_string(Colour) -> + Hex_string = begin + _pipe = to_rgb_hex(Colour), + gleam@int:to_base16(_pipe) + end, + case string:length(Hex_string) of + 6 -> + Hex_string; + + L -> + <<(gleam@string:repeat(<<"0"/utf8>>, 6 - L))/binary, + Hex_string/binary>> + end. + +-file("src/gleam_community/colour.gleam", 918). +-spec encode_rgba(float(), float(), float(), float()) -> gleam@json:json(). +encode_rgba(R, G, B, A) -> + gleam@json:object( + [{<<"r"/utf8>>, gleam@json:float(R)}, + {<<"g"/utf8>>, gleam@json:float(G)}, + {<<"b"/utf8>>, gleam@json:float(B)}, + {<<"a"/utf8>>, gleam@json:float(A)}] + ). + +-file("src/gleam_community/colour.gleam", 927). +-spec encode_hsla(float(), float(), float(), float()) -> gleam@json:json(). +encode_hsla(H, S, L, A) -> + gleam@json:object( + [{<<"h"/utf8>>, gleam@json:float(H)}, + {<<"s"/utf8>>, gleam@json:float(S)}, + {<<"l"/utf8>>, gleam@json:float(L)}, + {<<"a"/utf8>>, gleam@json:float(A)}] + ). + +-file("src/gleam_community/colour.gleam", 911). +?DOC( + " Encodes a `Colour` value as a Gleam [`Json`](https://hexdocs.pm/gleam_json/gleam/json.html#Json)\n" + " value. You'll need this if you want to send a `Colour` value over the network\n" + " in a HTTP request or response, for example.\n" + "\n" + " \n" +). +-spec encode(colour()) -> gleam@json:json(). +encode(Colour) -> + case Colour of + {rgba, R, G, B, A} -> + encode_rgba(R, G, B, A); + + {hsla, H, S, L, A@1} -> + encode_hsla(H, S, L, A@1) + end. + +-file("src/gleam_community/colour.gleam", 952). +-spec rgba_decoder() -> gleam@dynamic@decode:decoder(colour()). +rgba_decoder() -> + gleam@dynamic@decode:field( + <<"r"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(R) -> + gleam@dynamic@decode:field( + <<"g"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(G) -> + gleam@dynamic@decode:field( + <<"b"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(B) -> + gleam@dynamic@decode:field( + <<"a"/utf8>>, + {decoder, + fun gleam@dynamic@decode:decode_float/1}, + fun(A) -> + gleam@dynamic@decode:success( + {rgba, R, G, B, A} + ) + end + ) + end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 961). +-spec hsla_decoder() -> gleam@dynamic@decode:decoder(colour()). +hsla_decoder() -> + gleam@dynamic@decode:field( + <<"h"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(H) -> + gleam@dynamic@decode:field( + <<"s"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(S) -> + gleam@dynamic@decode:field( + <<"l"/utf8>>, + {decoder, fun gleam@dynamic@decode:decode_float/1}, + fun(L) -> + gleam@dynamic@decode:field( + <<"a"/utf8>>, + {decoder, + fun gleam@dynamic@decode:decode_float/1}, + fun(A) -> + gleam@dynamic@decode:success( + {hsla, H, S, L, A} + ) + end + ) + end + ) + end + ) + end + ). + +-file("src/gleam_community/colour.gleam", 948). +?DOC( + " Attempt to decode some [`Dynamic`](https://hexdocs.pm/gleam_stdlib/gleam/dynamic.html#Dynamic)\n" + " value into a `Colour`. Most often you'll use this to decode some JSON.\n" + "\n" + " \n" +). +-spec decoder() -> gleam@dynamic@decode:decoder(colour()). +decoder() -> + gleam@dynamic@decode:one_of(rgba_decoder(), [hsla_decoder()]). diff --git a/build/packages/gleam_community_colour/src/gleam_community@colour@accessibility.erl b/build/packages/gleam_community_colour/src/gleam_community@colour@accessibility.erl new file mode 100644 index 0000000..35c1d67 --- /dev/null +++ b/build/packages/gleam_community_colour/src/gleam_community@colour@accessibility.erl @@ -0,0 +1,203 @@ +-module(gleam_community@colour@accessibility). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). +-define(FILEPATH, "src/gleam_community/colour/accessibility.gleam"). +-export([luminance/1, contrast_ratio/2, maximum_contrast/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " \n" + " - **Accessibility**\n" + " - [`luminance`](#luminance)\n" + " - [`contrast_ratio`](#contrast_ratio)\n" + " - [`maximum_contrast`](#maximum_contrast)\n" + "\n" + " ---\n" + "\n" + " This package was heavily inspired by the `elm-color-extra` module.\n" + " The original source code can be found\n" + " here.\n" + "\n" + "
\n" + " The license of that package is produced below:\n" + " \n" + " \n" + " > MIT License\n" + "\n" + " > Copyright (c) 2016 Andreas Köberle\n" + "\n" + " > Permission is hereby granted, free of charge, to any person obtaining a copy\n" + " of this software and associated documentation files (the \"Software\"), to deal\n" + " in the Software without restriction, including without limitation the rights\n" + " to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n" + " copies of the Software, and to permit persons to whom the Software is\n" + " furnished to do so, subject to the following conditions:\n" + "\n" + " > The above copyright notice and this permission notice shall be included in all\n" + " copies or substantial portions of the Software.\n" + "\n" + " > THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n" + " IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n" + " FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n" + " AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n" + " LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n" + " OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n" + " SOFTWARE.\n" + "\n" + "
\n" + "\n" +). + +-file("src/gleam_community/colour/accessibility.gleam", 56). +-spec intensity(float()) -> float(). +intensity(Colour_value) -> + case true of + _ when Colour_value =< 0.03928 -> + Colour_value / 12.92; + + _ -> + I@1 = case gleam@float:power((Colour_value + 0.055) / 1.055, 2.4) of + {ok, I} -> I; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam_community/colour/accessibility"/utf8>>, + function => <<"intensity"/utf8>>, + line => 62, + value => _assert_fail, + start => 2399, + 'end' => 2470, + pattern_start => 2410, + pattern_end => 2415}) + end, + I@1 + end. + +-file("src/gleam_community/colour/accessibility.gleam", 92). +?DOC( + " Returns the relative brightness of the given `Colour` as a `Float` between\n" + " 0.0, and 1.0 with 0.0 being the darkest possible colour and 1.0 being the lightest.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " luminance(colour.white) // 1.0\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec luminance(gleam_community@colour:colour()) -> float(). +luminance(Colour) -> + {R, G, B, _} = gleam_community@colour:to_rgba(Colour), + R_intensity = intensity(R), + G_intensity = intensity(G), + B_intensity = intensity(B), + ((0.2126 * R_intensity) + (0.7152 * G_intensity)) + (0.0722 * B_intensity). + +-file("src/gleam_community/colour/accessibility.gleam", 125). +?DOC( + " Returns the contrast between two `Colour` values as a `Float` between 1.0,\n" + " and 21.0 with 1.0 being no contrast and, 21.0 being the highest possible contrast.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " contrast_ratio(between: colour.white, and: colour.black) // 21.0\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec contrast_ratio( + gleam_community@colour:colour(), + gleam_community@colour:colour() +) -> float(). +contrast_ratio(Colour_a, Colour_b) -> + Luminance_a = luminance(Colour_a) + 0.05, + Luminance_b = luminance(Colour_b) + 0.05, + case Luminance_a > Luminance_b of + true -> + case Luminance_b of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> Luminance_a / Gleam@denominator + end; + + false -> + case Luminance_a of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator@1 -> Luminance_b / Gleam@denominator@1 + end + end. + +-file("src/gleam_community/colour/accessibility.gleam", 161). +?DOC( + " Returns the `Colour` with the highest contrast between the base `Colour`,\n" + " and and the other provided `Colour` values.\n" + "\n" + "
\n" + " Example:\n" + "\n" + " ```gleam\n" + " fn example() {\n" + " maximum_contrast(\n" + " colour.yellow,\n" + " [colour.white, colour.dark_blue, colour.green],\n" + " )\n" + " }\n" + " ```\n" + "
\n" + "\n" + " \n" +). +-spec maximum_contrast( + gleam_community@colour:colour(), + list(gleam_community@colour:colour()) +) -> {ok, gleam_community@colour:colour()} | {error, nil}. +maximum_contrast(Base, Colours) -> + _pipe = Colours, + _pipe@1 = gleam@list:sort( + _pipe, + fun(Colour_a, Colour_b) -> + Contrast_a = contrast_ratio(Base, Colour_a), + Contrast_b = contrast_ratio(Base, Colour_b), + gleam@float:compare(Contrast_b, Contrast_a) + end + ), + gleam@list:first(_pipe@1). diff --git a/build/packages/gleam_community_colour/src/gleam_community_colour.app.src b/build/packages/gleam_community_colour/src/gleam_community_colour.app.src new file mode 100644 index 0000000..0d22534 --- /dev/null +++ b/build/packages/gleam_community_colour/src/gleam_community_colour.app.src @@ -0,0 +1,10 @@ +{application, gleam_community_colour, [ + {vsn, "2.0.2"}, + {applications, [gleam_json, + gleam_stdlib]}, + {description, "Colour types, conversions, and other utilities"}, + {modules, [gleam_community@colour, + gleam_community@colour@accessibility, + gleam_community_colour@@main]}, + {registered, []} +]}. diff --git a/build/packages/gleam_json/LICENCE b/build/packages/gleam_json/LICENCE new file mode 100644 index 0000000..3d89615 --- /dev/null +++ b/build/packages/gleam_json/LICENCE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021 - present, Louis Pilfold . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/build/packages/gleam_json/README.md b/build/packages/gleam_json/README.md new file mode 100644 index 0000000..1a06b52 --- /dev/null +++ b/build/packages/gleam_json/README.md @@ -0,0 +1,47 @@ +# json 🐑 + +Work with JSON in Gleam! + +## Installation + +```shell +gleam add gleam_json@3 +``` + +## Encoding + +```gleam +import myapp.{type Cat} +import gleam/json + +pub fn cat_to_json(cat: Cat) -> String { + json.object([ + #("name", json.string(cat.name)), + #("lives", json.int(cat.lives)), + #("flaws", json.null()), + #("nicknames", json.array(cat.nicknames, of: json.string)), + ]) + |> json.to_string +} +``` + +## Parsing + +JSON is parsed into a `Dynamic` value which can be decoded using the +`gleam/dynamic/decode` module from the Gleam standard library. + +```gleam +import myapp.{Cat} +import gleam/json +import gleam/dynamic/decode + +pub fn cat_from_json(json_string: String) -> Result(Cat, json.DecodeError) { + let cat_decoder = { + use name <- decode.field("name", decode.string) + use lives <- decode.field("lives", decode.int) + use nicknames <- decode.field("nicknames", decode.list(decode.string)) + decode.success(Cat(name:, lives:, nicknames:)) + } + json.parse(from: json_string, using: cat_decoder) +} +``` diff --git a/build/packages/gleam_json/gleam.toml b/build/packages/gleam_json/gleam.toml new file mode 100644 index 0000000..f7e7f8d --- /dev/null +++ b/build/packages/gleam_json/gleam.toml @@ -0,0 +1,18 @@ +name = "gleam_json" +version = "3.1.0" +gleam = ">= 1.13.0" + +licences = ["Apache-2.0"] +description = "Work with JSON in Gleam" + +repository = { type = "github", user = "gleam-lang", repo = "json" } +links = [ + { title = "Website", href = "https://gleam.run" }, + { title = "Sponsor", href = "https://github.com/sponsors/lpil" }, +] + +[dependencies] +gleam_stdlib = ">= 0.51.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.2.0 and < 2.0.0" diff --git a/build/packages/gleam_json/src/gleam/json.gleam b/build/packages/gleam_json/src/gleam/json.gleam new file mode 100644 index 0000000..e3a3cae --- /dev/null +++ b/build/packages/gleam_json/src/gleam/json.gleam @@ -0,0 +1,316 @@ +import gleam/bit_array +import gleam/dict.{type Dict} +import gleam/dynamic.{type Dynamic} +import gleam/dynamic/decode +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/result +import gleam/string_tree.{type StringTree} + +pub type Json + +pub type DecodeError { + UnexpectedEndOfInput + UnexpectedByte(String) + UnexpectedSequence(String) + UnableToDecode(List(decode.DecodeError)) +} + +/// Decode a JSON string into dynamically typed data which can be decoded into +/// typed data with the `gleam/dynamic` module. +/// +/// ## Examples +/// +/// ```gleam +/// > parse("[1,2,3]", decode.list(of: decode.int)) +/// Ok([1, 2, 3]) +/// ``` +/// +/// ```gleam +/// > parse("[", decode.list(of: decode.int)) +/// Error(UnexpectedEndOfInput) +/// ``` +/// +/// ```gleam +/// > parse("1", decode.string) +/// Error(UnableToDecode([decode.DecodeError("String", "Int", [])])) +/// ``` +/// +pub fn parse( + from json: String, + using decoder: decode.Decoder(t), +) -> Result(t, DecodeError) { + do_parse(from: json, using: decoder) +} + +@target(erlang) +fn do_parse( + from json: String, + using decoder: decode.Decoder(t), +) -> Result(t, DecodeError) { + let bits = bit_array.from_string(json) + parse_bits(bits, decoder) +} + +@target(javascript) +fn do_parse( + from json: String, + using decoder: decode.Decoder(t), +) -> Result(t, DecodeError) { + use dynamic_value <- result.try(decode_string(json)) + decode.run(dynamic_value, decoder) + |> result.map_error(UnableToDecode) +} + +@external(javascript, "../gleam_json_ffi.mjs", "decode") +fn decode_string(a: String) -> Result(Dynamic, DecodeError) + +/// Decode a JSON bit string into dynamically typed data which can be decoded +/// into typed data with the `gleam/dynamic` module. +/// +/// ## Examples +/// +/// ```gleam +/// > parse_bits(<<"[1,2,3]">>, decode.list(of: decode.int)) +/// Ok([1, 2, 3]) +/// ``` +/// +/// ```gleam +/// > parse_bits(<<"[">>, decode.list(of: decode.int)) +/// Error(UnexpectedEndOfInput) +/// ``` +/// +/// ```gleam +/// > parse_bits(<<"1">>, decode.string) +/// Error(UnableToDecode([decode.DecodeError("String", "Int", [])])), +/// ``` +/// +pub fn parse_bits( + from json: BitArray, + using decoder: decode.Decoder(t), +) -> Result(t, DecodeError) { + use dynamic_value <- result.try(decode_to_dynamic(json)) + decode.run(dynamic_value, decoder) + |> result.map_error(UnableToDecode) +} + +@external(erlang, "gleam_json_ffi", "decode") +fn decode_to_dynamic(json: BitArray) -> Result(Dynamic, DecodeError) { + case bit_array.to_string(json) { + Ok(string) -> decode_string(string) + Error(Nil) -> Error(UnexpectedByte("")) + } +} + +/// Convert a JSON value into a string. +/// +/// Where possible prefer the `to_string_tree` function as it is faster than +/// this function, and BEAM VM IO is optimised for sending `StringTree` data. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string(array([1, 2, 3], of: int)) +/// "[1,2,3]" +/// ``` +/// +pub fn to_string(json: Json) -> String { + do_to_string(json) +} + +@external(erlang, "gleam_json_ffi", "json_to_string") +@external(javascript, "../gleam_json_ffi.mjs", "json_to_string") +fn do_to_string(a: Json) -> String + +/// Convert a JSON value into a string tree. +/// +/// Where possible prefer this function to the `to_string` function as it is +/// slower than this function, and BEAM VM IO is optimised for sending +/// `StringTree` data. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string_tree(array([1, 2, 3], of: int)) +/// string_tree.from_string("[1,2,3]") +/// ``` +/// +@external(erlang, "gleam_json_ffi", "json_to_iodata") +@external(javascript, "../gleam_json_ffi.mjs", "json_to_string") +pub fn to_string_tree(json: Json) -> StringTree + +/// Encode a string into JSON, using normal JSON escaping. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string(string("Hello!")) +/// "\"Hello!\"" +/// ``` +/// +pub fn string(input: String) -> Json { + do_string(input) +} + +@external(erlang, "gleam_json_ffi", "string") +@external(javascript, "../gleam_json_ffi.mjs", "identity") +fn do_string(a: String) -> Json + +/// Encode a bool into JSON. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string(bool(False)) +/// "false" +/// ``` +/// +pub fn bool(input: Bool) -> Json { + do_bool(input) +} + +@external(erlang, "gleam_json_ffi", "bool") +@external(javascript, "../gleam_json_ffi.mjs", "identity") +fn do_bool(a: Bool) -> Json + +/// Encode an int into JSON. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string(int(50)) +/// "50" +/// ``` +/// +pub fn int(input: Int) -> Json { + do_int(input) +} + +@external(erlang, "gleam_json_ffi", "int") +@external(javascript, "../gleam_json_ffi.mjs", "identity") +fn do_int(a: Int) -> Json + +/// Encode a float into JSON. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string(float(4.7)) +/// "4.7" +/// ``` +/// +pub fn float(input: Float) -> Json { + do_float(input) +} + +@external(erlang, "gleam_json_ffi", "float") +@external(javascript, "../gleam_json_ffi.mjs", "identity") +fn do_float(input input: Float) -> Json + +/// The JSON value null. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string(null()) +/// "null" +/// ``` +/// +pub fn null() -> Json { + do_null() +} + +@external(erlang, "gleam_json_ffi", "null") +@external(javascript, "../gleam_json_ffi.mjs", "do_null") +fn do_null() -> Json + +/// Encode an optional value into JSON, using null if it is the `None` variant. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string(nullable(Some(50), of: int)) +/// "50" +/// ``` +/// +/// ```gleam +/// > to_string(nullable(None, of: int)) +/// "null" +/// ``` +/// +pub fn nullable(from input: Option(a), of inner_type: fn(a) -> Json) -> Json { + case input { + Some(value) -> inner_type(value) + None -> null() + } +} + +/// Encode a list of key-value pairs into a JSON object. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string(object([ +/// #("game", string("Pac-Man")), +/// #("score", int(3333360)), +/// ])) +/// "{\"game\":\"Pac-Mac\",\"score\":3333360}" +/// ``` +/// +pub fn object(entries: List(#(String, Json))) -> Json { + do_object(entries) +} + +@external(erlang, "gleam_json_ffi", "object") +@external(javascript, "../gleam_json_ffi.mjs", "object") +fn do_object(entries entries: List(#(String, Json))) -> Json + +/// Encode a list into a JSON array. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string(array([1, 2, 3], of: int)) +/// "[1, 2, 3]" +/// ``` +/// +pub fn array(from entries: List(a), of inner_type: fn(a) -> Json) -> Json { + entries + |> list.map(inner_type) + |> preprocessed_array +} + +/// Encode a list of JSON values into a JSON array. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string(preprocessed_array([int(1), float(2.0), string("3")])) +/// "[1, 2.0, \"3\"]" +/// ``` +/// +pub fn preprocessed_array(from: List(Json)) -> Json { + do_preprocessed_array(from) +} + +@external(erlang, "gleam_json_ffi", "array") +@external(javascript, "../gleam_json_ffi.mjs", "array") +fn do_preprocessed_array(from from: List(Json)) -> Json + +/// Encode a Dict into a JSON object using the supplied functions to encode +/// the keys and the values respectively. +/// +/// ## Examples +/// +/// ```gleam +/// > to_string(dict(dict.from_list([ #(3, 3.0), #(4, 4.0)]), int.to_string, float) +/// "{\"3\": 3.0, \"4\": 4.0}" +/// ``` +/// +pub fn dict( + dict: Dict(k, v), + keys: fn(k) -> String, + values: fn(v) -> Json, +) -> Json { + object(dict.fold(dict, [], fn(acc, k, v) { [#(keys(k), values(v)), ..acc] })) +} diff --git a/build/packages/gleam_json/src/gleam@json.erl b/build/packages/gleam_json/src/gleam@json.erl new file mode 100644 index 0000000..8a33e49 --- /dev/null +++ b/build/packages/gleam_json/src/gleam@json.erl @@ -0,0 +1,304 @@ +-module(gleam@json). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/json.gleam"). +-export([parse_bits/2, parse/2, to_string/1, to_string_tree/1, string/1, bool/1, int/1, float/1, null/0, nullable/2, object/1, preprocessed_array/1, array/2, dict/3]). +-export_type([json/0, decode_error/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type json() :: any(). + +-type decode_error() :: unexpected_end_of_input | + {unexpected_byte, binary()} | + {unexpected_sequence, binary()} | + {unable_to_decode, list(gleam@dynamic@decode:decode_error())}. + +-file("src/gleam/json.gleam", 88). +?DOC( + " Decode a JSON bit string into dynamically typed data which can be decoded\n" + " into typed data with the `gleam/dynamic` module.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > parse_bits(<<\"[1,2,3]\">>, decode.list(of: decode.int))\n" + " Ok([1, 2, 3])\n" + " ```\n" + "\n" + " ```gleam\n" + " > parse_bits(<<\"[\">>, decode.list(of: decode.int))\n" + " Error(UnexpectedEndOfInput)\n" + " ```\n" + "\n" + " ```gleam\n" + " > parse_bits(<<\"1\">>, decode.string)\n" + " Error(UnableToDecode([decode.DecodeError(\"String\", \"Int\", [])])),\n" + " ```\n" +). +-spec parse_bits(bitstring(), gleam@dynamic@decode:decoder(DNO)) -> {ok, DNO} | + {error, decode_error()}. +parse_bits(Json, Decoder) -> + gleam@result:'try'( + gleam_json_ffi:decode(Json), + fun(Dynamic_value) -> + _pipe = gleam@dynamic@decode:run(Dynamic_value, Decoder), + gleam@result:map_error( + _pipe, + fun(Field@0) -> {unable_to_decode, Field@0} end + ) + end + ). + +-file("src/gleam/json.gleam", 47). +-spec do_parse(binary(), gleam@dynamic@decode:decoder(DNI)) -> {ok, DNI} | + {error, decode_error()}. +do_parse(Json, Decoder) -> + Bits = gleam_stdlib:identity(Json), + parse_bits(Bits, Decoder). + +-file("src/gleam/json.gleam", 39). +?DOC( + " Decode a JSON string into dynamically typed data which can be decoded into\n" + " typed data with the `gleam/dynamic` module.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > parse(\"[1,2,3]\", decode.list(of: decode.int))\n" + " Ok([1, 2, 3])\n" + " ```\n" + "\n" + " ```gleam\n" + " > parse(\"[\", decode.list(of: decode.int))\n" + " Error(UnexpectedEndOfInput)\n" + " ```\n" + "\n" + " ```gleam\n" + " > parse(\"1\", decode.string)\n" + " Error(UnableToDecode([decode.DecodeError(\"String\", \"Int\", [])]))\n" + " ```\n" +). +-spec parse(binary(), gleam@dynamic@decode:decoder(DNE)) -> {ok, DNE} | + {error, decode_error()}. +parse(Json, Decoder) -> + do_parse(Json, Decoder). + +-file("src/gleam/json.gleam", 117). +?DOC( + " Convert a JSON value into a string.\n" + "\n" + " Where possible prefer the `to_string_tree` function as it is faster than\n" + " this function, and BEAM VM IO is optimised for sending `StringTree` data.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(array([1, 2, 3], of: int))\n" + " \"[1,2,3]\"\n" + " ```\n" +). +-spec to_string(json()) -> binary(). +to_string(Json) -> + gleam_json_ffi:json_to_string(Json). + +-file("src/gleam/json.gleam", 140). +?DOC( + " Convert a JSON value into a string tree.\n" + "\n" + " Where possible prefer this function to the `to_string` function as it is\n" + " slower than this function, and BEAM VM IO is optimised for sending\n" + " `StringTree` data.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string_tree(array([1, 2, 3], of: int))\n" + " string_tree.from_string(\"[1,2,3]\")\n" + " ```\n" +). +-spec to_string_tree(json()) -> gleam@string_tree:string_tree(). +to_string_tree(Json) -> + gleam_json_ffi:json_to_iodata(Json). + +-file("src/gleam/json.gleam", 151). +?DOC( + " Encode a string into JSON, using normal JSON escaping.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(string(\"Hello!\"))\n" + " \"\\\"Hello!\\\"\"\n" + " ```\n" +). +-spec string(binary()) -> json(). +string(Input) -> + gleam_json_ffi:string(Input). + +-file("src/gleam/json.gleam", 168). +?DOC( + " Encode a bool into JSON.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(bool(False))\n" + " \"false\"\n" + " ```\n" +). +-spec bool(boolean()) -> json(). +bool(Input) -> + gleam_json_ffi:bool(Input). + +-file("src/gleam/json.gleam", 185). +?DOC( + " Encode an int into JSON.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(int(50))\n" + " \"50\"\n" + " ```\n" +). +-spec int(integer()) -> json(). +int(Input) -> + gleam_json_ffi:int(Input). + +-file("src/gleam/json.gleam", 202). +?DOC( + " Encode a float into JSON.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(float(4.7))\n" + " \"4.7\"\n" + " ```\n" +). +-spec float(float()) -> json(). +float(Input) -> + gleam_json_ffi:float(Input). + +-file("src/gleam/json.gleam", 219). +?DOC( + " The JSON value null.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(null())\n" + " \"null\"\n" + " ```\n" +). +-spec null() -> json(). +null() -> + gleam_json_ffi:null(). + +-file("src/gleam/json.gleam", 241). +?DOC( + " Encode an optional value into JSON, using null if it is the `None` variant.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(nullable(Some(50), of: int))\n" + " \"50\"\n" + " ```\n" + "\n" + " ```gleam\n" + " > to_string(nullable(None, of: int))\n" + " \"null\"\n" + " ```\n" +). +-spec nullable(gleam@option:option(DNU), fun((DNU) -> json())) -> json(). +nullable(Input, Inner_type) -> + case Input of + {some, Value} -> + Inner_type(Value); + + none -> + null() + end. + +-file("src/gleam/json.gleam", 260). +?DOC( + " Encode a list of key-value pairs into a JSON object.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(object([\n" + " #(\"game\", string(\"Pac-Man\")),\n" + " #(\"score\", int(3333360)),\n" + " ]))\n" + " \"{\\\"game\\\":\\\"Pac-Mac\\\",\\\"score\\\":3333360}\"\n" + " ```\n" +). +-spec object(list({binary(), json()})) -> json(). +object(Entries) -> + gleam_json_ffi:object(Entries). + +-file("src/gleam/json.gleam", 292). +?DOC( + " Encode a list of JSON values into a JSON array.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(preprocessed_array([int(1), float(2.0), string(\"3\")]))\n" + " \"[1, 2.0, \\\"3\\\"]\"\n" + " ```\n" +). +-spec preprocessed_array(list(json())) -> json(). +preprocessed_array(From) -> + gleam_json_ffi:array(From). + +-file("src/gleam/json.gleam", 277). +?DOC( + " Encode a list into a JSON array.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(array([1, 2, 3], of: int))\n" + " \"[1, 2, 3]\"\n" + " ```\n" +). +-spec array(list(DNY), fun((DNY) -> json())) -> json(). +array(Entries, Inner_type) -> + _pipe = Entries, + _pipe@1 = gleam@list:map(_pipe, Inner_type), + preprocessed_array(_pipe@1). + +-file("src/gleam/json.gleam", 310). +?DOC( + " Encode a Dict into a JSON object using the supplied functions to encode\n" + " the keys and the values respectively.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " > to_string(dict(dict.from_list([ #(3, 3.0), #(4, 4.0)]), int.to_string, float)\n" + " \"{\\\"3\\\": 3.0, \\\"4\\\": 4.0}\"\n" + " ```\n" +). +-spec dict( + gleam@dict:dict(DOC, DOD), + fun((DOC) -> binary()), + fun((DOD) -> json()) +) -> json(). +dict(Dict, Keys, Values) -> + object( + gleam@dict:fold( + Dict, + [], + fun(Acc, K, V) -> [{Keys(K), Values(V)} | Acc] end + ) + ). diff --git a/build/packages/gleam_json/src/gleam_json.app.src b/build/packages/gleam_json/src/gleam_json.app.src new file mode 100644 index 0000000..3bad27e --- /dev/null +++ b/build/packages/gleam_json/src/gleam_json.app.src @@ -0,0 +1,9 @@ +{application, gleam_json, [ + {vsn, "3.1.0"}, + {applications, [gleam_stdlib]}, + {description, "Work with JSON in Gleam"}, + {modules, [gleam@json, + gleam_json@@main, + gleam_json_ffi]}, + {registered, []} +]}. diff --git a/build/packages/gleam_json/src/gleam_json_ffi.erl b/build/packages/gleam_json/src/gleam_json_ffi.erl new file mode 100644 index 0000000..06a26a0 --- /dev/null +++ b/build/packages/gleam_json/src/gleam_json_ffi.erl @@ -0,0 +1,66 @@ +-module(gleam_json_ffi). + +-export([ + decode/1, json_to_iodata/1, json_to_string/1, int/1, float/1, string/1, + bool/1, null/0, array/1, object/1 +]). + +-if(?OTP_RELEASE < 27). +-define(bad_version, + error({erlang_otp_27_required, << "Insufficient Erlang/OTP version. + +`gleam_json` uses the Erlang `json` module introduced in Erlang/OTP 27. +You are using Erlang/OTP "/utf8, (integer_to_binary(?OTP_RELEASE))/binary, " +Please upgrade your Erlang install or downgrade to `gleam_json` v1.0.1. +"/utf8>>})). + +decode(_) -> ?bad_version. +json_to_iodata(_) -> ?bad_version. +json_to_string(_) -> ?bad_version. +int(_) -> ?bad_version. +float(_) -> ?bad_version. +string(_) -> ?bad_version. +bool(_) -> ?bad_version. +array(_) -> ?bad_version. +object(_) -> ?bad_version. +null() -> ?bad_version. +-else. + +decode(Json) -> + try + {ok, json:decode(Json)} + catch + error:unexpected_end -> {error, unexpected_end_of_input}; + error:{invalid_byte, Byte} -> {error, {unexpected_byte, hex(Byte)}}; + error:{unexpected_sequence, Byte} -> {error, {unexpected_sequence, Byte}} + end. + +hex(I) -> + H = list_to_binary(integer_to_list(I, 16)), + <<"0x"/utf8, H/binary>>. + +json_to_iodata(Json) -> + Json. + +json_to_string(Json) when is_binary(Json) -> + Json; +json_to_string(Json) when is_list(Json) -> + list_to_binary(Json). + +null() -> <<"null">>. +bool(true) -> <<"true">>; +bool(false) -> <<"false">>. +int(X) -> json:encode_integer(X). +float(X) -> json:encode_float(X). +string(X) -> json:encode_binary(X). + +array([]) -> <<"[]">>; +array([First | Rest]) -> [$[, First | array_loop(Rest)]. +array_loop([]) -> "]"; +array_loop([Elem | Rest]) -> [$,, Elem | array_loop(Rest)]. + +object(List) -> encode_object([[$,, string(Key), $: | Value] || {Key, Value} <- List]). +encode_object([]) -> <<"{}">>; +encode_object([[_Comma | Entry] | Rest]) -> ["{", Entry, Rest, "}"]. + +-endif. diff --git a/build/packages/gleam_json/src/gleam_json_ffi.mjs b/build/packages/gleam_json/src/gleam_json_ffi.mjs new file mode 100644 index 0000000..1d8d3ff --- /dev/null +++ b/build/packages/gleam_json/src/gleam_json_ffi.mjs @@ -0,0 +1,201 @@ +import { + Result$Ok, + Result$Error, + List$isNonEmpty, + List$NonEmpty$first, + List$NonEmpty$rest, +} from "./gleam.mjs"; +import { + DecodeError$UnexpectedByte, + DecodeError$UnexpectedEndOfInput, +} from "./gleam/json.mjs"; + +export function json_to_string(json) { + return JSON.stringify(json); +} + +export function object(entries) { + return Object.fromEntries(entries); +} + +export function identity(x) { + return x; +} + +export function array(list) { + const array = []; + while (List$isNonEmpty(list)) { + array.push(List$NonEmpty$first(list)); + list = List$NonEmpty$rest(list); + } + return array; +} + +export function do_null() { + return null; +} + +export function decode(string) { + try { + const result = JSON.parse(string); + return Result$Ok(result); + } catch (err) { + return Result$Error(getJsonDecodeError(err, string)); + } +} + +export function getJsonDecodeError(stdErr, json) { + if (isUnexpectedEndOfInput(stdErr)) return DecodeError$UnexpectedEndOfInput(); + return toUnexpectedByteError(stdErr, json); +} + +/** + * Matches unexpected end of input messages in: + * - Chromium (edge, chrome, node) + * - Spidermonkey (firefox) + * - JavascriptCore (safari) + * + * Note that Spidermonkey and JavascriptCore will both incorrectly report some + * UnexpectedByte errors as UnexpectedEndOfInput errors. For example: + * + * @example + * // in JavascriptCore + * JSON.parse('{"a"]: "b"}) + * // => JSON Parse error: Expected ':' before value + * + * JSON.parse('{"a"') + * // => JSON Parse error: Expected ':' before value + * + * // in Chromium (correct) + * JSON.parse('{"a"]: "b"}) + * // => Unexpected token ] in JSON at position 4 + * + * JSON.parse('{"a"') + * // => Unexpected end of JSON input + */ +function isUnexpectedEndOfInput(err) { + const unexpectedEndOfInputRegex = + /((unexpected (end|eof))|(end of data)|(unterminated string)|(json( parse error|\.parse)\: expected '(\:|\}|\])'))/i; + return unexpectedEndOfInputRegex.test(err.message); +} + +/** + * Converts a SyntaxError to an UnexpectedByte error based on the JS runtime. + * + * For Chromium, the unexpected byte and position are reported by the runtime. + * + * For JavascriptCore, only the unexpected byte is reported by the runtime, so + * there is no way to know which position that character is in unless we then + * parse the string again ourselves. So instead, the position is reported as 0. + * + * For Spidermonkey, the position is reported by the runtime as a line and column number + * and the unexpected byte is found using those coordinates. + */ +function toUnexpectedByteError(err, json) { + let converters = [ + v8UnexpectedByteError, + oldV8UnexpectedByteError, + jsCoreUnexpectedByteError, + spidermonkeyUnexpectedByteError, + ]; + + for (let converter of converters) { + let result = converter(err, json); + if (result) return result; + } + + return DecodeError$UnexpectedByte(""); +} + +/** + * Matches unexpected byte messages in: + * - V8 (edge, chrome, node) + * + * Matches the character but not the position as this is no longer reported by + * V8. Boo! + */ +function v8UnexpectedByteError(err) { + const regex = /unexpected token '(.)', ".+" is not valid JSON/i; + const match = regex.exec(err.message); + if (!match) return null; + const byte = toHex(match[1]); + return DecodeError$UnexpectedByte(byte); +} + +/** + * Matches unexpected byte messages in: + * - V8 (edge, chrome, node) + * + * No longer works in current versions of V8. + * + * Matches the character and its position. + */ +function oldV8UnexpectedByteError(err) { + const regex = /unexpected token (.) in JSON at position (\d+)/i; + const match = regex.exec(err.message); + if (!match) return null; + const byte = toHex(match[1]); + return DecodeError$UnexpectedByte(byte); +} + +/** + * Matches unexpected byte messages in: + * - Spidermonkey (firefox) + * + * Matches the position in a 2d grid only and not the character. + */ +function spidermonkeyUnexpectedByteError(err, json) { + const regex = + /(unexpected character|expected .*) at line (\d+) column (\d+)/i; + const match = regex.exec(err.message); + if (!match) return null; + const line = Number(match[2]); + const column = Number(match[3]); + const position = getPositionFromMultiline(line, column, json); + const byte = toHex(json[position]); + return DecodeError$UnexpectedByte(byte); +} + +/** + * Matches unexpected byte messages in: + * - JavascriptCore (safari) + * + * JavascriptCore only reports what the character is and not its position. + */ +function jsCoreUnexpectedByteError(err) { + const regex = /unexpected (identifier|token) "(.)"/i; + const match = regex.exec(err.message); + if (!match) return null; + const byte = toHex(match[2]); + return DecodeError$UnexpectedByte(byte); +} + +function toHex(char) { + return "0x" + char.charCodeAt(0).toString(16).toUpperCase(); +} + +/** + * Gets the position of a character in a flattened (i.e. single line) string + * from a line and column number. Note that the position is 0-indexed and + * the line and column numbers are 1-indexed. + * + * @param {number} line + * @param {number} column + * @param {string} string + */ +function getPositionFromMultiline(line, column, string) { + if (line === 1) return column - 1; + + let currentLn = 1; + let position = 0; + string.split("").find((char, idx) => { + if (char === "\n") currentLn += 1; + if (currentLn === line) { + position = idx + column; + return true; + } + return false; + }); + + return position; +} diff --git a/build/packages/gleam_stdlib/LICENCE b/build/packages/gleam_stdlib/LICENCE new file mode 100644 index 0000000..c1dabd0 --- /dev/null +++ b/build/packages/gleam_stdlib/LICENCE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2018, Louis Pilfold . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/build/packages/gleam_stdlib/README.md b/build/packages/gleam_stdlib/README.md new file mode 100644 index 0000000..5e1ed4c --- /dev/null +++ b/build/packages/gleam_stdlib/README.md @@ -0,0 +1,34 @@ +# stdlib + +[![Package Version](https://img.shields.io/hexpm/v/gleam_stdlib)](https://hex.pm/packages/gleam_stdlib) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gleam_stdlib/) +[![Discord chat](https://img.shields.io/discord/768594524158427167?color=blue)](https://discord.gg/Fm8Pwmy) + +Gleam's standard library! +Documentation available on [HexDocs](https://hexdocs.pm/gleam_stdlib/). + +## Installation + +Add `gleam_stdlib` to your Gleam project. + +```sh +gleam add gleam_stdlib +``` +```gleam +import gleam/io + +pub fn greet(name: String) -> Nil { + io.println("Hello " <> name <> "!") +} +``` + +## Targets + +Gleam's standard library supports both targets: Erlang and JavaScript. + +### Compatibility + +This library is compatible with all versions of Erlang/OTP 26 and higher, +as well as all NodeJS, Deno, Bun, and major browsers that are currently +supported by their maintainers. If you have a compatibility issue with +any platform open an issue and we'll see what we can do to help. diff --git a/build/packages/gleam_stdlib/gleam.toml b/build/packages/gleam_stdlib/gleam.toml new file mode 100644 index 0000000..27c3f0e --- /dev/null +++ b/build/packages/gleam_stdlib/gleam.toml @@ -0,0 +1,14 @@ +name = "gleam_stdlib" +version = "0.65.0" +gleam = ">= 1.11.0" +licences = ["Apache-2.0"] +description = "A standard library for the Gleam programming language" + +repository = { type = "github", user = "gleam-lang", repo = "stdlib" } +links = [ + { title = "Website", href = "https://gleam.run" }, + { title = "Sponsor", href = "https://github.com/sponsors/lpil" }, +] + +[javascript.deno] +allow_read = ["./"] diff --git a/build/packages/gleam_stdlib/include/gleam@dynamic@decode_DecodeError.hrl b/build/packages/gleam_stdlib/include/gleam@dynamic@decode_DecodeError.hrl new file mode 100644 index 0000000..b1135f2 --- /dev/null +++ b/build/packages/gleam_stdlib/include/gleam@dynamic@decode_DecodeError.hrl @@ -0,0 +1,5 @@ +-record(decode_error, { + expected :: binary(), + found :: binary(), + path :: list(binary()) +}). diff --git a/build/packages/gleam_stdlib/include/gleam@dynamic@decode_Decoder.hrl b/build/packages/gleam_stdlib/include/gleam@dynamic@decode_Decoder.hrl new file mode 100644 index 0000000..a2fcfc6 --- /dev/null +++ b/build/packages/gleam_stdlib/include/gleam@dynamic@decode_Decoder.hrl @@ -0,0 +1,4 @@ +-record(decoder, { + function :: fun((gleam@dynamic:dynamic_()) -> {any(), + list(gleam@dynamic@decode:decode_error())}) +}). diff --git a/build/packages/gleam_stdlib/include/gleam@set_Set.hrl b/build/packages/gleam_stdlib/include/gleam@set_Set.hrl new file mode 100644 index 0000000..51fb778 --- /dev/null +++ b/build/packages/gleam_stdlib/include/gleam@set_Set.hrl @@ -0,0 +1 @@ +-record(set, {dict :: gleam@dict:dict(any(), list(nil))}). diff --git a/build/packages/gleam_stdlib/include/gleam@uri_Uri.hrl b/build/packages/gleam_stdlib/include/gleam@uri_Uri.hrl new file mode 100644 index 0000000..50150f4 --- /dev/null +++ b/build/packages/gleam_stdlib/include/gleam@uri_Uri.hrl @@ -0,0 +1,9 @@ +-record(uri, { + scheme :: gleam@option:option(binary()), + userinfo :: gleam@option:option(binary()), + host :: gleam@option:option(binary()), + port :: gleam@option:option(integer()), + path :: binary(), + 'query' :: gleam@option:option(binary()), + fragment :: gleam@option:option(binary()) +}). diff --git a/build/packages/gleam_stdlib/src/dict.mjs b/build/packages/gleam_stdlib/src/dict.mjs new file mode 100644 index 0000000..f39cd54 --- /dev/null +++ b/build/packages/gleam_stdlib/src/dict.mjs @@ -0,0 +1,993 @@ +/** + * This file uses jsdoc to annotate types. + * These types can be checked using the typescript compiler with "checkjs" option. + */ + +import { isEqual } from "./gleam.mjs"; + +const referenceMap = /* @__PURE__ */ new WeakMap(); +const tempDataView = /* @__PURE__ */ new DataView( + /* @__PURE__ */ new ArrayBuffer(8), +); +let referenceUID = 0; +/** + * hash the object by reference using a weak map and incrementing uid + * @param {any} o + * @returns {number} + */ +function hashByReference(o) { + const known = referenceMap.get(o); + if (known !== undefined) { + return known; + } + const hash = referenceUID++; + if (referenceUID === 0x7fffffff) { + referenceUID = 0; + } + referenceMap.set(o, hash); + return hash; +} + +/** + * merge two hashes in an order sensitive way + * @param {number} a + * @param {number} b + * @returns {number} + */ +function hashMerge(a, b) { + return (a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2))) | 0; +} + +/** + * standard string hash popularised by java + * @param {string} s + * @returns {number} + */ +function hashString(s) { + let hash = 0; + const len = s.length; + for (let i = 0; i < len; i++) { + hash = (Math.imul(31, hash) + s.charCodeAt(i)) | 0; + } + return hash; +} + +/** + * hash a number by converting to two integers and do some jumbling + * @param {number} n + * @returns {number} + */ +function hashNumber(n) { + tempDataView.setFloat64(0, n); + const i = tempDataView.getInt32(0); + const j = tempDataView.getInt32(4); + return Math.imul(0x45d9f3b, (i >> 16) ^ i) ^ j; +} + +/** + * hash a BigInt by converting it to a string and hashing that + * @param {BigInt} n + * @returns {number} + */ +function hashBigInt(n) { + return hashString(n.toString()); +} + +/** + * hash any js object + * @param {any} o + * @returns {number} + */ +function hashObject(o) { + const proto = Object.getPrototypeOf(o); + if (proto !== null && typeof proto.hashCode === "function") { + try { + const code = o.hashCode(o); + if (typeof code === "number") { + return code; + } + } catch {} + } + if (o instanceof Promise || o instanceof WeakSet || o instanceof WeakMap) { + return hashByReference(o); + } + if (o instanceof Date) { + return hashNumber(o.getTime()); + } + let h = 0; + if (o instanceof ArrayBuffer) { + o = new Uint8Array(o); + } + if (Array.isArray(o) || o instanceof Uint8Array) { + for (let i = 0; i < o.length; i++) { + h = (Math.imul(31, h) + getHash(o[i])) | 0; + } + } else if (o instanceof Set) { + o.forEach((v) => { + h = (h + getHash(v)) | 0; + }); + } else if (o instanceof Map) { + o.forEach((v, k) => { + h = (h + hashMerge(getHash(v), getHash(k))) | 0; + }); + } else { + const keys = Object.keys(o); + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + const v = o[k]; + h = (h + hashMerge(getHash(v), hashString(k))) | 0; + } + } + return h; +} + +/** + * hash any js value + * @param {any} u + * @returns {number} + */ +export function getHash(u) { + if (u === null) return 0x42108422; + if (u === undefined) return 0x42108423; + if (u === true) return 0x42108421; + if (u === false) return 0x42108420; + switch (typeof u) { + case "number": + return hashNumber(u); + case "string": + return hashString(u); + case "bigint": + return hashBigInt(u); + case "object": + return hashObject(u); + case "symbol": + return hashByReference(u); + case "function": + return hashByReference(u); + default: + return 0; // should be unreachable + } +} + +/** + * @template K,V + * @typedef {ArrayNode | IndexNode | CollisionNode} Node + */ +/** + * @template K,V + * @typedef {{ type: typeof ENTRY, k: K, v: V }} Entry + */ +/** + * @template K,V + * @typedef {{ type: typeof ARRAY_NODE, size: number, array: (undefined | Entry | Node)[] }} ArrayNode + */ +/** + * @template K,V + * @typedef {{ type: typeof INDEX_NODE, bitmap: number, array: (Entry | Node)[] }} IndexNode + */ +/** + * @template K,V + * @typedef {{ type: typeof COLLISION_NODE, hash: number, array: Entry[] }} CollisionNode + */ +/** + * @typedef {{ val: boolean }} Flag + */ +const SHIFT = 5; // number of bits you need to shift by to get the next bucket +const BUCKET_SIZE = Math.pow(2, SHIFT); +const MASK = BUCKET_SIZE - 1; // used to zero out all bits not in the bucket +const MAX_INDEX_NODE = BUCKET_SIZE / 2; // when does index node grow into array node +const MIN_ARRAY_NODE = BUCKET_SIZE / 4; // when does array node shrink to index node +const ENTRY = 0; +const ARRAY_NODE = 1; +const INDEX_NODE = 2; +const COLLISION_NODE = 3; + +/** @type {IndexNode} */ +const EMPTY = { + type: INDEX_NODE, + bitmap: 0, + array: [], +}; +/** + * Mask the hash to get only the bucket corresponding to shift + * @param {number} hash + * @param {number} shift + * @returns {number} + */ +function mask(hash, shift) { + return (hash >>> shift) & MASK; +} + +/** + * Set only the Nth bit where N is the masked hash + * @param {number} hash + * @param {number} shift + * @returns {number} + */ +function bitpos(hash, shift) { + return 1 << mask(hash, shift); +} + +/** + * Count the number of 1 bits in a number + * @param {number} x + * @returns {number} + */ +function bitcount(x) { + x -= (x >> 1) & 0x55555555; + x = (x & 0x33333333) + ((x >> 2) & 0x33333333); + x = (x + (x >> 4)) & 0x0f0f0f0f; + x += x >> 8; + x += x >> 16; + return x & 0x7f; +} + +/** + * Calculate the array index of an item in a bitmap index node + * @param {number} bitmap + * @param {number} bit + * @returns {number} + */ +function index(bitmap, bit) { + return bitcount(bitmap & (bit - 1)); +} + +/** + * Efficiently copy an array and set one value at an index + * @template T + * @param {T[]} arr + * @param {number} at + * @param {T} val + * @returns {T[]} + */ +function cloneAndSet(arr, at, val) { + const len = arr.length; + const out = new Array(len); + for (let i = 0; i < len; ++i) { + out[i] = arr[i]; + } + out[at] = val; + return out; +} + +/** + * Efficiently copy an array and insert one value at an index + * @template T + * @param {T[]} arr + * @param {number} at + * @param {T} val + * @returns {T[]} + */ +function spliceIn(arr, at, val) { + const len = arr.length; + const out = new Array(len + 1); + let i = 0; + let g = 0; + while (i < at) { + out[g++] = arr[i++]; + } + out[g++] = val; + while (i < len) { + out[g++] = arr[i++]; + } + return out; +} + +/** + * Efficiently copy an array and remove one value at an index + * @template T + * @param {T[]} arr + * @param {number} at + * @returns {T[]} + */ +function spliceOut(arr, at) { + const len = arr.length; + const out = new Array(len - 1); + let i = 0; + let g = 0; + while (i < at) { + out[g++] = arr[i++]; + } + ++i; + while (i < len) { + out[g++] = arr[i++]; + } + return out; +} + +/** + * Create a new node containing two entries + * @template K,V + * @param {number} shift + * @param {K} key1 + * @param {V} val1 + * @param {number} key2hash + * @param {K} key2 + * @param {V} val2 + * @returns {Node} + */ +function createNode(shift, key1, val1, key2hash, key2, val2) { + const key1hash = getHash(key1); + if (key1hash === key2hash) { + return { + type: COLLISION_NODE, + hash: key1hash, + array: [ + { type: ENTRY, k: key1, v: val1 }, + { type: ENTRY, k: key2, v: val2 }, + ], + }; + } + const addedLeaf = { val: false }; + return assoc( + assocIndex(EMPTY, shift, key1hash, key1, val1, addedLeaf), + shift, + key2hash, + key2, + val2, + addedLeaf, + ); +} + +/** + * @template T,K,V + * @callback AssocFunction + * @param {T} root + * @param {number} shift + * @param {number} hash + * @param {K} key + * @param {V} val + * @param {Flag} addedLeaf + * @returns {Node} + */ +/** + * Associate a node with a new entry, creating a new node + * @template T,K,V + * @type {AssocFunction,K,V>} + */ +function assoc(root, shift, hash, key, val, addedLeaf) { + switch (root.type) { + case ARRAY_NODE: + return assocArray(root, shift, hash, key, val, addedLeaf); + case INDEX_NODE: + return assocIndex(root, shift, hash, key, val, addedLeaf); + case COLLISION_NODE: + return assocCollision(root, shift, hash, key, val, addedLeaf); + } +} +/** + * @template T,K,V + * @type {AssocFunction,K,V>} + */ +function assocArray(root, shift, hash, key, val, addedLeaf) { + const idx = mask(hash, shift); + const node = root.array[idx]; + // if the corresponding index is empty set the index to a newly created node + if (node === undefined) { + addedLeaf.val = true; + return { + type: ARRAY_NODE, + size: root.size + 1, + array: cloneAndSet(root.array, idx, { type: ENTRY, k: key, v: val }), + }; + } + if (node.type === ENTRY) { + // if keys are equal replace the entry + if (isEqual(key, node.k)) { + if (val === node.v) { + return root; + } + return { + type: ARRAY_NODE, + size: root.size, + array: cloneAndSet(root.array, idx, { + type: ENTRY, + k: key, + v: val, + }), + }; + } + // otherwise upgrade the entry to a node and insert + addedLeaf.val = true; + return { + type: ARRAY_NODE, + size: root.size, + array: cloneAndSet( + root.array, + idx, + createNode(shift + SHIFT, node.k, node.v, hash, key, val), + ), + }; + } + // otherwise call assoc on the child node + const n = assoc(node, shift + SHIFT, hash, key, val, addedLeaf); + // if the child node hasn't changed just return the old root + if (n === node) { + return root; + } + // otherwise set the index to the new node + return { + type: ARRAY_NODE, + size: root.size, + array: cloneAndSet(root.array, idx, n), + }; +} +/** + * @template T,K,V + * @type {AssocFunction,K,V>} + */ +function assocIndex(root, shift, hash, key, val, addedLeaf) { + const bit = bitpos(hash, shift); + const idx = index(root.bitmap, bit); + // if there is already a item at this hash index.. + if ((root.bitmap & bit) !== 0) { + // if there is a node at the index (not an entry), call assoc on the child node + const node = root.array[idx]; + if (node.type !== ENTRY) { + const n = assoc(node, shift + SHIFT, hash, key, val, addedLeaf); + if (n === node) { + return root; + } + return { + type: INDEX_NODE, + bitmap: root.bitmap, + array: cloneAndSet(root.array, idx, n), + }; + } + // otherwise there is an entry at the index + // if the keys are equal replace the entry with the updated value + const nodeKey = node.k; + if (isEqual(key, nodeKey)) { + if (val === node.v) { + return root; + } + return { + type: INDEX_NODE, + bitmap: root.bitmap, + array: cloneAndSet(root.array, idx, { + type: ENTRY, + k: key, + v: val, + }), + }; + } + // if the keys are not equal, replace the entry with a new child node + addedLeaf.val = true; + return { + type: INDEX_NODE, + bitmap: root.bitmap, + array: cloneAndSet( + root.array, + idx, + createNode(shift + SHIFT, nodeKey, node.v, hash, key, val), + ), + }; + } else { + // else there is currently no item at the hash index + const n = root.array.length; + // if the number of nodes is at the maximum, expand this node into an array node + if (n >= MAX_INDEX_NODE) { + // create a 32 length array for the new array node (one for each bit in the hash) + const nodes = new Array(32); + // create and insert a node for the new entry + const jdx = mask(hash, shift); + nodes[jdx] = assocIndex(EMPTY, shift + SHIFT, hash, key, val, addedLeaf); + let j = 0; + let bitmap = root.bitmap; + // place each item in the index node into the correct spot in the array node + // loop through all 32 bits / array positions + for (let i = 0; i < 32; i++) { + if ((bitmap & 1) !== 0) { + const node = root.array[j++]; + nodes[i] = node; + } + // shift the bitmap to process the next bit + bitmap = bitmap >>> 1; + } + return { + type: ARRAY_NODE, + size: n + 1, + array: nodes, + }; + } else { + // else there is still space in this index node + // simply insert a new entry at the hash index + const newArray = spliceIn(root.array, idx, { + type: ENTRY, + k: key, + v: val, + }); + addedLeaf.val = true; + return { + type: INDEX_NODE, + bitmap: root.bitmap | bit, + array: newArray, + }; + } + } +} +/** + * @template T,K,V + * @type {AssocFunction,K,V>} + */ +function assocCollision(root, shift, hash, key, val, addedLeaf) { + // if there is a hash collision + if (hash === root.hash) { + const idx = collisionIndexOf(root, key); + // if this key already exists replace the entry with the new value + if (idx !== -1) { + const entry = root.array[idx]; + if (entry.v === val) { + return root; + } + return { + type: COLLISION_NODE, + hash: hash, + array: cloneAndSet(root.array, idx, { type: ENTRY, k: key, v: val }), + }; + } + // otherwise insert the entry at the end of the array + const size = root.array.length; + addedLeaf.val = true; + return { + type: COLLISION_NODE, + hash: hash, + array: cloneAndSet(root.array, size, { type: ENTRY, k: key, v: val }), + }; + } + // if there is no hash collision, upgrade to an index node + return assoc( + { + type: INDEX_NODE, + bitmap: bitpos(root.hash, shift), + array: [root], + }, + shift, + hash, + key, + val, + addedLeaf, + ); +} +/** + * Find the index of a key in the collision node's array + * @template K,V + * @param {CollisionNode} root + * @param {K} key + * @returns {number} + */ +function collisionIndexOf(root, key) { + const size = root.array.length; + for (let i = 0; i < size; i++) { + if (isEqual(key, root.array[i].k)) { + return i; + } + } + return -1; +} +/** + * @template T,K,V + * @callback FindFunction + * @param {T} root + * @param {number} shift + * @param {number} hash + * @param {K} key + * @returns {undefined | Entry} + */ +/** + * Return the found entry or undefined if not present in the root + * @template K,V + * @type {FindFunction,K,V>} + */ +function find(root, shift, hash, key) { + switch (root.type) { + case ARRAY_NODE: + return findArray(root, shift, hash, key); + case INDEX_NODE: + return findIndex(root, shift, hash, key); + case COLLISION_NODE: + return findCollision(root, key); + } +} +/** + * @template K,V + * @type {FindFunction,K,V>} + */ +function findArray(root, shift, hash, key) { + const idx = mask(hash, shift); + const node = root.array[idx]; + if (node === undefined) { + return undefined; + } + if (node.type !== ENTRY) { + return find(node, shift + SHIFT, hash, key); + } + if (isEqual(key, node.k)) { + return node; + } + return undefined; +} +/** + * @template K,V + * @type {FindFunction,K,V>} + */ +function findIndex(root, shift, hash, key) { + const bit = bitpos(hash, shift); + if ((root.bitmap & bit) === 0) { + return undefined; + } + const idx = index(root.bitmap, bit); + const node = root.array[idx]; + if (node.type !== ENTRY) { + return find(node, shift + SHIFT, hash, key); + } + if (isEqual(key, node.k)) { + return node; + } + return undefined; +} +/** + * @template K,V + * @param {CollisionNode} root + * @param {K} key + * @returns {undefined | Entry} + */ +function findCollision(root, key) { + const idx = collisionIndexOf(root, key); + if (idx < 0) { + return undefined; + } + return root.array[idx]; +} +/** + * @template T,K,V + * @callback WithoutFunction + * @param {T} root + * @param {number} shift + * @param {number} hash + * @param {K} key + * @returns {undefined | Node} + */ +/** + * Remove an entry from the root, returning the updated root. + * Returns undefined if the node should be removed from the parent. + * @template K,V + * @type {WithoutFunction,K,V>} + * */ +function without(root, shift, hash, key) { + switch (root.type) { + case ARRAY_NODE: + return withoutArray(root, shift, hash, key); + case INDEX_NODE: + return withoutIndex(root, shift, hash, key); + case COLLISION_NODE: + return withoutCollision(root, key); + } +} +/** + * @template K,V + * @type {WithoutFunction,K,V>} + */ +function withoutArray(root, shift, hash, key) { + const idx = mask(hash, shift); + const node = root.array[idx]; + if (node === undefined) { + return root; // already empty + } + let n = undefined; + // if node is an entry and the keys are not equal there is nothing to remove + // if node is not an entry do a recursive call + if (node.type === ENTRY) { + if (!isEqual(node.k, key)) { + return root; // no changes + } + } else { + n = without(node, shift + SHIFT, hash, key); + if (n === node) { + return root; // no changes + } + } + // if the recursive call returned undefined the node should be removed + if (n === undefined) { + // if the number of child nodes is at the minimum, pack into an index node + if (root.size <= MIN_ARRAY_NODE) { + const arr = root.array; + const out = new Array(root.size - 1); + let i = 0; + let j = 0; + let bitmap = 0; + while (i < idx) { + const nv = arr[i]; + if (nv !== undefined) { + out[j] = nv; + bitmap |= 1 << i; + ++j; + } + ++i; + } + ++i; // skip copying the removed node + while (i < arr.length) { + const nv = arr[i]; + if (nv !== undefined) { + out[j] = nv; + bitmap |= 1 << i; + ++j; + } + ++i; + } + return { + type: INDEX_NODE, + bitmap: bitmap, + array: out, + }; + } + return { + type: ARRAY_NODE, + size: root.size - 1, + array: cloneAndSet(root.array, idx, n), + }; + } + return { + type: ARRAY_NODE, + size: root.size, + array: cloneAndSet(root.array, idx, n), + }; +} +/** + * @template K,V + * @type {WithoutFunction,K,V>} + */ +function withoutIndex(root, shift, hash, key) { + const bit = bitpos(hash, shift); + if ((root.bitmap & bit) === 0) { + return root; // already empty + } + const idx = index(root.bitmap, bit); + const node = root.array[idx]; + // if the item is not an entry + if (node.type !== ENTRY) { + const n = without(node, shift + SHIFT, hash, key); + if (n === node) { + return root; // no changes + } + // if not undefined, the child node still has items, so update it + if (n !== undefined) { + return { + type: INDEX_NODE, + bitmap: root.bitmap, + array: cloneAndSet(root.array, idx, n), + }; + } + // otherwise the child node should be removed + // if it was the only child node, remove this node from the parent + if (root.bitmap === bit) { + return undefined; + } + // otherwise just remove the child node + return { + type: INDEX_NODE, + bitmap: root.bitmap ^ bit, + array: spliceOut(root.array, idx), + }; + } + // otherwise the item is an entry, remove it if the key matches + if (isEqual(key, node.k)) { + if (root.bitmap === bit) { + return undefined; + } + return { + type: INDEX_NODE, + bitmap: root.bitmap ^ bit, + array: spliceOut(root.array, idx), + }; + } + return root; +} +/** + * @template K,V + * @param {CollisionNode} root + * @param {K} key + * @returns {undefined | Node} + */ +function withoutCollision(root, key) { + const idx = collisionIndexOf(root, key); + // if the key not found, no changes + if (idx < 0) { + return root; + } + // otherwise the entry was found, remove it + // if it was the only entry in this node, remove the whole node + if (root.array.length === 1) { + return undefined; + } + // otherwise just remove the entry + return { + type: COLLISION_NODE, + hash: root.hash, + array: spliceOut(root.array, idx), + }; +} +/** + * @template K,V + * @param {undefined | Node} root + * @param {(value:V,key:K)=>void} fn + * @returns {void} + */ +function forEach(root, fn) { + if (root === undefined) { + return; + } + const items = root.array; + const size = items.length; + for (let i = 0; i < size; i++) { + const item = items[i]; + if (item === undefined) { + continue; + } + if (item.type === ENTRY) { + fn(item.v, item.k); + continue; + } + forEach(item, fn); + } +} + +/** + * Extra wrapper to keep track of Dict size and clean up the API + * @template K,V + */ +export default class Dict { + /** + * @template V + * @param {Record} o + * @returns {Dict} + */ + static fromObject(o) { + const keys = Object.keys(o); + /** @type Dict */ + let m = Dict.new(); + for (let i = 0; i < keys.length; i++) { + const k = keys[i]; + m = m.set(k, o[k]); + } + return m; + } + + /** + * @template K,V + * @param {Map} o + * @returns {Dict} + */ + static fromMap(o) { + /** @type Dict */ + let m = Dict.new(); + o.forEach((v, k) => { + m = m.set(k, v); + }); + return m; + } + + static new() { + return new Dict(undefined, 0); + } + + /** + * @param {undefined | Node} root + * @param {number} size + */ + constructor(root, size) { + this.root = root; + this.size = size; + } + /** + * @template NotFound + * @param {K} key + * @param {NotFound} notFound + * @returns {NotFound | V} + */ + get(key, notFound) { + if (this.root === undefined) { + return notFound; + } + const found = find(this.root, 0, getHash(key), key); + if (found === undefined) { + return notFound; + } + return found.v; + } + /** + * @param {K} key + * @param {V} val + * @returns {Dict} + */ + set(key, val) { + const addedLeaf = { val: false }; + const root = this.root === undefined ? EMPTY : this.root; + const newRoot = assoc(root, 0, getHash(key), key, val, addedLeaf); + if (newRoot === this.root) { + return this; + } + return new Dict(newRoot, addedLeaf.val ? this.size + 1 : this.size); + } + /** + * @param {K} key + * @returns {Dict} + */ + delete(key) { + if (this.root === undefined) { + return this; + } + const newRoot = without(this.root, 0, getHash(key), key); + if (newRoot === this.root) { + return this; + } + if (newRoot === undefined) { + return Dict.new(); + } + return new Dict(newRoot, this.size - 1); + } + /** + * @param {K} key + * @returns {boolean} + */ + has(key) { + if (this.root === undefined) { + return false; + } + return find(this.root, 0, getHash(key), key) !== undefined; + } + /** + * @returns {[K,V][]} + */ + entries() { + if (this.root === undefined) { + return []; + } + /** @type [K,V][] */ + const result = []; + this.forEach((v, k) => result.push([k, v])); + return result; + } + /** + * + * @param {(val:V,key:K)=>void} fn + */ + forEach(fn) { + forEach(this.root, fn); + } + hashCode() { + let h = 0; + this.forEach((v, k) => { + h = (h + hashMerge(getHash(v), getHash(k))) | 0; + }); + return h; + } + /** + * @param {unknown} o + * @returns {boolean} + */ + equals(o) { + if (!(o instanceof Dict) || this.size !== o.size) { + return false; + } + + try { + this.forEach((v, k) => { + if (!isEqual(o.get(k, !v), v)) { + throw unequalDictSymbol; + } + }); + return true; + } catch (e) { + if (e === unequalDictSymbol) { + return false; + } + + throw e; + } + } +} + +// This is thrown internally in Dict.equals() so that it returns false as soon +// as a non-matching key is found +const unequalDictSymbol = /* @__PURE__ */ Symbol(); diff --git a/build/packages/gleam_stdlib/src/gleam/bit_array.gleam b/build/packages/gleam_stdlib/src/gleam/bit_array.gleam new file mode 100644 index 0000000..544b74a --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/bit_array.gleam @@ -0,0 +1,280 @@ +//// BitArrays are a sequence of binary data of any length. + +import gleam/int +import gleam/order +import gleam/string + +/// Converts a UTF-8 `String` type into a `BitArray`. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "bit_array_from_string") +pub fn from_string(x: String) -> BitArray + +/// Returns an integer which is the number of bits in the bit array. +/// +@external(erlang, "erlang", "bit_size") +@external(javascript, "../gleam_stdlib.mjs", "bit_array_bit_size") +pub fn bit_size(x: BitArray) -> Int + +/// Returns an integer which is the number of bytes in the bit array. +/// +@external(erlang, "erlang", "byte_size") +@external(javascript, "../gleam_stdlib.mjs", "bit_array_byte_size") +pub fn byte_size(x: BitArray) -> Int + +/// Pads a bit array with zeros so that it is a whole number of bytes. +/// +@external(erlang, "gleam_stdlib", "bit_array_pad_to_bytes") +@external(javascript, "../gleam_stdlib.mjs", "bit_array_pad_to_bytes") +pub fn pad_to_bytes(x: BitArray) -> BitArray + +/// Creates a new bit array by joining two bit arrays. +/// +/// ## Examples +/// +/// ```gleam +/// append(to: from_string("butter"), suffix: from_string("fly")) +/// // -> from_string("butterfly") +/// ``` +/// +pub fn append(to first: BitArray, suffix second: BitArray) -> BitArray { + concat([first, second]) +} + +/// Extracts a sub-section of a bit array. +/// +/// The slice will start at given position and continue up to specified +/// length. +/// A negative length can be used to extract bytes at the end of a bit array. +/// +/// This function runs in constant time. +/// +@external(erlang, "gleam_stdlib", "bit_array_slice") +@external(javascript, "../gleam_stdlib.mjs", "bit_array_slice") +pub fn slice( + from string: BitArray, + at position: Int, + take length: Int, +) -> Result(BitArray, Nil) + +/// Tests to see whether a bit array is valid UTF-8. +/// +pub fn is_utf8(bits: BitArray) -> Bool { + is_utf8_loop(bits) +} + +@target(erlang) +fn is_utf8_loop(bits: BitArray) -> Bool { + case bits { + <<>> -> True + <<_:utf8, rest:bytes>> -> is_utf8_loop(rest) + _ -> False + } +} + +@target(javascript) +fn is_utf8_loop(bits: BitArray) -> Bool { + case to_string(bits) { + Ok(_) -> True + Error(_) -> False + } +} + +/// Converts a bit array to a string. +/// +/// Returns an error if the bit array is invalid UTF-8 data. +/// +@external(javascript, "../gleam_stdlib.mjs", "bit_array_to_string") +pub fn to_string(bits: BitArray) -> Result(String, Nil) { + case is_utf8(bits) { + True -> Ok(unsafe_to_string(bits)) + False -> Error(Nil) + } +} + +@external(erlang, "gleam_stdlib", "identity") +fn unsafe_to_string(a: BitArray) -> String + +/// Creates a new bit array by joining multiple binaries. +/// +/// ## Examples +/// +/// ```gleam +/// concat([from_string("butter"), from_string("fly")]) +/// // -> from_string("butterfly") +/// ``` +/// +@external(erlang, "gleam_stdlib", "bit_array_concat") +@external(javascript, "../gleam_stdlib.mjs", "bit_array_concat") +pub fn concat(bit_arrays: List(BitArray)) -> BitArray + +/// Encodes a BitArray into a base 64 encoded string. +/// +/// If the bit array does not contain a whole number of bytes then it is padded +/// with zero bits prior to being encoded. +/// +@external(erlang, "gleam_stdlib", "base64_encode") +@external(javascript, "../gleam_stdlib.mjs", "base64_encode") +pub fn base64_encode(input: BitArray, padding: Bool) -> String + +/// Decodes a base 64 encoded string into a `BitArray`. +/// +pub fn base64_decode(encoded: String) -> Result(BitArray, Nil) { + let padded = case byte_size(from_string(encoded)) % 4 { + 0 -> encoded + n -> string.append(encoded, string.repeat("=", 4 - n)) + } + decode64(padded) +} + +@external(erlang, "gleam_stdlib", "base64_decode") +@external(javascript, "../gleam_stdlib.mjs", "base64_decode") +fn decode64(a: String) -> Result(BitArray, Nil) + +/// Encodes a `BitArray` into a base 64 encoded string with URL and filename +/// safe alphabet. +/// +/// If the bit array does not contain a whole number of bytes then it is padded +/// with zero bits prior to being encoded. +/// +pub fn base64_url_encode(input: BitArray, padding: Bool) -> String { + input + |> base64_encode(padding) + |> string.replace("+", "-") + |> string.replace("/", "_") +} + +/// Decodes a base 64 encoded string with URL and filename safe alphabet into a +/// `BitArray`. +/// +pub fn base64_url_decode(encoded: String) -> Result(BitArray, Nil) { + encoded + |> string.replace("-", "+") + |> string.replace("_", "/") + |> base64_decode() +} + +/// Encodes a `BitArray` into a base 16 encoded string. +/// +/// If the bit array does not contain a whole number of bytes then it is padded +/// with zero bits prior to being encoded. +/// +@external(erlang, "gleam_stdlib", "base16_encode") +@external(javascript, "../gleam_stdlib.mjs", "base16_encode") +pub fn base16_encode(input: BitArray) -> String + +/// Decodes a base 16 encoded string into a `BitArray`. +/// +@external(erlang, "gleam_stdlib", "base16_decode") +@external(javascript, "../gleam_stdlib.mjs", "base16_decode") +pub fn base16_decode(input: String) -> Result(BitArray, Nil) + +/// Converts a bit array to a string containing the decimal value of each byte. +/// +/// Use this over `string.inspect` when you have a bit array you want printed +/// in the array syntax even if it is valid UTF-8. +/// +/// ## Examples +/// +/// ```gleam +/// inspect(<<0, 20, 0x20, 255>>) +/// // -> "<<0, 20, 32, 255>>" +/// +/// inspect(<<100, 5:3>>) +/// // -> "<<100, 5:size(3)>>" +/// ``` +/// +pub fn inspect(input: BitArray) -> String { + inspect_loop(input, "<<") <> ">>" +} + +fn inspect_loop(input: BitArray, accumulator: String) -> String { + case input { + <<>> -> accumulator + + <> -> accumulator <> int.to_string(x) <> ":size(1)" + <> -> accumulator <> int.to_string(x) <> ":size(2)" + <> -> accumulator <> int.to_string(x) <> ":size(3)" + <> -> accumulator <> int.to_string(x) <> ":size(4)" + <> -> accumulator <> int.to_string(x) <> ":size(5)" + <> -> accumulator <> int.to_string(x) <> ":size(6)" + <> -> accumulator <> int.to_string(x) <> ":size(7)" + + <> -> { + let suffix = case rest { + <<>> -> "" + _ -> ", " + } + + let accumulator = accumulator <> int.to_string(x) <> suffix + inspect_loop(rest, accumulator) + } + + _ -> accumulator + } +} + +/// Compare two bit arrays as sequences of bytes. +/// +/// ## Examples +/// +/// ```gleam +/// compare(<<1>>, <<2>>) +/// // -> Lt +/// +/// compare(<<"AB":utf8>>, <<"AA":utf8>>) +/// // -> Gt +/// +/// compare(<<1, 2:size(2)>>, with: <<1, 2:size(2)>>) +/// // -> Eq +/// ``` +/// +pub fn compare(a: BitArray, with b: BitArray) -> order.Order { + case a, b { + <>, <> -> + case first_byte, second_byte { + f, s if f > s -> order.Gt + f, s if f < s -> order.Lt + _, _ -> compare(first_rest, second_rest) + } + + <<>>, <<>> -> order.Eq + // First has more items, example: "AB" > "A": + _, <<>> -> order.Gt + // Second has more items, example: "A" < "AB": + <<>>, _ -> order.Lt + // This happens when there's unusually sized elements. + // Handle these special cases via custom erlang function. + first, second -> + case bit_array_to_int_and_size(first), bit_array_to_int_and_size(second) { + #(a, _), #(b, _) if a > b -> order.Gt + #(a, _), #(b, _) if a < b -> order.Lt + #(_, size_a), #(_, size_b) if size_a > size_b -> order.Gt + #(_, size_a), #(_, size_b) if size_a < size_b -> order.Lt + _, _ -> order.Eq + } + } +} + +@external(erlang, "gleam_stdlib", "bit_array_to_int_and_size") +@external(javascript, "../gleam_stdlib.mjs", "bit_array_to_int_and_size") +fn bit_array_to_int_and_size(a: BitArray) -> #(Int, Int) + +/// Checks whether the first `BitArray` starts with the second one. +/// +/// ## Examples +/// +/// ```gleam +/// starts_with(<<1, 2, 3, 4>>, <<1, 2>>) +/// // -> True +/// ``` +/// +@external(javascript, "../gleam_stdlib.mjs", "bit_array_starts_with") +pub fn starts_with(bits: BitArray, prefix: BitArray) -> Bool { + let prefix_size = bit_size(prefix) + + case bits { + <> if pref == prefix -> True + _ -> False + } +} diff --git a/build/packages/gleam_stdlib/src/gleam/bool.gleam b/build/packages/gleam_stdlib/src/gleam/bool.gleam new file mode 100644 index 0000000..26a6ac4 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/bool.gleam @@ -0,0 +1,316 @@ +//// A type with two possible values, `True` and `False`. Used to indicate whether +//// things are... true or false! +//// +//// Often is it clearer and offers more type safety to define a custom type +//// than to use `Bool`. For example, rather than having a `is_teacher: Bool` +//// field consider having a `role: SchoolRole` field where `SchoolRole` is a custom +//// type that can be either `Student` or `Teacher`. + +/// Returns the and of two bools, but it evaluates both arguments. +/// +/// It's the function equivalent of the `&&` operator. +/// This function is useful in higher order functions or pipes. +/// +/// ## Examples +/// +/// ```gleam +/// and(True, True) +/// // -> True +/// ``` +/// +/// ```gleam +/// and(False, True) +/// // -> False +/// ``` +/// +/// ```gleam +/// False |> and(True) +/// // -> False +/// ``` +/// +pub fn and(a: Bool, b: Bool) -> Bool { + a && b +} + +/// Returns the or of two bools, but it evaluates both arguments. +/// +/// It's the function equivalent of the `||` operator. +/// This function is useful in higher order functions or pipes. +/// +/// ## Examples +/// +/// ```gleam +/// or(True, True) +/// // -> True +/// ``` +/// +/// ```gleam +/// or(False, True) +/// // -> True +/// ``` +/// +/// ```gleam +/// False |> or(True) +/// // -> True +/// ``` +/// +pub fn or(a: Bool, b: Bool) -> Bool { + a || b +} + +/// Returns the opposite bool value. +/// +/// This is the same as the `!` or `not` operators in some other languages. +/// +/// ## Examples +/// +/// ```gleam +/// negate(True) +/// // -> False +/// ``` +/// +/// ```gleam +/// negate(False) +/// // -> True +/// ``` +/// +pub fn negate(bool: Bool) -> Bool { + !bool +} + +/// Returns the nor of two bools. +/// +/// ## Examples +/// +/// ```gleam +/// nor(False, False) +/// // -> True +/// ``` +/// +/// ```gleam +/// nor(False, True) +/// // -> False +/// ``` +/// +/// ```gleam +/// nor(True, False) +/// // -> False +/// ``` +/// +/// ```gleam +/// nor(True, True) +/// // -> False +/// ``` +/// +pub fn nor(a: Bool, b: Bool) -> Bool { + !{ a || b } +} + +/// Returns the nand of two bools. +/// +/// ## Examples +/// +/// ```gleam +/// nand(False, False) +/// // -> True +/// ``` +/// +/// ```gleam +/// nand(False, True) +/// // -> True +/// ``` +/// +/// ```gleam +/// nand(True, False) +/// // -> True +/// ``` +/// +/// ```gleam +/// nand(True, True) +/// // -> False +/// ``` +/// +pub fn nand(a: Bool, b: Bool) -> Bool { + !{ a && b } +} + +/// Returns the exclusive or of two bools. +/// +/// ## Examples +/// +/// ```gleam +/// exclusive_or(False, False) +/// // -> False +/// ``` +/// +/// ```gleam +/// exclusive_or(False, True) +/// // -> True +/// ``` +/// +/// ```gleam +/// exclusive_or(True, False) +/// // -> True +/// ``` +/// +/// ```gleam +/// exclusive_or(True, True) +/// // -> False +/// ``` +/// +pub fn exclusive_or(a: Bool, b: Bool) -> Bool { + a != b +} + +/// Returns the exclusive nor of two bools. +/// +/// ## Examples +/// +/// ```gleam +/// exclusive_nor(False, False) +/// // -> True +/// ``` +/// +/// ```gleam +/// exclusive_nor(False, True) +/// // -> False +/// ``` +/// +/// ```gleam +/// exclusive_nor(True, False) +/// // -> False +/// ``` +/// +/// ```gleam +/// exclusive_nor(True, True) +/// // -> True +/// ``` +/// +pub fn exclusive_nor(a: Bool, b: Bool) -> Bool { + a == b +} + +/// Returns a string representation of the given bool. +/// +/// ## Examples +/// +/// ```gleam +/// to_string(True) +/// // -> "True" +/// ``` +/// +/// ```gleam +/// to_string(False) +/// // -> "False" +/// ``` +/// +pub fn to_string(bool: Bool) -> String { + case bool { + False -> "False" + True -> "True" + } +} + +/// Run a callback function if the given bool is `False`, otherwise return a +/// default value. +/// +/// With a `use` expression this function can simulate the early-return pattern +/// found in some other programming languages. +/// +/// In a procedural language: +/// +/// ```js +/// if (predicate) return value; +/// // ... +/// ``` +/// +/// In Gleam with a `use` expression: +/// +/// ```gleam +/// use <- guard(when: predicate, return: value) +/// // ... +/// ``` +/// +/// Like everything in Gleam `use` is an expression, so it short circuits the +/// current block, not the entire function. As a result you can assign the value +/// to a variable: +/// +/// ```gleam +/// let x = { +/// use <- guard(when: predicate, return: value) +/// // ... +/// } +/// ``` +/// +/// Note that unlike in procedural languages the `return` value is evaluated +/// even when the predicate is `False`, so it is advisable not to perform +/// expensive computation nor side-effects there. +/// +/// +/// ## Examples +/// +/// ```gleam +/// let name = "" +/// use <- guard(when: name == "", return: "Welcome!") +/// "Hello, " <> name +/// // -> "Welcome!" +/// ``` +/// +/// ```gleam +/// let name = "Kamaka" +/// use <- guard(when: name == "", return: "Welcome!") +/// "Hello, " <> name +/// // -> "Hello, Kamaka" +/// ``` +/// +pub fn guard( + when requirement: Bool, + return consequence: a, + otherwise alternative: fn() -> a, +) -> a { + case requirement { + True -> consequence + False -> alternative() + } +} + +/// Runs a callback function if the given bool is `True`, otherwise runs an +/// alternative callback function. +/// +/// Useful when further computation should be delayed regardless of the given +/// bool's value. +/// +/// See [`guard`](#guard) for more info. +/// +/// ## Examples +/// +/// ```gleam +/// let name = "Kamaka" +/// let inquiry = fn() { "How may we address you?" } +/// use <- lazy_guard(when: name == "", return: inquiry) +/// "Hello, " <> name +/// // -> "Hello, Kamaka" +/// ``` +/// +/// ```gleam +/// import gleam/int +/// +/// let name = "" +/// let greeting = fn() { "Hello, " <> name } +/// use <- lazy_guard(when: name == "", otherwise: greeting) +/// let number = int.random(99) +/// let name = "User " <> int.to_string(number) +/// "Welcome, " <> name +/// // -> "Welcome, User 54" +/// ``` +/// +pub fn lazy_guard( + when requirement: Bool, + return consequence: fn() -> a, + otherwise alternative: fn() -> a, +) -> a { + case requirement { + True -> consequence() + False -> alternative() + } +} diff --git a/build/packages/gleam_stdlib/src/gleam/bytes_tree.gleam b/build/packages/gleam_stdlib/src/gleam/bytes_tree.gleam new file mode 100644 index 0000000..832fbee --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/bytes_tree.gleam @@ -0,0 +1,190 @@ +//// `BytesTree` is a type used for efficiently building binary content to be +//// written to a file or a socket. Internally it is represented as tree so to +//// append or prepend to a bytes tree is a constant time operation that +//// allocates a new node in the tree without copying any of the content. When +//// writing to an output stream the tree is traversed and the content is sent +//// directly rather than copying it into a single buffer beforehand. +//// +//// If we append one bit array to another the bit arrays must be copied to a +//// new location in memory so that they can sit together. This behaviour +//// enables efficient reading of the data but copying can be expensive, +//// especially if we want to join many bit arrays together. +//// +//// BytesTree is different in that it can be joined together in constant +//// time using minimal memory, and then can be efficiently converted to a +//// bit array using the `to_bit_array` function. +//// +//// Byte trees are always byte aligned, so that a number of bits that is not +//// divisible by 8 will be padded with 0s. +//// +//// On Erlang this type is compatible with Erlang's iolists. + +import gleam/bit_array +import gleam/list +import gleam/string_tree.{type StringTree} + +pub opaque type BytesTree { + Bytes(BitArray) + Text(StringTree) + Many(List(BytesTree)) +} + +/// Create an empty `BytesTree`. Useful as the start of a pipe chaining many +/// trees together. +/// +pub fn new() -> BytesTree { + concat([]) +} + +/// Prepends a bit array to the start of a bytes tree. +/// +/// Runs in constant time. +/// +pub fn prepend(to second: BytesTree, prefix first: BitArray) -> BytesTree { + append_tree(from_bit_array(first), second) +} + +/// Appends a bit array to the end of a bytes tree. +/// +/// Runs in constant time. +/// +pub fn append(to first: BytesTree, suffix second: BitArray) -> BytesTree { + append_tree(first, from_bit_array(second)) +} + +/// Prepends a bytes tree onto the start of another. +/// +/// Runs in constant time. +/// +pub fn prepend_tree(to second: BytesTree, prefix first: BytesTree) -> BytesTree { + append_tree(first, second) +} + +/// Appends a bytes tree onto the end of another. +/// +/// Runs in constant time. +/// +@external(erlang, "gleam_stdlib", "iodata_append") +pub fn append_tree(to first: BytesTree, suffix second: BytesTree) -> BytesTree { + case second { + Many(trees) -> Many([first, ..trees]) + Text(_) | Bytes(_) -> Many([first, second]) + } +} + +/// Prepends a string onto the start of a bytes tree. +/// +/// Runs in constant time when running on Erlang. +/// Runs in linear time with the length of the string otherwise. +/// +pub fn prepend_string(to second: BytesTree, prefix first: String) -> BytesTree { + append_tree(from_string(first), second) +} + +/// Appends a string onto the end of a bytes tree. +/// +/// Runs in constant time when running on Erlang. +/// Runs in linear time with the length of the string otherwise. +/// +pub fn append_string(to first: BytesTree, suffix second: String) -> BytesTree { + append_tree(first, from_string(second)) +} + +/// Joins a list of bytes trees into a single one. +/// +/// Runs in constant time. +/// +@external(erlang, "gleam_stdlib", "identity") +pub fn concat(trees: List(BytesTree)) -> BytesTree { + Many(trees) +} + +/// Joins a list of bit arrays into a single bytes tree. +/// +/// Runs in constant time. +/// +pub fn concat_bit_arrays(bits: List(BitArray)) -> BytesTree { + bits + |> list.map(from_bit_array) + |> concat() +} + +/// Creates a new bytes tree from a string. +/// +/// Runs in constant time when running on Erlang. +/// Runs in linear time otherwise. +/// +@external(erlang, "gleam_stdlib", "wrap_list") +pub fn from_string(string: String) -> BytesTree { + Text(string_tree.from_string(string)) +} + +/// Creates a new bytes tree from a string tree. +/// +/// Runs in constant time when running on Erlang. +/// Runs in linear time otherwise. +/// +@external(erlang, "gleam_stdlib", "wrap_list") +pub fn from_string_tree(tree: string_tree.StringTree) -> BytesTree { + Text(tree) +} + +/// Creates a new bytes tree from a bit array. +/// +/// Runs in constant time. +/// +pub fn from_bit_array(bits: BitArray) -> BytesTree { + bits + |> bit_array.pad_to_bytes + |> wrap_list +} + +@external(erlang, "gleam_stdlib", "wrap_list") +fn wrap_list(bits: BitArray) -> BytesTree { + Bytes(bits) +} + +/// Turns a bytes tree into a bit array. +/// +/// Runs in linear time. +/// +/// When running on Erlang this function is implemented natively by the +/// virtual machine and is highly optimised. +/// +@external(erlang, "erlang", "list_to_bitstring") +pub fn to_bit_array(tree: BytesTree) -> BitArray { + [[tree]] + |> to_list([]) + |> list.reverse + |> bit_array.concat +} + +fn to_list(stack: List(List(BytesTree)), acc: List(BitArray)) -> List(BitArray) { + case stack { + [] -> acc + + [[], ..remaining_stack] -> to_list(remaining_stack, acc) + + [[Bytes(bits), ..rest], ..remaining_stack] -> + to_list([rest, ..remaining_stack], [bits, ..acc]) + + [[Text(tree), ..rest], ..remaining_stack] -> { + let bits = bit_array.from_string(string_tree.to_string(tree)) + to_list([rest, ..remaining_stack], [bits, ..acc]) + } + + [[Many(trees), ..rest], ..remaining_stack] -> + to_list([trees, rest, ..remaining_stack], acc) + } +} + +/// Returns the size of the bytes tree's content in bytes. +/// +/// Runs in linear time. +/// +@external(erlang, "erlang", "iolist_size") +pub fn byte_size(tree: BytesTree) -> Int { + [[tree]] + |> to_list([]) + |> list.fold(0, fn(acc, bits) { bit_array.byte_size(bits) + acc }) +} diff --git a/build/packages/gleam_stdlib/src/gleam/dict.gleam b/build/packages/gleam_stdlib/src/gleam/dict.gleam new file mode 100644 index 0000000..2942727 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/dict.gleam @@ -0,0 +1,548 @@ +import gleam/option.{type Option} + +/// A dictionary of keys and values. +/// +/// Any type can be used for the keys and values of a dict, but all the keys +/// must be of the same type and all the values must be of the same type. +/// +/// Each key can only be present in a dict once. +/// +/// Dicts are not ordered in any way, and any unintentional ordering is not to +/// be relied upon in your code as it may change in future versions of Erlang +/// or Gleam. +/// +/// See [the Erlang map module](https://erlang.org/doc/man/maps.html) for more +/// information. +/// +pub type Dict(key, value) + +/// Determines the number of key-value pairs in the dict. +/// This function runs in constant time and does not need to iterate the dict. +/// +/// ## Examples +/// +/// ```gleam +/// new() |> size +/// // -> 0 +/// ``` +/// +/// ```gleam +/// new() |> insert("key", "value") |> size +/// // -> 1 +/// ``` +/// +@external(erlang, "maps", "size") +@external(javascript, "../gleam_stdlib.mjs", "map_size") +pub fn size(dict: Dict(k, v)) -> Int + +/// Determines whether or not the dict is empty. +/// +/// ## Examples +/// +/// ```gleam +/// new() |> is_empty +/// // -> True +/// ``` +/// +/// ```gleam +/// new() |> insert("b", 1) |> is_empty +/// // -> False +/// ``` +/// +pub fn is_empty(dict: Dict(k, v)) -> Bool { + size(dict) == 0 +} + +/// Converts the dict to a list of 2-element tuples `#(key, value)`, one for +/// each key-value pair in the dict. +/// +/// The tuples in the list have no specific order. +/// +/// ## Examples +/// +/// Calling `to_list` on an empty `dict` returns an empty list. +/// +/// ```gleam +/// new() |> to_list +/// // -> [] +/// ``` +/// +/// The ordering of elements in the resulting list is an implementation detail +/// that should not be relied upon. +/// +/// ```gleam +/// new() |> insert("b", 1) |> insert("a", 0) |> insert("c", 2) |> to_list +/// // -> [#("a", 0), #("b", 1), #("c", 2)] +/// ``` +/// +@external(erlang, "maps", "to_list") +@external(javascript, "../gleam_stdlib.mjs", "map_to_list") +pub fn to_list(dict: Dict(k, v)) -> List(#(k, v)) + +/// Converts a list of 2-element tuples `#(key, value)` to a dict. +/// +/// If two tuples have the same key the last one in the list will be the one +/// that is present in the dict. +/// +@external(erlang, "maps", "from_list") +pub fn from_list(list: List(#(k, v))) -> Dict(k, v) { + from_list_loop(list, new()) +} + +fn from_list_loop( + over list: List(#(k, v)), + from initial: Dict(k, v), +) -> Dict(k, v) { + case list { + [] -> initial + [#(key, value), ..rest] -> from_list_loop(rest, insert(initial, key, value)) + } +} + +/// Determines whether or not a value present in the dict for a given key. +/// +/// ## Examples +/// +/// ```gleam +/// new() |> insert("a", 0) |> has_key("a") +/// // -> True +/// ``` +/// +/// ```gleam +/// new() |> insert("a", 0) |> has_key("b") +/// // -> False +/// ``` +/// +pub fn has_key(dict: Dict(k, v), key: k) -> Bool { + do_has_key(key, dict) +} + +@external(erlang, "maps", "is_key") +fn do_has_key(key: k, dict: Dict(k, v)) -> Bool { + get(dict, key) != Error(Nil) +} + +/// Creates a fresh dict that contains no values. +/// +@external(erlang, "maps", "new") +@external(javascript, "../gleam_stdlib.mjs", "new_map") +pub fn new() -> Dict(k, v) + +/// Fetches a value from a dict for a given key. +/// +/// The dict may not have a value for the key, so the value is wrapped in a +/// `Result`. +/// +/// ## Examples +/// +/// ```gleam +/// new() |> insert("a", 0) |> get("a") +/// // -> Ok(0) +/// ``` +/// +/// ```gleam +/// new() |> insert("a", 0) |> get("b") +/// // -> Error(Nil) +/// ``` +/// +@external(erlang, "gleam_stdlib", "map_get") +@external(javascript, "../gleam_stdlib.mjs", "map_get") +pub fn get(from: Dict(k, v), get: k) -> Result(v, Nil) + +/// Inserts a value into the dict with the given key. +/// +/// If the dict already has a value for the given key then the value is +/// replaced with the new value. +/// +/// ## Examples +/// +/// ```gleam +/// new() |> insert("a", 0) +/// // -> from_list([#("a", 0)]) +/// ``` +/// +/// ```gleam +/// new() |> insert("a", 0) |> insert("a", 5) +/// // -> from_list([#("a", 5)]) +/// ``` +/// +pub fn insert(into dict: Dict(k, v), for key: k, insert value: v) -> Dict(k, v) { + do_insert(key, value, dict) +} + +@external(erlang, "maps", "put") +@external(javascript, "../gleam_stdlib.mjs", "map_insert") +fn do_insert(key: k, value: v, dict: Dict(k, v)) -> Dict(k, v) + +/// Updates all values in a given dict by calling a given function on each key +/// and value. +/// +/// ## Examples +/// +/// ```gleam +/// from_list([#(3, 3), #(2, 4)]) +/// |> map_values(fn(key, value) { key * value }) +/// // -> from_list([#(3, 9), #(2, 8)]) +/// ``` +/// +pub fn map_values(in dict: Dict(k, v), with fun: fn(k, v) -> a) -> Dict(k, a) { + do_map_values(fun, dict) +} + +@external(erlang, "maps", "map") +fn do_map_values(f: fn(k, v) -> a, dict: Dict(k, v)) -> Dict(k, a) { + let f = fn(dict, k, v) { insert(dict, k, f(k, v)) } + fold(dict, from: new(), with: f) +} + +/// Gets a list of all keys in a given dict. +/// +/// Dicts are not ordered so the keys are not returned in any specific order. Do +/// not write code that relies on the order keys are returned by this function +/// as it may change in later versions of Gleam or Erlang. +/// +/// ## Examples +/// +/// ```gleam +/// from_list([#("a", 0), #("b", 1)]) |> keys +/// // -> ["a", "b"] +/// ``` +/// +@external(erlang, "maps", "keys") +pub fn keys(dict: Dict(k, v)) -> List(k) { + do_keys_loop(to_list(dict), []) +} + +fn do_keys_loop(list: List(#(k, v)), acc: List(k)) -> List(k) { + case list { + [] -> reverse_and_concat(acc, []) + [#(key, _value), ..rest] -> do_keys_loop(rest, [key, ..acc]) + } +} + +fn reverse_and_concat(remaining: List(a), accumulator: List(a)) -> List(a) { + case remaining { + [] -> accumulator + [first, ..rest] -> reverse_and_concat(rest, [first, ..accumulator]) + } +} + +/// Gets a list of all values in a given dict. +/// +/// Dicts are not ordered so the values are not returned in any specific order. Do +/// not write code that relies on the order values are returned by this function +/// as it may change in later versions of Gleam or Erlang. +/// +/// ## Examples +/// +/// ```gleam +/// from_list([#("a", 0), #("b", 1)]) |> values +/// // -> [0, 1] +/// ``` +/// +@external(erlang, "maps", "values") +pub fn values(dict: Dict(k, v)) -> List(v) { + let list_of_pairs = to_list(dict) + do_values_loop(list_of_pairs, []) +} + +fn do_values_loop(list: List(#(k, v)), acc: List(v)) -> List(v) { + case list { + [] -> reverse_and_concat(acc, []) + [#(_key, value), ..rest] -> do_values_loop(rest, [value, ..acc]) + } +} + +/// Creates a new dict from a given dict, minus any entries that a given function +/// returns `False` for. +/// +/// ## Examples +/// +/// ```gleam +/// from_list([#("a", 0), #("b", 1)]) +/// |> filter(fn(key, value) { value != 0 }) +/// // -> from_list([#("b", 1)]) +/// ``` +/// +/// ```gleam +/// from_list([#("a", 0), #("b", 1)]) +/// |> filter(fn(key, value) { True }) +/// // -> from_list([#("a", 0), #("b", 1)]) +/// ``` +/// +pub fn filter( + in dict: Dict(k, v), + keeping predicate: fn(k, v) -> Bool, +) -> Dict(k, v) { + do_filter(predicate, dict) +} + +@external(erlang, "maps", "filter") +fn do_filter(f: fn(k, v) -> Bool, dict: Dict(k, v)) -> Dict(k, v) { + let insert = fn(dict, k, v) { + case f(k, v) { + True -> insert(dict, k, v) + False -> dict + } + } + + fold(dict, from: new(), with: insert) +} + +/// Creates a new dict from a given dict, only including any entries for which the +/// keys are in a given list. +/// +/// ## Examples +/// +/// ```gleam +/// from_list([#("a", 0), #("b", 1)]) +/// |> take(["b"]) +/// // -> from_list([#("b", 1)]) +/// ``` +/// +/// ```gleam +/// from_list([#("a", 0), #("b", 1)]) +/// |> take(["a", "b", "c"]) +/// // -> from_list([#("a", 0), #("b", 1)]) +/// ``` +/// +pub fn take(from dict: Dict(k, v), keeping desired_keys: List(k)) -> Dict(k, v) { + do_take(desired_keys, dict) +} + +@external(erlang, "maps", "with") +fn do_take(desired_keys: List(k), dict: Dict(k, v)) -> Dict(k, v) { + do_take_loop(dict, desired_keys, new()) +} + +fn do_take_loop( + dict: Dict(k, v), + desired_keys: List(k), + acc: Dict(k, v), +) -> Dict(k, v) { + let insert = fn(taken, key) { + case get(dict, key) { + Ok(value) -> insert(taken, key, value) + Error(_) -> taken + } + } + case desired_keys { + [] -> acc + [first, ..rest] -> do_take_loop(dict, rest, insert(acc, first)) + } +} + +/// Creates a new dict from a pair of given dicts by combining their entries. +/// +/// If there are entries with the same keys in both dicts the entry from the +/// second dict takes precedence. +/// +/// ## Examples +/// +/// ```gleam +/// let a = from_list([#("a", 0), #("b", 1)]) +/// let b = from_list([#("b", 2), #("c", 3)]) +/// merge(a, b) +/// // -> from_list([#("a", 0), #("b", 2), #("c", 3)]) +/// ``` +/// +@external(erlang, "maps", "merge") +pub fn merge(into dict: Dict(k, v), from new_entries: Dict(k, v)) -> Dict(k, v) { + new_entries + |> to_list + |> fold_inserts(dict) +} + +fn fold_inserts(new_entries: List(#(k, v)), dict: Dict(k, v)) -> Dict(k, v) { + case new_entries { + [] -> dict + [first, ..rest] -> fold_inserts(rest, insert_pair(dict, first)) + } +} + +fn insert_pair(dict: Dict(k, v), pair: #(k, v)) -> Dict(k, v) { + insert(dict, pair.0, pair.1) +} + +/// Creates a new dict from a given dict with all the same entries except for the +/// one with a given key, if it exists. +/// +/// ## Examples +/// +/// ```gleam +/// from_list([#("a", 0), #("b", 1)]) |> delete("a") +/// // -> from_list([#("b", 1)]) +/// ``` +/// +/// ```gleam +/// from_list([#("a", 0), #("b", 1)]) |> delete("c") +/// // -> from_list([#("a", 0), #("b", 1)]) +/// ``` +/// +pub fn delete(from dict: Dict(k, v), delete key: k) -> Dict(k, v) { + do_delete(key, dict) +} + +@external(erlang, "maps", "remove") +@external(javascript, "../gleam_stdlib.mjs", "map_remove") +fn do_delete(a: k, b: Dict(k, v)) -> Dict(k, v) + +/// Creates a new dict from a given dict with all the same entries except any with +/// keys found in a given list. +/// +/// ## Examples +/// +/// ```gleam +/// from_list([#("a", 0), #("b", 1)]) |> drop(["a"]) +/// // -> from_list([#("b", 1)]) +/// ``` +/// +/// ```gleam +/// from_list([#("a", 0), #("b", 1)]) |> drop(["c"]) +/// // -> from_list([#("a", 0), #("b", 1)]) +/// ``` +/// +/// ```gleam +/// from_list([#("a", 0), #("b", 1)]) |> drop(["a", "b", "c"]) +/// // -> from_list([]) +/// ``` +/// +pub fn drop(from dict: Dict(k, v), drop disallowed_keys: List(k)) -> Dict(k, v) { + case disallowed_keys { + [] -> dict + [first, ..rest] -> drop(delete(dict, first), rest) + } +} + +/// Creates a new dict with one entry inserted or updated using a given function. +/// +/// If there was not an entry in the dict for the given key then the function +/// gets `None` as its argument, otherwise it gets `Some(value)`. +/// +/// ## Example +/// +/// ```gleam +/// let dict = from_list([#("a", 0)]) +/// let increment = fn(x) { +/// case x { +/// Some(i) -> i + 1 +/// None -> 0 +/// } +/// } +/// +/// upsert(dict, "a", increment) +/// // -> from_list([#("a", 1)]) +/// +/// upsert(dict, "b", increment) +/// // -> from_list([#("a", 0), #("b", 0)]) +/// ``` +/// +pub fn upsert( + in dict: Dict(k, v), + update key: k, + with fun: fn(Option(v)) -> v, +) -> Dict(k, v) { + case get(dict, key) { + Ok(value) -> insert(dict, key, fun(option.Some(value))) + Error(_) -> insert(dict, key, fun(option.None)) + } +} + +/// Combines all entries into a single value by calling a given function on each +/// one. +/// +/// Dicts are not ordered so the values are not returned in any specific order. Do +/// not write code that relies on the order entries are used by this function +/// as it may change in later versions of Gleam or Erlang. +/// +/// # Examples +/// +/// ```gleam +/// let dict = from_list([#("a", 1), #("b", 3), #("c", 9)]) +/// fold(dict, 0, fn(accumulator, key, value) { accumulator + value }) +/// // -> 13 +/// ``` +/// +/// ```gleam +/// import gleam/string +/// +/// let dict = from_list([#("a", 1), #("b", 3), #("c", 9)]) +/// fold(dict, "", fn(accumulator, key, value) { +/// string.append(accumulator, key) +/// }) +/// // -> "abc" +/// ``` +/// +pub fn fold( + over dict: Dict(k, v), + from initial: acc, + with fun: fn(acc, k, v) -> acc, +) -> acc { + fold_loop(to_list(dict), initial, fun) +} + +fn fold_loop( + list: List(#(k, v)), + initial: acc, + fun: fn(acc, k, v) -> acc, +) -> acc { + case list { + [] -> initial + [#(k, v), ..rest] -> fold_loop(rest, fun(initial, k, v), fun) + } +} + +/// Calls a function for each key and value in a dict, discarding the return +/// value. +/// +/// Useful for producing a side effect for every item of a dict. +/// +/// ```gleam +/// import gleam/io +/// +/// let dict = from_list([#("a", "apple"), #("b", "banana"), #("c", "cherry")]) +/// +/// each(dict, fn(k, v) { +/// io.println(key <> " => " <> value) +/// }) +/// // -> Nil +/// // a => apple +/// // b => banana +/// // c => cherry +/// ``` +/// +/// The order of elements in the iteration is an implementation detail that +/// should not be relied upon. +/// +pub fn each(dict: Dict(k, v), fun: fn(k, v) -> a) -> Nil { + fold(dict, Nil, fn(nil, k, v) { + fun(k, v) + nil + }) +} + +/// Creates a new dict from a pair of given dicts by combining their entries. +/// +/// If there are entries with the same keys in both dicts the given function is +/// used to determine the new value to use in the resulting dict. +/// +/// ## Examples +/// +/// ```gleam +/// let a = from_list([#("a", 0), #("b", 1)]) +/// let b = from_list([#("a", 2), #("c", 3)]) +/// combine(a, b, fn(one, other) { one + other }) +/// // -> from_list([#("a", 2), #("b", 1), #("c", 3)]) +/// ``` +/// +pub fn combine( + dict: Dict(k, v), + other: Dict(k, v), + with fun: fn(v, v) -> v, +) -> Dict(k, v) { + use acc, key, value <- fold(over: dict, from: other) + case get(acc, key) { + Ok(other_value) -> insert(acc, key, fun(value, other_value)) + Error(_) -> insert(acc, key, value) + } +} diff --git a/build/packages/gleam_stdlib/src/gleam/dynamic.gleam b/build/packages/gleam_stdlib/src/gleam/dynamic.gleam new file mode 100644 index 0000000..6dfcbe6 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/dynamic.gleam @@ -0,0 +1,100 @@ +import gleam/dict + +/// `Dynamic` data is data that we don't know the type of yet. +/// We likely get data like this from interop with Erlang, or from +/// IO with the outside world. +/// +/// This module contains code for forming dynamic data, and the +/// `gleam/dynamic/decode` module contains code for turning dynamic data back +/// into Gleam data with known types. You will likely mostly use the other +/// module in your projects. +/// +/// The exact runtime representation of dynamic values will depend on the +/// compilation target used. +/// +pub type Dynamic + +/// Return a string indicating the type of the dynamic value. +/// +/// This function may be useful for constructing error messages or logs. If you +/// want to turn dynamic data into well typed data then you want the +/// `gleam/dynamic/decode` module. +/// +/// ```gleam +/// classify(string("Hello")) +/// // -> "String" +/// ``` +/// +@external(erlang, "gleam_stdlib", "classify_dynamic") +@external(javascript, "../gleam_stdlib.mjs", "classify_dynamic") +pub fn classify(data: Dynamic) -> String + +/// Create a dynamic value from a bool. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn bool(a: Bool) -> Dynamic + +/// Create a dynamic value from a string. +/// +/// On Erlang this will be a binary string rather than a character list. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn string(a: String) -> Dynamic + +/// Create a dynamic value from a float. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn float(a: Float) -> Dynamic + +/// Create a dynamic value from an int. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn int(a: Int) -> Dynamic + +/// Create a dynamic value from a bit array. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn bit_array(a: BitArray) -> Dynamic + +/// Create a dynamic value from a list. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn list(a: List(Dynamic)) -> Dynamic + +/// Create a dynamic value from a list, converting it to a sequential runtime +/// format rather than the regular list format. +/// +/// On Erlang this will be a tuple, on JavaScript this will be an array. +/// +@external(erlang, "erlang", "list_to_tuple") +@external(javascript, "../gleam_stdlib.mjs", "list_to_array") +pub fn array(a: List(Dynamic)) -> Dynamic + +/// Create a dynamic value made an unordered series of keys and values, where +/// the keys are unique. +/// +/// On Erlang this will be a map, on JavaScript this will be a Gleam dict +/// object. +/// +pub fn properties(entries: List(#(Dynamic, Dynamic))) -> Dynamic { + cast(dict.from_list(entries)) +} + +/// A dynamic value representing nothing. +/// +/// On Erlang this will be the atom `nil`, on JavaScript this will be +/// `undefined`. +/// +pub fn nil() -> Dynamic { + cast(Nil) +} + +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +fn cast(a: anything) -> Dynamic diff --git a/build/packages/gleam_stdlib/src/gleam/dynamic/decode.gleam b/build/packages/gleam_stdlib/src/gleam/dynamic/decode.gleam new file mode 100644 index 0000000..03ee2ac --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/dynamic/decode.gleam @@ -0,0 +1,1061 @@ +//// The `Dynamic` type is used to represent dynamically typed data. That is, data +//// that we don't know the precise type of yet, so we need to introspect the data to +//// see if it is of the desired type before we can use it. Typically data like this +//// would come from user input or from untyped languages such as Erlang or JavaScript. +//// +//// This module provides the `Decoder` type and associated functions, which provides +//// a type-safe and composable way to convert dynamic data into some desired type, +//// or into errors if the data doesn't have the desired structure. +//// +//// The `Decoder` type is generic and has 1 type parameter, which is the type that +//// it attempts to decode. A `Decoder(String)` can be used to decode strings, and a +//// `Decoder(Option(Int))` can be used to decode `Option(Int)`s +//// +//// Decoders work using _runtime reflection_ and the data structures of the target +//// platform. Differences between Erlang and JavaScript data structures may impact +//// your decoders, so it is important to test your decoders on all supported +//// platforms. +//// +//// The decoding technique used by this module was inspired by Juraj Petráš' +//// [Toy](https://github.com/Hackder/toy), Go's `encoding/json`, and Elm's +//// `Json.Decode`. Thank you to them! +//// +//// # Examples +//// +//// Dynamic data may come from various sources and so many different syntaxes could +//// be used to describe or construct them. In these examples a pseudocode +//// syntax is used to describe the data. +//// +//// ## Simple types +//// +//// This module defines decoders for simple data types such as [`string`](#string), +//// [`int`](#int), [`float`](#float), [`bit_array`](#bit_array), and [`bool`](#bool). +//// +//// ```gleam +//// // Data: +//// // "Hello, Joe!" +//// +//// let result = decode.run(data, decode.string) +//// assert result == Ok("Hello, Joe!") +//// ``` +//// +//// ## Lists +//// +//// The [`list`](#list) decoder decodes `List`s. To use it you must construct it by +//// passing in another decoder into the `list` function, which is the decoder that +//// is to be used for the elements of the list, type checking both the list and its +//// elements. +//// +//// ```gleam +//// // Data: +//// // [1, 2, 3, 4] +//// +//// let result = decode.run(data, decode.list(decode.int)) +//// assert result == Ok([1, 2, 3, 4]) +//// ``` +//// +//// On Erlang this decoder can decode from lists, and on JavaScript it can +//// decode from lists as well as JavaScript arrays. +//// +//// ## Options +//// +//// The [`optional`](#optional) decoder is used to decode values that may or may not +//// be present. In other environment these might be called "nullable" values. +//// +//// Like the `list` decoder, the `optional` decoder takes another decoder, +//// which is used to decode the value if it is present. +//// +//// ```gleam +//// // Data: +//// // 12.45 +//// +//// let result = decode.run(data, decode.optional(decode.float)) +//// assert result == Ok(option.Some(12.45)) +//// ``` +//// ```gleam +//// // Data: +//// // null +//// +//// let result = decode.run(data, decode.optional(decode.int)) +//// assert result == Ok(option.None) +//// ``` +//// +//// This decoder knows how to handle multiple different runtime representations of +//// absent values, including `Nil`, `None`, `null`, and `undefined`. +//// +//// ## Dicts +//// +//// The [`dict`](#dict) decoder decodes `Dicts` and contains two other decoders, one +//// for the keys, one for the values. +//// +//// ```gleam +//// // Data: +//// // { "Lucy" -> 10, "Nubi" -> 20 } +//// +//// let result = decode.run(data, decode.dict(decode.string, decode.int)) +//// assert result == Ok(dict.from_list([ +//// #("Lucy", 10), +//// #("Nubi", 20), +//// ])) +//// ``` +//// +//// ## Indexing objects +//// +//// The [`at`](#at) decoder can be used to decode a value that is nested within +//// key-value containers such as Gleam dicts, Erlang maps, or JavaScript objects. +//// +//// ```gleam +//// // Data: +//// // { "one" -> { "two" -> 123 } } +//// +//// let result = decode.run(data, decode.at(["one", "two"], decode.int)) +//// assert result == Ok(123) +//// ``` +//// +//// ## Indexing arrays +//// +//// If you use ints as keys then the [`at`](#at) decoder can be used to index into +//// array-like containers such as Gleam or Erlang tuples, or JavaScript arrays. +//// +//// ```gleam +//// // Data: +//// // ["one", "two", "three"] +//// +//// let result = decode.run(data, decode.at([1], decode.string)) +//// assert result == Ok("two") +//// ``` +//// +//// ## Records +//// +//// Decoding records from dynamic data is more complex and requires combining a +//// decoder for each field and a special constructor that builds your records with +//// the decoded field values. +//// +//// ```gleam +//// // Data: +//// // { +//// // "score" -> 180, +//// // "name" -> "Mel Smith", +//// // "is-admin" -> false, +//// // "enrolled" -> true, +//// // "colour" -> "Red", +//// // } +//// +//// let decoder = { +//// use name <- decode.field("name", decode.string) +//// use score <- decode.field("score", decode.int) +//// use colour <- decode.field("colour", decode.string) +//// use enrolled <- decode.field("enrolled", decode.bool) +//// decode.success(Player(name:, score:, colour:, enrolled:)) +//// } +//// +//// let result = decode.run(data, decoder) +//// assert result == Ok(Player("Mel Smith", 180, "Red", True)) +//// ``` +//// +//// ## Enum variants +//// +//// Imagine you have a custom type where all the variants do not contain any values. +//// +//// ```gleam +//// pub type PocketMonsterType { +//// Fire +//// Water +//// Grass +//// Electric +//// } +//// ``` +//// +//// You might choose to encode these variants as strings, `"fire"` for `Fire`, +//// `"water"` for `Water`, and so on. To decode them you'll need to decode the dynamic +//// data as a string, but then you'll need to decode it further still as not all +//// strings are valid values for the enum. This can be done with the `then` +//// function, which enables running a second decoder after the first one +//// succeeds. +//// +//// ```gleam +//// let decoder = { +//// use decoded_string <- decode.then(decode.string) +//// case decoded_string { +//// // Return succeeding decoders for valid strings +//// "fire" -> decode.success(Fire) +//// "water" -> decode.success(Water) +//// "grass" -> decode.success(Grass) +//// "electric" -> decode.success(Electric) +//// // Return a failing decoder for any other strings +//// _ -> decode.failure(Fire, "PocketMonsterType") +//// } +//// } +//// +//// let result = decode.run(dynamic.string("water"), decoder) +//// assert result == Ok(Water) +//// +//// let result = decode.run(dynamic.string("wobble"), decoder) +//// assert result == Error([DecodeError("PocketMonsterType", "String", [])]) +//// ``` +//// +//// ## Record variants +//// +//// Decoding type variants that contain other values is done by combining the +//// techniques from the "enum variants" and "records" examples. Imagine you have +//// this custom type that you want to decode: +//// +//// ```gleam +//// pub type PocketMonsterPerson { +//// Trainer(name: String, badge_count: Int) +//// GymLeader(name: String, speciality: PocketMonsterType) +//// } +//// ``` +//// And you would like to be able to decode these from dynamic data like this: +//// ```erlang +//// { +//// "type" -> "trainer", +//// "name" -> "Ash", +//// "badge-count" -> 1, +//// } +//// ``` +//// ```erlang +//// { +//// "type" -> "gym-leader", +//// "name" -> "Misty", +//// "speciality" -> "water", +//// } +//// ``` +//// +//// Notice how both documents have a `"type"` field, which is used to indicate which +//// variant the data is for. +//// +//// First, define decoders for each of the variants: +//// +//// ```gleam +//// let trainer_decoder = { +//// use name <- decode.field("name", decode.string) +//// use badge_count <- decode.field("badge-count", decode.int) +//// decode.success(Trainer(name, badge_count)) +//// } +//// +//// let gym_leader_decoder = { +//// use name <- decode.field("name", decode.string) +//// use speciality <- decode.field("speciality", pocket_monster_type_decoder) +//// decode.success(GymLeader(name, speciality)) +//// } +//// ``` +//// +//// A third decoder can be used to extract and decode the `"type"` field, and the +//// expression can evaluate to whichever decoder is suitable for the document. +//// +//// ```gleam +//// // Data: +//// // { +//// // "type" -> "gym-leader", +//// // "name" -> "Misty", +//// // "speciality" -> "water", +//// // } +//// +//// let decoder = { +//// use tag <- decode.field("type", decode.string) +//// case tag { +//// "gym-leader" -> gym_leader_decoder +//// _ -> trainer_decoder +//// } +//// } +//// +//// let result = decode.run(data, decoder) +//// assert result == Ok(GymLeader("Misty", Water)) +//// ``` + +import gleam/bit_array +import gleam/dict.{type Dict} +import gleam/dynamic +import gleam/int +import gleam/list +import gleam/option.{type Option, None, Some} + +/// `Dynamic` data is data that we don't know the type of yet, originating from +/// external untyped systems. +/// +/// You should never be converting your well typed data to dynamic data. +/// +pub type Dynamic = + dynamic.Dynamic + +/// Error returned when unexpected data is encountered +/// +pub type DecodeError { + DecodeError(expected: String, found: String, path: List(String)) +} + +/// A decoder is a value that can be used to turn dynamically typed `Dynamic` +/// data into typed data using the `run` function. +/// +/// Several smaller decoders can be combined to make larger decoders using +/// functions such as `list` and `field`. +/// +pub opaque type Decoder(t) { + Decoder(function: fn(Dynamic) -> #(t, List(DecodeError))) +} + +/// The same as [`field`](#field), except taking a path to the value rather +/// than a field name. +/// +/// This function will index into dictionaries with any key type, and if the key is +/// an int then it'll also index into Erlang tuples and JavaScript arrays, and +/// the first eight elements of Gleam lists. +/// +/// # Examples +/// +/// ```gleam +/// let data = dynamic.properties([ +/// #(dynamic.string("data"), dynamic.properties([ +/// #(dynamic.string("email"), dynamic.string("lucy@example.com")), +/// #(dynamic.string("name"), dynamic.string("Lucy")), +/// ]) +/// ])) +/// +/// let decoder = { +/// use name <- decode.subfield(["data", "name"], decode.string) +/// use email <- decode.subfield(["data", "email"], decode.string) +/// decode.success(SignUp(name: name, email: email)) +/// } +/// let result = decode.run(data, decoder) +/// assert result == Ok(SignUp(name: "Lucy", email: "lucy@example.com")) +/// ``` +/// +pub fn subfield( + field_path: List(name), + field_decoder: Decoder(t), + next: fn(t) -> Decoder(final), +) -> Decoder(final) { + Decoder(function: fn(data) { + let #(out, errors1) = + index(field_path, [], field_decoder.function, data, fn(data, position) { + let #(default, _) = field_decoder.function(data) + #(default, [DecodeError("Field", "Nothing", [])]) + |> push_path(list.reverse(position)) + }) + let #(out, errors2) = next(out).function(data) + #(out, list.append(errors1, errors2)) + }) +} + +/// Run a decoder on a `Dynamic` value, decoding the value if it is of the +/// desired type, or returning errors. +/// +/// # Examples +/// +/// ```gleam +/// let decoder = { +/// use name <- decode.field("email", decode.string) +/// use email <- decode.field("password", decode.string) +/// decode.success(SignUp(name: name, email: email)) +/// } +/// +/// decode.run(data, decoder) +/// ``` +/// +pub fn run(data: Dynamic, decoder: Decoder(t)) -> Result(t, List(DecodeError)) { + let #(maybe_invalid_data, errors) = decoder.function(data) + case errors { + [] -> Ok(maybe_invalid_data) + [_, ..] -> Error(errors) + } +} + +/// A decoder that decodes a value that is nested within other values. For +/// example, decoding a value that is within some deeply nested JSON objects. +/// +/// This function will index into dictionaries with any key type, and if the key is +/// an int then it'll also index into Erlang tuples and JavaScript arrays, and +/// the first eight elements of Gleam lists. +/// +/// # Examples +/// +/// ```gleam +/// let decoder = decode.at(["one", "two"], decode.int) +/// +/// let data = dynamic.properties([ +/// #(dynamic.string("one"), dynamic.properties([ +/// #(dynamic.string("two"), dynamic.int(1000)), +/// ])), +/// ])) +/// +/// +/// decode.run(data, decoder) +/// // -> Ok(1000) +/// ``` +/// +/// ```gleam +/// dynamic.nil() +/// |> decode.run(decode.optional(decode.int)) +/// // -> Ok(option.None) +/// ``` +/// +pub fn at(path: List(segment), inner: Decoder(a)) -> Decoder(a) { + Decoder(function: fn(data) { + index(path, [], inner.function, data, fn(data, position) { + let #(default, _) = inner.function(data) + #(default, [DecodeError("Field", "Nothing", [])]) + |> push_path(list.reverse(position)) + }) + }) +} + +fn index( + path: List(a), + position: List(a), + inner: fn(Dynamic) -> #(b, List(DecodeError)), + data: Dynamic, + handle_miss: fn(Dynamic, List(a)) -> #(b, List(DecodeError)), +) -> #(b, List(DecodeError)) { + case path { + [] -> { + data + |> inner + |> push_path(list.reverse(position)) + } + + [key, ..path] -> { + case bare_index(data, key) { + Ok(Some(data)) -> { + index(path, [key, ..position], inner, data, handle_miss) + } + Ok(None) -> { + handle_miss(data, [key, ..position]) + } + Error(kind) -> { + let #(default, _) = inner(data) + #(default, [DecodeError(kind, dynamic.classify(data), [])]) + |> push_path(list.reverse(position)) + } + } + } + } +} + +@external(erlang, "gleam_stdlib", "index") +@external(javascript, "../../gleam_stdlib.mjs", "index") +fn bare_index(data: Dynamic, key: anything) -> Result(Option(Dynamic), String) + +fn push_path( + layer: #(t, List(DecodeError)), + path: List(key), +) -> #(t, List(DecodeError)) { + let decoder = one_of(string, [int |> map(int.to_string)]) + let path = + list.map(path, fn(key) { + let key = cast(key) + case run(key, decoder) { + Ok(key) -> key + Error(_) -> "<" <> dynamic.classify(key) <> ">" + } + }) + let errors = + list.map(layer.1, fn(error) { + DecodeError(..error, path: list.append(path, error.path)) + }) + #(layer.0, errors) +} + +/// Finalise a decoder having successfully extracted a value. +/// +/// # Examples +/// +/// ```gleam +/// let data = dynamic.properties([ +/// #(dynamic.string("email"), dynamic.string("lucy@example.com")), +/// #(dynamic.string("name"), dynamic.string("Lucy")), +/// ])) +/// +/// let decoder = { +/// use name <- decode.field("name", string) +/// use email <- decode.field("email", string) +/// decode.success(SignUp(name: name, email: email)) +/// } +/// +/// let result = decode.run(data, decoder) +/// assert result == Ok(SignUp(name: "Lucy", email: "lucy@example.com")) +/// ``` +/// +pub fn success(data: t) -> Decoder(t) { + Decoder(function: fn(_) { #(data, []) }) +} + +/// Construct a decode error for some unexpected dynamic data. +/// +pub fn decode_error( + expected expected: String, + found found: Dynamic, +) -> List(DecodeError) { + [DecodeError(expected: expected, found: dynamic.classify(found), path: [])] +} + +/// Run a decoder on a field of a `Dynamic` value, decoding the value if it is +/// of the desired type, or returning errors. An error is returned if there is +/// no field for the specified key. +/// +/// This function will index into dictionaries with any key type, and if the key is +/// an int then it'll also index into Erlang tuples and JavaScript arrays, and +/// the first eight elements of Gleam lists. +/// +/// # Examples +/// +/// ```gleam +/// let data = dynamic.properties([ +/// #(dynamic.string("email"), dynamic.string("lucy@example.com")), +/// #(dynamic.string("name"), dynamic.string("Lucy")), +/// ])) +/// +/// let decoder = { +/// use name <- decode.field("name", string) +/// use email <- decode.field("email", string) +/// decode.success(SignUp(name: name, email: email)) +/// } +/// +/// let result = decode.run(data, decoder) +/// assert result == Ok(SignUp(name: "Lucy", email: "lucy@example.com")) +/// ``` +/// +/// If you wish to decode a value that is more deeply nested within the dynamic +/// data, see [`subfield`](#subfield) and [`at`](#at). +/// +/// If you wish to return a default in the event that a field is not present, +/// see [`optional_field`](#optional_field) and / [`optionally_at`](#optionally_at). +/// +pub fn field( + field_name: name, + field_decoder: Decoder(t), + next: fn(t) -> Decoder(final), +) -> Decoder(final) { + subfield([field_name], field_decoder, next) +} + +/// Run a decoder on a field of a `Dynamic` value, decoding the value if it is +/// of the desired type, or returning errors. The given default value is +/// returned if there is no field for the specified key. +/// +/// This function will index into dictionaries with any key type, and if the key is +/// an int then it'll also index into Erlang tuples and JavaScript arrays, and +/// the first eight elements of Gleam lists. +/// +/// # Examples +/// +/// ```gleam +/// let data = dynamic.properties([ +/// #(dynamic.string("name"), dynamic.string("Lucy")), +/// ])) +/// +/// let decoder = { +/// use name <- decode.field("name", string) +/// use email <- decode.optional_field("email", "n/a", string) +/// decode.success(SignUp(name: name, email: email)) +/// } +/// +/// let result = decode.run(data, decoder) +/// assert result == Ok(SignUp(name: "Lucy", email: "n/a")) +/// ``` +/// +pub fn optional_field( + key: name, + default: t, + field_decoder: Decoder(t), + next: fn(t) -> Decoder(final), +) -> Decoder(final) { + Decoder(function: fn(data) { + let #(out, errors1) = + case bare_index(data, key) { + Ok(Some(data)) -> field_decoder.function(data) + Ok(None) -> #(default, []) + Error(kind) -> #(default, [ + DecodeError(kind, dynamic.classify(data), []), + ]) + } + |> push_path([key]) + let #(out, errors2) = next(out).function(data) + #(out, list.append(errors1, errors2)) + }) +} + +/// A decoder that decodes a value that is nested within other values. For +/// example, decoding a value that is within some deeply nested JSON objects. +/// +/// This function will index into dictionaries with any key type, and if the key is +/// an int then it'll also index into Erlang tuples and JavaScript arrays, and +/// the first eight elements of Gleam lists. +/// +/// # Examples +/// +/// ```gleam +/// let decoder = decode.optionally_at(["one", "two"], 100, decode.int) +/// +/// let data = dynamic.properties([ +/// #(dynamic.string("one"), dynamic.properties([])), +/// ])) +/// +/// +/// decode.run(data, decoder) +/// // -> Ok(100) +/// ``` +/// +pub fn optionally_at( + path: List(segment), + default: a, + inner: Decoder(a), +) -> Decoder(a) { + Decoder(function: fn(data) { + index(path, [], inner.function, data, fn(_, _) { #(default, []) }) + }) +} + +fn run_dynamic_function( + data: Dynamic, + name: String, + f: fn(Dynamic) -> Result(t, t), +) -> #(t, List(DecodeError)) { + case f(data) { + Ok(data) -> #(data, []) + Error(zero) -> #(zero, [DecodeError(name, dynamic.classify(data), [])]) + } +} + +/// A decoder that decodes `String` values. +/// +/// # Examples +/// +/// ```gleam +/// let result = decode.run(dynamic.string("Hello!"), decode.string) +/// assert result == Ok("Hello!") +/// ``` +/// +pub const string: Decoder(String) = Decoder(decode_string) + +fn decode_string(data: Dynamic) -> #(String, List(DecodeError)) { + run_dynamic_function(data, "String", dynamic_string) +} + +@external(javascript, "../../gleam_stdlib.mjs", "string") +fn dynamic_string(from data: Dynamic) -> Result(String, String) { + case dynamic_bit_array(data) { + Ok(data) -> + case bit_array.to_string(data) { + Ok(string) -> Ok(string) + Error(_) -> Error("") + } + Error(_) -> Error("") + } +} + +/// A decoder that decodes `Bool` values. +/// +/// # Examples +/// +/// ```gleam +/// let result = decode.run(dynamic.bool(True), decode.bool) +/// assert result == Ok(True) +/// ``` +/// +pub const bool: Decoder(Bool) = Decoder(decode_bool) + +fn decode_bool(data: Dynamic) -> #(Bool, List(DecodeError)) { + case cast(True) == data { + True -> #(True, []) + False -> + case cast(False) == data { + True -> #(False, []) + False -> #(False, decode_error("Bool", data)) + } + } +} + +/// A decoder that decodes `Int` values. +/// +/// # Examples +/// +/// ```gleam +/// let result = decode.run(dynamic.int(147), decode.int) +/// assert result == Ok(147) +/// ``` +/// +pub const int: Decoder(Int) = Decoder(decode_int) + +fn decode_int(data: Dynamic) -> #(Int, List(DecodeError)) { + run_dynamic_function(data, "Int", dynamic_int) +} + +@external(erlang, "gleam_stdlib", "int") +@external(javascript, "../../gleam_stdlib.mjs", "int") +fn dynamic_int(data: Dynamic) -> Result(Int, Int) + +/// A decoder that decodes `Float` values. +/// +/// # Examples +/// +/// ```gleam +/// let result = decode.run(dynamic.float(3.14), decode.float) +/// assert result == Ok(3.14) +/// ``` +/// +pub const float: Decoder(Float) = Decoder(decode_float) + +fn decode_float(data: Dynamic) -> #(Float, List(DecodeError)) { + run_dynamic_function(data, "Float", dynamic_float) +} + +@external(erlang, "gleam_stdlib", "float") +@external(javascript, "../../gleam_stdlib.mjs", "float") +fn dynamic_float(data: Dynamic) -> Result(Float, Float) + +/// A decoder that decodes `Dynamic` values. This decoder never returns an error. +/// +/// # Examples +/// +/// ```gleam +/// let result = decode.run(dynamic.float(3.14), decode.dynamic) +/// assert result == Ok(dynamic.float(3.14)) +/// ``` +/// +pub const dynamic: Decoder(Dynamic) = Decoder(decode_dynamic) + +fn decode_dynamic(data: Dynamic) -> #(Dynamic, List(DecodeError)) { + #(data, []) +} + +/// A decoder that decodes `BitArray` values. This decoder never returns an error. +/// +/// # Examples +/// +/// ```gleam +/// let result = decode.run(dynamic.bit_array(<<5, 7>>), decode.bit_array) +/// assert result == Ok(<<5, 7>>) +/// ``` +/// +pub const bit_array: Decoder(BitArray) = Decoder(decode_bit_array) + +fn decode_bit_array(data: Dynamic) -> #(BitArray, List(DecodeError)) { + run_dynamic_function(data, "BitArray", dynamic_bit_array) +} + +@external(erlang, "gleam_stdlib", "bit_array") +@external(javascript, "../../gleam_stdlib.mjs", "bit_array") +fn dynamic_bit_array(data: Dynamic) -> Result(BitArray, BitArray) + +/// A decoder that decodes lists where all elements are decoded with a given +/// decoder. +/// +/// # Examples +/// +/// ```gleam +/// let result = +/// [1, 2, 3] +/// |> list.map(dynamic.int) +/// |> dynamic.list +/// |> decode.run(decode.list(of: decode.int)) +/// assert result == Ok([1, 2, 3]) +/// ``` +/// +pub fn list(of inner: Decoder(a)) -> Decoder(List(a)) { + Decoder(fn(data) { + decode_list(data, inner.function, fn(p, k) { push_path(p, [k]) }, 0, []) + }) +} + +@external(erlang, "gleam_stdlib", "list") +@external(javascript, "../../gleam_stdlib.mjs", "list") +fn decode_list( + data: Dynamic, + item: fn(Dynamic) -> #(t, List(DecodeError)), + push_path: fn(#(t, List(DecodeError)), key) -> #(t, List(DecodeError)), + index: Int, + acc: List(t), +) -> #(List(t), List(DecodeError)) + +/// A decoder that decodes dicts where all keys and vales are decoded with +/// given decoders. +/// +/// # Examples +/// +/// ```gleam +/// let values = dynamic.properties([ +/// #(dynamic.string("one"), dynamic.int(1)), +/// #(dynamic.string("two"), dynamic.int(2)), +/// ]) +/// +/// let result = +/// decode.run(values, decode.dict(decode.string, decode.int)) +/// assert result == Ok(values) +/// ``` +/// +pub fn dict( + key: Decoder(key), + value: Decoder(value), +) -> Decoder(Dict(key, value)) { + Decoder(fn(data) { + case decode_dict(data) { + Error(_) -> #(dict.new(), decode_error("Dict", data)) + Ok(dict) -> + dict.fold(dict, #(dict.new(), []), fn(a, k, v) { + // If there are any errors from previous key-value pairs then we + // don't need to run the decoders, instead return the existing acc. + case a.1 { + [] -> fold_dict(a, k, v, key.function, value.function) + [_, ..] -> a + } + }) + } + }) +} + +fn fold_dict( + acc: #(Dict(k, v), List(DecodeError)), + key: Dynamic, + value: Dynamic, + key_decoder: fn(Dynamic) -> #(k, List(DecodeError)), + value_decoder: fn(Dynamic) -> #(v, List(DecodeError)), +) -> #(Dict(k, v), List(DecodeError)) { + // First we decode the key. + case key_decoder(key) { + #(key, []) -> + // Then we decode the value. + case value_decoder(value) { + #(value, []) -> { + // It worked! Insert the new key-value pair so we can move onto the next. + let dict = dict.insert(acc.0, key, value) + #(dict, acc.1) + } + #(_, errors) -> push_path(#(dict.new(), errors), ["values"]) + } + #(_, errors) -> push_path(#(dict.new(), errors), ["keys"]) + } +} + +@external(erlang, "gleam_stdlib", "dict") +@external(javascript, "../../gleam_stdlib.mjs", "dict") +fn decode_dict(data: Dynamic) -> Result(Dict(Dynamic, Dynamic), Nil) + +/// A decoder that decodes nullable values of a type decoded by with a given +/// decoder. +/// +/// This function can handle common representations of null on all runtimes, such as +/// `nil`, `null`, and `undefined` on Erlang, and `undefined` and `null` on +/// JavaScript. +/// +/// # Examples +/// +/// ```gleam +/// let result = decode.run(dynamic.int(100), decode.optional(decode.int)) +/// assert result == Ok(option.Some(100)) +/// ``` +/// +/// ```gleam +/// let result = decode.run(dynamic.nil(), decode.optional(decode.int)) +/// assert result == Ok(option.None) +/// ``` +/// +pub fn optional(inner: Decoder(a)) -> Decoder(Option(a)) { + Decoder(function: fn(data) { + case is_null(data) { + True -> #(option.None, []) + False -> { + let #(data, errors) = inner.function(data) + #(option.Some(data), errors) + } + } + }) +} + +/// Apply a transformation function to any value decoded by the decoder. +/// +/// # Examples +/// +/// ```gleam +/// let decoder = decode.int |> decode.map(int.to_string) +/// let result = decode.run(dynamic.int(1000), decoder) +/// assert result == Ok("1000") +/// ``` +/// +pub fn map(decoder: Decoder(a), transformer: fn(a) -> b) -> Decoder(b) { + Decoder(function: fn(d) { + let #(data, errors) = decoder.function(d) + #(transformer(data), errors) + }) +} + +/// Apply a transformation function to any errors returned by the decoder. +/// +pub fn map_errors( + decoder: Decoder(a), + transformer: fn(List(DecodeError)) -> List(DecodeError), +) -> Decoder(a) { + Decoder(function: fn(d) { + let #(data, errors) = decoder.function(d) + #(data, transformer(errors)) + }) +} + +/// Replace all errors produced by a decoder with one single error for a named +/// expected type. +/// +/// This function may be useful if you wish to simplify errors before +/// presenting them to a user, particularly when using the `one_of` function. +/// +/// # Examples +/// +/// ```gleam +/// let decoder = decode.string |> decode.collapse_errors("MyThing") +/// let result = decode.run(dynamic.int(1000), decoder) +/// assert result == Error([DecodeError("MyThing", "Int", [])]) +/// ``` +/// +pub fn collapse_errors(decoder: Decoder(a), name: String) -> Decoder(a) { + Decoder(function: fn(dynamic_data) { + let #(data, errors) as layer = decoder.function(dynamic_data) + case errors { + [] -> layer + [_, ..] -> #(data, decode_error(name, dynamic_data)) + } + }) +} + +/// Create a new decoder based upon the value of a previous decoder. +/// +/// This may be useful to run one previous decoder to use in further decoding. +/// +pub fn then(decoder: Decoder(a), next: fn(a) -> Decoder(b)) -> Decoder(b) { + Decoder(function: fn(dynamic_data) { + let #(data, errors) = decoder.function(dynamic_data) + let decoder = next(data) + let #(data, _) as layer = decoder.function(dynamic_data) + case errors { + [] -> layer + [_, ..] -> #(data, errors) + } + }) +} + +/// Create a new decoder from several other decoders. Each of the inner +/// decoders is run in turn, and the value from the first to succeed is used. +/// +/// If no decoder succeeds then the errors from the first decoder is used. +/// If you wish for different errors then you may wish to use the +/// `collapse_errors` or `map_errors` functions. +/// +/// # Examples +/// +/// ```gleam +/// let decoder = decode.one_of(decode.string, or: [ +/// decode.int |> decode.map(int.to_string), +/// decode.float |> decode.map(float.to_string), +/// ]) +/// decode.run(dynamic.int(1000), decoder) +/// // -> Ok("1000") +/// ``` +/// +pub fn one_of( + first: Decoder(a), + or alternatives: List(Decoder(a)), +) -> Decoder(a) { + Decoder(function: fn(dynamic_data) { + let #(_, errors) as layer = first.function(dynamic_data) + case errors { + [] -> layer + [_, ..] -> run_decoders(dynamic_data, layer, alternatives) + } + }) +} + +fn run_decoders( + data: Dynamic, + failure: #(a, List(DecodeError)), + decoders: List(Decoder(a)), +) -> #(a, List(DecodeError)) { + case decoders { + [] -> failure + + [decoder, ..decoders] -> { + let #(_, errors) as layer = decoder.function(data) + case errors { + [] -> layer + [_, ..] -> run_decoders(data, failure, decoders) + } + } + } +} + +/// Define a decoder that always fails. The parameter for this function is the +/// name of the type that has failed to decode. +/// +pub fn failure(zero: a, expected: String) -> Decoder(a) { + Decoder(function: fn(d) { #(zero, decode_error(expected, d)) }) +} + +/// Create a decoder for a new data type from a decoding function. +/// +/// This function is used for new primitive types. For example, you might +/// define a decoder for Erlang's pid type. +/// +/// A default "zero" value is also required to make a decoder. When this +/// decoder is used as part of a larger decoder this zero value used as +/// a placeholder so that the rest of the decoder can continue to run and +/// collect all decoding errors. +/// +/// If you were to make a decoder for the `String` type (rather than using the +/// build-in `string` decoder) you would define it like so: +/// +/// ```gleam +/// pub fn string_decoder() -> decode.Decoder(String) { +/// let default = "" +/// decode.new_primitive_decoder("String", fn(data) { +/// case dynamic.string(data) { +/// Ok(x) -> Ok(x) +/// Error(_) -> Error(default) +/// } +/// }) +/// } +/// ``` +/// +pub fn new_primitive_decoder( + name: String, + decoding_function: fn(Dynamic) -> Result(t, t), +) -> Decoder(t) { + Decoder(function: fn(d) { + case decoding_function(d) { + Ok(t) -> #(t, []) + Error(zero) -> #(zero, [DecodeError(name, dynamic.classify(d), [])]) + } + }) +} + +/// Create a decoder that can refer to itself, useful for decoding deeply +/// nested data. +/// +/// Attempting to create a recursive decoder without this function could result +/// in an infinite loop. If you are using `field` or other `use`able functions +/// then you may not need to use this function. +/// +/// ```gleam +/// type Nested { +/// Nested(List(Nested)) +/// Value(String) +/// } +/// +/// fn nested_decoder() -> decode.Decoder(Nested) { +/// use <- decode.recursive +/// decode.one_of(decode.string |> decode.map(Value), [ +/// decode.list(nested_decoder()) |> decode.map(Nested), +/// ]) +/// } +/// ``` +/// +pub fn recursive(inner: fn() -> Decoder(a)) -> Decoder(a) { + Decoder(function: fn(data) { + let decoder = inner() + decoder.function(data) + }) +} + +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../../gleam_stdlib.mjs", "identity") +fn cast(a: anything) -> Dynamic + +@external(erlang, "gleam_stdlib", "is_null") +@external(javascript, "../../gleam_stdlib.mjs", "is_null") +fn is_null(a: Dynamic) -> Bool diff --git a/build/packages/gleam_stdlib/src/gleam/float.gleam b/build/packages/gleam_stdlib/src/gleam/float.gleam new file mode 100644 index 0000000..83bfa6e --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/float.gleam @@ -0,0 +1,661 @@ +//// Functions for working with floats. +//// +//// ## Float representation +//// +//// Floats are represented as 64 bit floating point numbers on both the Erlang +//// and JavaScript runtimes. The floating point behaviour is native to their +//// respective runtimes, so their exact behaviour will be slightly different on +//// the two runtimes. +//// +//// ### Infinity and NaN +//// +//// Under the JavaScript runtime, exceeding the maximum (or minimum) +//// representable value for a floating point value will result in Infinity (or +//// -Infinity). Should you try to divide two infinities you will get NaN as a +//// result. +//// +//// When running on BEAM, exceeding the maximum (or minimum) representable +//// value for a floating point value will raise an error. +//// +//// ## Division by zero +//// +//// Gleam runs on the Erlang virtual machine, which does not follow the IEEE +//// 754 standard for floating point arithmetic and does not have an `Infinity` +//// value. In Erlang division by zero results in a crash, however Gleam does +//// not have partial functions and operators in core so instead division by zero +//// returns zero, a behaviour taken from Pony, Coq, and Lean. +//// +//// This may seem unexpected at first, but it is no less mathematically valid +//// than crashing or returning a special value. Division by zero is undefined +//// in mathematics. + +import gleam/order.{type Order} + +/// Attempts to parse a string as a `Float`, returning `Error(Nil)` if it was +/// not possible. +/// +/// ## Examples +/// +/// ```gleam +/// parse("2.3") +/// // -> Ok(2.3) +/// ``` +/// +/// ```gleam +/// parse("ABC") +/// // -> Error(Nil) +/// ``` +/// +@external(erlang, "gleam_stdlib", "parse_float") +@external(javascript, "../gleam_stdlib.mjs", "parse_float") +pub fn parse(string: String) -> Result(Float, Nil) + +/// Returns the string representation of the provided `Float`. +/// +/// ## Examples +/// +/// ```gleam +/// to_string(2.3) +/// // -> "2.3" +/// ``` +/// +@external(erlang, "gleam_stdlib", "float_to_string") +@external(javascript, "../gleam_stdlib.mjs", "float_to_string") +pub fn to_string(x: Float) -> String + +/// Restricts a `Float` between a lower and upper bound. +/// +/// ## Examples +/// +/// ```gleam +/// clamp(1.2, min: 1.4, max: 1.6) +/// // -> 1.4 +/// ``` +/// +pub fn clamp(x: Float, min min_bound: Float, max max_bound: Float) -> Float { + x + |> min(max_bound) + |> max(min_bound) +} + +/// Compares two `Float`s, returning an `Order`: +/// `Lt` for lower than, `Eq` for equals, or `Gt` for greater than. +/// +/// ## Examples +/// +/// ```gleam +/// compare(2.0, 2.3) +/// // -> Lt +/// ``` +/// +/// To handle +/// [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems) +/// you may use [`loosely_compare`](#loosely_compare) instead. +/// +pub fn compare(a: Float, with b: Float) -> Order { + case a == b { + True -> order.Eq + False -> + case a <. b { + True -> order.Lt + False -> order.Gt + } + } +} + +/// Compares two `Float`s within a tolerance, returning an `Order`: +/// `Lt` for lower than, `Eq` for equals, or `Gt` for greater than. +/// +/// This function allows Float comparison while handling +/// [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems). +/// +/// Notice: For `Float`s the tolerance won't be exact: +/// `5.3 - 5.0` is not exactly `0.3`. +/// +/// ## Examples +/// +/// ```gleam +/// loosely_compare(5.0, with: 5.3, tolerating: 0.5) +/// // -> Eq +/// ``` +/// +/// If you want to check only for equality you may use +/// [`loosely_equals`](#loosely_equals) instead. +/// +pub fn loosely_compare( + a: Float, + with b: Float, + tolerating tolerance: Float, +) -> Order { + let difference = absolute_value(a -. b) + case difference <=. tolerance { + True -> order.Eq + False -> compare(a, b) + } +} + +/// Checks for equality of two `Float`s within a tolerance, +/// returning an `Bool`. +/// +/// This function allows Float comparison while handling +/// [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems). +/// +/// Notice: For `Float`s the tolerance won't be exact: +/// `5.3 - 5.0` is not exactly `0.3`. +/// +/// ## Examples +/// +/// ```gleam +/// loosely_equals(5.0, with: 5.3, tolerating: 0.5) +/// // -> True +/// ``` +/// +/// ```gleam +/// loosely_equals(5.0, with: 5.1, tolerating: 0.1) +/// // -> False +/// ``` +/// +pub fn loosely_equals( + a: Float, + with b: Float, + tolerating tolerance: Float, +) -> Bool { + let difference = absolute_value(a -. b) + difference <=. tolerance +} + +/// Compares two `Float`s, returning the smaller of the two. +/// +/// ## Examples +/// +/// ```gleam +/// min(2.0, 2.3) +/// // -> 2.0 +/// ``` +/// +pub fn min(a: Float, b: Float) -> Float { + case a <. b { + True -> a + False -> b + } +} + +/// Compares two `Float`s, returning the larger of the two. +/// +/// ## Examples +/// +/// ```gleam +/// max(2.0, 2.3) +/// // -> 2.3 +/// ``` +/// +pub fn max(a: Float, b: Float) -> Float { + case a >. b { + True -> a + False -> b + } +} + +/// Rounds the value to the next highest whole number as a `Float`. +/// +/// ## Examples +/// +/// ```gleam +/// ceiling(2.3) +/// // -> 3.0 +/// ``` +/// +@external(erlang, "math", "ceil") +@external(javascript, "../gleam_stdlib.mjs", "ceiling") +pub fn ceiling(x: Float) -> Float + +/// Rounds the value to the next lowest whole number as a `Float`. +/// +/// ## Examples +/// +/// ```gleam +/// floor(2.3) +/// // -> 2.0 +/// ``` +/// +@external(erlang, "math", "floor") +@external(javascript, "../gleam_stdlib.mjs", "floor") +pub fn floor(x: Float) -> Float + +/// Rounds the value to the nearest whole number as an `Int`. +/// +/// ## Examples +/// +/// ```gleam +/// round(2.3) +/// // -> 2 +/// ``` +/// +/// ```gleam +/// round(2.5) +/// // -> 3 +/// ``` +/// +@external(erlang, "erlang", "round") +pub fn round(x: Float) -> Int { + case x >=. 0.0 { + True -> js_round(x) + False -> 0 - js_round(negate(x)) + } +} + +@external(javascript, "../gleam_stdlib.mjs", "round") +fn js_round(a: Float) -> Int + +/// Returns the value as an `Int`, truncating all decimal digits. +/// +/// ## Examples +/// +/// ```gleam +/// truncate(2.4343434847383438) +/// // -> 2 +/// ``` +/// +@external(erlang, "erlang", "trunc") +@external(javascript, "../gleam_stdlib.mjs", "truncate") +pub fn truncate(x: Float) -> Int + +/// Converts the value to a given precision as a `Float`. +/// The precision is the number of allowed decimal places. +/// Negative precisions are allowed and force rounding +/// to the nearest tenth, hundredth, thousandth etc. +/// +/// ## Examples +/// +/// ```gleam +/// to_precision(2.43434348473, precision: 2) +/// // -> 2.43 +/// ``` +/// +/// ```gleam +/// to_precision(547890.453444, precision: -3) +/// // -> 548000.0 +/// ``` +/// +pub fn to_precision(x: Float, precision: Int) -> Float { + case precision <= 0 { + True -> { + let factor = do_power(10.0, do_to_float(-precision)) + do_to_float(round(x /. factor)) *. factor + } + False -> { + let factor = do_power(10.0, do_to_float(precision)) + do_to_float(round(x *. factor)) /. factor + } + } +} + +@external(erlang, "erlang", "float") +@external(javascript, "../gleam_stdlib.mjs", "identity") +fn do_to_float(a: Int) -> Float + +/// Returns the absolute value of the input as a `Float`. +/// +/// ## Examples +/// +/// ```gleam +/// absolute_value(-12.5) +/// // -> 12.5 +/// ``` +/// +/// ```gleam +/// absolute_value(10.2) +/// // -> 10.2 +/// ``` +/// +pub fn absolute_value(x: Float) -> Float { + case x >=. 0.0 { + True -> x + False -> 0.0 -. x + } +} + +/// Returns the results of the base being raised to the power of the +/// exponent, as a `Float`. +/// +/// ## Examples +/// +/// ```gleam +/// power(2.0, -1.0) +/// // -> Ok(0.5) +/// ``` +/// +/// ```gleam +/// power(2.0, 2.0) +/// // -> Ok(4.0) +/// ``` +/// +/// ```gleam +/// power(8.0, 1.5) +/// // -> Ok(22.627416997969522) +/// ``` +/// +/// ```gleam +/// 4.0 |> power(of: 2.0) +/// // -> Ok(16.0) +/// ``` +/// +/// ```gleam +/// power(-1.0, 0.5) +/// // -> Error(Nil) +/// ``` +/// +pub fn power(base: Float, of exponent: Float) -> Result(Float, Nil) { + let fractional: Bool = ceiling(exponent) -. exponent >. 0.0 + // In the following check: + // 1. If the base is negative and the exponent is fractional then + // return an error as it will otherwise be an imaginary number + // 2. If the base is 0 and the exponent is negative then the expression + // is equivalent to the exponent divided by 0 and an error should be + // returned + case base <. 0.0 && fractional || base == 0.0 && exponent <. 0.0 { + True -> Error(Nil) + False -> Ok(do_power(base, exponent)) + } +} + +@external(erlang, "math", "pow") +@external(javascript, "../gleam_stdlib.mjs", "power") +fn do_power(a: Float, b: Float) -> Float + +/// Returns the square root of the input as a `Float`. +/// +/// ## Examples +/// +/// ```gleam +/// square_root(4.0) +/// // -> Ok(2.0) +/// ``` +/// +/// ```gleam +/// square_root(-16.0) +/// // -> Error(Nil) +/// ``` +/// +pub fn square_root(x: Float) -> Result(Float, Nil) { + power(x, 0.5) +} + +/// Returns the negative of the value provided. +/// +/// ## Examples +/// +/// ```gleam +/// negate(1.0) +/// // -> -1.0 +/// ``` +/// +pub fn negate(x: Float) -> Float { + -1.0 *. x +} + +/// Sums a list of `Float`s. +/// +/// ## Example +/// +/// ```gleam +/// sum([1.0, 2.2, 3.3]) +/// // -> 6.5 +/// ``` +/// +pub fn sum(numbers: List(Float)) -> Float { + sum_loop(numbers, 0.0) +} + +fn sum_loop(numbers: List(Float), initial: Float) -> Float { + case numbers { + [first, ..rest] -> sum_loop(rest, first +. initial) + [] -> initial + } +} + +/// Multiplies a list of `Float`s and returns the product. +/// +/// ## Example +/// +/// ```gleam +/// product([2.5, 3.2, 4.2]) +/// // -> 33.6 +/// ``` +/// +pub fn product(numbers: List(Float)) -> Float { + product_loop(numbers, 1.0) +} + +fn product_loop(numbers: List(Float), initial: Float) -> Float { + case numbers { + [first, ..rest] -> product_loop(rest, first *. initial) + [] -> initial + } +} + +/// Generates a random float between the given zero (inclusive) and one +/// (exclusive). +/// +/// On Erlang this updates the random state in the process dictionary. +/// See: +/// +/// ## Examples +/// +/// ```gleam +/// random() +/// // -> 0.646355926896028 +/// ``` +/// +@external(erlang, "rand", "uniform") +@external(javascript, "../gleam_stdlib.mjs", "random_uniform") +pub fn random() -> Float + +/// Computes the modulo of an float division of inputs as a `Result`. +/// +/// Returns division of the inputs as a `Result`: If the given divisor equals +/// `0`, this function returns an `Error`. +/// +/// ## Examples +/// +/// ```gleam +/// modulo(13.3, by: 3.3) +/// // -> Ok(0.1) +/// ``` +/// +/// ```gleam +/// modulo(-13.3, by: 3.3) +/// // -> Ok(3.2) +/// ``` +/// +/// ```gleam +/// modulo(13.3, by: -3.3) +/// // -> Ok(-3.2) +/// ``` +/// +/// ```gleam +/// modulo(-13.3, by: -3.3) +/// // -> Ok(-0.1) +/// ``` +/// +pub fn modulo(dividend: Float, by divisor: Float) -> Result(Float, Nil) { + case divisor { + 0.0 -> Error(Nil) + _ -> Ok(dividend -. floor(dividend /. divisor) *. divisor) + } +} + +/// Returns division of the inputs as a `Result`. +/// +/// ## Examples +/// +/// ```gleam +/// divide(0.0, 1.0) +/// // -> Ok(0.0) +/// ``` +/// +/// ```gleam +/// divide(1.0, 0.0) +/// // -> Error(Nil) +/// ``` +/// +pub fn divide(a: Float, by b: Float) -> Result(Float, Nil) { + case b { + 0.0 -> Error(Nil) + b -> Ok(a /. b) + } +} + +/// Adds two floats together. +/// +/// It's the function equivalent of the `+.` operator. +/// This function is useful in higher order functions or pipes. +/// +/// ## Examples +/// +/// ```gleam +/// add(1.0, 2.0) +/// // -> 3.0 +/// ``` +/// +/// ```gleam +/// import gleam/list +/// +/// list.fold([1.0, 2.0, 3.0], 0.0, add) +/// // -> 6.0 +/// ``` +/// +/// ```gleam +/// 3.0 |> add(2.0) +/// // -> 5.0 +/// ``` +/// +pub fn add(a: Float, b: Float) -> Float { + a +. b +} + +/// Multiplies two floats together. +/// +/// It's the function equivalent of the `*.` operator. +/// This function is useful in higher order functions or pipes. +/// +/// ## Examples +/// +/// ```gleam +/// multiply(2.0, 4.0) +/// // -> 8.0 +/// ``` +/// +/// ```gleam +/// import gleam/list +/// +/// list.fold([2.0, 3.0, 4.0], 1.0, multiply) +/// // -> 24.0 +/// ``` +/// +/// ```gleam +/// 3.0 |> multiply(2.0) +/// // -> 6.0 +/// ``` +/// +pub fn multiply(a: Float, b: Float) -> Float { + a *. b +} + +/// Subtracts one float from another. +/// +/// It's the function equivalent of the `-.` operator. +/// This function is useful in higher order functions or pipes. +/// +/// ## Examples +/// +/// ```gleam +/// subtract(3.0, 1.0) +/// // -> 2.0 +/// ``` +/// +/// ```gleam +/// import gleam/list +/// +/// list.fold([1.0, 2.0, 3.0], 10.0, subtract) +/// // -> 4.0 +/// ``` +/// +/// ```gleam +/// 3.0 |> subtract(_, 2.0) +/// // -> 1.0 +/// ``` +/// +/// ```gleam +/// 3.0 |> subtract(2.0, _) +/// // -> -1.0 +/// ``` +/// +pub fn subtract(a: Float, b: Float) -> Float { + a -. b +} + +/// Returns the natural logarithm (base e) of the given as a `Result`. If the +/// input is less than or equal to 0, returns `Error(Nil)`. +/// +/// ## Examples +/// +/// ```gleam +/// logarithm(1.0) +/// // -> Ok(0.0) +/// ``` +/// +/// ```gleam +/// logarithm(2.718281828459045) // e +/// // -> Ok(1.0) +/// ``` +/// +/// ```gleam +/// logarithm(0.0) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// logarithm(-1.0) +/// // -> Error(Nil) +/// ``` +/// +pub fn logarithm(x: Float) -> Result(Float, Nil) { + // In the following check: + // 1. If x is negative then return an error as the natural logarithm + // of a negative number is undefined (would be a complex number) + // 2. If x is 0 then return an error as the natural logarithm of 0 + // approaches negative infinity + case x <=. 0.0 { + True -> Error(Nil) + False -> Ok(do_log(x)) + } +} + +@external(erlang, "math", "log") +@external(javascript, "../gleam_stdlib.mjs", "log") +fn do_log(x: Float) -> Float + +/// Returns e (Euler's number) raised to the power of the given exponent, as +/// a `Float`. +/// +/// ## Examples +/// +/// ```gleam +/// exponential(0.0) +/// // -> Ok(1.0) +/// ``` +/// +/// ```gleam +/// exponential(1.0) +/// // -> Ok(2.718281828459045) +/// ``` +/// +/// ```gleam +/// exponential(-1.0) +/// // -> Ok(0.36787944117144233) +/// ``` +/// +@external(erlang, "math", "exp") +@external(javascript, "../gleam_stdlib.mjs", "exp") +pub fn exponential(x: Float) -> Float diff --git a/build/packages/gleam_stdlib/src/gleam/function.gleam b/build/packages/gleam_stdlib/src/gleam/function.gleam new file mode 100644 index 0000000..6ae3a62 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/function.gleam @@ -0,0 +1,15 @@ +/// Takes a single argument and always returns its input value. +/// +pub fn identity(x: a) -> a { + x +} + +/// Takes an argument and a single function, calls that function with that +/// argument and returns that argument instead of the function return value. +/// +/// Useful for running synchronous side effects in a pipeline. +/// +pub fn tap(arg: a, effect: fn(a) -> b) -> a { + effect(arg) + arg +} diff --git a/build/packages/gleam_stdlib/src/gleam/int.gleam b/build/packages/gleam_stdlib/src/gleam/int.gleam new file mode 100644 index 0000000..fa8aaef --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/int.gleam @@ -0,0 +1,828 @@ +//// Functions for working with integers. +//// +//// ## Division by zero +//// +//// In Erlang division by zero results in a crash, however Gleam does not have +//// partial functions and operators in core so instead division by zero returns +//// zero, a behaviour taken from Pony, Coq, and Lean. +//// +//// This may seem unexpected at first, but it is no less mathematically valid +//// than crashing or returning a special value. Division by zero is undefined +//// in mathematics. + +import gleam/float +import gleam/order.{type Order} + +/// Returns the absolute value of the input. +/// +/// ## Examples +/// +/// ```gleam +/// absolute_value(-12) +/// // -> 12 +/// ``` +/// +/// ```gleam +/// absolute_value(10) +/// // -> 10 +/// ``` +/// +pub fn absolute_value(x: Int) -> Int { + case x >= 0 { + True -> x + False -> x * -1 + } +} + +/// Returns the results of the base being raised to the power of the +/// exponent, as a `Float`. +/// +/// ## Examples +/// +/// ```gleam +/// power(2, -1.0) +/// // -> Ok(0.5) +/// ``` +/// +/// ```gleam +/// power(2, 2.0) +/// // -> Ok(4.0) +/// ``` +/// +/// ```gleam +/// power(8, 1.5) +/// // -> Ok(22.627416997969522) +/// ``` +/// +/// ```gleam +/// 4 |> power(of: 2.0) +/// // -> Ok(16.0) +/// ``` +/// +/// ```gleam +/// power(-1, 0.5) +/// // -> Error(Nil) +/// ``` +/// +pub fn power(base: Int, of exponent: Float) -> Result(Float, Nil) { + base + |> to_float + |> float.power(exponent) +} + +/// Returns the square root of the input as a `Float`. +/// +/// ## Examples +/// +/// ```gleam +/// square_root(4) +/// // -> Ok(2.0) +/// ``` +/// +/// ```gleam +/// square_root(-16) +/// // -> Error(Nil) +/// ``` +/// +pub fn square_root(x: Int) -> Result(Float, Nil) { + x + |> to_float + |> float.square_root() +} + +/// Parses a given string as an int if possible. +/// +/// ## Examples +/// +/// ```gleam +/// parse("2") +/// // -> Ok(2) +/// ``` +/// +/// ```gleam +/// parse("ABC") +/// // -> Error(Nil) +/// ``` +/// +@external(erlang, "gleam_stdlib", "parse_int") +@external(javascript, "../gleam_stdlib.mjs", "parse_int") +pub fn parse(string: String) -> Result(Int, Nil) + +/// Parses a given string as an int in a given base if possible. +/// Supports only bases 2 to 36, for values outside of which this function returns an `Error(Nil)`. +/// +/// ## Examples +/// +/// ```gleam +/// base_parse("10", 2) +/// // -> Ok(2) +/// ``` +/// +/// ```gleam +/// base_parse("30", 16) +/// // -> Ok(48) +/// ``` +/// +/// ```gleam +/// base_parse("1C", 36) +/// // -> Ok(48) +/// ``` +/// +/// ```gleam +/// base_parse("48", 1) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// base_parse("48", 37) +/// // -> Error(Nil) +/// ``` +/// +pub fn base_parse(string: String, base: Int) -> Result(Int, Nil) { + case base >= 2 && base <= 36 { + True -> do_base_parse(string, base) + False -> Error(Nil) + } +} + +@external(erlang, "gleam_stdlib", "int_from_base_string") +@external(javascript, "../gleam_stdlib.mjs", "int_from_base_string") +fn do_base_parse(a: String, b: Int) -> Result(Int, Nil) + +/// Prints a given int to a string. +/// +/// ## Examples +/// +/// ```gleam +/// to_string(2) +/// // -> "2" +/// ``` +/// +@external(erlang, "erlang", "integer_to_binary") +@external(javascript, "../gleam_stdlib.mjs", "to_string") +pub fn to_string(x: Int) -> String + +/// Prints a given int to a string using the base number provided. +/// Supports only bases 2 to 36, for values outside of which this function returns an `Error(Nil)`. +/// For common bases (2, 8, 16, 36), use the `to_baseN` functions. +/// +/// ## Examples +/// +/// ```gleam +/// to_base_string(2, 2) +/// // -> Ok("10") +/// ``` +/// +/// ```gleam +/// to_base_string(48, 16) +/// // -> Ok("30") +/// ``` +/// +/// ```gleam +/// to_base_string(48, 36) +/// // -> Ok("1C") +/// ``` +/// +/// ```gleam +/// to_base_string(48, 1) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// to_base_string(48, 37) +/// // -> Error(Nil) +/// ``` +/// +pub fn to_base_string(x: Int, base: Int) -> Result(String, Nil) { + case base >= 2 && base <= 36 { + True -> Ok(do_to_base_string(x, base)) + False -> Error(Nil) + } +} + +@external(erlang, "erlang", "integer_to_binary") +@external(javascript, "../gleam_stdlib.mjs", "int_to_base_string") +fn do_to_base_string(a: Int, b: Int) -> String + +/// Prints a given int to a string using base-2. +/// +/// ## Examples +/// +/// ```gleam +/// to_base2(2) +/// // -> "10" +/// ``` +/// +pub fn to_base2(x: Int) -> String { + do_to_base_string(x, 2) +} + +/// Prints a given int to a string using base-8. +/// +/// ## Examples +/// +/// ```gleam +/// to_base8(15) +/// // -> "17" +/// ``` +/// +pub fn to_base8(x: Int) -> String { + do_to_base_string(x, 8) +} + +/// Prints a given int to a string using base-16. +/// +/// ## Examples +/// +/// ```gleam +/// to_base16(48) +/// // -> "30" +/// ``` +/// +pub fn to_base16(x: Int) -> String { + do_to_base_string(x, 16) +} + +/// Prints a given int to a string using base-36. +/// +/// ## Examples +/// +/// ```gleam +/// to_base36(48) +/// // -> "1C" +/// ``` +/// +pub fn to_base36(x: Int) -> String { + do_to_base_string(x, 36) +} + +/// Takes an int and returns its value as a float. +/// +/// ## Examples +/// +/// ```gleam +/// to_float(5) +/// // -> 5.0 +/// ``` +/// +/// ```gleam +/// to_float(0) +/// // -> 0.0 +/// ``` +/// +/// ```gleam +/// to_float(-3) +/// // -> -3.0 +/// ``` +/// +@external(erlang, "erlang", "float") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn to_float(x: Int) -> Float + +/// Restricts an int between a lower and upper bound. +/// +/// ## Examples +/// +/// ```gleam +/// clamp(40, min: 50, max: 60) +/// // -> 50 +/// ``` +/// +pub fn clamp(x: Int, min min_bound: Int, max max_bound: Int) -> Int { + x + |> min(max_bound) + |> max(min_bound) +} + +/// Compares two ints, returning an order. +/// +/// ## Examples +/// +/// ```gleam +/// compare(2, 3) +/// // -> Lt +/// ``` +/// +/// ```gleam +/// compare(4, 3) +/// // -> Gt +/// ``` +/// +/// ```gleam +/// compare(3, 3) +/// // -> Eq +/// ``` +/// +pub fn compare(a: Int, with b: Int) -> Order { + case a == b { + True -> order.Eq + False -> + case a < b { + True -> order.Lt + False -> order.Gt + } + } +} + +/// Compares two ints, returning the smaller of the two. +/// +/// ## Examples +/// +/// ```gleam +/// min(2, 3) +/// // -> 2 +/// ``` +/// +pub fn min(a: Int, b: Int) -> Int { + case a < b { + True -> a + False -> b + } +} + +/// Compares two ints, returning the larger of the two. +/// +/// ## Examples +/// +/// ```gleam +/// max(2, 3) +/// // -> 3 +/// ``` +/// +pub fn max(a: Int, b: Int) -> Int { + case a > b { + True -> a + False -> b + } +} + +/// Returns whether the value provided is even. +/// +/// ## Examples +/// +/// ```gleam +/// is_even(2) +/// // -> True +/// ``` +/// +/// ```gleam +/// is_even(3) +/// // -> False +/// ``` +/// +pub fn is_even(x: Int) -> Bool { + x % 2 == 0 +} + +/// Returns whether the value provided is odd. +/// +/// ## Examples +/// +/// ```gleam +/// is_odd(3) +/// // -> True +/// ``` +/// +/// ```gleam +/// is_odd(2) +/// // -> False +/// ``` +/// +pub fn is_odd(x: Int) -> Bool { + x % 2 != 0 +} + +/// Returns the negative of the value provided. +/// +/// ## Examples +/// +/// ```gleam +/// negate(1) +/// // -> -1 +/// ``` +/// +pub fn negate(x: Int) -> Int { + -1 * x +} + +/// Sums a list of ints. +/// +/// ## Example +/// +/// ```gleam +/// sum([1, 2, 3]) +/// // -> 6 +/// ``` +/// +pub fn sum(numbers: List(Int)) -> Int { + sum_loop(numbers, 0) +} + +fn sum_loop(numbers: List(Int), initial: Int) -> Int { + case numbers { + [first, ..rest] -> sum_loop(rest, first + initial) + [] -> initial + } +} + +/// Multiplies a list of ints and returns the product. +/// +/// ## Example +/// +/// ```gleam +/// product([2, 3, 4]) +/// // -> 24 +/// ``` +/// +pub fn product(numbers: List(Int)) -> Int { + product_loop(numbers, 1) +} + +fn product_loop(numbers: List(Int), initial: Int) -> Int { + case numbers { + [first, ..rest] -> product_loop(rest, first * initial) + [] -> initial + } +} + +@deprecated("Vendor this function into your codebase") +pub fn digits(x: Int, base: Int) -> Result(List(Int), Nil) { + case base < 2 { + True -> Error(Nil) + False -> Ok(digits_loop(x, base, [])) + } +} + +fn digits_loop(x: Int, base: Int, acc: List(Int)) -> List(Int) { + case absolute_value(x) < base { + True -> [x, ..acc] + False -> digits_loop(x / base, base, [x % base, ..acc]) + } +} + +@deprecated("Vendor this function into your codebase") +pub fn undigits(numbers: List(Int), base: Int) -> Result(Int, Nil) { + case base < 2 { + True -> Error(Nil) + False -> undigits_loop(numbers, base, 0) + } +} + +fn undigits_loop(numbers: List(Int), base: Int, acc: Int) -> Result(Int, Nil) { + case numbers { + [] -> Ok(acc) + [digit, ..] if digit >= base -> Error(Nil) + [digit, ..rest] -> undigits_loop(rest, base, acc * base + digit) + } +} + +/// Generates a random int between zero and the given maximum. +/// +/// The lower number is inclusive, the upper number is exclusive. +/// +/// ## Examples +/// +/// ```gleam +/// random(10) +/// // -> 4 +/// ``` +/// +/// ```gleam +/// random(1) +/// // -> 0 +/// ``` +/// +/// ```gleam +/// random(-1) +/// // -> -1 +/// ``` +/// +pub fn random(max: Int) -> Int { + { float.random() *. to_float(max) } + |> float.floor + |> float.round +} + +/// Performs a truncated integer division. +/// +/// Returns division of the inputs as a `Result`: If the given divisor equals +/// `0`, this function returns an `Error`. +/// +/// ## Examples +/// +/// ```gleam +/// divide(0, 1) +/// // -> Ok(0) +/// ``` +/// +/// ```gleam +/// divide(1, 0) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// divide(5, 2) +/// // -> Ok(2) +/// ``` +/// +/// ```gleam +/// divide(-99, 2) +/// // -> Ok(-49) +/// ``` +/// +pub fn divide(dividend: Int, by divisor: Int) -> Result(Int, Nil) { + case divisor { + 0 -> Error(Nil) + divisor -> Ok(dividend / divisor) + } +} + +/// Computes the remainder of an integer division of inputs as a `Result`. +/// +/// Returns division of the inputs as a `Result`: If the given divisor equals +/// `0`, this function returns an `Error`. +/// +/// Most the time you will want to use the `%` operator instead of this +/// function. +/// +/// ## Examples +/// +/// ```gleam +/// remainder(3, 2) +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// remainder(1, 0) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// remainder(10, -1) +/// // -> Ok(0) +/// ``` +/// +/// ```gleam +/// remainder(13, by: 3) +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// remainder(-13, by: 3) +/// // -> Ok(-1) +/// ``` +/// +/// ```gleam +/// remainder(13, by: -3) +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// remainder(-13, by: -3) +/// // -> Ok(-1) +/// ``` +/// +pub fn remainder(dividend: Int, by divisor: Int) -> Result(Int, Nil) { + case divisor { + 0 -> Error(Nil) + divisor -> Ok(dividend % divisor) + } +} + +/// Computes the modulo of an integer division of inputs as a `Result`. +/// +/// Returns division of the inputs as a `Result`: If the given divisor equals +/// `0`, this function returns an `Error`. +/// +/// Most the time you will want to use the `%` operator instead of this +/// function. +/// +/// ## Examples +/// +/// ```gleam +/// modulo(3, 2) +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// modulo(1, 0) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// modulo(10, -1) +/// // -> Ok(0) +/// ``` +/// +/// ```gleam +/// modulo(13, by: 3) +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// modulo(-13, by: 3) +/// // -> Ok(2) +/// ``` +/// +pub fn modulo(dividend: Int, by divisor: Int) -> Result(Int, Nil) { + case divisor { + 0 -> Error(Nil) + _ -> { + let remainder = dividend % divisor + case remainder * divisor < 0 { + True -> Ok(remainder + divisor) + False -> Ok(remainder) + } + } + } +} + +/// Performs a *floored* integer division, which means that the result will +/// always be rounded towards negative infinity. +/// +/// If you want to perform truncated integer division (rounding towards zero), +/// use `int.divide()` or the `/` operator instead. +/// +/// Returns division of the inputs as a `Result`: If the given divisor equals +/// `0`, this function returns an `Error`. +/// +/// ## Examples +/// +/// ```gleam +/// floor_divide(1, 0) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// floor_divide(5, 2) +/// // -> Ok(2) +/// ``` +/// +/// ```gleam +/// floor_divide(6, -4) +/// // -> Ok(-2) +/// ``` +/// +/// ```gleam +/// floor_divide(-99, 2) +/// // -> Ok(-50) +/// ``` +/// +pub fn floor_divide(dividend: Int, by divisor: Int) -> Result(Int, Nil) { + case divisor { + 0 -> Error(Nil) + divisor -> + case dividend * divisor < 0 && dividend % divisor != 0 { + True -> Ok(dividend / divisor - 1) + False -> Ok(dividend / divisor) + } + } +} + +/// Adds two integers together. +/// +/// It's the function equivalent of the `+` operator. +/// This function is useful in higher order functions or pipes. +/// +/// ## Examples +/// +/// ```gleam +/// add(1, 2) +/// // -> 3 +/// ``` +/// +/// ```gleam +/// import gleam/list +/// list.fold([1, 2, 3], 0, add) +/// // -> 6 +/// ``` +/// +/// ```gleam +/// 3 |> add(2) +/// // -> 5 +/// ``` +/// +pub fn add(a: Int, b: Int) -> Int { + a + b +} + +/// Multiplies two integers together. +/// +/// It's the function equivalent of the `*` operator. +/// This function is useful in higher order functions or pipes. +/// +/// ## Examples +/// +/// ```gleam +/// multiply(2, 4) +/// // -> 8 +/// ``` +/// +/// ```gleam +/// import gleam/list +/// +/// list.fold([2, 3, 4], 1, multiply) +/// // -> 24 +/// ``` +/// +/// ```gleam +/// 3 |> multiply(2) +/// // -> 6 +/// ``` +/// +pub fn multiply(a: Int, b: Int) -> Int { + a * b +} + +/// Subtracts one int from another. +/// +/// It's the function equivalent of the `-` operator. +/// This function is useful in higher order functions or pipes. +/// +/// ## Examples +/// +/// ```gleam +/// subtract(3, 1) +/// // -> 2 +/// ``` +/// +/// ```gleam +/// import gleam/list +/// +/// list.fold([1, 2, 3], 10, subtract) +/// // -> 4 +/// ``` +/// +/// ```gleam +/// 3 |> subtract(2) +/// // -> 1 +/// ``` +/// +/// ```gleam +/// 3 |> subtract(2, _) +/// // -> -1 +/// ``` +/// +pub fn subtract(a: Int, b: Int) -> Int { + a - b +} + +/// Calculates the bitwise AND of its arguments. +/// +/// The exact behaviour of this function depends on the target platform. +/// On Erlang it is equivalent to bitwise operations on ints, on JavaScript it +/// is equivalent to bitwise operations on big-ints. +/// +@external(erlang, "erlang", "band") +@external(javascript, "../gleam_stdlib.mjs", "bitwise_and") +pub fn bitwise_and(x: Int, y: Int) -> Int + +/// Calculates the bitwise NOT of its argument. +/// +/// The exact behaviour of this function depends on the target platform. +/// On Erlang it is equivalent to bitwise operations on ints, on JavaScript it +/// is equivalent to bitwise operations on big-ints. +/// +@external(erlang, "erlang", "bnot") +@external(javascript, "../gleam_stdlib.mjs", "bitwise_not") +pub fn bitwise_not(x: Int) -> Int + +/// Calculates the bitwise OR of its arguments. +/// +/// The exact behaviour of this function depends on the target platform. +/// On Erlang it is equivalent to bitwise operations on ints, on JavaScript it +/// is equivalent to bitwise operations on big-ints. +/// +@external(erlang, "erlang", "bor") +@external(javascript, "../gleam_stdlib.mjs", "bitwise_or") +pub fn bitwise_or(x: Int, y: Int) -> Int + +/// Calculates the bitwise XOR of its arguments. +/// +/// The exact behaviour of this function depends on the target platform. +/// On Erlang it is equivalent to bitwise operations on ints, on JavaScript it +/// is equivalent to bitwise operations on big-ints. +/// +@external(erlang, "erlang", "bxor") +@external(javascript, "../gleam_stdlib.mjs", "bitwise_exclusive_or") +pub fn bitwise_exclusive_or(x: Int, y: Int) -> Int + +/// Calculates the result of an arithmetic left bitshift. +/// +/// The exact behaviour of this function depends on the target platform. +/// On Erlang it is equivalent to bitwise operations on ints, on JavaScript it +/// is equivalent to bitwise operations on big-ints. +/// +@external(erlang, "erlang", "bsl") +@external(javascript, "../gleam_stdlib.mjs", "bitwise_shift_left") +pub fn bitwise_shift_left(x: Int, y: Int) -> Int + +/// Calculates the result of an arithmetic right bitshift. +/// +/// The exact behaviour of this function depends on the target platform. +/// On Erlang it is equivalent to bitwise operations on ints, on JavaScript it +/// is equivalent to bitwise operations on big-ints. +/// +@external(erlang, "erlang", "bsr") +@external(javascript, "../gleam_stdlib.mjs", "bitwise_shift_right") +pub fn bitwise_shift_right(x: Int, y: Int) -> Int diff --git a/build/packages/gleam_stdlib/src/gleam/io.gleam b/build/packages/gleam_stdlib/src/gleam/io.gleam new file mode 100644 index 0000000..67088eb --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/io.gleam @@ -0,0 +1,59 @@ +/// Writes a string to standard output (stdout). +/// +/// If you want your output to be printed on its own line see `println`. +/// +/// ## Example +/// +/// ```gleam +/// io.print("Hi mum") +/// // -> Nil +/// // Hi mum +/// ``` +/// +@external(erlang, "gleam_stdlib", "print") +@external(javascript, "../gleam_stdlib.mjs", "print") +pub fn print(string: String) -> Nil + +/// Writes a string to standard error (stderr). +/// +/// If you want your output to be printed on its own line see `println_error`. +/// +/// ## Example +/// +/// ``` +/// io.print_error("Hi pop") +/// // -> Nil +/// // Hi pop +/// ``` +/// +@external(erlang, "gleam_stdlib", "print_error") +@external(javascript, "../gleam_stdlib.mjs", "print_error") +pub fn print_error(string: String) -> Nil + +/// Writes a string to standard output (stdout), appending a newline to the end. +/// +/// ## Example +/// +/// ```gleam +/// io.println("Hi mum") +/// // -> Nil +/// // Hi mum +/// ``` +/// +@external(erlang, "gleam_stdlib", "println") +@external(javascript, "../gleam_stdlib.mjs", "console_log") +pub fn println(string: String) -> Nil + +/// Writes a string to standard error (stderr), appending a newline to the end. +/// +/// ## Example +/// +/// ```gleam +/// io.println_error("Hi pop") +/// // -> Nil +/// // Hi pop +/// ``` +/// +@external(erlang, "gleam_stdlib", "println_error") +@external(javascript, "../gleam_stdlib.mjs", "console_error") +pub fn println_error(string: String) -> Nil diff --git a/build/packages/gleam_stdlib/src/gleam/list.gleam b/build/packages/gleam_stdlib/src/gleam/list.gleam new file mode 100644 index 0000000..6373efb --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/list.gleam @@ -0,0 +1,2426 @@ +//// Lists are an ordered sequence of elements and are one of the most common +//// data types in Gleam. +//// +//// New elements can be added and removed from the front of a list in +//// constant time, while adding and removing from the end requires traversing +//// and copying the whole list, so keep this in mind when designing your +//// programs. +//// +//// There is a dedicated syntax for prefixing to a list: +//// +//// ```gleam +//// let new_list = [1, 2, ..existing_list] +//// ``` +//// +//// And a matching syntax for getting the first elements of a list: +//// +//// ```gleam +//// case list { +//// [first_element, ..rest] -> first_element +//// _ -> "this pattern matches when the list is empty" +//// } +//// ``` +//// + +import gleam/dict.{type Dict} +import gleam/float +import gleam/int +import gleam/order.{type Order} + +/// Counts the number of elements in a given list. +/// +/// This function has to traverse the list to determine the number of elements, +/// so it runs in linear time. +/// +/// This function is natively implemented by the virtual machine and is highly +/// optimised. +/// +/// ## Examples +/// +/// ```gleam +/// length([]) +/// // -> 0 +/// ``` +/// +/// ```gleam +/// length([1]) +/// // -> 1 +/// ``` +/// +/// ```gleam +/// length([1, 2]) +/// // -> 2 +/// ``` +/// +@external(erlang, "erlang", "length") +pub fn length(of list: List(a)) -> Int { + length_loop(list, 0) +} + +fn length_loop(list: List(a), count: Int) -> Int { + case list { + [_, ..list] -> length_loop(list, count + 1) + [] -> count + } +} + +/// Counts the number of elements in a given list satisfying a given predicate. +/// +/// This function has to traverse the list to determine the number of elements, +/// so it runs in linear time. +/// +/// ## Examples +/// +/// ```gleam +/// count([], fn(a) { a > 0 }) +/// // -> 0 +/// ``` +/// +/// ```gleam +/// count([1], fn(a) { a > 0 }) +/// // -> 1 +/// ``` +/// +/// ```gleam +/// count([1, 2, 3], int.is_odd) +/// // -> 2 +/// ``` +/// +pub fn count(list: List(a), where predicate: fn(a) -> Bool) -> Int { + count_loop(list, predicate, 0) +} + +fn count_loop(list: List(a), predicate: fn(a) -> Bool, acc: Int) -> Int { + case list { + [] -> acc + [first, ..rest] -> + case predicate(first) { + True -> count_loop(rest, predicate, acc + 1) + False -> count_loop(rest, predicate, acc) + } + } +} + +/// Creates a new list from a given list containing the same elements but in the +/// opposite order. +/// +/// This function has to traverse the list to create the new reversed list, so +/// it runs in linear time. +/// +/// This function is natively implemented by the virtual machine and is highly +/// optimised. +/// +/// ## Examples +/// +/// ```gleam +/// reverse([]) +/// // -> [] +/// ``` +/// +/// ```gleam +/// reverse([1]) +/// // -> [1] +/// ``` +/// +/// ```gleam +/// reverse([1, 2]) +/// // -> [2, 1] +/// ``` +/// +@external(erlang, "lists", "reverse") +pub fn reverse(list: List(a)) -> List(a) { + reverse_and_prepend(list, []) +} + +/// Reverses a list and prepends it to another list. +/// This function runs in linear time, proportional to the length of the list +/// to prepend. +/// +@external(erlang, "lists", "reverse") +fn reverse_and_prepend(list prefix: List(a), to suffix: List(a)) -> List(a) { + case prefix { + [] -> suffix + [first, ..rest] -> reverse_and_prepend(list: rest, to: [first, ..suffix]) + } +} + +/// Determines whether or not the list is empty. +/// +/// This function runs in constant time. +/// +/// ## Examples +/// +/// ```gleam +/// is_empty([]) +/// // -> True +/// ``` +/// +/// ```gleam +/// is_empty([1]) +/// // -> False +/// ``` +/// +/// ```gleam +/// is_empty([1, 1]) +/// // -> False +/// ``` +/// +pub fn is_empty(list: List(a)) -> Bool { + list == [] +} + +/// Determines whether or not a given element exists within a given list. +/// +/// This function traverses the list to find the element, so it runs in linear +/// time. +/// +/// ## Examples +/// +/// ```gleam +/// [] |> contains(any: 0) +/// // -> False +/// ``` +/// +/// ```gleam +/// [0] |> contains(any: 0) +/// // -> True +/// ``` +/// +/// ```gleam +/// [1] |> contains(any: 0) +/// // -> False +/// ``` +/// +/// ```gleam +/// [1, 1] |> contains(any: 0) +/// // -> False +/// ``` +/// +/// ```gleam +/// [1, 0] |> contains(any: 0) +/// // -> True +/// ``` +/// +pub fn contains(list: List(a), any elem: a) -> Bool { + case list { + [] -> False + [first, ..] if first == elem -> True + [_, ..rest] -> contains(rest, elem) + } +} + +/// Gets the first element from the start of the list, if there is one. +/// +/// ## Examples +/// +/// ```gleam +/// first([]) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// first([0]) +/// // -> Ok(0) +/// ``` +/// +/// ```gleam +/// first([1, 2]) +/// // -> Ok(1) +/// ``` +/// +pub fn first(list: List(a)) -> Result(a, Nil) { + case list { + [] -> Error(Nil) + [first, ..] -> Ok(first) + } +} + +/// Returns the list minus the first element. If the list is empty, `Error(Nil)` is +/// returned. +/// +/// This function runs in constant time and does not make a copy of the list. +/// +/// ## Examples +/// +/// ```gleam +/// rest([]) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// rest([0]) +/// // -> Ok([]) +/// ``` +/// +/// ```gleam +/// rest([1, 2]) +/// // -> Ok([2]) +/// ``` +/// +pub fn rest(list: List(a)) -> Result(List(a), Nil) { + case list { + [] -> Error(Nil) + [_, ..rest] -> Ok(rest) + } +} + +/// Groups the elements from the given list by the given key function. +/// +/// Does not preserve the initial value order. +/// +/// ## Examples +/// +/// ```gleam +/// import gleam/dict +/// +/// [Ok(3), Error("Wrong"), Ok(200), Ok(73)] +/// |> group(by: fn(i) { +/// case i { +/// Ok(_) -> "Successful" +/// Error(_) -> "Failed" +/// } +/// }) +/// |> dict.to_list +/// // -> [ +/// // #("Failed", [Error("Wrong")]), +/// // #("Successful", [Ok(73), Ok(200), Ok(3)]) +/// // ] +/// ``` +/// +/// ```gleam +/// import gleam/dict +/// +/// group([1,2,3,4,5], by: fn(i) { i - i / 3 * 3 }) +/// |> dict.to_list +/// // -> [#(0, [3]), #(1, [4, 1]), #(2, [5, 2])] +/// ``` +/// +pub fn group(list: List(v), by key: fn(v) -> k) -> Dict(k, List(v)) { + group_loop(list, key, dict.new()) +} + +fn group_loop( + list: List(v), + to_key: fn(v) -> k, + groups: Dict(k, List(v)), +) -> Dict(k, List(v)) { + case list { + [] -> groups + [first, ..rest] -> { + let key = to_key(first) + let groups = case dict.get(groups, key) { + Error(_) -> dict.insert(groups, key, [first]) + Ok(existing) -> dict.insert(groups, key, [first, ..existing]) + } + group_loop(rest, to_key, groups) + } + } +} + +/// Returns a new list containing only the elements from the first list for +/// which the given functions returns `True`. +/// +/// ## Examples +/// +/// ```gleam +/// filter([2, 4, 6, 1], fn(x) { x > 2 }) +/// // -> [4, 6] +/// ``` +/// +/// ```gleam +/// filter([2, 4, 6, 1], fn(x) { x > 6 }) +/// // -> [] +/// ``` +/// +pub fn filter(list: List(a), keeping predicate: fn(a) -> Bool) -> List(a) { + filter_loop(list, predicate, []) +} + +fn filter_loop(list: List(a), fun: fn(a) -> Bool, acc: List(a)) -> List(a) { + case list { + [] -> reverse(acc) + [first, ..rest] -> { + let new_acc = case fun(first) { + True -> [first, ..acc] + False -> acc + } + filter_loop(rest, fun, new_acc) + } + } +} + +/// Returns a new list containing only the elements from the first list for +/// which the given functions returns `Ok(_)`. +/// +/// ## Examples +/// +/// ```gleam +/// filter_map([2, 4, 6, 1], Error) +/// // -> [] +/// ``` +/// +/// ```gleam +/// filter_map([2, 4, 6, 1], fn(x) { Ok(x + 1) }) +/// // -> [3, 5, 7, 2] +/// ``` +/// +pub fn filter_map(list: List(a), with fun: fn(a) -> Result(b, e)) -> List(b) { + filter_map_loop(list, fun, []) +} + +fn filter_map_loop( + list: List(a), + fun: fn(a) -> Result(b, e), + acc: List(b), +) -> List(b) { + case list { + [] -> reverse(acc) + [first, ..rest] -> { + let new_acc = case fun(first) { + Ok(first) -> [first, ..acc] + Error(_) -> acc + } + filter_map_loop(rest, fun, new_acc) + } + } +} + +/// Returns a new list containing only the elements of the first list after the +/// function has been applied to each one. +/// +/// ## Examples +/// +/// ```gleam +/// map([2, 4, 6], fn(x) { x * 2 }) +/// // -> [4, 8, 12] +/// ``` +/// +pub fn map(list: List(a), with fun: fn(a) -> b) -> List(b) { + map_loop(list, fun, []) +} + +fn map_loop(list: List(a), fun: fn(a) -> b, acc: List(b)) -> List(b) { + case list { + [] -> reverse(acc) + [first, ..rest] -> map_loop(rest, fun, [fun(first), ..acc]) + } +} + +/// Combines two lists into a single list using the given function. +/// +/// If a list is longer than the other the extra elements are dropped. +/// +/// ## Examples +/// +/// ```gleam +/// map2([1, 2, 3], [4, 5, 6], fn(x, y) { x + y }) +/// // -> [5, 7, 9] +/// ``` +/// +/// ```gleam +/// map2([1, 2], ["a", "b", "c"], fn(i, x) { #(i, x) }) +/// // -> [#(1, "a"), #(2, "b")] +/// ``` +/// +pub fn map2(list1: List(a), list2: List(b), with fun: fn(a, b) -> c) -> List(c) { + map2_loop(list1, list2, fun, []) +} + +fn map2_loop( + list1: List(a), + list2: List(b), + fun: fn(a, b) -> c, + acc: List(c), +) -> List(c) { + case list1, list2 { + [], _ | _, [] -> reverse(acc) + [a, ..as_], [b, ..bs] -> map2_loop(as_, bs, fun, [fun(a, b), ..acc]) + } +} + +/// Similar to `map` but also lets you pass around an accumulated value. +/// +/// ## Examples +/// +/// ```gleam +/// map_fold( +/// over: [1, 2, 3], +/// from: 100, +/// with: fn(memo, i) { #(memo + i, i * 2) } +/// ) +/// // -> #(106, [2, 4, 6]) +/// ``` +/// +pub fn map_fold( + over list: List(a), + from initial: acc, + with fun: fn(acc, a) -> #(acc, b), +) -> #(acc, List(b)) { + map_fold_loop(list, fun, initial, []) +} + +fn map_fold_loop( + list: List(a), + fun: fn(acc, a) -> #(acc, b), + acc: acc, + list_acc: List(b), +) -> #(acc, List(b)) { + case list { + [] -> #(acc, reverse(list_acc)) + [first, ..rest] -> { + let #(acc, first) = fun(acc, first) + map_fold_loop(rest, fun, acc, [first, ..list_acc]) + } + } +} + +/// Returns a new list containing only the elements of the first list after the +/// function has been applied to each one and their index. +/// +/// The index starts at 0, so the first element is 0, the second is 1, and so +/// on. +/// +/// ## Examples +/// +/// ```gleam +/// index_map(["a", "b"], fn(x, i) { #(i, x) }) +/// // -> [#(0, "a"), #(1, "b")] +/// ``` +/// +pub fn index_map(list: List(a), with fun: fn(a, Int) -> b) -> List(b) { + index_map_loop(list, fun, 0, []) +} + +fn index_map_loop( + list: List(a), + fun: fn(a, Int) -> b, + index: Int, + acc: List(b), +) -> List(b) { + case list { + [] -> reverse(acc) + [first, ..rest] -> { + let acc = [fun(first, index), ..acc] + index_map_loop(rest, fun, index + 1, acc) + } + } +} + +/// Takes a function that returns a `Result` and applies it to each element in a +/// given list in turn. +/// +/// If the function returns `Ok(new_value)` for all elements in the list then a +/// list of the new values is returned. +/// +/// If the function returns `Error(reason)` for any of the elements then it is +/// returned immediately. None of the elements in the list are processed after +/// one returns an `Error`. +/// +/// ## Examples +/// +/// ```gleam +/// try_map([1, 2, 3], fn(x) { Ok(x + 2) }) +/// // -> Ok([3, 4, 5]) +/// ``` +/// +/// ```gleam +/// try_map([1, 2, 3], fn(_) { Error(0) }) +/// // -> Error(0) +/// ``` +/// +/// ```gleam +/// try_map([[1], [2, 3]], first) +/// // -> Ok([1, 2]) +/// ``` +/// +/// ```gleam +/// try_map([[1], [], [2]], first) +/// // -> Error(Nil) +/// ``` +/// +pub fn try_map( + over list: List(a), + with fun: fn(a) -> Result(b, e), +) -> Result(List(b), e) { + try_map_loop(list, fun, []) +} + +fn try_map_loop( + list: List(a), + fun: fn(a) -> Result(b, e), + acc: List(b), +) -> Result(List(b), e) { + case list { + [] -> Ok(reverse(acc)) + [first, ..rest] -> + case fun(first) { + Ok(first) -> try_map_loop(rest, fun, [first, ..acc]) + Error(error) -> Error(error) + } + } +} + +/// Returns a list that is the given list with up to the given number of +/// elements removed from the front of the list. +/// +/// If the element has less than the number of elements an empty list is +/// returned. +/// +/// This function runs in linear time but does not copy the list. +/// +/// ## Examples +/// +/// ```gleam +/// drop([1, 2, 3, 4], 2) +/// // -> [3, 4] +/// ``` +/// +/// ```gleam +/// drop([1, 2, 3, 4], 9) +/// // -> [] +/// ``` +/// +pub fn drop(from list: List(a), up_to n: Int) -> List(a) { + case n <= 0 { + True -> list + False -> + case list { + [] -> [] + [_, ..rest] -> drop(rest, n - 1) + } + } +} + +/// Returns a list containing the first given number of elements from the given +/// list. +/// +/// If the element has less than the number of elements then the full list is +/// returned. +/// +/// This function runs in linear time. +/// +/// ## Examples +/// +/// ```gleam +/// take([1, 2, 3, 4], 2) +/// // -> [1, 2] +/// ``` +/// +/// ```gleam +/// take([1, 2, 3, 4], 9) +/// // -> [1, 2, 3, 4] +/// ``` +/// +pub fn take(from list: List(a), up_to n: Int) -> List(a) { + take_loop(list, n, []) +} + +fn take_loop(list: List(a), n: Int, acc: List(a)) -> List(a) { + case n <= 0 { + True -> reverse(acc) + False -> + case list { + [] -> reverse(acc) + [first, ..rest] -> take_loop(rest, n - 1, [first, ..acc]) + } + } +} + +/// Returns a new empty list. +/// +/// ## Examples +/// +/// ```gleam +/// new() +/// // -> [] +/// ``` +/// +pub fn new() -> List(a) { + [] +} + +/// Returns the given item wrapped in a list. +/// +/// ## Examples +/// +/// ```gleam +/// wrap(1) +/// // -> [1] +/// +/// wrap(["a", "b", "c"]) +/// // -> [["a", "b", "c"]] +/// +/// wrap([[]]) +/// // -> [[[]]] +/// ``` +/// +/// +pub fn wrap(item: a) -> List(a) { + [item] +} + +/// Joins one list onto the end of another. +/// +/// This function runs in linear time, and it traverses and copies the first +/// list. +/// +/// ## Examples +/// +/// ```gleam +/// append([1, 2], [3]) +/// // -> [1, 2, 3] +/// ``` +/// +@external(erlang, "lists", "append") +pub fn append(first: List(a), second: List(a)) -> List(a) { + append_loop(reverse(first), second) +} + +fn append_loop(first: List(a), second: List(a)) -> List(a) { + case first { + [] -> second + [first, ..rest] -> append_loop(rest, [first, ..second]) + } +} + +/// Prefixes an item to a list. This can also be done using the dedicated +/// syntax instead +/// +/// ```gleam +/// let existing_list = [2, 3, 4] +/// +/// [1, ..existing_list] +/// // -> [1, 2, 3, 4] +/// +/// prepend(to: existing_list, this: 1) +/// // -> [1, 2, 3, 4] +/// ``` +/// +pub fn prepend(to list: List(a), this item: a) -> List(a) { + [item, ..list] +} + +/// Joins a list of lists into a single list. +/// +/// This function traverses all elements twice on the JavaScript target. +/// This function traverses all elements once on the Erlang target. +/// +/// ## Examples +/// +/// ```gleam +/// flatten([[1], [2, 3], []]) +/// // -> [1, 2, 3] +/// ``` +/// +@external(erlang, "lists", "append") +pub fn flatten(lists: List(List(a))) -> List(a) { + flatten_loop(lists, []) +} + +fn flatten_loop(lists: List(List(a)), acc: List(a)) -> List(a) { + case lists { + [] -> reverse(acc) + [list, ..further_lists] -> + flatten_loop(further_lists, reverse_and_prepend(list, to: acc)) + } +} + +/// Maps the list with the given function into a list of lists, and then flattens it. +/// +/// ## Examples +/// +/// ```gleam +/// flat_map([2, 4, 6], fn(x) { [x, x + 1] }) +/// // -> [2, 3, 4, 5, 6, 7] +/// ``` +/// +pub fn flat_map(over list: List(a), with fun: fn(a) -> List(b)) -> List(b) { + flatten(map(list, fun)) +} + +/// Reduces a list of elements into a single value by calling a given function +/// on each element, going from left to right. +/// +/// `fold([1, 2, 3], 0, add)` is the equivalent of +/// `add(add(add(0, 1), 2), 3)`. +/// +/// This function runs in linear time. +/// +pub fn fold( + over list: List(a), + from initial: acc, + with fun: fn(acc, a) -> acc, +) -> acc { + case list { + [] -> initial + [first, ..rest] -> fold(rest, fun(initial, first), fun) + } +} + +/// Reduces a list of elements into a single value by calling a given function +/// on each element, going from right to left. +/// +/// `fold_right([1, 2, 3], 0, add)` is the equivalent of +/// `add(add(add(0, 3), 2), 1)`. +/// +/// This function runs in linear time. +/// +/// Unlike `fold` this function is not tail recursive. Where possible use +/// `fold` instead as it will use less memory. +/// +pub fn fold_right( + over list: List(a), + from initial: acc, + with fun: fn(acc, a) -> acc, +) -> acc { + case list { + [] -> initial + [first, ..rest] -> fun(fold_right(rest, initial, fun), first) + } +} + +/// Like fold but the folding function also receives the index of the current element. +/// +/// ## Examples +/// +/// ```gleam +/// ["a", "b", "c"] +/// |> index_fold("", fn(acc, item, index) { +/// acc <> int.to_string(index) <> ":" <> item <> " " +/// }) +/// // -> "0:a 1:b 2:c" +/// ``` +/// +/// ```gleam +/// [10, 20, 30] +/// |> index_fold(0, fn(acc, item, index) { acc + item * index }) +/// // -> 80 +/// ``` +/// +pub fn index_fold( + over list: List(a), + from initial: acc, + with fun: fn(acc, a, Int) -> acc, +) -> acc { + index_fold_loop(list, initial, fun, 0) +} + +fn index_fold_loop( + over: List(a), + acc: acc, + with: fn(acc, a, Int) -> acc, + index: Int, +) -> acc { + case over { + [] -> acc + [first, ..rest] -> + index_fold_loop(rest, with(acc, first, index), with, index + 1) + } +} + +/// A variant of fold that might fail. +/// +/// The folding function should return `Result(accumulator, error)`. +/// If the returned value is `Ok(accumulator)` try_fold will try the next value in the list. +/// If the returned value is `Error(error)` try_fold will stop and return that error. +/// +/// ## Examples +/// +/// ```gleam +/// [1, 2, 3, 4] +/// |> try_fold(0, fn(acc, i) { +/// case i < 3 { +/// True -> Ok(acc + i) +/// False -> Error(Nil) +/// } +/// }) +/// // -> Error(Nil) +/// ``` +/// +pub fn try_fold( + over list: List(a), + from initial: acc, + with fun: fn(acc, a) -> Result(acc, e), +) -> Result(acc, e) { + case list { + [] -> Ok(initial) + [first, ..rest] -> + case fun(initial, first) { + Ok(result) -> try_fold(rest, result, fun) + Error(_) as error -> error + } + } +} + +pub type ContinueOrStop(a) { + Continue(a) + Stop(a) +} + +/// A variant of fold that allows to stop folding earlier. +/// +/// The folding function should return `ContinueOrStop(accumulator)`. +/// If the returned value is `Continue(accumulator)` fold_until will try the next value in the list. +/// If the returned value is `Stop(accumulator)` fold_until will stop and return that accumulator. +/// +/// ## Examples +/// +/// ```gleam +/// [1, 2, 3, 4] +/// |> fold_until(0, fn(acc, i) { +/// case i < 3 { +/// True -> Continue(acc + i) +/// False -> Stop(acc) +/// } +/// }) +/// // -> 3 +/// ``` +/// +pub fn fold_until( + over list: List(a), + from initial: acc, + with fun: fn(acc, a) -> ContinueOrStop(acc), +) -> acc { + case list { + [] -> initial + [first, ..rest] -> + case fun(initial, first) { + Continue(next_accumulator) -> fold_until(rest, next_accumulator, fun) + Stop(b) -> b + } + } +} + +/// Finds the first element in a given list for which the given function returns +/// `True`. +/// +/// Returns `Error(Nil)` if no such element is found. +/// +/// ## Examples +/// +/// ```gleam +/// find([1, 2, 3], fn(x) { x > 2 }) +/// // -> Ok(3) +/// ``` +/// +/// ```gleam +/// find([1, 2, 3], fn(x) { x > 4 }) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// find([], fn(_) { True }) +/// // -> Error(Nil) +/// ``` +/// +pub fn find( + in list: List(a), + one_that is_desired: fn(a) -> Bool, +) -> Result(a, Nil) { + case list { + [] -> Error(Nil) + [first, ..rest] -> + case is_desired(first) { + True -> Ok(first) + False -> find(in: rest, one_that: is_desired) + } + } +} + +/// Finds the first element in a given list for which the given function returns +/// `Ok(new_value)`, then returns the wrapped `new_value`. +/// +/// Returns `Error(Nil)` if no such element is found. +/// +/// ## Examples +/// +/// ```gleam +/// find_map([[], [2], [3]], first) +/// // -> Ok(2) +/// ``` +/// +/// ```gleam +/// find_map([[], []], first) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// find_map([], first) +/// // -> Error(Nil) +/// ``` +/// +pub fn find_map( + in list: List(a), + with fun: fn(a) -> Result(b, c), +) -> Result(b, Nil) { + case list { + [] -> Error(Nil) + [first, ..rest] -> + case fun(first) { + Ok(first) -> Ok(first) + Error(_) -> find_map(in: rest, with: fun) + } + } +} + +/// Returns `True` if the given function returns `True` for all the elements in +/// the given list. If the function returns `False` for any of the elements it +/// immediately returns `False` without checking the rest of the list. +/// +/// ## Examples +/// +/// ```gleam +/// all([], fn(x) { x > 3 }) +/// // -> True +/// ``` +/// +/// ```gleam +/// all([4, 5], fn(x) { x > 3 }) +/// // -> True +/// ``` +/// +/// ```gleam +/// all([4, 3], fn(x) { x > 3 }) +/// // -> False +/// ``` +/// +pub fn all(in list: List(a), satisfying predicate: fn(a) -> Bool) -> Bool { + case list { + [] -> True + [first, ..rest] -> + case predicate(first) { + True -> all(rest, predicate) + False -> False + } + } +} + +/// Returns `True` if the given function returns `True` for any the elements in +/// the given list. If the function returns `True` for any of the elements it +/// immediately returns `True` without checking the rest of the list. +/// +/// ## Examples +/// +/// ```gleam +/// any([], fn(x) { x > 3 }) +/// // -> False +/// ``` +/// +/// ```gleam +/// any([4, 5], fn(x) { x > 3 }) +/// // -> True +/// ``` +/// +/// ```gleam +/// any([4, 3], fn(x) { x > 4 }) +/// // -> False +/// ``` +/// +/// ```gleam +/// any([3, 4], fn(x) { x > 3 }) +/// // -> True +/// ``` +/// +pub fn any(in list: List(a), satisfying predicate: fn(a) -> Bool) -> Bool { + case list { + [] -> False + [first, ..rest] -> + case predicate(first) { + True -> True + False -> any(rest, predicate) + } + } +} + +/// Takes two lists and returns a single list of 2-element tuples. +/// +/// If one of the lists is longer than the other, the remaining elements from +/// the longer list are not used. +/// +/// ## Examples +/// +/// ```gleam +/// zip([], []) +/// // -> [] +/// ``` +/// +/// ```gleam +/// zip([1, 2], [3]) +/// // -> [#(1, 3)] +/// ``` +/// +/// ```gleam +/// zip([1], [3, 4]) +/// // -> [#(1, 3)] +/// ``` +/// +/// ```gleam +/// zip([1, 2], [3, 4]) +/// // -> [#(1, 3), #(2, 4)] +/// ``` +/// +pub fn zip(list: List(a), with other: List(b)) -> List(#(a, b)) { + zip_loop(list, other, []) +} + +fn zip_loop(one: List(a), other: List(b), acc: List(#(a, b))) -> List(#(a, b)) { + case one, other { + [first_one, ..rest_one], [first_other, ..rest_other] -> + zip_loop(rest_one, rest_other, [#(first_one, first_other), ..acc]) + _, _ -> reverse(acc) + } +} + +/// Takes two lists and returns a single list of 2-element tuples. +/// +/// If one of the lists is longer than the other, an `Error` is returned. +/// +/// ## Examples +/// +/// ```gleam +/// strict_zip([], []) +/// // -> Ok([]) +/// ``` +/// +/// ```gleam +/// strict_zip([1, 2], [3]) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// strict_zip([1], [3, 4]) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// strict_zip([1, 2], [3, 4]) +/// // -> Ok([#(1, 3), #(2, 4)]) +/// ``` +/// +pub fn strict_zip( + list: List(a), + with other: List(b), +) -> Result(List(#(a, b)), Nil) { + strict_zip_loop(list, other, []) +} + +fn strict_zip_loop( + one: List(a), + other: List(b), + acc: List(#(a, b)), +) -> Result(List(#(a, b)), Nil) { + case one, other { + [], [] -> Ok(reverse(acc)) + [], _ | _, [] -> Error(Nil) + [first_one, ..rest_one], [first_other, ..rest_other] -> + strict_zip_loop(rest_one, rest_other, [#(first_one, first_other), ..acc]) + } +} + +/// Takes a single list of 2-element tuples and returns two lists. +/// +/// ## Examples +/// +/// ```gleam +/// unzip([#(1, 2), #(3, 4)]) +/// // -> #([1, 3], [2, 4]) +/// ``` +/// +/// ```gleam +/// unzip([]) +/// // -> #([], []) +/// ``` +/// +pub fn unzip(input: List(#(a, b))) -> #(List(a), List(b)) { + unzip_loop(input, [], []) +} + +fn unzip_loop( + input: List(#(a, b)), + one: List(a), + other: List(b), +) -> #(List(a), List(b)) { + case input { + [] -> #(reverse(one), reverse(other)) + [#(first_one, first_other), ..rest] -> + unzip_loop(rest, [first_one, ..one], [first_other, ..other]) + } +} + +/// Inserts a given value between each existing element in a given list. +/// +/// This function runs in linear time and copies the list. +/// +/// ## Examples +/// +/// ```gleam +/// intersperse([1, 1, 1], 2) +/// // -> [1, 2, 1, 2, 1] +/// ``` +/// +/// ```gleam +/// intersperse([], 2) +/// // -> [] +/// ``` +/// +pub fn intersperse(list: List(a), with elem: a) -> List(a) { + case list { + [] | [_] -> list + [first, ..rest] -> intersperse_loop(rest, elem, [first]) + } +} + +fn intersperse_loop(list: List(a), separator: a, acc: List(a)) -> List(a) { + case list { + [] -> reverse(acc) + [first, ..rest] -> + intersperse_loop(rest, separator, [first, separator, ..acc]) + } +} + +/// Removes any duplicate elements from a given list. +/// +/// This function returns in loglinear time. +/// +/// ## Examples +/// +/// ```gleam +/// unique([1, 1, 1, 4, 7, 3, 3, 4]) +/// // -> [1, 4, 7, 3] +/// ``` +/// +pub fn unique(list: List(a)) -> List(a) { + unique_loop(list, dict.new(), []) +} + +fn unique_loop(list: List(a), seen: Dict(a, Nil), acc: List(a)) -> List(a) { + case list { + [] -> reverse(acc) + [first, ..rest] -> + case dict.has_key(seen, first) { + True -> unique_loop(rest, seen, acc) + False -> + unique_loop(rest, dict.insert(seen, first, Nil), [first, ..acc]) + } + } +} + +/// Sorts from smallest to largest based upon the ordering specified by a given +/// function. +/// +/// ## Examples +/// +/// ```gleam +/// import gleam/int +/// +/// sort([4, 3, 6, 5, 4, 1, 2], by: int.compare) +/// // -> [1, 2, 3, 4, 4, 5, 6] +/// ``` +/// +pub fn sort(list: List(a), by compare: fn(a, a) -> Order) -> List(a) { + // This is a natural, tail recursive, stable merge sort: + // - natural: it is very efficient if you call it on a list that is already + // (pre)sorted because it works on slices of the original list. + // - tail recursive: the stack won't grow linearly with the size of the list. + // - stable: if two items are considered to be equal then their original + // relative order is preserved. + case list { + // If the list has zero/one item then it's already sorted. + [] -> [] + [x] -> [x] + + // Otherwise the algorithm works as follow: we split the list in sequences + // of already sorted values as they appear in the list and then we merge + // those together two by two using `merge_all`. + [x, y, ..rest] -> { + // We need to compare the first two items to properly call `sequences` + // with the correct initial values. If the second item is <= than the + // first, then we know we'll start by growing a descending sequence + // (and an ascending one in the opposite case). + let direction = case compare(x, y) { + order.Lt | order.Eq -> Ascending + order.Gt -> Descending + } + + // `sequences` produces sequences in ascending order so we call the + // `merge_all` function saying it to expect all sequences to be sorted + // that way. + let sequences = sequences(rest, compare, [x], direction, y, []) + merge_all(sequences, Ascending, compare) + } + } +} + +type Sorting { + Ascending + Descending +} + +/// Given a list it returns slices of it that are locally sorted in ascending +/// order. +/// +/// Imagine you have this list: +/// +/// ``` +/// [1, 2, 3, 2, 1, 0] +/// ^^^^^^^ ^^^^^^^ This is a slice in descending order +/// | +/// | This is a slice that is sorted in ascending order +/// ``` +/// +/// So the produced result will contain these two slices, each one sorted in +/// ascending order: `[[1, 2, 3], [0, 1, 2]]`. +/// +/// - `growing` is an accumulator with the current slice being grown +/// - `direction` is the growing direction of the slice being grown, it could +/// either be ascending or strictly descending +/// - `prev` is the previous element that needs to be added to the growing slice +/// it is carried around to check whether we have to keep growing the current +/// slice or not +/// - `acc` is the accumulator containing the slices sorted in ascending order +/// +fn sequences( + list: List(a), + compare: fn(a, a) -> Order, + growing: List(a), + direction: Sorting, + prev: a, + acc: List(List(a)), +) -> List(List(a)) { + // First of all we must not forget to add the previous element to the + // currently growing slice. + let growing = [prev, ..growing] + + case list { + [] -> + case direction { + // Notice how we have to reverse the accumulator we're growing: since + // we always add items to the head, `growing` is built in the opposite + // sorting order of what it actually is in the original list. + Ascending -> [reverse(growing), ..acc] + Descending -> [growing, ..acc] + } + + [new, ..rest] -> + case compare(prev, new), direction { + // In case the new element respects the ordering of the growing + // sequence, then we just keep growing it. + // Notice how a growing sequence is weakly growing (that is it can have + // consecutive equal items) while a decreasing sequence is strictly + // decreasing (no consecutive equal items), this is needed to make the + // algorithm stable! + order.Gt, Descending | order.Lt, Ascending | order.Eq, Ascending -> + sequences(rest, compare, growing, direction, new, acc) + + // We were growing an ascending (descending) sequence and the new item + // is smaller (bigger) than the previous one, this means we have to stop + // growing this sequence and start with a new one whose first item will + // be the one we just found. + order.Gt, Ascending | order.Lt, Descending | order.Eq, Descending -> { + let acc = case direction { + Ascending -> [reverse(growing), ..acc] + Descending -> [growing, ..acc] + } + case rest { + // The list is over so we just create a sequence containing the last + // item we saw and add it to the accumulator before returning it. + [] -> [[new], ..acc] + + // If the list is not over we have a peek at the next item to decide + // in which direction is growing the new sequence and make the + // recursive call with the appropriate arguments. + [next, ..rest] -> { + let direction = case compare(new, next) { + order.Lt | order.Eq -> Ascending + order.Gt -> Descending + } + sequences(rest, compare, [new], direction, next, acc) + } + } + } + } + } +} + +/// Given some some sorted sequences (assumed to be sorted in `direction`) it +/// merges them all together until we're left with just a list sorted in +/// ascending order. +/// +fn merge_all( + sequences: List(List(a)), + direction: Sorting, + compare: fn(a, a) -> Order, +) -> List(a) { + case sequences, direction { + [], _ -> [] + + // If we have a single list in ascending order then we're done. + [sequence], Ascending -> sequence + + // If we have a single list in descending order, we reverse it to make sure + // it's in ascending order and we're done. + [sequence], Descending -> reverse(sequence) + + // Merging together sequences that are in ascending (descending) order + // reverses their order, so the recursive call will assume to be merging + // lists sorted in the opposite order! + _, Ascending -> { + let sequences = merge_ascending_pairs(sequences, compare, []) + merge_all(sequences, Descending, compare) + } + + _, Descending -> { + let sequences = merge_descending_pairs(sequences, compare, []) + merge_all(sequences, Ascending, compare) + } + } +} + +/// Given a list of ascending lists, it merges adjacent pairs into a single +/// descending list, halving their number. +/// It returns a list of the remaining descending lists. +/// +fn merge_ascending_pairs( + sequences: List(List(a)), + compare: fn(a, a) -> Order, + acc: List(List(a)), +) { + case sequences { + [] -> reverse(acc) + + // Beware, if we have just one item left we must reverse it: we take + // ascending lists as input and have to return descending ones. + // If we returned it like it is it would be sorted in ascending order. + [sequence] -> reverse([reverse(sequence), ..acc]) + + [ascending1, ascending2, ..rest] -> { + let descending = merge_ascendings(ascending1, ascending2, compare, []) + merge_ascending_pairs(rest, compare, [descending, ..acc]) + } + } +} + +/// This is the same as merge_ascending_pairs but flipped for descending lists. +/// +fn merge_descending_pairs( + sequences: List(List(a)), + compare: fn(a, a) -> Order, + acc: List(List(a)), +) { + case sequences { + [] -> reverse(acc) + + [sequence] -> reverse([reverse(sequence), ..acc]) + + [descending1, descending2, ..rest] -> { + let ascending = merge_descendings(descending1, descending2, compare, []) + merge_descending_pairs(rest, compare, [ascending, ..acc]) + } + } +} + +/// Merges two lists sorted in ascending order into a single list sorted in +/// descending order according to the given comparator function. +/// +/// This reversing of the sort order is not avoidable if we want to implement +/// merge as a tail recursive function. We could reverse the accumulator before +/// returning it but that would end up being less efficient; so the merging +/// algorithm has to play around this. +/// +fn merge_ascendings( + list1: List(a), + list2: List(a), + compare: fn(a, a) -> Order, + acc: List(a), +) -> List(a) { + case list1, list2 { + [], list | list, [] -> reverse_and_prepend(list, acc) + + [first1, ..rest1], [first2, ..rest2] -> + case compare(first1, first2) { + order.Lt -> merge_ascendings(rest1, list2, compare, [first1, ..acc]) + order.Gt | order.Eq -> + merge_ascendings(list1, rest2, compare, [first2, ..acc]) + } + } +} + +/// This is exactly the same as merge_ascendings but mirrored: it merges two +/// lists sorted in descending order into a single list sorted in ascending +/// order according to the given comparator function. +/// +/// This reversing of the sort order is not avoidable if we want to implement +/// merge as a tail recursive function. We could reverse the accumulator before +/// returning it but that would end up being less efficient; so the merging +/// algorithm has to play around this. +/// +fn merge_descendings( + list1: List(a), + list2: List(a), + compare: fn(a, a) -> Order, + acc: List(a), +) -> List(a) { + case list1, list2 { + [], list | list, [] -> reverse_and_prepend(list, acc) + [first1, ..rest1], [first2, ..rest2] -> + case compare(first1, first2) { + order.Lt -> merge_descendings(list1, rest2, compare, [first2, ..acc]) + order.Gt | order.Eq -> + merge_descendings(rest1, list2, compare, [first1, ..acc]) + } + } +} + +/// Creates a list of ints ranging from a given start and finish. +/// +/// ## Examples +/// +/// ```gleam +/// range(0, 0) +/// // -> [0] +/// ``` +/// +/// ```gleam +/// range(0, 5) +/// // -> [0, 1, 2, 3, 4, 5] +/// ``` +/// +/// ```gleam +/// range(1, -5) +/// // -> [1, 0, -1, -2, -3, -4, -5] +/// ``` +/// +pub fn range(from start: Int, to stop: Int) -> List(Int) { + range_loop(start, stop, []) +} + +fn range_loop(start: Int, stop: Int, acc: List(Int)) -> List(Int) { + case int.compare(start, stop) { + order.Eq -> [stop, ..acc] + order.Gt -> range_loop(start, stop + 1, [stop, ..acc]) + order.Lt -> range_loop(start, stop - 1, [stop, ..acc]) + } +} + +/// Builds a list of a given value a given number of times. +/// +/// ## Examples +/// +/// ```gleam +/// repeat("a", times: 0) +/// // -> [] +/// ``` +/// +/// ```gleam +/// repeat("a", times: 5) +/// // -> ["a", "a", "a", "a", "a"] +/// ``` +/// +pub fn repeat(item a: a, times times: Int) -> List(a) { + repeat_loop(a, times, []) +} + +fn repeat_loop(item: a, times: Int, acc: List(a)) -> List(a) { + case times <= 0 { + True -> acc + False -> repeat_loop(item, times - 1, [item, ..acc]) + } +} + +/// Splits a list in two before the given index. +/// +/// If the list is not long enough to have the given index the before list will +/// be the input list, and the after list will be empty. +/// +/// ## Examples +/// +/// ```gleam +/// split([6, 7, 8, 9], 0) +/// // -> #([], [6, 7, 8, 9]) +/// ``` +/// +/// ```gleam +/// split([6, 7, 8, 9], 2) +/// // -> #([6, 7], [8, 9]) +/// ``` +/// +/// ```gleam +/// split([6, 7, 8, 9], 4) +/// // -> #([6, 7, 8, 9], []) +/// ``` +/// +pub fn split(list list: List(a), at index: Int) -> #(List(a), List(a)) { + split_loop(list, index, []) +} + +fn split_loop(list: List(a), n: Int, taken: List(a)) -> #(List(a), List(a)) { + case n <= 0 { + True -> #(reverse(taken), list) + False -> + case list { + [] -> #(reverse(taken), []) + [first, ..rest] -> split_loop(rest, n - 1, [first, ..taken]) + } + } +} + +/// Splits a list in two before the first element that a given function returns +/// `False` for. +/// +/// If the function returns `True` for all elements the first list will be the +/// input list, and the second list will be empty. +/// +/// ## Examples +/// +/// ```gleam +/// split_while([1, 2, 3, 4, 5], fn(x) { x <= 3 }) +/// // -> #([1, 2, 3], [4, 5]) +/// ``` +/// +/// ```gleam +/// split_while([1, 2, 3, 4, 5], fn(x) { x <= 5 }) +/// // -> #([1, 2, 3, 4, 5], []) +/// ``` +/// +pub fn split_while( + list list: List(a), + satisfying predicate: fn(a) -> Bool, +) -> #(List(a), List(a)) { + split_while_loop(list, predicate, []) +} + +fn split_while_loop( + list: List(a), + f: fn(a) -> Bool, + acc: List(a), +) -> #(List(a), List(a)) { + case list { + [] -> #(reverse(acc), []) + [first, ..rest] -> + case f(first) { + True -> split_while_loop(rest, f, [first, ..acc]) + False -> #(reverse(acc), list) + } + } +} + +/// Given a list of 2-element tuples, finds the first tuple that has a given +/// key as the first element and returns the second element. +/// +/// If no tuple is found with the given key then `Error(Nil)` is returned. +/// +/// This function may be useful for interacting with Erlang code where lists of +/// tuples are common. +/// +/// ## Examples +/// +/// ```gleam +/// key_find([#("a", 0), #("b", 1)], "a") +/// // -> Ok(0) +/// ``` +/// +/// ```gleam +/// key_find([#("a", 0), #("b", 1)], "b") +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// key_find([#("a", 0), #("b", 1)], "c") +/// // -> Error(Nil) +/// ``` +/// +pub fn key_find( + in keyword_list: List(#(k, v)), + find desired_key: k, +) -> Result(v, Nil) { + find_map(keyword_list, fn(keyword) { + let #(key, value) = keyword + case key == desired_key { + True -> Ok(value) + False -> Error(Nil) + } + }) +} + +/// Given a list of 2-element tuples, finds all tuples that have a given +/// key as the first element and returns the second element. +/// +/// This function may be useful for interacting with Erlang code where lists of +/// tuples are common. +/// +/// ## Examples +/// +/// ```gleam +/// key_filter([#("a", 0), #("b", 1), #("a", 2)], "a") +/// // -> [0, 2] +/// ``` +/// +/// ```gleam +/// key_filter([#("a", 0), #("b", 1)], "c") +/// // -> [] +/// ``` +/// +pub fn key_filter( + in keyword_list: List(#(k, v)), + find desired_key: k, +) -> List(v) { + filter_map(keyword_list, fn(keyword) { + let #(key, value) = keyword + case key == desired_key { + True -> Ok(value) + False -> Error(Nil) + } + }) +} + +/// Given a list of 2-element tuples, finds the first tuple that has a given +/// key as the first element. This function will return the second element +/// of the found tuple and list with tuple removed. +/// +/// If no tuple is found with the given key then `Error(Nil)` is returned. +/// +/// ## Examples +/// +/// ```gleam +/// key_pop([#("a", 0), #("b", 1)], "a") +/// // -> Ok(#(0, [#("b", 1)])) +/// ``` +/// +/// ```gleam +/// key_pop([#("a", 0), #("b", 1)], "b") +/// // -> Ok(#(1, [#("a", 0)])) +/// ``` +/// +/// ```gleam +/// key_pop([#("a", 0), #("b", 1)], "c") +/// // -> Error(Nil) +/// ``` +/// +pub fn key_pop(list: List(#(k, v)), key: k) -> Result(#(v, List(#(k, v))), Nil) { + key_pop_loop(list, key, []) +} + +fn key_pop_loop( + list: List(#(k, v)), + key: k, + checked: List(#(k, v)), +) -> Result(#(v, List(#(k, v))), Nil) { + case list { + [] -> Error(Nil) + [#(k, v), ..rest] if k == key -> + Ok(#(v, reverse_and_prepend(checked, rest))) + [first, ..rest] -> key_pop_loop(rest, key, [first, ..checked]) + } +} + +/// Given a list of 2-element tuples, inserts a key and value into the list. +/// +/// If there was already a tuple with the key then it is replaced, otherwise it +/// is added to the end of the list. +/// +/// ## Examples +/// +/// ```gleam +/// key_set([#(5, 0), #(4, 1)], 4, 100) +/// // -> [#(5, 0), #(4, 100)] +/// ``` +/// +/// ```gleam +/// key_set([#(5, 0), #(4, 1)], 1, 100) +/// // -> [#(5, 0), #(4, 1), #(1, 100)] +/// ``` +/// +pub fn key_set(list: List(#(k, v)), key: k, value: v) -> List(#(k, v)) { + key_set_loop(list, key, value, []) +} + +fn key_set_loop( + list: List(#(k, v)), + key: k, + value: v, + inspected: List(#(k, v)), +) -> List(#(k, v)) { + case list { + [#(k, _), ..rest] if k == key -> + reverse_and_prepend(inspected, [#(k, value), ..rest]) + [first, ..rest] -> key_set_loop(rest, key, value, [first, ..inspected]) + [] -> reverse([#(key, value), ..inspected]) + } +} + +/// Calls a function for each element in a list, discarding the return value. +/// +/// Useful for calling a side effect for every item of a list. +/// +/// ```gleam +/// import gleam/io +/// +/// each(["1", "2", "3"], io.println) +/// // -> Nil +/// // 1 +/// // 2 +/// // 3 +/// ``` +/// +pub fn each(list: List(a), f: fn(a) -> b) -> Nil { + case list { + [] -> Nil + [first, ..rest] -> { + f(first) + each(rest, f) + } + } +} + +/// Calls a `Result` returning function for each element in a list, discarding +/// the return value. If the function returns `Error` then the iteration is +/// stopped and the error is returned. +/// +/// Useful for calling a side effect for every item of a list. +/// +/// ## Examples +/// +/// ```gleam +/// try_each( +/// over: [1, 2, 3], +/// with: function_that_might_fail, +/// ) +/// // -> Ok(Nil) +/// ``` +/// +pub fn try_each( + over list: List(a), + with fun: fn(a) -> Result(b, e), +) -> Result(Nil, e) { + case list { + [] -> Ok(Nil) + [first, ..rest] -> + case fun(first) { + Ok(_) -> try_each(over: rest, with: fun) + Error(e) -> Error(e) + } + } +} + +/// Partitions a list into a tuple/pair of lists +/// by a given categorisation function. +/// +/// ## Examples +/// +/// ```gleam +/// import gleam/int +/// +/// [1, 2, 3, 4, 5] |> partition(int.is_odd) +/// // -> #([1, 3, 5], [2, 4]) +/// ``` +/// +pub fn partition( + list: List(a), + with categorise: fn(a) -> Bool, +) -> #(List(a), List(a)) { + partition_loop(list, categorise, [], []) +} + +fn partition_loop(list, categorise, trues, falses) { + case list { + [] -> #(reverse(trues), reverse(falses)) + [first, ..rest] -> + case categorise(first) { + True -> partition_loop(rest, categorise, [first, ..trues], falses) + False -> partition_loop(rest, categorise, trues, [first, ..falses]) + } + } +} + +/// Returns all the permutations of a list. +/// +/// ## Examples +/// +/// ```gleam +/// permutations([1, 2]) +/// // -> [[1, 2], [2, 1]] +/// ``` +/// +pub fn permutations(list: List(a)) -> List(List(a)) { + case list { + [] -> [[]] + l -> permutation_zip(l, [], []) + } +} + +fn permutation_zip( + list: List(a), + rest: List(a), + acc: List(List(a)), +) -> List(List(a)) { + case list { + [] -> reverse(acc) + [head, ..tail] -> + permutation_prepend( + head, + permutations(reverse_and_prepend(rest, tail)), + tail, + [head, ..rest], + acc, + ) + } +} + +fn permutation_prepend( + el: a, + permutations: List(List(a)), + list_1: List(a), + list_2: List(a), + acc: List(List(a)), +) -> List(List(a)) { + case permutations { + [] -> permutation_zip(list_1, list_2, acc) + [head, ..tail] -> + permutation_prepend(el, tail, list_1, list_2, [[el, ..head], ..acc]) + } +} + +/// Returns a list of sliding windows. +/// +/// ## Examples +/// +/// ```gleam +/// window([1,2,3,4,5], 3) +/// // -> [[1, 2, 3], [2, 3, 4], [3, 4, 5]] +/// ``` +/// +/// ```gleam +/// window([1, 2], 4) +/// // -> [] +/// ``` +/// +pub fn window(list: List(a), by n: Int) -> List(List(a)) { + case n <= 0 { + True -> [] + False -> window_loop([], list, n) + } +} + +fn window_loop(acc: List(List(a)), list: List(a), n: Int) -> List(List(a)) { + let window = take(list, n) + + case length(window) == n { + True -> window_loop([window, ..acc], drop(list, 1), n) + False -> reverse(acc) + } +} + +/// Returns a list of tuples containing two contiguous elements. +/// +/// ## Examples +/// +/// ```gleam +/// window_by_2([1,2,3,4]) +/// // -> [#(1, 2), #(2, 3), #(3, 4)] +/// ``` +/// +/// ```gleam +/// window_by_2([1]) +/// // -> [] +/// ``` +/// +pub fn window_by_2(list: List(a)) -> List(#(a, a)) { + zip(list, drop(list, 1)) +} + +/// Drops the first elements in a given list for which the predicate function returns `True`. +/// +/// ## Examples +/// +/// ```gleam +/// drop_while([1, 2, 3, 4], fn (x) { x < 3 }) +/// // -> [3, 4] +/// ``` +/// +pub fn drop_while( + in list: List(a), + satisfying predicate: fn(a) -> Bool, +) -> List(a) { + case list { + [] -> [] + [first, ..rest] -> + case predicate(first) { + True -> drop_while(rest, predicate) + False -> [first, ..rest] + } + } +} + +/// Takes the first elements in a given list for which the predicate function returns `True`. +/// +/// ## Examples +/// +/// ```gleam +/// take_while([1, 2, 3, 2, 4], fn (x) { x < 3 }) +/// // -> [1, 2] +/// ``` +/// +pub fn take_while( + in list: List(a), + satisfying predicate: fn(a) -> Bool, +) -> List(a) { + take_while_loop(list, predicate, []) +} + +fn take_while_loop( + list: List(a), + predicate: fn(a) -> Bool, + acc: List(a), +) -> List(a) { + case list { + [] -> reverse(acc) + [first, ..rest] -> + case predicate(first) { + True -> take_while_loop(rest, predicate, [first, ..acc]) + False -> reverse(acc) + } + } +} + +/// Returns a list of chunks in which +/// the return value of calling `f` on each element is the same. +/// +/// ## Examples +/// +/// ```gleam +/// [1, 2, 2, 3, 4, 4, 6, 7, 7] |> chunk(by: fn(n) { n % 2 }) +/// // -> [[1], [2, 2], [3], [4, 4, 6], [7, 7]] +/// ``` +/// +pub fn chunk(in list: List(a), by f: fn(a) -> k) -> List(List(a)) { + case list { + [] -> [] + [first, ..rest] -> chunk_loop(rest, f, f(first), [first], []) + } +} + +fn chunk_loop( + list: List(a), + f: fn(a) -> k, + previous_key: k, + current_chunk: List(a), + acc: List(List(a)), +) -> List(List(a)) { + case list { + [first, ..rest] -> { + let key = f(first) + case key == previous_key { + True -> chunk_loop(rest, f, key, [first, ..current_chunk], acc) + False -> { + let new_acc = [reverse(current_chunk), ..acc] + chunk_loop(rest, f, key, [first], new_acc) + } + } + } + [] -> reverse([reverse(current_chunk), ..acc]) + } +} + +/// Returns a list of chunks containing `count` elements each. +/// +/// If the last chunk does not have `count` elements, it is instead +/// a partial chunk, with less than `count` elements. +/// +/// For any `count` less than 1 this function behaves as if it was set to 1. +/// +/// ## Examples +/// +/// ```gleam +/// [1, 2, 3, 4, 5, 6] |> sized_chunk(into: 2) +/// // -> [[1, 2], [3, 4], [5, 6]] +/// ``` +/// +/// ```gleam +/// [1, 2, 3, 4, 5, 6, 7, 8] |> sized_chunk(into: 3) +/// // -> [[1, 2, 3], [4, 5, 6], [7, 8]] +/// ``` +/// +pub fn sized_chunk(in list: List(a), into count: Int) -> List(List(a)) { + sized_chunk_loop(list, count, count, [], []) +} + +fn sized_chunk_loop( + list: List(a), + count: Int, + left: Int, + current_chunk: List(a), + acc: List(List(a)), +) -> List(List(a)) { + case list { + [] -> + case current_chunk { + [] -> reverse(acc) + remaining -> reverse([reverse(remaining), ..acc]) + } + [first, ..rest] -> { + let chunk = [first, ..current_chunk] + case left > 1 { + True -> sized_chunk_loop(rest, count, left - 1, chunk, acc) + False -> + sized_chunk_loop(rest, count, count, [], [reverse(chunk), ..acc]) + } + } + } +} + +/// This function acts similar to fold, but does not take an initial state. +/// Instead, it starts from the first element in the list +/// and combines it with each subsequent element in turn using the given +/// function. The function is called as `fun(accumulator, current_element)`. +/// +/// Returns `Ok` to indicate a successful run, and `Error` if called on an +/// empty list. +/// +/// ## Examples +/// +/// ```gleam +/// [] |> reduce(fn(acc, x) { acc + x }) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// [1, 2, 3, 4, 5] |> reduce(fn(acc, x) { acc + x }) +/// // -> Ok(15) +/// ``` +/// +pub fn reduce(over list: List(a), with fun: fn(a, a) -> a) -> Result(a, Nil) { + case list { + [] -> Error(Nil) + [first, ..rest] -> Ok(fold(rest, first, fun)) + } +} + +/// Similar to `fold`, but yields the state of the accumulator at each stage. +/// +/// ## Examples +/// +/// ```gleam +/// scan(over: [1, 2, 3], from: 100, with: fn(acc, i) { acc + i }) +/// // -> [101, 103, 106] +/// ``` +/// +pub fn scan( + over list: List(a), + from initial: acc, + with fun: fn(acc, a) -> acc, +) -> List(acc) { + scan_loop(list, initial, [], fun) +} + +fn scan_loop( + list: List(a), + accumulator: acc, + accumulated: List(acc), + fun: fn(acc, a) -> acc, +) -> List(acc) { + case list { + [] -> reverse(accumulated) + [first, ..rest] -> { + let next = fun(accumulator, first) + scan_loop(rest, next, [next, ..accumulated], fun) + } + } +} + +/// Returns the last element in the given list. +/// +/// Returns `Error(Nil)` if the list is empty. +/// +/// This function runs in linear time. +/// +/// ## Examples +/// +/// ```gleam +/// last([]) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// last([1, 2, 3, 4, 5]) +/// // -> Ok(5) +/// ``` +/// +pub fn last(list: List(a)) -> Result(a, Nil) { + case list { + [] -> Error(Nil) + [last] -> Ok(last) + [_, ..rest] -> last(rest) + } +} + +/// Return unique combinations of elements in the list. +/// +/// ## Examples +/// +/// ```gleam +/// combinations([1, 2, 3], 2) +/// // -> [[1, 2], [1, 3], [2, 3]] +/// ``` +/// +/// ```gleam +/// combinations([1, 2, 3, 4], 3) +/// // -> [[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]] +/// ``` +/// +pub fn combinations(items: List(a), by n: Int) -> List(List(a)) { + case n, items { + 0, _ -> [[]] + _, [] -> [] + _, [first, ..rest] -> + rest + |> combinations(n - 1) + |> map(fn(combination) { [first, ..combination] }) + |> reverse + |> fold(combinations(rest, n), fn(acc, c) { [c, ..acc] }) + } +} + +/// Return unique pair combinations of elements in the list. +/// +/// ## Examples +/// +/// ```gleam +/// combination_pairs([1, 2, 3]) +/// // -> [#(1, 2), #(1, 3), #(2, 3)] +/// ``` +/// +pub fn combination_pairs(items: List(a)) -> List(#(a, a)) { + combination_pairs_loop(items, []) +} + +fn combination_pairs_loop(items: List(a), acc: List(#(a, a))) -> List(#(a, a)) { + case items { + [] -> reverse(acc) + [first, ..rest] -> { + let first_combinations = map(rest, with: fn(other) { #(first, other) }) + let acc = reverse_and_prepend(first_combinations, acc) + combination_pairs_loop(rest, acc) + } + } +} + +/// Make a list alternating the elements from the given lists +/// +/// ## Examples +/// +/// ```gleam +/// interleave([[1, 2], [101, 102], [201, 202]]) +/// // -> [1, 101, 201, 2, 102, 202] +/// ``` +/// +pub fn interleave(list: List(List(a))) -> List(a) { + list + |> transpose + |> flatten +} + +/// Transpose rows and columns of the list of lists. +/// +/// Notice: This function is not tail recursive, +/// and thus may exceed stack size if called, +/// with large lists (on the JavaScript target). +/// +/// ## Examples +/// +/// ```gleam +/// transpose([[1, 2, 3], [101, 102, 103]]) +/// // -> [[1, 101], [2, 102], [3, 103]] +/// ``` +/// +pub fn transpose(list_of_lists: List(List(a))) -> List(List(a)) { + transpose_loop(list_of_lists, []) +} + +fn transpose_loop(rows: List(List(a)), columns: List(List(a))) -> List(List(a)) { + case rows { + [] -> reverse(columns) + _ -> { + let #(column, rest) = take_firsts(rows, [], []) + case column { + [_, ..] -> transpose_loop(rest, [column, ..columns]) + [] -> transpose_loop(rest, columns) + } + } + } +} + +fn take_firsts( + rows: List(List(a)), + column: List(a), + remaining_rows: List(List(a)), +) -> #(List(a), List(List(a))) { + case rows { + [] -> #(reverse(column), reverse(remaining_rows)) + [[], ..rest] -> take_firsts(rest, column, remaining_rows) + [[first, ..remaining_row], ..rest_rows] -> { + let remaining_rows = [remaining_row, ..remaining_rows] + take_firsts(rest_rows, [first, ..column], remaining_rows) + } + } +} + +/// Takes a list, randomly sorts all items and returns the shuffled list. +/// +/// This function uses `float.random` to decide the order of the elements. +/// +/// ## Example +/// +/// ```gleam +/// range(1, 10) |> shuffle() +/// // -> [1, 6, 9, 10, 3, 8, 4, 2, 7, 5] +/// ``` +/// +pub fn shuffle(list: List(a)) -> List(a) { + list + |> fold(from: [], with: fn(acc, a) { [#(float.random(), a), ..acc] }) + |> do_shuffle_by_pair_indexes() + |> shuffle_pair_unwrap_loop([]) +} + +fn shuffle_pair_unwrap_loop(list: List(#(Float, a)), acc: List(a)) -> List(a) { + case list { + [] -> acc + [elem_pair, ..enumerable] -> + shuffle_pair_unwrap_loop(enumerable, [elem_pair.1, ..acc]) + } +} + +fn do_shuffle_by_pair_indexes( + list_of_pairs: List(#(Float, a)), +) -> List(#(Float, a)) { + sort(list_of_pairs, fn(a_pair: #(Float, a), b_pair: #(Float, a)) -> Order { + float.compare(a_pair.0, b_pair.0) + }) +} + +/// Takes a list and a comparator, and returns the maximum element in the list +/// +/// +/// ## Example +/// +/// ```gleam +/// range(1, 10) |> list.max(int.compare) +/// // -> Ok(10) +/// ``` +/// +/// ```gleam +/// ["a", "c", "b"] |> list.max(string.compare) +/// // -> Ok("c") +/// ``` +pub fn max( + over list: List(a), + with compare: fn(a, a) -> Order, +) -> Result(a, Nil) { + case list { + [] -> Error(Nil) + [first, ..rest] -> Ok(max_loop(rest, compare, first)) + } +} + +fn max_loop(list, compare, max) { + case list { + [] -> max + [first, ..rest] -> + case compare(first, max) { + order.Gt -> max_loop(rest, compare, first) + order.Lt | order.Eq -> max_loop(rest, compare, max) + } + } +} + +/// Returns a random sample of up to n elements from a list using reservoir +/// sampling via [Algorithm L](https://en.wikipedia.org/wiki/Reservoir_sampling#Optimal:_Algorithm_L). +/// Returns an empty list if the sample size is less than or equal to 0. +/// +/// Order is not random, only selection is. +/// +/// ## Examples +/// +/// ```gleam +/// reservoir_sample([1, 2, 3, 4, 5], 3) +/// // -> [2, 4, 5] // A random sample of 3 items +/// ``` +/// +pub fn sample(from list: List(a), up_to n: Int) -> List(a) { + let #(reservoir, rest) = build_reservoir(from: list, sized: n) + + case dict.is_empty(reservoir) { + // If the reservoire is empty that means we were asking to sample 0 or + // less items. That doesn't make much sense, so we just return an empty + // list. + True -> [] + + // Otherwise we keep looping over the remaining part of the list replacing + // random elements in the reservoir. + False -> { + let w = float.exponential(log_random() /. int.to_float(n)) + dict.values(sample_loop(rest, reservoir, n, w)) + } + } +} + +fn sample_loop( + list: List(a), + reservoir: Dict(Int, a), + n: Int, + w: Float, +) -> Dict(Int, a) { + let skip = { + let assert Ok(log) = float.logarithm(1.0 -. w) + float.round(float.floor(log_random() /. log)) + } + + case drop(list, skip) { + [] -> reservoir + [first, ..rest] -> { + let reservoir = dict.insert(reservoir, int.random(n), first) + let w = w *. float.exponential(log_random() /. int.to_float(n)) + sample_loop(rest, reservoir, n, w) + } + } +} + +const min_positive = 2.2250738585072014e-308 + +fn log_random() -> Float { + let assert Ok(random) = float.logarithm(float.random() +. min_positive) + random +} + +/// Builds the initial reservoir used by Algorithm L. +/// This is a dictionary with keys ranging from `0` up to `n - 1` where each +/// value is the corresponding element at that position in `list`. +/// +/// This also returns the remaining elements of `list` that didn't end up in +/// the reservoir. +/// +fn build_reservoir(from list: List(a), sized n: Int) -> #(Dict(Int, a), List(a)) { + build_reservoir_loop(list, n, dict.new()) +} + +fn build_reservoir_loop( + list: List(a), + size: Int, + reservoir: Dict(Int, a), +) -> #(Dict(Int, a), List(a)) { + let reservoir_size = dict.size(reservoir) + case reservoir_size >= size { + // The reservoir already has the size we wanted. + True -> #(reservoir, list) + + // Otherwise we add another element from the list to the reservoir + False -> + case list { + [] -> #(reservoir, []) + [first, ..rest] -> { + let reservoir = dict.insert(reservoir, reservoir_size, first) + build_reservoir_loop(rest, size, reservoir) + } + } + } +} diff --git a/build/packages/gleam_stdlib/src/gleam/option.gleam b/build/packages/gleam_stdlib/src/gleam/option.gleam new file mode 100644 index 0000000..af5d864 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/option.gleam @@ -0,0 +1,358 @@ +/// `Option` represents a value that may be present or not. `Some` means the value is +/// present, `None` means the value is not. +/// +/// This is Gleam's alternative to having a value that could be Null, as is +/// possible in some other languages. +/// +/// ## `Option` and `Result` +/// +/// In other languages fallible functions may return either `Result` or +/// `Option` depending on whether there is more information to be given about the +/// failure. In Gleam all fallible functions return `Result`, and `Nil` is used +/// as the error if there is no extra detail to give. This consistency removes +/// the boilerplate that would otherwise be needed to convert between `Option` +/// and `Result` types, and makes APIs more predictable. +/// +/// The `Option` type should only be used for taking optional values as +/// function arguments, or for storing them in other data structures. +/// +pub type Option(a) { + Some(a) + None +} + +/// Combines a list of `Option`s into a single `Option`. +/// If all elements in the list are `Some` then returns a `Some` holding the list of values. +/// If any element is `None` then returns`None`. +/// +/// ## Examples +/// +/// ```gleam +/// all([Some(1), Some(2)]) +/// // -> Some([1, 2]) +/// ``` +/// +/// ```gleam +/// all([Some(1), None]) +/// // -> None +/// ``` +/// +pub fn all(list: List(Option(a))) -> Option(List(a)) { + all_loop(list, []) +} + +fn all_loop(list: List(Option(a)), acc: List(a)) -> Option(List(a)) { + case list { + [] -> Some(reverse(acc)) + [None, ..] -> None + [Some(first), ..rest] -> all_loop(rest, [first, ..acc]) + } +} + +// This is copied from the list module and not imported as importing it would +// result in a circular dependency! +@external(erlang, "lists", "reverse") +fn reverse(list: List(a)) -> List(a) { + reverse_and_prepend(list, []) +} + +fn reverse_and_prepend(list prefix: List(a), to suffix: List(a)) -> List(a) { + case prefix { + [] -> suffix + [first, ..rest] -> reverse_and_prepend(list: rest, to: [first, ..suffix]) + } +} + +/// Checks whether the `Option` is a `Some` value. +/// +/// ## Examples +/// +/// ```gleam +/// is_some(Some(1)) +/// // -> True +/// ``` +/// +/// ```gleam +/// is_some(None) +/// // -> False +/// ``` +/// +pub fn is_some(option: Option(a)) -> Bool { + option != None +} + +/// Checks whether the `Option` is a `None` value. +/// +/// ## Examples +/// +/// ```gleam +/// is_none(Some(1)) +/// // -> False +/// ``` +/// +/// ```gleam +/// is_none(None) +/// // -> True +/// ``` +/// +pub fn is_none(option: Option(a)) -> Bool { + option == None +} + +/// Converts an `Option` type to a `Result` type. +/// +/// ## Examples +/// +/// ```gleam +/// to_result(Some(1), "some_error") +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// to_result(None, "some_error") +/// // -> Error("some_error") +/// ``` +/// +pub fn to_result(option: Option(a), e) -> Result(a, e) { + case option { + Some(a) -> Ok(a) + None -> Error(e) + } +} + +/// Converts a `Result` type to an `Option` type. +/// +/// ## Examples +/// +/// ```gleam +/// from_result(Ok(1)) +/// // -> Some(1) +/// ``` +/// +/// ```gleam +/// from_result(Error("some_error")) +/// // -> None +/// ``` +/// +pub fn from_result(result: Result(a, e)) -> Option(a) { + case result { + Ok(a) -> Some(a) + Error(_) -> None + } +} + +/// Extracts the value from an `Option`, returning a default value if there is none. +/// +/// ## Examples +/// +/// ```gleam +/// unwrap(Some(1), 0) +/// // -> 1 +/// ``` +/// +/// ```gleam +/// unwrap(None, 0) +/// // -> 0 +/// ``` +/// +pub fn unwrap(option: Option(a), or default: a) -> a { + case option { + Some(x) -> x + None -> default + } +} + +/// Extracts the value from an `Option`, evaluating the default function if the option is `None`. +/// +/// ## Examples +/// +/// ```gleam +/// lazy_unwrap(Some(1), fn() { 0 }) +/// // -> 1 +/// ``` +/// +/// ```gleam +/// lazy_unwrap(None, fn() { 0 }) +/// // -> 0 +/// ``` +/// +pub fn lazy_unwrap(option: Option(a), or default: fn() -> a) -> a { + case option { + Some(x) -> x + None -> default() + } +} + +/// Updates a value held within the `Some` of an `Option` by calling a given function +/// on it. +/// +/// If the `Option` is a `None` rather than `Some`, the function is not called and the +/// `Option` stays the same. +/// +/// ## Examples +/// +/// ```gleam +/// map(over: Some(1), with: fn(x) { x + 1 }) +/// // -> Some(2) +/// ``` +/// +/// ```gleam +/// map(over: None, with: fn(x) { x + 1 }) +/// // -> None +/// ``` +/// +pub fn map(over option: Option(a), with fun: fn(a) -> b) -> Option(b) { + case option { + Some(x) -> Some(fun(x)) + None -> None + } +} + +/// Merges a nested `Option` into a single layer. +/// +/// ## Examples +/// +/// ```gleam +/// flatten(Some(Some(1))) +/// // -> Some(1) +/// ``` +/// +/// ```gleam +/// flatten(Some(None)) +/// // -> None +/// ``` +/// +/// ```gleam +/// flatten(None) +/// // -> None +/// ``` +/// +pub fn flatten(option: Option(Option(a))) -> Option(a) { + case option { + Some(x) -> x + None -> None + } +} + +/// Updates a value held within the `Some` of an `Option` by calling a given function +/// on it, where the given function also returns an `Option`. The two options are +/// then merged together into one `Option`. +/// +/// If the `Option` is a `None` rather than `Some` the function is not called and the +/// option stays the same. +/// +/// This function is the equivalent of calling `map` followed by `flatten`, and +/// it is useful for chaining together multiple functions that return `Option`. +/// +/// ## Examples +/// +/// ```gleam +/// then(Some(1), fn(x) { Some(x + 1) }) +/// // -> Some(2) +/// ``` +/// +/// ```gleam +/// then(Some(1), fn(x) { Some(#("a", x)) }) +/// // -> Some(#("a", 1)) +/// ``` +/// +/// ```gleam +/// then(Some(1), fn(_) { None }) +/// // -> None +/// ``` +/// +/// ```gleam +/// then(None, fn(x) { Some(x + 1) }) +/// // -> None +/// ``` +/// +pub fn then(option: Option(a), apply fun: fn(a) -> Option(b)) -> Option(b) { + case option { + Some(x) -> fun(x) + None -> None + } +} + +/// Returns the first value if it is `Some`, otherwise returns the second value. +/// +/// ## Examples +/// +/// ```gleam +/// or(Some(1), Some(2)) +/// // -> Some(1) +/// ``` +/// +/// ```gleam +/// or(Some(1), None) +/// // -> Some(1) +/// ``` +/// +/// ```gleam +/// or(None, Some(2)) +/// // -> Some(2) +/// ``` +/// +/// ```gleam +/// or(None, None) +/// // -> None +/// ``` +/// +pub fn or(first: Option(a), second: Option(a)) -> Option(a) { + case first { + Some(_) -> first + None -> second + } +} + +/// Returns the first value if it is `Some`, otherwise evaluates the given function for a fallback value. +/// +/// ## Examples +/// +/// ```gleam +/// lazy_or(Some(1), fn() { Some(2) }) +/// // -> Some(1) +/// ``` +/// +/// ```gleam +/// lazy_or(Some(1), fn() { None }) +/// // -> Some(1) +/// ``` +/// +/// ```gleam +/// lazy_or(None, fn() { Some(2) }) +/// // -> Some(2) +/// ``` +/// +/// ```gleam +/// lazy_or(None, fn() { None }) +/// // -> None +/// ``` +/// +pub fn lazy_or(first: Option(a), second: fn() -> Option(a)) -> Option(a) { + case first { + Some(_) -> first + None -> second() + } +} + +/// Given a list of `Option`s, +/// returns only the values inside `Some`. +/// +/// ## Examples +/// +/// ```gleam +/// values([Some(1), None, Some(3)]) +/// // -> [1, 3] +/// ``` +/// +pub fn values(options: List(Option(a))) -> List(a) { + values_loop(options, []) +} + +fn values_loop(list: List(Option(a)), acc: List(a)) -> List(a) { + case list { + [] -> reverse(acc) + [None, ..rest] -> values_loop(rest, acc) + [Some(first), ..rest] -> values_loop(rest, [first, ..acc]) + } +} diff --git a/build/packages/gleam_stdlib/src/gleam/order.gleam b/build/packages/gleam_stdlib/src/gleam/order.gleam new file mode 100644 index 0000000..be8b599 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/order.gleam @@ -0,0 +1,156 @@ +/// Represents the result of a single comparison to determine the precise +/// ordering of two values. +/// +pub type Order { + /// Less-than + Lt + + /// Equal + Eq + + /// Greater than + Gt +} + +/// Inverts an order, so less-than becomes greater-than and greater-than +/// becomes less-than. +/// +/// ## Examples +/// +/// ```gleam +/// negate(Lt) +/// // -> Gt +/// ``` +/// +/// ```gleam +/// negate(Eq) +/// // -> Eq +/// ``` +/// +/// ```gleam +/// negate(Gt) +/// // -> Lt +/// ``` +/// +pub fn negate(order: Order) -> Order { + case order { + Lt -> Gt + Eq -> Eq + Gt -> Lt + } +} + +/// Produces a numeric representation of the order. +/// +/// ## Examples +/// +/// ```gleam +/// to_int(Lt) +/// // -> -1 +/// ``` +/// +/// ```gleam +/// to_int(Eq) +/// // -> 0 +/// ``` +/// +/// ```gleam +/// to_int(Gt) +/// // -> 1 +/// ``` +/// +pub fn to_int(order: Order) -> Int { + case order { + Lt -> -1 + Eq -> 0 + Gt -> 1 + } +} + +/// Compares two `Order` values to one another, producing a new `Order`. +/// +/// ## Examples +/// +/// ```gleam +/// compare(Eq, with: Lt) +/// // -> Gt +/// ``` +/// +pub fn compare(a: Order, with b: Order) -> Order { + case a, b { + x, y if x == y -> Eq + Lt, _ | Eq, Gt -> Lt + _, _ -> Gt + } +} + +/// Inverts an ordering function, so less-than becomes greater-than and greater-than +/// becomes less-than. +/// +/// ## Examples +/// +/// ```gleam +/// import gleam/int +/// import gleam/list +/// +/// list.sort([1, 5, 4], by: reverse(int.compare)) +/// // -> [5, 4, 1] +/// ``` +/// +pub fn reverse(orderer: fn(a, a) -> Order) -> fn(a, a) -> Order { + fn(a, b) { orderer(b, a) } +} + +/// Return a fallback `Order` in case the first argument is `Eq`. +/// +/// ## Examples +/// +/// ```gleam +/// import gleam/int +/// +/// break_tie(in: int.compare(1, 1), with: Lt) +/// // -> Lt +/// ``` +/// +/// ```gleam +/// import gleam/int +/// +/// break_tie(in: int.compare(1, 0), with: Eq) +/// // -> Gt +/// ``` +/// +pub fn break_tie(in order: Order, with other: Order) -> Order { + case order { + Lt | Gt -> order + Eq -> other + } +} + +/// Invokes a fallback function returning an `Order` in case the first argument +/// is `Eq`. +/// +/// This can be useful when the fallback comparison might be expensive and it +/// needs to be delayed until strictly necessary. +/// +/// ## Examples +/// +/// ```gleam +/// import gleam/int +/// +/// lazy_break_tie(in: int.compare(1, 1), with: fn() { Lt }) +/// // -> Lt +/// ``` +/// +/// ```gleam +/// import gleam/int +/// +/// lazy_break_tie(in: int.compare(1, 0), with: fn() { Eq }) +/// // -> Gt +/// ``` +/// +pub fn lazy_break_tie(in order: Order, with comparison: fn() -> Order) -> Order { + case order { + Lt | Gt -> order + Eq -> comparison() + } +} diff --git a/build/packages/gleam_stdlib/src/gleam/pair.gleam b/build/packages/gleam_stdlib/src/gleam/pair.gleam new file mode 100644 index 0000000..566fc9c --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/pair.gleam @@ -0,0 +1,85 @@ +/// Returns the first element in a pair. +/// +/// ## Examples +/// +/// ```gleam +/// first(#(1, 2)) +/// // -> 1 +/// ``` +/// +pub fn first(pair: #(a, b)) -> a { + let #(a, _) = pair + a +} + +/// Returns the second element in a pair. +/// +/// ## Examples +/// +/// ```gleam +/// second(#(1, 2)) +/// // -> 2 +/// ``` +/// +pub fn second(pair: #(a, b)) -> b { + let #(_, a) = pair + a +} + +/// Returns a new pair with the elements swapped. +/// +/// ## Examples +/// +/// ```gleam +/// swap(#(1, 2)) +/// // -> #(2, 1) +/// ``` +/// +pub fn swap(pair: #(a, b)) -> #(b, a) { + let #(a, b) = pair + #(b, a) +} + +/// Returns a new pair with the first element having had `with` applied to +/// it. +/// +/// ## Examples +/// +/// ```gleam +/// #(1, 2) |> map_first(fn(n) { n * 2 }) +/// // -> #(2, 2) +/// ``` +/// +pub fn map_first(of pair: #(a, b), with fun: fn(a) -> c) -> #(c, b) { + let #(a, b) = pair + #(fun(a), b) +} + +/// Returns a new pair with the second element having had `with` applied to +/// it. +/// +/// ## Examples +/// +/// ```gleam +/// #(1, 2) |> map_second(fn(n) { n * 2 }) +/// // -> #(1, 4) +/// ``` +/// +pub fn map_second(of pair: #(a, b), with fun: fn(b) -> c) -> #(a, c) { + let #(a, b) = pair + #(a, fun(b)) +} + +/// Returns a new pair with the given elements. This can also be done using the dedicated +/// syntax instead: `new(1, 2) == #(1, 2)`. +/// +/// ## Examples +/// +/// ```gleam +/// new(1, 2) +/// // -> #(1, 2) +/// ``` +/// +pub fn new(first: a, second: b) -> #(a, b) { + #(first, second) +} diff --git a/build/packages/gleam_stdlib/src/gleam/result.gleam b/build/packages/gleam_stdlib/src/gleam/result.gleam new file mode 100644 index 0000000..741754d --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/result.gleam @@ -0,0 +1,453 @@ +//// Result represents the result of something that may succeed or not. +//// `Ok` means it was successful, `Error` means it was not successful. + +import gleam/list + +/// Checks whether the result is an `Ok` value. +/// +/// ## Examples +/// +/// ```gleam +/// is_ok(Ok(1)) +/// // -> True +/// ``` +/// +/// ```gleam +/// is_ok(Error(Nil)) +/// // -> False +/// ``` +/// +pub fn is_ok(result: Result(a, e)) -> Bool { + case result { + Error(_) -> False + Ok(_) -> True + } +} + +/// Checks whether the result is an `Error` value. +/// +/// ## Examples +/// +/// ```gleam +/// is_error(Ok(1)) +/// // -> False +/// ``` +/// +/// ```gleam +/// is_error(Error(Nil)) +/// // -> True +/// ``` +/// +pub fn is_error(result: Result(a, e)) -> Bool { + case result { + Ok(_) -> False + Error(_) -> True + } +} + +/// Updates a value held within the `Ok` of a result by calling a given function +/// on it. +/// +/// If the result is an `Error` rather than `Ok` the function is not called and the +/// result stays the same. +/// +/// ## Examples +/// +/// ```gleam +/// map(over: Ok(1), with: fn(x) { x + 1 }) +/// // -> Ok(2) +/// ``` +/// +/// ```gleam +/// map(over: Error(1), with: fn(x) { x + 1 }) +/// // -> Error(1) +/// ``` +/// +pub fn map(over result: Result(a, e), with fun: fn(a) -> b) -> Result(b, e) { + case result { + Ok(x) -> Ok(fun(x)) + Error(e) -> Error(e) + } +} + +/// Updates a value held within the `Error` of a result by calling a given function +/// on it. +/// +/// If the result is `Ok` rather than `Error` the function is not called and the +/// result stays the same. +/// +/// ## Examples +/// +/// ```gleam +/// map_error(over: Error(1), with: fn(x) { x + 1 }) +/// // -> Error(2) +/// ``` +/// +/// ```gleam +/// map_error(over: Ok(1), with: fn(x) { x + 1 }) +/// // -> Ok(1) +/// ``` +/// +pub fn map_error( + over result: Result(a, e), + with fun: fn(e) -> f, +) -> Result(a, f) { + case result { + Ok(x) -> Ok(x) + Error(error) -> Error(fun(error)) + } +} + +/// Merges a nested `Result` into a single layer. +/// +/// ## Examples +/// +/// ```gleam +/// flatten(Ok(Ok(1))) +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// flatten(Ok(Error(""))) +/// // -> Error("") +/// ``` +/// +/// ```gleam +/// flatten(Error(Nil)) +/// // -> Error(Nil) +/// ``` +/// +pub fn flatten(result: Result(Result(a, e), e)) -> Result(a, e) { + case result { + Ok(x) -> x + Error(error) -> Error(error) + } +} + +/// "Updates" an `Ok` result by passing its value to a function that yields a result, +/// and returning the yielded result. (This may "replace" the `Ok` with an `Error`.) +/// +/// If the input is an `Error` rather than an `Ok`, the function is not called and +/// the original `Error` is returned. +/// +/// This function is the equivalent of calling `map` followed by `flatten`, and +/// it is useful for chaining together multiple functions that may fail. +/// +/// ## Examples +/// +/// ```gleam +/// try(Ok(1), fn(x) { Ok(x + 1) }) +/// // -> Ok(2) +/// ``` +/// +/// ```gleam +/// try(Ok(1), fn(x) { Ok(#("a", x)) }) +/// // -> Ok(#("a", 1)) +/// ``` +/// +/// ```gleam +/// try(Ok(1), fn(_) { Error("Oh no") }) +/// // -> Error("Oh no") +/// ``` +/// +/// ```gleam +/// try(Error(Nil), fn(x) { Ok(x + 1) }) +/// // -> Error(Nil) +/// ``` +/// +pub fn try( + result: Result(a, e), + apply fun: fn(a) -> Result(b, e), +) -> Result(b, e) { + case result { + Ok(x) -> fun(x) + Error(e) -> Error(e) + } +} + +@deprecated("This function is an alias of result.try, use that instead") +pub fn then( + result: Result(a, e), + apply fun: fn(a) -> Result(b, e), +) -> Result(b, e) { + try(result, fun) +} + +/// Extracts the `Ok` value from a result, returning a default value if the result +/// is an `Error`. +/// +/// ## Examples +/// +/// ```gleam +/// unwrap(Ok(1), 0) +/// // -> 1 +/// ``` +/// +/// ```gleam +/// unwrap(Error(""), 0) +/// // -> 0 +/// ``` +/// +pub fn unwrap(result: Result(a, e), or default: a) -> a { + case result { + Ok(v) -> v + Error(_) -> default + } +} + +/// Extracts the `Ok` value from a result, evaluating the default function if the result +/// is an `Error`. +/// +/// ## Examples +/// +/// ```gleam +/// lazy_unwrap(Ok(1), fn() { 0 }) +/// // -> 1 +/// ``` +/// +/// ```gleam +/// lazy_unwrap(Error(""), fn() { 0 }) +/// // -> 0 +/// ``` +/// +pub fn lazy_unwrap(result: Result(a, e), or default: fn() -> a) -> a { + case result { + Ok(v) -> v + Error(_) -> default() + } +} + +/// Extracts the `Error` value from a result, returning a default value if the result +/// is an `Ok`. +/// +/// ## Examples +/// +/// ```gleam +/// unwrap_error(Error(1), 0) +/// // -> 1 +/// ``` +/// +/// ```gleam +/// unwrap_error(Ok(""), 0) +/// // -> 0 +/// ``` +/// +pub fn unwrap_error(result: Result(a, e), or default: e) -> e { + case result { + Ok(_) -> default + Error(e) -> e + } +} + +@deprecated("Use a case expression instead of this function") +pub fn unwrap_both(result: Result(a, a)) -> a { + case result { + Ok(a) -> a + Error(a) -> a + } +} + +/// Returns the first value if it is `Ok`, otherwise returns the second value. +/// +/// ## Examples +/// +/// ```gleam +/// or(Ok(1), Ok(2)) +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// or(Ok(1), Error("Error 2")) +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// or(Error("Error 1"), Ok(2)) +/// // -> Ok(2) +/// ``` +/// +/// ```gleam +/// or(Error("Error 1"), Error("Error 2")) +/// // -> Error("Error 2") +/// ``` +/// +pub fn or(first: Result(a, e), second: Result(a, e)) -> Result(a, e) { + case first { + Ok(_) -> first + Error(_) -> second + } +} + +/// Returns the first value if it is `Ok`, otherwise evaluates the given function for a fallback value. +/// +/// If you need access to the initial error value, use `result.try_recover`. +/// +/// ## Examples +/// +/// ```gleam +/// lazy_or(Ok(1), fn() { Ok(2) }) +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// lazy_or(Ok(1), fn() { Error("Error 2") }) +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// lazy_or(Error("Error 1"), fn() { Ok(2) }) +/// // -> Ok(2) +/// ``` +/// +/// ```gleam +/// lazy_or(Error("Error 1"), fn() { Error("Error 2") }) +/// // -> Error("Error 2") +/// ``` +/// +pub fn lazy_or( + first: Result(a, e), + second: fn() -> Result(a, e), +) -> Result(a, e) { + case first { + Ok(_) -> first + Error(_) -> second() + } +} + +/// Combines a list of results into a single result. +/// If all elements in the list are `Ok` then returns an `Ok` holding the list of values. +/// If any element is `Error` then returns the first error. +/// +/// ## Examples +/// +/// ```gleam +/// all([Ok(1), Ok(2)]) +/// // -> Ok([1, 2]) +/// ``` +/// +/// ```gleam +/// all([Ok(1), Error("e")]) +/// // -> Error("e") +/// ``` +/// +pub fn all(results: List(Result(a, e))) -> Result(List(a), e) { + list.try_map(results, fn(result) { result }) +} + +/// Given a list of results, returns a pair where the first element is a list +/// of all the values inside `Ok` and the second element is a list with all the +/// values inside `Error`. The values in both lists appear in reverse order with +/// respect to their position in the original list of results. +/// +/// ## Examples +/// +/// ```gleam +/// partition([Ok(1), Error("a"), Error("b"), Ok(2)]) +/// // -> #([2, 1], ["b", "a"]) +/// ``` +/// +pub fn partition(results: List(Result(a, e))) -> #(List(a), List(e)) { + partition_loop(results, [], []) +} + +fn partition_loop(results: List(Result(a, e)), oks: List(a), errors: List(e)) { + case results { + [] -> #(oks, errors) + [Ok(a), ..rest] -> partition_loop(rest, [a, ..oks], errors) + [Error(e), ..rest] -> partition_loop(rest, oks, [e, ..errors]) + } +} + +/// Replace the value within a result +/// +/// ## Examples +/// +/// ```gleam +/// replace(Ok(1), Nil) +/// // -> Ok(Nil) +/// ``` +/// +/// ```gleam +/// replace(Error(1), Nil) +/// // -> Error(1) +/// ``` +/// +pub fn replace(result: Result(a, e), value: b) -> Result(b, e) { + case result { + Ok(_) -> Ok(value) + Error(error) -> Error(error) + } +} + +/// Replace the error within a result +/// +/// ## Examples +/// +/// ```gleam +/// replace_error(Error(1), Nil) +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// replace_error(Ok(1), Nil) +/// // -> Ok(1) +/// ``` +/// +pub fn replace_error(result: Result(a, e), error: f) -> Result(a, f) { + case result { + Ok(x) -> Ok(x) + Error(_) -> Error(error) + } +} + +/// Given a list of results, returns only the values inside `Ok`. +/// +/// ## Examples +/// +/// ```gleam +/// values([Ok(1), Error("a"), Ok(3)]) +/// // -> [1, 3] +/// ``` +/// +pub fn values(results: List(Result(a, e))) -> List(a) { + list.filter_map(results, fn(result) { result }) +} + +/// Updates a value held within the `Error` of a result by calling a given function +/// on it, where the given function also returns a result. The two results are +/// then merged together into one result. +/// +/// If the result is an `Ok` rather than `Error` the function is not called and the +/// result stays the same. +/// +/// This function is useful for chaining together computations that may fail +/// and trying to recover from possible errors. +/// +/// If you do not need access to the initial error value, use `result.lazy_or`. +/// +/// ## Examples +/// +/// ```gleam +/// Ok(1) |> try_recover(with: fn(_) { Error("failed to recover") }) +/// // -> Ok(1) +/// ``` +/// +/// ```gleam +/// Error(1) |> try_recover(with: fn(error) { Ok(error + 1) }) +/// // -> Ok(2) +/// ``` +/// +/// ```gleam +/// Error(1) |> try_recover(with: fn(error) { Error("failed to recover") }) +/// // -> Error("failed to recover") +/// ``` +/// +pub fn try_recover( + result: Result(a, e), + with fun: fn(e) -> Result(a, f), +) -> Result(a, f) { + case result { + Ok(value) -> Ok(value) + Error(error) -> fun(error) + } +} diff --git a/build/packages/gleam_stdlib/src/gleam/set.gleam b/build/packages/gleam_stdlib/src/gleam/set.gleam new file mode 100644 index 0000000..6ae5e9e --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/set.gleam @@ -0,0 +1,407 @@ +import gleam/dict.{type Dict} +import gleam/list +import gleam/result + +// A list is used as the dict value as an empty list has the smallest +// representation in Erlang's binary format +@target(erlang) +type Token = + List(Nil) + +@target(erlang) +const token = [] + +@target(javascript) +type Token = + Nil + +@target(javascript) +const token = Nil + +/// A set is a collection of unique members of the same type. +/// +/// It is implemented using the `gleam/dict` module, so inserts and lookups have +/// logarithmic time complexity. +/// +pub opaque type Set(member) { + Set(dict: Dict(member, Token)) +} + +/// Creates a new empty set. +/// +pub fn new() -> Set(member) { + Set(dict.new()) +} + +/// Gets the number of members in a set. +/// +/// This function runs in constant time. +/// +/// ## Examples +/// +/// ```gleam +/// new() +/// |> insert(1) +/// |> insert(2) +/// |> size +/// // -> 2 +/// ``` +/// +pub fn size(set: Set(member)) -> Int { + dict.size(set.dict) +} + +/// Determines whether or not the set is empty. +/// +/// ## Examples +/// +/// ```gleam +/// new() |> is_empty +/// // -> True +/// ``` +/// +/// ```gleam +/// new() |> insert(1) |> is_empty +/// // -> False +/// ``` +/// +pub fn is_empty(set: Set(member)) -> Bool { + set == new() +} + +/// Inserts an member into the set. +/// +/// This function runs in logarithmic time. +/// +/// ## Examples +/// +/// ```gleam +/// new() +/// |> insert(1) +/// |> insert(2) +/// |> size +/// // -> 2 +/// ``` +/// +pub fn insert(into set: Set(member), this member: member) -> Set(member) { + Set(dict: dict.insert(set.dict, member, token)) +} + +/// Checks whether a set contains a given member. +/// +/// This function runs in logarithmic time. +/// +/// ## Examples +/// +/// ```gleam +/// new() +/// |> insert(2) +/// |> contains(2) +/// // -> True +/// ``` +/// +/// ```gleam +/// new() +/// |> insert(2) +/// |> contains(1) +/// // -> False +/// ``` +/// +pub fn contains(in set: Set(member), this member: member) -> Bool { + set.dict + |> dict.get(member) + |> result.is_ok +} + +/// Removes a member from a set. If the set does not contain the member then +/// the set is returned unchanged. +/// +/// This function runs in logarithmic time. +/// +/// ## Examples +/// +/// ```gleam +/// new() +/// |> insert(2) +/// |> delete(2) +/// |> contains(1) +/// // -> False +/// ``` +/// +pub fn delete(from set: Set(member), this member: member) -> Set(member) { + Set(dict: dict.delete(set.dict, member)) +} + +/// Converts the set into a list of the contained members. +/// +/// The list has no specific ordering, any unintentional ordering may change in +/// future versions of Gleam or Erlang. +/// +/// This function runs in linear time. +/// +/// ## Examples +/// +/// ```gleam +/// new() |> insert(2) |> to_list +/// // -> [2] +/// ``` +/// +pub fn to_list(set: Set(member)) -> List(member) { + dict.keys(set.dict) +} + +/// Creates a new set of the members in a given list. +/// +/// This function runs in loglinear time. +/// +/// ## Examples +/// +/// ```gleam +/// import gleam/int +/// import gleam/list +/// +/// [1, 1, 2, 4, 3, 2] |> from_list |> to_list |> list.sort(by: int.compare) +/// // -> [1, 2, 3, 4] +/// ``` +/// +pub fn from_list(members: List(member)) -> Set(member) { + let dict = + list.fold(over: members, from: dict.new(), with: fn(m, k) { + dict.insert(m, k, token) + }) + Set(dict) +} + +/// Combines all entries into a single value by calling a given function on each +/// one. +/// +/// Sets are not ordered so the values are not returned in any specific order. +/// Do not write code that relies on the order entries are used by this +/// function as it may change in later versions of Gleam or Erlang. +/// +/// # Examples +/// +/// ```gleam +/// from_list([1, 3, 9]) +/// |> fold(0, fn(accumulator, member) { accumulator + member }) +/// // -> 13 +/// ``` +/// +pub fn fold( + over set: Set(member), + from initial: acc, + with reducer: fn(acc, member) -> acc, +) -> acc { + dict.fold(over: set.dict, from: initial, with: fn(a, k, _) { reducer(a, k) }) +} + +/// Creates a new set from an existing set, minus any members that a given +/// function returns `False` for. +/// +/// This function runs in loglinear time. +/// +/// ## Examples +/// +/// ```gleam +/// import gleam/int +/// +/// from_list([1, 4, 6, 3, 675, 44, 67]) +/// |> filter(keeping: int.is_even) +/// |> to_list +/// // -> [4, 6, 44] +/// ``` +/// +pub fn filter( + in set: Set(member), + keeping predicate: fn(member) -> Bool, +) -> Set(member) { + Set(dict.filter(in: set.dict, keeping: fn(m, _) { predicate(m) })) +} + +/// Creates a new set from a given set with the result of applying the given +/// function to each member. +/// +/// ## Examples +/// +/// ```gleam +/// from_list([1, 2, 3, 4]) +/// |> map(with: fn(x) { x * 2 }) +/// |> to_list +/// // -> [2, 4, 6, 8] +/// ``` +pub fn map(set: Set(member), with fun: fn(member) -> mapped) -> Set(mapped) { + fold(over: set, from: new(), with: fn(acc, member) { + insert(acc, fun(member)) + }) +} + +/// Creates a new set from a given set with all the same entries except any +/// entry found on the given list. +/// +/// ## Examples +/// +/// ```gleam +/// from_list([1, 2, 3, 4]) +/// |> drop([1, 3]) +/// |> to_list +/// // -> [2, 4] +/// ``` +pub fn drop(from set: Set(member), drop disallowed: List(member)) -> Set(member) { + list.fold(over: disallowed, from: set, with: delete) +} + +/// Creates a new set from a given set, only including any members which are in +/// a given list. +/// +/// This function runs in loglinear time. +/// +/// ## Examples +/// +/// ```gleam +/// from_list([1, 2, 3]) +/// |> take([1, 3, 5]) +/// |> to_list +/// // -> [1, 3] +/// ``` +/// +pub fn take(from set: Set(member), keeping desired: List(member)) -> Set(member) { + Set(dict.take(from: set.dict, keeping: desired)) +} + +/// Creates a new set that contains all members of both given sets. +/// +/// This function runs in loglinear time. +/// +/// ## Examples +/// +/// ```gleam +/// union(from_list([1, 2]), from_list([2, 3])) |> to_list +/// // -> [1, 2, 3] +/// ``` +/// +pub fn union(of first: Set(member), and second: Set(member)) -> Set(member) { + let #(larger, smaller) = order(first, second) + fold(over: smaller, from: larger, with: insert) +} + +fn order(first: Set(member), second: Set(member)) -> #(Set(member), Set(member)) { + case dict.size(first.dict) > dict.size(second.dict) { + True -> #(first, second) + False -> #(second, first) + } +} + +/// Creates a new set that contains members that are present in both given sets. +/// +/// This function runs in loglinear time. +/// +/// ## Examples +/// +/// ```gleam +/// intersection(from_list([1, 2]), from_list([2, 3])) |> to_list +/// // -> [2] +/// ``` +/// +pub fn intersection( + of first: Set(member), + and second: Set(member), +) -> Set(member) { + let #(larger, smaller) = order(first, second) + take(from: larger, keeping: to_list(smaller)) +} + +/// Creates a new set that contains members that are present in the first set +/// but not the second. +/// +/// ## Examples +/// +/// ```gleam +/// difference(from_list([1, 2]), from_list([2, 3, 4])) |> to_list +/// // -> [1] +/// ``` +/// +pub fn difference( + from first: Set(member), + minus second: Set(member), +) -> Set(member) { + drop(from: first, drop: to_list(second)) +} + +/// Determines if a set is fully contained by another. +/// +/// ## Examples +/// +/// ```gleam +/// is_subset(from_list([1]), from_list([1, 2])) +/// // -> True +/// ``` +/// +/// ```gleam +/// is_subset(from_list([1, 2, 3]), from_list([3, 4, 5])) +/// // -> False +/// ``` +/// +pub fn is_subset(first: Set(member), of second: Set(member)) -> Bool { + intersection(of: first, and: second) == first +} + +/// Determines if two sets contain no common members +/// +/// ## Examples +/// +/// ```gleam +/// is_disjoint(from_list([1, 2, 3]), from_list([4, 5, 6])) +/// // -> True +/// ``` +/// +/// ```gleam +/// is_disjoint(from_list([1, 2, 3]), from_list([3, 4, 5])) +/// // -> False +/// ``` +/// +pub fn is_disjoint(first: Set(member), from second: Set(member)) -> Bool { + intersection(of: first, and: second) == new() +} + +/// Creates a new set that contains members that are present in either set, but +/// not both. +/// +/// ```gleam +/// symmetric_difference(from_list([1, 2, 3]), from_list([3, 4])) |> to_list +/// // -> [1, 2, 4] +/// ``` +/// +pub fn symmetric_difference( + of first: Set(member), + and second: Set(member), +) -> Set(member) { + difference( + from: union(of: first, and: second), + minus: intersection(of: first, and: second), + ) +} + +/// Calls a function for each member in a set, discarding the return +/// value. +/// +/// Useful for producing a side effect for every item of a set. +/// +/// ```gleam +/// let set = from_list(["apple", "banana", "cherry"]) +/// +/// each(set, io.println) +/// // -> Nil +/// // apple +/// // banana +/// // cherry +/// ``` +/// +/// The order of elements in the iteration is an implementation detail that +/// should not be relied upon. +/// +pub fn each(set: Set(member), fun: fn(member) -> a) -> Nil { + fold(set, Nil, fn(nil, member) { + fun(member) + nil + }) +} diff --git a/build/packages/gleam_stdlib/src/gleam/string.gleam b/build/packages/gleam_stdlib/src/gleam/string.gleam new file mode 100644 index 0000000..c5945b5 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/string.gleam @@ -0,0 +1,900 @@ +//// Strings in Gleam are UTF-8 binaries. They can be written in your code as +//// text surrounded by `"double quotes"`. + +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/order +import gleam/string_tree.{type StringTree} + +/// Determines if a `String` is empty. +/// +/// ## Examples +/// +/// ```gleam +/// is_empty("") +/// // -> True +/// ``` +/// +/// ```gleam +/// is_empty("the world") +/// // -> False +/// ``` +/// +pub fn is_empty(str: String) -> Bool { + str == "" +} + +/// Gets the number of grapheme clusters in a given `String`. +/// +/// This function has to iterate across the whole string to count the number of +/// graphemes, so it runs in linear time. Avoid using this in a loop. +/// +/// ## Examples +/// +/// ```gleam +/// length("Gleam") +/// // -> 5 +/// ``` +/// +/// ```gleam +/// length("ß↑e̊") +/// // -> 3 +/// ``` +/// +/// ```gleam +/// length("") +/// // -> 0 +/// ``` +/// +@external(erlang, "string", "length") +@external(javascript, "../gleam_stdlib.mjs", "string_length") +pub fn length(string: String) -> Int + +/// Reverses a `String`. +/// +/// This function has to iterate across the whole `String` so it runs in linear +/// time. Avoid using this in a loop. +/// +/// ## Examples +/// +/// ```gleam +/// reverse("stressed") +/// // -> "desserts" +/// ``` +/// +pub fn reverse(string: String) -> String { + string + |> string_tree.from_string + |> string_tree.reverse + |> string_tree.to_string +} + +/// Creates a new `String` by replacing all occurrences of a given substring. +/// +/// ## Examples +/// +/// ```gleam +/// replace("www.example.com", each: ".", with: "-") +/// // -> "www-example-com" +/// ``` +/// +/// ```gleam +/// replace("a,b,c,d,e", each: ",", with: "/") +/// // -> "a/b/c/d/e" +/// ``` +/// +pub fn replace( + in string: String, + each pattern: String, + with substitute: String, +) -> String { + string + |> string_tree.from_string + |> string_tree.replace(each: pattern, with: substitute) + |> string_tree.to_string +} + +/// Creates a new `String` with all the graphemes in the input `String` converted to +/// lowercase. +/// +/// Useful for case-insensitive comparisons. +/// +/// ## Examples +/// +/// ```gleam +/// lowercase("X-FILES") +/// // -> "x-files" +/// ``` +/// +@external(erlang, "string", "lowercase") +@external(javascript, "../gleam_stdlib.mjs", "lowercase") +pub fn lowercase(string: String) -> String + +/// Creates a new `String` with all the graphemes in the input `String` converted to +/// uppercase. +/// +/// Useful for case-insensitive comparisons and VIRTUAL YELLING. +/// +/// ## Examples +/// +/// ```gleam +/// uppercase("skinner") +/// // -> "SKINNER" +/// ``` +/// +@external(erlang, "string", "uppercase") +@external(javascript, "../gleam_stdlib.mjs", "uppercase") +pub fn uppercase(string: String) -> String + +/// Compares two `String`s to see which is "larger" by comparing their graphemes. +/// +/// This does not compare the size or length of the given `String`s. +/// +/// ## Examples +/// +/// ```gleam +/// compare("Anthony", "Anthony") +/// // -> order.Eq +/// ``` +/// +/// ```gleam +/// compare("A", "B") +/// // -> order.Lt +/// ``` +/// +pub fn compare(a: String, b: String) -> order.Order { + case a == b { + True -> order.Eq + _ -> + case less_than(a, b) { + True -> order.Lt + False -> order.Gt + } + } +} + +@external(erlang, "gleam_stdlib", "less_than") +@external(javascript, "../gleam_stdlib.mjs", "less_than") +fn less_than(a: String, b: String) -> Bool + +/// Takes a substring given a start grapheme index and a length. Negative indexes +/// are taken starting from the *end* of the list. +/// +/// This function runs in linear time with the size of the index and the +/// length. Negative indexes are linear with the size of the input string in +/// addition to the other costs. +/// +/// ## Examples +/// +/// ```gleam +/// slice(from: "gleam", at_index: 1, length: 2) +/// // -> "le" +/// ``` +/// +/// ```gleam +/// slice(from: "gleam", at_index: 1, length: 10) +/// // -> "leam" +/// ``` +/// +/// ```gleam +/// slice(from: "gleam", at_index: 10, length: 3) +/// // -> "" +/// ``` +/// +/// ```gleam +/// slice(from: "gleam", at_index: -2, length: 2) +/// // -> "am" +/// ``` +/// +/// ```gleam +/// slice(from: "gleam", at_index: -12, length: 2) +/// // -> "" +/// ``` +/// +pub fn slice(from string: String, at_index idx: Int, length len: Int) -> String { + case len <= 0 { + True -> "" + False -> + case idx < 0 { + True -> { + let translated_idx = length(string) + idx + case translated_idx < 0 { + True -> "" + False -> grapheme_slice(string, translated_idx, len) + } + } + False -> grapheme_slice(string, idx, len) + } + } +} + +@external(erlang, "gleam_stdlib", "slice") +@external(javascript, "../gleam_stdlib.mjs", "string_grapheme_slice") +fn grapheme_slice(string: String, index: Int, length: Int) -> String + +@external(erlang, "binary", "part") +@external(javascript, "../gleam_stdlib.mjs", "string_byte_slice") +fn unsafe_byte_slice(string: String, index: Int, length: Int) -> String + +/// Drops contents of the first `String` that occur before the second `String`. +/// If the `from` string does not contain the `before` string, `from` is +/// returned unchanged. +/// +/// ## Examples +/// +/// ```gleam +/// crop(from: "The Lone Gunmen", before: "Lone") +/// // -> "Lone Gunmen" +/// ``` +/// +@external(erlang, "gleam_stdlib", "crop_string") +@external(javascript, "../gleam_stdlib.mjs", "crop_string") +pub fn crop(from string: String, before substring: String) -> String + +/// Drops *n* graphemes from the start of a `String`. +/// +/// This function runs in linear time with the number of graphemes to drop. +/// +/// ## Examples +/// +/// ```gleam +/// drop_start(from: "The Lone Gunmen", up_to: 2) +/// // -> "e Lone Gunmen" +/// ``` +/// +pub fn drop_start(from string: String, up_to num_graphemes: Int) -> String { + case num_graphemes <= 0 { + True -> string + False -> { + let prefix = grapheme_slice(string, 0, num_graphemes) + let prefix_size = byte_size(prefix) + unsafe_byte_slice(string, prefix_size, byte_size(string) - prefix_size) + } + } +} + +/// Drops *n* graphemes from the end of a `String`. +/// +/// This function traverses the full string, so it runs in linear time with the +/// size of the string. Avoid using this in a loop. +/// +/// ## Examples +/// +/// ```gleam +/// drop_end(from: "Cigarette Smoking Man", up_to: 2) +/// // -> "Cigarette Smoking M" +/// ``` +/// +pub fn drop_end(from string: String, up_to num_graphemes: Int) -> String { + case num_graphemes <= 0 { + True -> string + False -> slice(string, 0, length(string) - num_graphemes) + } +} + +/// Checks if the first `String` contains the second. +/// +/// ## Examples +/// +/// ```gleam +/// contains(does: "theory", contain: "ory") +/// // -> True +/// ``` +/// +/// ```gleam +/// contains(does: "theory", contain: "the") +/// // -> True +/// ``` +/// +/// ```gleam +/// contains(does: "theory", contain: "THE") +/// // -> False +/// ``` +/// +@external(erlang, "gleam_stdlib", "contains_string") +@external(javascript, "../gleam_stdlib.mjs", "contains_string") +pub fn contains(does haystack: String, contain needle: String) -> Bool + +/// Checks whether the first `String` starts with the second one. +/// +/// ## Examples +/// +/// ```gleam +/// starts_with("theory", "ory") +/// // -> False +/// ``` +/// +@external(erlang, "gleam_stdlib", "string_starts_with") +@external(javascript, "../gleam_stdlib.mjs", "starts_with") +pub fn starts_with(string: String, prefix: String) -> Bool + +/// Checks whether the first `String` ends with the second one. +/// +/// ## Examples +/// +/// ```gleam +/// ends_with("theory", "ory") +/// // -> True +/// ``` +/// +@external(erlang, "gleam_stdlib", "string_ends_with") +@external(javascript, "../gleam_stdlib.mjs", "ends_with") +pub fn ends_with(string: String, suffix: String) -> Bool + +/// Creates a list of `String`s by splitting a given string on a given substring. +/// +/// ## Examples +/// +/// ```gleam +/// split("home/gleam/desktop/", on: "/") +/// // -> ["home", "gleam", "desktop", ""] +/// ``` +/// +pub fn split(x: String, on substring: String) -> List(String) { + case substring { + "" -> to_graphemes(x) + _ -> + x + |> string_tree.from_string + |> string_tree.split(on: substring) + |> list.map(with: string_tree.to_string) + } +} + +/// Splits a `String` a single time on the given substring. +/// +/// Returns an `Error` if substring not present. +/// +/// ## Examples +/// +/// ```gleam +/// split_once("home/gleam/desktop/", on: "/") +/// // -> Ok(#("home", "gleam/desktop/")) +/// ``` +/// +/// ```gleam +/// split_once("home/gleam/desktop/", on: "?") +/// // -> Error(Nil) +/// ``` +/// +@external(javascript, "../gleam_stdlib.mjs", "split_once") +pub fn split_once( + string: String, + on substring: String, +) -> Result(#(String, String), Nil) { + case erl_split(string, substring) { + [first, rest] -> Ok(#(first, rest)) + _ -> Error(Nil) + } +} + +@external(erlang, "string", "split") +fn erl_split(a: String, b: String) -> List(String) + +/// Creates a new `String` by joining two `String`s together. +/// +/// This function typically copies both `String`s and runs in linear time, but +/// the exact behaviour will depend on how the runtime you are using optimises +/// your code. Benchmark and profile your code if you need to understand its +/// performance better. +/// +/// If you are joining together large string and want to avoid copying any data +/// you may want to investigate using the [`string_tree`](../gleam/string_tree.html) +/// module. +/// +/// ## Examples +/// +/// ```gleam +/// append(to: "butter", suffix: "fly") +/// // -> "butterfly" +/// ``` +/// +pub fn append(to first: String, suffix second: String) -> String { + first <> second +} + +/// Creates a new `String` by joining many `String`s together. +/// +/// This function copies all the `String`s and runs in linear time. +/// +/// ## Examples +/// +/// ```gleam +/// concat(["never", "the", "less"]) +/// // -> "nevertheless" +/// ``` +/// +@external(erlang, "erlang", "list_to_binary") +pub fn concat(strings: List(String)) -> String { + concat_loop(strings, "") +} + +fn concat_loop(strings: List(String), accumulator: String) -> String { + case strings { + [string, ..strings] -> concat_loop(strings, accumulator <> string) + [] -> accumulator + } +} + +/// Creates a new `String` by repeating a `String` a given number of times. +/// +/// This function runs in loglinear time. +/// +/// ## Examples +/// +/// ```gleam +/// repeat("ha", times: 3) +/// // -> "hahaha" +/// ``` +/// +pub fn repeat(string: String, times times: Int) -> String { + case times <= 0 { + True -> "" + False -> repeat_loop(times, string, "") + } +} + +fn repeat_loop(times: Int, doubling_acc: String, acc: String) -> String { + let acc = case times % 2 { + 0 -> acc + _ -> acc <> doubling_acc + } + let times = times / 2 + case times <= 0 { + True -> acc + False -> repeat_loop(times, doubling_acc <> doubling_acc, acc) + } +} + +/// Joins many `String`s together with a given separator. +/// +/// This function runs in linear time. +/// +/// ## Examples +/// +/// ```gleam +/// join(["home","evan","Desktop"], with: "/") +/// // -> "home/evan/Desktop" +/// ``` +/// +pub fn join(strings: List(String), with separator: String) -> String { + case strings { + [] -> "" + [first, ..rest] -> join_loop(rest, separator, first) + } +} + +fn join_loop( + strings: List(String), + separator: String, + accumulator: String, +) -> String { + case strings { + [] -> accumulator + [string, ..strings] -> + join_loop(strings, separator, accumulator <> separator <> string) + } +} + +/// Pads the start of a `String` until it has a given length. +/// +/// ## Examples +/// +/// ```gleam +/// pad_start("121", to: 5, with: ".") +/// // -> "..121" +/// ``` +/// +/// ```gleam +/// pad_start("121", to: 3, with: ".") +/// // -> "121" +/// ``` +/// +/// ```gleam +/// pad_start("121", to: 2, with: ".") +/// // -> "121" +/// ``` +/// +pub fn pad_start( + string: String, + to desired_length: Int, + with pad_string: String, +) -> String { + let current_length = length(string) + let to_pad_length = desired_length - current_length + + case to_pad_length <= 0 { + True -> string + False -> padding(to_pad_length, pad_string) <> string + } +} + +/// Pads the end of a `String` until it has a given length. +/// +/// ## Examples +/// +/// ```gleam +/// pad_end("123", to: 5, with: ".") +/// // -> "123.." +/// ``` +/// +/// ```gleam +/// pad_end("123", to: 3, with: ".") +/// // -> "123" +/// ``` +/// +/// ```gleam +/// pad_end("123", to: 2, with: ".") +/// // -> "123" +/// ``` +/// +pub fn pad_end( + string: String, + to desired_length: Int, + with pad_string: String, +) -> String { + let current_length = length(string) + let to_pad_length = desired_length - current_length + + case to_pad_length <= 0 { + True -> string + False -> string <> padding(to_pad_length, pad_string) + } +} + +fn padding(size: Int, pad_string: String) -> String { + let pad_string_length = length(pad_string) + let num_pads = size / pad_string_length + let extra = size % pad_string_length + + repeat(pad_string, num_pads) <> slice(pad_string, 0, extra) +} + +/// Removes whitespace on both sides of a `String`. +/// +/// Whitespace in this function is the set of nonbreakable whitespace +/// codepoints, defined as Pattern_White_Space in [Unicode Standard Annex #31][1]. +/// +/// [1]: https://unicode.org/reports/tr31/ +/// +/// ## Examples +/// +/// ```gleam +/// trim(" hats \n") +/// // -> "hats" +/// ``` +/// +pub fn trim(string: String) -> String { + string |> trim_start |> trim_end +} + +@external(erlang, "string", "trim") +fn erl_trim(a: String, b: Direction) -> String + +type Direction { + Leading + Trailing +} + +/// Removes whitespace at the start of a `String`. +/// +/// ## Examples +/// +/// ```gleam +/// trim_start(" hats \n") +/// // -> "hats \n" +/// ``` +/// +@external(javascript, "../gleam_stdlib.mjs", "trim_start") +pub fn trim_start(string: String) -> String { + erl_trim(string, Leading) +} + +/// Removes whitespace at the end of a `String`. +/// +/// ## Examples +/// +/// ```gleam +/// trim_end(" hats \n") +/// // -> " hats" +/// ``` +/// +@external(javascript, "../gleam_stdlib.mjs", "trim_end") +pub fn trim_end(string: String) -> String { + erl_trim(string, Trailing) +} + +/// Splits a non-empty `String` into its first element (head) and rest (tail). +/// This lets you pattern match on `String`s exactly as you would with lists. +/// +/// ## Performance +/// +/// There is a notable overhead to using this function, so you may not want to +/// use it in a tight loop. If you wish to efficiently parse a string you may +/// want to use alternatives such as the [splitter package](https://hex.pm/packages/splitter). +/// +/// ## Examples +/// +/// ```gleam +/// pop_grapheme("gleam") +/// // -> Ok(#("g", "leam")) +/// ``` +/// +/// ```gleam +/// pop_grapheme("") +/// // -> Error(Nil) +/// ``` +/// +@external(erlang, "gleam_stdlib", "string_pop_grapheme") +@external(javascript, "../gleam_stdlib.mjs", "pop_grapheme") +pub fn pop_grapheme(string: String) -> Result(#(String, String), Nil) + +/// Converts a `String` to a list of +/// [graphemes](https://en.wikipedia.org/wiki/Grapheme). +/// +/// ```gleam +/// to_graphemes("abc") +/// // -> ["a", "b", "c"] +/// ``` +/// +@external(javascript, "../gleam_stdlib.mjs", "graphemes") +pub fn to_graphemes(string: String) -> List(String) { + string + |> to_graphemes_loop([]) + |> list.reverse +} + +fn to_graphemes_loop(string: String, acc: List(String)) -> List(String) { + case pop_grapheme(string) { + Ok(#(grapheme, rest)) -> to_graphemes_loop(rest, [grapheme, ..acc]) + Error(_) -> acc + } +} + +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "codepoint") +fn unsafe_int_to_utf_codepoint(a: Int) -> UtfCodepoint + +/// Converts a `String` to a `List` of `UtfCodepoint`. +/// +/// See and +/// for an +/// explanation on code points. +/// +/// ## Examples +/// +/// ```gleam +/// "a" |> to_utf_codepoints +/// // -> [UtfCodepoint(97)] +/// ``` +/// +/// ```gleam +/// // Semantically the same as: +/// // ["🏳", "️", "‍", "🌈"] or: +/// // [waving_white_flag, variant_selector_16, zero_width_joiner, rainbow] +/// "🏳️‍🌈" |> to_utf_codepoints +/// // -> [ +/// // UtfCodepoint(127987), +/// // UtfCodepoint(65039), +/// // UtfCodepoint(8205), +/// // UtfCodepoint(127752), +/// // ] +/// ``` +/// +pub fn to_utf_codepoints(string: String) -> List(UtfCodepoint) { + do_to_utf_codepoints(string) +} + +@target(erlang) +fn do_to_utf_codepoints(string: String) -> List(UtfCodepoint) { + to_utf_codepoints_loop(<>, []) +} + +@target(erlang) +fn to_utf_codepoints_loop( + bit_array: BitArray, + acc: List(UtfCodepoint), +) -> List(UtfCodepoint) { + case bit_array { + <> -> + to_utf_codepoints_loop(rest, [first, ..acc]) + _ -> list.reverse(acc) + } +} + +@target(javascript) +fn do_to_utf_codepoints(string: String) -> List(UtfCodepoint) { + string + |> string_to_codepoint_integer_list + |> list.map(unsafe_int_to_utf_codepoint) +} + +@target(javascript) +@external(javascript, "../gleam_stdlib.mjs", "string_to_codepoint_integer_list") +fn string_to_codepoint_integer_list(string: String) -> List(Int) + +/// Converts a `List` of `UtfCodepoint`s to a `String`. +/// +/// See and +/// for an +/// explanation on code points. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(a) = utf_codepoint(97) +/// let assert Ok(b) = utf_codepoint(98) +/// let assert Ok(c) = utf_codepoint(99) +/// from_utf_codepoints([a, b, c]) +/// // -> "abc" +/// ``` +/// +@external(erlang, "gleam_stdlib", "utf_codepoint_list_to_string") +@external(javascript, "../gleam_stdlib.mjs", "utf_codepoint_list_to_string") +pub fn from_utf_codepoints(utf_codepoints: List(UtfCodepoint)) -> String + +/// Converts an integer to a `UtfCodepoint`. +/// +/// Returns an `Error` if the integer does not represent a valid UTF codepoint. +/// +pub fn utf_codepoint(value: Int) -> Result(UtfCodepoint, Nil) { + case value { + i if i > 1_114_111 -> Error(Nil) + i if i >= 55_296 && i <= 57_343 -> Error(Nil) + i if i < 0 -> Error(Nil) + i -> Ok(unsafe_int_to_utf_codepoint(i)) + } +} + +/// Converts an UtfCodepoint to its ordinal code point value. +/// +/// ## Examples +/// +/// ```gleam +/// let assert [utf_codepoint, ..] = to_utf_codepoints("💜") +/// utf_codepoint_to_int(utf_codepoint) +/// // -> 128156 +/// ``` +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "utf_codepoint_to_int") +pub fn utf_codepoint_to_int(cp: UtfCodepoint) -> Int + +/// Converts a `String` into `Option(String)` where an empty `String` becomes +/// `None`. +/// +/// ## Examples +/// +/// ```gleam +/// to_option("") +/// // -> None +/// ``` +/// +/// ```gleam +/// to_option("hats") +/// // -> Some("hats") +/// ``` +/// +pub fn to_option(string: String) -> Option(String) { + case string { + "" -> None + _ -> Some(string) + } +} + +/// Returns the first grapheme cluster in a given `String` and wraps it in a +/// `Result(String, Nil)`. If the `String` is empty, it returns `Error(Nil)`. +/// Otherwise, it returns `Ok(String)`. +/// +/// ## Examples +/// +/// ```gleam +/// first("") +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// first("icecream") +/// // -> Ok("i") +/// ``` +/// +pub fn first(string: String) -> Result(String, Nil) { + case pop_grapheme(string) { + Ok(#(first, _)) -> Ok(first) + Error(e) -> Error(e) + } +} + +/// Returns the last grapheme cluster in a given `String` and wraps it in a +/// `Result(String, Nil)`. If the `String` is empty, it returns `Error(Nil)`. +/// Otherwise, it returns `Ok(String)`. +/// +/// This function traverses the full string, so it runs in linear time with the +/// length of the string. Avoid using this in a loop. +/// +/// ## Examples +/// +/// ```gleam +/// last("") +/// // -> Error(Nil) +/// ``` +/// +/// ```gleam +/// last("icecream") +/// // -> Ok("m") +/// ``` +/// +pub fn last(string: String) -> Result(String, Nil) { + case pop_grapheme(string) { + Ok(#(first, "")) -> Ok(first) + Ok(#(_, rest)) -> Ok(slice(rest, -1, 1)) + Error(e) -> Error(e) + } +} + +/// Creates a new `String` with the first grapheme in the input `String` +/// converted to uppercase and the remaining graphemes to lowercase. +/// +/// ## Examples +/// +/// ```gleam +/// capitalise("mamouna") +/// // -> "Mamouna" +/// ``` +/// +pub fn capitalise(string: String) -> String { + case pop_grapheme(string) { + Ok(#(first, rest)) -> append(to: uppercase(first), suffix: lowercase(rest)) + Error(_) -> "" + } +} + +/// Returns a `String` representation of a term in Gleam syntax. +/// +/// This may be occasionally useful for quick-and-dirty printing of values in +/// scripts. For error reporting and other uses prefer constructing strings by +/// pattern matching on the values. +/// +/// ## Limitations +/// +/// The output format of this function is not stable and could change at any +/// time. The output is not suitable for parsing. +/// +/// This function works using runtime reflection, so the output may not be +/// perfectly accurate for data structures where the runtime structure doesn't +/// hold enough information to determine the original syntax. For example, +/// tuples with an Erlang atom in the first position will be mistaken for Gleam +/// records. +/// +/// ## Security and safety +/// +/// There is no limit to how large the strings that this function can produce. +/// Be careful not to call this function with large data structures or you +/// could use very large amounts of memory, potentially causing runtime +/// problems. +/// +pub fn inspect(term: anything) -> String { + term + |> do_inspect + |> string_tree.to_string +} + +@external(erlang, "gleam_stdlib", "inspect") +@external(javascript, "../gleam_stdlib.mjs", "inspect") +fn do_inspect(term: anything) -> StringTree + +/// Returns the number of bytes in a `String`. +/// +/// This function runs in constant time on Erlang and in linear time on +/// JavaScript. +/// +/// ## Examples +/// +/// ```gleam +/// byte_size("🏳️‍⚧️🏳️‍🌈👩🏾‍❤️‍👨🏻") +/// // -> 58 +/// ``` +/// +@external(erlang, "erlang", "byte_size") +@external(javascript, "../gleam_stdlib.mjs", "byte_size") +pub fn byte_size(string: String) -> Int diff --git a/build/packages/gleam_stdlib/src/gleam/string_tree.gleam b/build/packages/gleam_stdlib/src/gleam/string_tree.gleam new file mode 100644 index 0000000..22937e2 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/string_tree.gleam @@ -0,0 +1,208 @@ +import gleam/list + +/// `StringTree` is a type used for efficiently building text content to be +/// written to a file or a socket. Internally it is represented as tree so to +/// append or prepend to a string tree is a constant time operation that +/// allocates a new node in the tree without copying any of the content. When +/// writing to an output stream the tree is traversed and the content is sent +/// directly rather than copying it into a single buffer beforehand. +/// +/// On Erlang this type is compatible with Erlang's iodata. On JavaScript this +/// type is compatible with normal strings. +/// +/// The BEAM virtual machine has an optimisation for appending strings, where it +/// will mutate the string buffer when safe to do so, so if you are looking to +/// build a string through appending many small strings then you may get better +/// performance by not using a string tree. Always benchmark your performance +/// sensitive code. +/// +pub type StringTree + +/// Create an empty `StringTree`. Useful as the start of a pipe chaining many +/// trees together. +/// +pub fn new() -> StringTree { + from_strings([]) +} + +/// Prepends a `String` onto the start of some `StringTree`. +/// +/// Runs in constant time. +/// +pub fn prepend(to tree: StringTree, prefix prefix: String) -> StringTree { + append_tree(from_string(prefix), tree) +} + +/// Appends a `String` onto the end of some `StringTree`. +/// +/// Runs in constant time. +/// +pub fn append(to tree: StringTree, suffix second: String) -> StringTree { + append_tree(tree, from_string(second)) +} + +/// Prepends some `StringTree` onto the start of another. +/// +/// Runs in constant time. +/// +pub fn prepend_tree( + to tree: StringTree, + prefix prefix: StringTree, +) -> StringTree { + append_tree(prefix, tree) +} + +/// Appends some `StringTree` onto the end of another. +/// +/// Runs in constant time. +/// +@external(erlang, "gleam_stdlib", "iodata_append") +@external(javascript, "../gleam_stdlib.mjs", "add") +pub fn append_tree(to tree: StringTree, suffix suffix: StringTree) -> StringTree + +/// Converts a list of strings into a `StringTree`. +/// +/// Runs in constant time. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "concat") +pub fn from_strings(strings: List(String)) -> StringTree + +/// Joins a list of trees into a single tree. +/// +/// Runs in constant time. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "concat") +pub fn concat(trees: List(StringTree)) -> StringTree + +/// Converts a string into a `StringTree`. +/// +/// Runs in constant time. +/// +@external(erlang, "gleam_stdlib", "identity") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn from_string(string: String) -> StringTree + +/// Turns a `StringTree` into a `String` +/// +/// This function is implemented natively by the virtual machine and is highly +/// optimised. +/// +@external(erlang, "unicode", "characters_to_binary") +@external(javascript, "../gleam_stdlib.mjs", "identity") +pub fn to_string(tree: StringTree) -> String + +/// Returns the size of the `StringTree` in bytes. +/// +@external(erlang, "erlang", "iolist_size") +@external(javascript, "../gleam_stdlib.mjs", "length") +pub fn byte_size(tree: StringTree) -> Int + +/// Joins the given trees into a new tree separated with the given string. +/// +pub fn join(trees: List(StringTree), with sep: String) -> StringTree { + trees + |> list.intersperse(from_string(sep)) + |> concat +} + +/// Converts a `StringTree` to a new one where the contents have been +/// lowercased. +/// +@external(erlang, "string", "lowercase") +@external(javascript, "../gleam_stdlib.mjs", "lowercase") +pub fn lowercase(tree: StringTree) -> StringTree + +/// Converts a `StringTree` to a new one where the contents have been +/// uppercased. +/// +@external(erlang, "string", "uppercase") +@external(javascript, "../gleam_stdlib.mjs", "uppercase") +pub fn uppercase(tree: StringTree) -> StringTree + +/// Converts a `StringTree` to a new one with the contents reversed. +/// +@external(erlang, "string", "reverse") +pub fn reverse(tree: StringTree) -> StringTree { + tree + |> to_string + |> do_to_graphemes + |> list.reverse + |> from_strings +} + +@external(javascript, "../gleam_stdlib.mjs", "graphemes") +fn do_to_graphemes(string: String) -> List(String) + +type Direction { + All +} + +/// Splits a `StringTree` on a given pattern into a list of trees. +/// +@external(javascript, "../gleam_stdlib.mjs", "split") +pub fn split(tree: StringTree, on pattern: String) -> List(StringTree) { + erl_split(tree, pattern, All) +} + +@external(erlang, "string", "split") +fn erl_split(a: StringTree, b: String, c: Direction) -> List(StringTree) + +/// Replaces all instances of a pattern with a given string substitute. +/// +@external(erlang, "gleam_stdlib", "string_replace") +@external(javascript, "../gleam_stdlib.mjs", "string_replace") +pub fn replace( + in tree: StringTree, + each pattern: String, + with substitute: String, +) -> StringTree + +/// Compares two string trees to determine if they have the same textual +/// content. +/// +/// Comparing two string trees using the `==` operator may return `False` even +/// if they have the same content as they may have been build in different ways, +/// so using this function is often preferred. +/// +/// ## Examples +/// +/// ```gleam +/// from_strings(["a", "b"]) == from_string("ab") +/// // -> False +/// ``` +/// +/// ```gleam +/// is_equal(from_strings(["a", "b"]), from_string("ab")) +/// // -> True +/// ``` +/// +@external(erlang, "string", "equal") +pub fn is_equal(a: StringTree, b: StringTree) -> Bool { + a == b +} + +/// Inspects a `StringTree` to determine if it is equivalent to an empty string. +/// +/// ## Examples +/// +/// ```gleam +/// from_string("ok") |> is_empty +/// // -> False +/// ``` +/// +/// ```gleam +/// from_string("") |> is_empty +/// // -> True +/// ``` +/// +/// ```gleam +/// from_strings([]) |> is_empty +/// // -> True +/// ``` +/// +@external(erlang, "string", "is_empty") +pub fn is_empty(tree: StringTree) -> Bool { + from_string("") == tree +} diff --git a/build/packages/gleam_stdlib/src/gleam/uri.gleam b/build/packages/gleam_stdlib/src/gleam/uri.gleam new file mode 100644 index 0000000..9413b99 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam/uri.gleam @@ -0,0 +1,770 @@ +//// Utilities for working with URIs +//// +//// This module provides functions for working with URIs (for example, parsing +//// URIs or encoding query strings). The functions in this module are implemented +//// according to [RFC 3986](https://tools.ietf.org/html/rfc3986). +//// +//// Query encoding (Form encoding) is defined in the +//// [W3C specification](https://www.w3.org/TR/html52/sec-forms.html#urlencoded-form-data). + +import gleam/int +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/string +import gleam/string_tree.{type StringTree} + +/// Type representing holding the parsed components of an URI. +/// All components of a URI are optional, except the path. +/// +pub type Uri { + Uri( + scheme: Option(String), + userinfo: Option(String), + host: Option(String), + port: Option(Int), + path: String, + query: Option(String), + fragment: Option(String), + ) +} + +/// Constant representing an empty URI, equivalent to "". +/// +/// ## Examples +/// +/// ```gleam +/// let uri = Uri(..empty, scheme: Some("https"), host: Some("example.com")) +/// // -> Uri( +/// // scheme: Some("https"), +/// // userinfo: None, +/// // host: Some("example.com"), +/// // port: None, +/// // path: "", +/// // query: None, +/// // fragment: None, +/// // ) +/// ``` +/// +pub const empty = Uri( + scheme: None, + userinfo: None, + host: None, + port: None, + path: "", + query: None, + fragment: None, +) + +/// Parses a compliant URI string into the `Uri` Type. +/// If the string is not a valid URI string then an error is returned. +/// +/// The opposite operation is `uri.to_string`. +/// +/// ## Examples +/// +/// ```gleam +/// parse("https://example.com:1234/a/b?query=true#fragment") +/// // -> Ok( +/// // Uri( +/// // scheme: Some("https"), +/// // userinfo: None, +/// // host: Some("example.com"), +/// // port: Some(1234), +/// // path: "/a/b", +/// // query: Some("query=true"), +/// // fragment: Some("fragment") +/// // ) +/// // ) +/// ``` +/// +@external(erlang, "gleam_stdlib", "uri_parse") +pub fn parse(uri_string: String) -> Result(Uri, Nil) { + // This parses a uri_string following the regex defined in + // https://tools.ietf.org/html/rfc3986#appendix-B + // + // TODO: This is not perfect and will be more permissive than its Erlang + // counterpart, ideally we want to replicate Erlang's implementation on the js + // target as well. + parse_scheme_loop(uri_string, uri_string, empty, 0) +} + +fn parse_scheme_loop( + original: String, + uri_string: String, + pieces: Uri, + size: Int, +) -> Result(Uri, Nil) { + case uri_string { + // `/` is not allowed to appear in a scheme so we know it's over and we can + // start parsing the authority with slashes. + "/" <> _ if size == 0 -> parse_authority_with_slashes(uri_string, pieces) + "/" <> _ -> { + let scheme = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, scheme: Some(string.lowercase(scheme))) + parse_authority_with_slashes(uri_string, pieces) + } + + // `?` is not allowed to appear in a schemem, in an authority, or in a path; + // so if we see it we know it marks the beginning of the query part. + "?" <> rest if size == 0 -> parse_query_with_question_mark(rest, pieces) + "?" <> rest -> { + let scheme = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, scheme: Some(string.lowercase(scheme))) + parse_query_with_question_mark(rest, pieces) + } + + // `#` is not allowed to appear in a scheme, in an authority, in a path or + // in a query; so if we see it we know it marks the beginning of the final + // fragment. + "#" <> rest if size == 0 -> parse_fragment(rest, pieces) + "#" <> rest -> { + let scheme = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, scheme: Some(string.lowercase(scheme))) + parse_fragment(rest, pieces) + } + + // A colon marks the end of a uri scheme, but if it is not preceded by any + // character then it's not a valid URI. + ":" <> _ if size == 0 -> Error(Nil) + ":" <> rest -> { + let scheme = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, scheme: Some(string.lowercase(scheme))) + parse_authority_with_slashes(rest, pieces) + } + + // If we could get to the end of the string and we've met no special + // chars whatsoever, that means the entire string is just a long path. + "" -> Ok(Uri(..pieces, path: original)) + + // In all other cases the first character is just a valid URI scheme + // character and we just keep munching characters until we reach the end of + // the uri scheme (or the end of the string and that would mean this is not + // a valid uri scheme since we found no `:`). + _ -> { + let #(_, rest) = pop_codeunit(uri_string) + parse_scheme_loop(original, rest, pieces, size + 1) + } + } +} + +fn parse_authority_with_slashes( + uri_string: String, + pieces: Uri, +) -> Result(Uri, Nil) { + case uri_string { + // To be a valid authority the string must start with a `//`, otherwise + // there's no authority and we just skip ahead to parsing the path. + "//" -> Ok(Uri(..pieces, host: Some(""))) + "//" <> rest -> parse_authority_pieces(rest, pieces) + _ -> parse_path(uri_string, pieces) + } +} + +fn parse_authority_pieces(string: String, pieces: Uri) -> Result(Uri, Nil) { + parse_userinfo_loop(string, string, pieces, 0) +} + +fn parse_userinfo_loop( + original: String, + uri_string: String, + pieces: Uri, + size: Int, +) -> Result(Uri, Nil) { + case uri_string { + // `@` marks the end of the userinfo and the start of the host part in the + // authority string. + "@" <> rest if size == 0 -> parse_host(rest, pieces) + "@" <> rest -> { + let userinfo = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, userinfo: Some(userinfo)) + parse_host(rest, pieces) + } + + // If we reach the end of the authority string without finding an `@` + // special character, then we know that the authority doesn't actually + // contain the userinfo part. + // The entire string we just went through was a host! So we parse it as + // such. + "" | "/" <> _ | "?" <> _ | "#" <> _ -> parse_host(original, pieces) + + // In all other cases we just keep munching characters increasing the size + // of the userinfo bit. + _ -> { + let #(_, rest) = pop_codeunit(uri_string) + parse_userinfo_loop(original, rest, pieces, size + 1) + } + } +} + +fn parse_host(uri_string: String, pieces: Uri) -> Result(Uri, Nil) { + // A host string can be in two formats: + // - \[[:.a-zA-Z0-9]*\] + // - [^:] + case uri_string { + // If we find an opening bracket we know it's the first format. + "[" <> _ -> parse_host_within_brackets(uri_string, pieces) + + // A `:` marks the beginning of the port part of the authority string. + ":" <> _ -> { + let pieces = Uri(..pieces, host: Some("")) + parse_port(uri_string, pieces) + } + + // If the string is empty then there's no need to keep going. The host is + // empty. + "" -> Ok(Uri(..pieces, host: Some(""))) + + // Otherwise it's the second format + _ -> parse_host_outside_of_brackets(uri_string, pieces) + } +} + +fn parse_host_within_brackets( + uri_string: String, + pieces: Uri, +) -> Result(Uri, Nil) { + parse_host_within_brackets_loop(uri_string, uri_string, pieces, 0) +} + +fn parse_host_within_brackets_loop( + original: String, + uri_string: String, + pieces: Uri, + size: Int, +) -> Result(Uri, Nil) { + case uri_string { + // If the string is over the entire string we were iterating through is the + // host part. + "" -> Ok(Uri(..pieces, host: Some(uri_string))) + + // A `]` marks the end of the host and the start of the port part. + "]" <> rest if size == 0 -> parse_port(rest, pieces) + "]" <> rest -> { + let host = codeunit_slice(original, at_index: 0, length: size + 1) + let pieces = Uri(..pieces, host: Some(host)) + parse_port(rest, pieces) + } + + // `/` marks the beginning of a path. + "/" <> _ if size == 0 -> parse_path(uri_string, pieces) + "/" <> _ -> { + let host = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, host: Some(host)) + parse_path(uri_string, pieces) + } + + // `?` marks the beginning of the query with question mark. + "?" <> rest if size == 0 -> parse_query_with_question_mark(rest, pieces) + "?" <> rest -> { + let host = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, host: Some(host)) + parse_query_with_question_mark(rest, pieces) + } + + // `#` marks the beginning of the fragment part. + "#" <> rest if size == 0 -> parse_fragment(rest, pieces) + "#" <> rest -> { + let host = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, host: Some(host)) + parse_fragment(rest, pieces) + } + + // In all other cases we just keep iterating. + _ -> { + let #(char, rest) = pop_codeunit(uri_string) + // Inside `[...]` there can only be some characters, if we find a special + // one then we know that we're actually parsing the other format for the + // host and we switch to that! + case is_valid_host_within_brackets_char(char) { + True -> + parse_host_within_brackets_loop(original, rest, pieces, size + 1) + + False -> + parse_host_outside_of_brackets_loop(original, original, pieces, 0) + } + } + } +} + +fn is_valid_host_within_brackets_char(char: Int) -> Bool { + // [0-9] + { 48 >= char && char <= 57 } + // [A-Z] + || { 65 >= char && char <= 90 } + // [a-z] + || { 97 >= char && char <= 122 } + // : + || char == 58 + // . + || char == 46 +} + +fn parse_host_outside_of_brackets( + uri_string: String, + pieces: Uri, +) -> Result(Uri, Nil) { + parse_host_outside_of_brackets_loop(uri_string, uri_string, pieces, 0) +} + +fn parse_host_outside_of_brackets_loop( + original: String, + uri_string: String, + pieces: Uri, + size: Int, +) -> Result(Uri, Nil) { + case uri_string { + "" -> Ok(Uri(..pieces, host: Some(original))) + + // `:` marks the beginning of the port. + ":" <> _ -> { + let host = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, host: Some(host)) + parse_port(uri_string, pieces) + } + + // `/` marks the beginning of a path. + "/" <> _ -> { + let host = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, host: Some(host)) + parse_path(uri_string, pieces) + } + + // `?` marks the beginning of the query with question mark. + "?" <> rest -> { + let host = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, host: Some(host)) + parse_query_with_question_mark(rest, pieces) + } + + // `#` marks the beginning of the fragment part. + "#" <> rest -> { + let host = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, host: Some(host)) + parse_fragment(rest, pieces) + } + + _ -> { + let #(_, rest) = pop_codeunit(uri_string) + parse_host_outside_of_brackets_loop(original, rest, pieces, size + 1) + } + } +} + +fn parse_port(uri_string: String, pieces: Uri) -> Result(Uri, Nil) { + case uri_string { + ":0" <> rest -> parse_port_loop(rest, pieces, 0) + ":1" <> rest -> parse_port_loop(rest, pieces, 1) + ":2" <> rest -> parse_port_loop(rest, pieces, 2) + ":3" <> rest -> parse_port_loop(rest, pieces, 3) + ":4" <> rest -> parse_port_loop(rest, pieces, 4) + ":5" <> rest -> parse_port_loop(rest, pieces, 5) + ":6" <> rest -> parse_port_loop(rest, pieces, 6) + ":7" <> rest -> parse_port_loop(rest, pieces, 7) + ":8" <> rest -> parse_port_loop(rest, pieces, 8) + ":9" <> rest -> parse_port_loop(rest, pieces, 9) + + // The port could be empty and be followed by any of the next delimiters. + // Like `:#`, `:?` or `:/` + ":" | "" -> Ok(pieces) + + // `?` marks the beginning of the query with question mark. + "?" <> rest | ":?" <> rest -> parse_query_with_question_mark(rest, pieces) + + // `#` marks the beginning of the fragment part. + "#" <> rest | ":#" <> rest -> parse_fragment(rest, pieces) + + // `/` marks the beginning of a path. + "/" <> _ -> parse_path(uri_string, pieces) + ":" <> rest -> + case rest { + "/" <> _ -> parse_path(rest, pieces) + _ -> Error(Nil) + } + + _ -> Error(Nil) + } +} + +fn parse_port_loop( + uri_string: String, + pieces: Uri, + port: Int, +) -> Result(Uri, Nil) { + case uri_string { + // As long as we find port numbers we keep accumulating those. + "0" <> rest -> parse_port_loop(rest, pieces, port * 10) + "1" <> rest -> parse_port_loop(rest, pieces, port * 10 + 1) + "2" <> rest -> parse_port_loop(rest, pieces, port * 10 + 2) + "3" <> rest -> parse_port_loop(rest, pieces, port * 10 + 3) + "4" <> rest -> parse_port_loop(rest, pieces, port * 10 + 4) + "5" <> rest -> parse_port_loop(rest, pieces, port * 10 + 5) + "6" <> rest -> parse_port_loop(rest, pieces, port * 10 + 6) + "7" <> rest -> parse_port_loop(rest, pieces, port * 10 + 7) + "8" <> rest -> parse_port_loop(rest, pieces, port * 10 + 8) + "9" <> rest -> parse_port_loop(rest, pieces, port * 10 + 9) + + // `?` marks the beginning of the query with question mark. + "?" <> rest -> { + let pieces = Uri(..pieces, port: Some(port)) + parse_query_with_question_mark(rest, pieces) + } + + // `#` marks the beginning of the fragment part. + "#" <> rest -> { + let pieces = Uri(..pieces, port: Some(port)) + parse_fragment(rest, pieces) + } + + // `/` marks the beginning of a path. + "/" <> _ -> { + let pieces = Uri(..pieces, port: Some(port)) + parse_path(uri_string, pieces) + } + + // The string (and so the port) is over, we return what we parsed so far. + "" -> Ok(Uri(..pieces, port: Some(port))) + + // In all other cases we've ran into some invalid character inside the port + // so the uri is invalid! + _ -> Error(Nil) + } +} + +fn parse_path(uri_string: String, pieces: Uri) -> Result(Uri, Nil) { + parse_path_loop(uri_string, uri_string, pieces, 0) +} + +fn parse_path_loop( + original: String, + uri_string: String, + pieces: Uri, + size: Int, +) -> Result(Uri, Nil) { + case uri_string { + // `?` marks the beginning of the query with question mark. + "?" <> rest -> { + let path = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, path: path) + parse_query_with_question_mark(rest, pieces) + } + + // `#` marks the beginning of the fragment part. + "#" <> rest -> { + let path = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, path: path) + parse_fragment(rest, pieces) + } + + // If the string is over that means the entirety of the string was the path + // and it has an empty query and fragment. + "" -> Ok(Uri(..pieces, path: original)) + + // In all other cases the character is allowed to be part of the path so we + // just keep munching until we reach to its end. + _ -> { + let #(_, rest) = pop_codeunit(uri_string) + parse_path_loop(original, rest, pieces, size + 1) + } + } +} + +fn parse_query_with_question_mark( + uri_string: String, + pieces: Uri, +) -> Result(Uri, Nil) { + parse_query_with_question_mark_loop(uri_string, uri_string, pieces, 0) +} + +fn parse_query_with_question_mark_loop( + original: String, + uri_string: String, + pieces: Uri, + size: Int, +) -> Result(Uri, Nil) { + case uri_string { + // `#` marks the beginning of the fragment part. + "#" <> rest if size == 0 -> parse_fragment(rest, pieces) + "#" <> rest -> { + let query = codeunit_slice(original, at_index: 0, length: size) + let pieces = Uri(..pieces, query: Some(query)) + parse_fragment(rest, pieces) + } + + // If the string is over that means the entirety of the string was the query + // and it has an empty fragment. + "" -> Ok(Uri(..pieces, query: Some(original))) + + // In all other cases the character is allowed to be part of the query so we + // just keep munching until we reach to its end. + _ -> { + let #(_, rest) = pop_codeunit(uri_string) + parse_query_with_question_mark_loop(original, rest, pieces, size + 1) + } + } +} + +fn parse_fragment(rest: String, pieces: Uri) -> Result(Uri, Nil) { + Ok(Uri(..pieces, fragment: Some(rest))) +} + +// WARN: this function returns invalid strings! +// We need to return a String anyways to have this as the representation on the +// JavaScript target. +// Alternatively, we could rewrite the entire code to use a single +// `fold_codeunits`-style loop and a state machine. +@external(erlang, "gleam_stdlib", "string_pop_codeunit") +@external(javascript, "../gleam_stdlib.mjs", "pop_codeunit") +fn pop_codeunit(str: String) -> #(Int, String) + +@external(erlang, "binary", "part") +@external(javascript, "../gleam_stdlib.mjs", "string_codeunit_slice") +fn codeunit_slice(str: String, at_index from: Int, length length: Int) -> String + +/// Parses an urlencoded query string into a list of key value pairs. +/// Returns an error for invalid encoding. +/// +/// The opposite operation is `uri.query_to_string`. +/// +/// ## Examples +/// +/// ```gleam +/// parse_query("a=1&b=2") +/// // -> Ok([#("a", "1"), #("b", "2")]) +/// ``` +/// +@external(erlang, "gleam_stdlib", "parse_query") +@external(javascript, "../gleam_stdlib.mjs", "parse_query") +pub fn parse_query(query: String) -> Result(List(#(String, String)), Nil) + +/// Encodes a list of key value pairs as a URI query string. +/// +/// The opposite operation is `uri.parse_query`. +/// +/// ## Examples +/// +/// ```gleam +/// query_to_string([#("a", "1"), #("b", "2")]) +/// // -> "a=1&b=2" +/// ``` +/// +pub fn query_to_string(query: List(#(String, String))) -> String { + query + |> list.map(query_pair) + |> list.intersperse(string_tree.from_string("&")) + |> string_tree.concat + |> string_tree.to_string +} + +fn query_pair(pair: #(String, String)) -> StringTree { + string_tree.from_strings([percent_encode(pair.0), "=", percent_encode(pair.1)]) +} + +/// Encodes a string into a percent encoded representation. +/// +/// ## Examples +/// +/// ```gleam +/// percent_encode("100% great") +/// // -> "100%25%20great" +/// ``` +/// +@external(erlang, "gleam_stdlib", "percent_encode") +@external(javascript, "../gleam_stdlib.mjs", "percent_encode") +pub fn percent_encode(value: String) -> String + +/// Decodes a percent encoded string. +/// +/// ## Examples +/// +/// ```gleam +/// percent_decode("100%25%20great+fun") +/// // -> Ok("100% great+fun") +/// ``` +/// +@external(erlang, "gleam_stdlib", "percent_decode") +@external(javascript, "../gleam_stdlib.mjs", "percent_decode") +pub fn percent_decode(value: String) -> Result(String, Nil) + +/// Splits the path section of a URI into it's constituent segments. +/// +/// Removes empty segments and resolves dot-segments as specified in +/// [section 5.2](https://www.ietf.org/rfc/rfc3986.html#section-5.2) of the RFC. +/// +/// ## Examples +/// +/// ```gleam +/// path_segments("/users/1") +/// // -> ["users" ,"1"] +/// ``` +/// +pub fn path_segments(path: String) -> List(String) { + remove_dot_segments(string.split(path, "/")) +} + +fn remove_dot_segments(input: List(String)) -> List(String) { + remove_dot_segments_loop(input, []) +} + +fn remove_dot_segments_loop( + input: List(String), + accumulator: List(String), +) -> List(String) { + case input { + [] -> list.reverse(accumulator) + [segment, ..rest] -> { + let accumulator = case segment, accumulator { + "", accumulator -> accumulator + ".", accumulator -> accumulator + "..", [] -> [] + "..", [_, ..accumulator] -> accumulator + segment, accumulator -> [segment, ..accumulator] + } + remove_dot_segments_loop(rest, accumulator) + } + } +} + +/// Encodes a `Uri` value as a URI string. +/// +/// The opposite operation is `uri.parse`. +/// +/// ## Examples +/// +/// ```gleam +/// let uri = Uri(..empty, scheme: Some("https"), host: Some("example.com")) +/// to_string(uri) +/// // -> "https://example.com" +/// ``` +/// +pub fn to_string(uri: Uri) -> String { + let parts = case uri.fragment { + Some(fragment) -> ["#", fragment] + None -> [] + } + let parts = case uri.query { + Some(query) -> ["?", query, ..parts] + None -> parts + } + let parts = [uri.path, ..parts] + let parts = case uri.host, string.starts_with(uri.path, "/") { + Some(host), False if host != "" -> ["/", ..parts] + _, _ -> parts + } + let parts = case uri.host, uri.port { + Some(_), Some(port) -> [":", int.to_string(port), ..parts] + _, _ -> parts + } + let parts = case uri.scheme, uri.userinfo, uri.host { + Some(s), Some(u), Some(h) -> [s, "://", u, "@", h, ..parts] + Some(s), None, Some(h) -> [s, "://", h, ..parts] + Some(s), Some(_), None | Some(s), None, None -> [s, ":", ..parts] + None, None, Some(h) -> ["//", h, ..parts] + _, _, _ -> parts + } + string.concat(parts) +} + +/// Fetches the origin of a URI. +/// +/// Returns the origin of a uri as defined in +/// [RFC 6454](https://tools.ietf.org/html/rfc6454) +/// +/// The supported URI schemes are `http` and `https`. +/// URLs without a scheme will return `Error`. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(uri) = parse("https://example.com/path?foo#bar") +/// origin(uri) +/// // -> Ok("https://example.com") +/// ``` +/// +pub fn origin(uri: Uri) -> Result(String, Nil) { + let Uri(scheme: scheme, host: host, port: port, ..) = uri + case host, scheme { + Some(h), Some("https") if port == Some(443) -> + Ok(string.concat(["https://", h])) + Some(h), Some("http") if port == Some(80) -> + Ok(string.concat(["http://", h])) + Some(h), Some(s) if s == "http" || s == "https" -> { + case port { + Some(p) -> Ok(string.concat([s, "://", h, ":", int.to_string(p)])) + None -> Ok(string.concat([s, "://", h])) + } + } + _, _ -> Error(Nil) + } +} + +/// Resolves a URI with respect to the given base URI. +/// +/// The base URI must be an absolute URI or this function will return an error. +/// The algorithm for merging uris is described in +/// [RFC 3986](https://tools.ietf.org/html/rfc3986#section-5.2). +/// +pub fn merge(base: Uri, relative: Uri) -> Result(Uri, Nil) { + case base { + Uri(scheme: Some(_), host: Some(_), ..) -> + case relative { + Uri(host: Some(_), ..) -> { + let path = + relative.path + |> string.split("/") + |> remove_dot_segments() + |> join_segments() + let resolved = + Uri( + option.or(relative.scheme, base.scheme), + None, + relative.host, + option.or(relative.port, base.port), + path, + relative.query, + relative.fragment, + ) + Ok(resolved) + } + _ -> { + let #(new_path, new_query) = case relative.path { + "" -> #(base.path, option.or(relative.query, base.query)) + _ -> { + let path_segments = case string.starts_with(relative.path, "/") { + True -> string.split(relative.path, "/") + False -> + base.path + |> string.split("/") + |> drop_last() + |> list.append(string.split(relative.path, "/")) + } + let path = + path_segments + |> remove_dot_segments() + |> join_segments() + #(path, relative.query) + } + } + let resolved = + Uri( + base.scheme, + None, + base.host, + base.port, + new_path, + new_query, + relative.fragment, + ) + Ok(resolved) + } + } + _ -> Error(Nil) + } +} + +fn drop_last(elements: List(a)) -> List(a) { + list.take(from: elements, up_to: list.length(elements) - 1) +} + +fn join_segments(segments: List(String)) -> String { + string.join(["", ..segments], "/") +} diff --git a/build/packages/gleam_stdlib/src/gleam@bit_array.erl b/build/packages/gleam_stdlib/src/gleam@bit_array.erl new file mode 100644 index 0000000..7df56ce --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@bit_array.erl @@ -0,0 +1,347 @@ +-module(gleam@bit_array). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/bit_array.gleam"). +-export([from_string/1, bit_size/1, byte_size/1, pad_to_bytes/1, slice/3, is_utf8/1, to_string/1, concat/1, append/2, base64_encode/2, base64_decode/1, base64_url_encode/2, base64_url_decode/1, base16_encode/1, base16_decode/1, inspect/1, compare/2, starts_with/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC(" BitArrays are a sequence of binary data of any length.\n"). + +-file("src/gleam/bit_array.gleam", 11). +?DOC(" Converts a UTF-8 `String` type into a `BitArray`.\n"). +-spec from_string(binary()) -> bitstring(). +from_string(X) -> + gleam_stdlib:identity(X). + +-file("src/gleam/bit_array.gleam", 17). +?DOC(" Returns an integer which is the number of bits in the bit array.\n"). +-spec bit_size(bitstring()) -> integer(). +bit_size(X) -> + erlang:bit_size(X). + +-file("src/gleam/bit_array.gleam", 23). +?DOC(" Returns an integer which is the number of bytes in the bit array.\n"). +-spec byte_size(bitstring()) -> integer(). +byte_size(X) -> + erlang:byte_size(X). + +-file("src/gleam/bit_array.gleam", 29). +?DOC(" Pads a bit array with zeros so that it is a whole number of bytes.\n"). +-spec pad_to_bytes(bitstring()) -> bitstring(). +pad_to_bytes(X) -> + gleam_stdlib:bit_array_pad_to_bytes(X). + +-file("src/gleam/bit_array.gleam", 54). +?DOC( + " Extracts a sub-section of a bit array.\n" + "\n" + " The slice will start at given position and continue up to specified\n" + " length.\n" + " A negative length can be used to extract bytes at the end of a bit array.\n" + "\n" + " This function runs in constant time.\n" +). +-spec slice(bitstring(), integer(), integer()) -> {ok, bitstring()} | + {error, nil}. +slice(String, Position, Length) -> + gleam_stdlib:bit_array_slice(String, Position, Length). + +-file("src/gleam/bit_array.gleam", 67). +-spec is_utf8_loop(bitstring()) -> boolean(). +is_utf8_loop(Bits) -> + case Bits of + <<>> -> + true; + + <<_/utf8, Rest/binary>> -> + is_utf8_loop(Rest); + + _ -> + false + end. + +-file("src/gleam/bit_array.gleam", 62). +?DOC(" Tests to see whether a bit array is valid UTF-8.\n"). +-spec is_utf8(bitstring()) -> boolean(). +is_utf8(Bits) -> + is_utf8_loop(Bits). + +-file("src/gleam/bit_array.gleam", 88). +?DOC( + " Converts a bit array to a string.\n" + "\n" + " Returns an error if the bit array is invalid UTF-8 data.\n" +). +-spec to_string(bitstring()) -> {ok, binary()} | {error, nil}. +to_string(Bits) -> + case is_utf8(Bits) of + true -> + {ok, gleam_stdlib:identity(Bits)}; + + false -> + {error, nil} + end. + +-file("src/gleam/bit_array.gleam", 109). +?DOC( + " Creates a new bit array by joining multiple binaries.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " concat([from_string(\"butter\"), from_string(\"fly\")])\n" + " // -> from_string(\"butterfly\")\n" + " ```\n" +). +-spec concat(list(bitstring())) -> bitstring(). +concat(Bit_arrays) -> + gleam_stdlib:bit_array_concat(Bit_arrays). + +-file("src/gleam/bit_array.gleam", 40). +?DOC( + " Creates a new bit array by joining two bit arrays.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " append(to: from_string(\"butter\"), suffix: from_string(\"fly\"))\n" + " // -> from_string(\"butterfly\")\n" + " ```\n" +). +-spec append(bitstring(), bitstring()) -> bitstring(). +append(First, Second) -> + gleam_stdlib:bit_array_concat([First, Second]). + +-file("src/gleam/bit_array.gleam", 118). +?DOC( + " Encodes a BitArray into a base 64 encoded string.\n" + "\n" + " If the bit array does not contain a whole number of bytes then it is padded\n" + " with zero bits prior to being encoded.\n" +). +-spec base64_encode(bitstring(), boolean()) -> binary(). +base64_encode(Input, Padding) -> + gleam_stdlib:base64_encode(Input, Padding). + +-file("src/gleam/bit_array.gleam", 122). +?DOC(" Decodes a base 64 encoded string into a `BitArray`.\n"). +-spec base64_decode(binary()) -> {ok, bitstring()} | {error, nil}. +base64_decode(Encoded) -> + Padded = case erlang:byte_size(gleam_stdlib:identity(Encoded)) rem 4 of + 0 -> + Encoded; + + N -> + gleam@string:append( + Encoded, + gleam@string:repeat(<<"="/utf8>>, 4 - N) + ) + end, + gleam_stdlib:base64_decode(Padded). + +-file("src/gleam/bit_array.gleam", 140). +?DOC( + " Encodes a `BitArray` into a base 64 encoded string with URL and filename\n" + " safe alphabet.\n" + "\n" + " If the bit array does not contain a whole number of bytes then it is padded\n" + " with zero bits prior to being encoded.\n" +). +-spec base64_url_encode(bitstring(), boolean()) -> binary(). +base64_url_encode(Input, Padding) -> + _pipe = Input, + _pipe@1 = gleam_stdlib:base64_encode(_pipe, Padding), + _pipe@2 = gleam@string:replace(_pipe@1, <<"+"/utf8>>, <<"-"/utf8>>), + gleam@string:replace(_pipe@2, <<"/"/utf8>>, <<"_"/utf8>>). + +-file("src/gleam/bit_array.gleam", 150). +?DOC( + " Decodes a base 64 encoded string with URL and filename safe alphabet into a\n" + " `BitArray`.\n" +). +-spec base64_url_decode(binary()) -> {ok, bitstring()} | {error, nil}. +base64_url_decode(Encoded) -> + _pipe = Encoded, + _pipe@1 = gleam@string:replace(_pipe, <<"-"/utf8>>, <<"+"/utf8>>), + _pipe@2 = gleam@string:replace(_pipe@1, <<"_"/utf8>>, <<"/"/utf8>>), + base64_decode(_pipe@2). + +-file("src/gleam/bit_array.gleam", 164). +?DOC( + " Encodes a `BitArray` into a base 16 encoded string.\n" + "\n" + " If the bit array does not contain a whole number of bytes then it is padded\n" + " with zero bits prior to being encoded.\n" +). +-spec base16_encode(bitstring()) -> binary(). +base16_encode(Input) -> + gleam_stdlib:base16_encode(Input). + +-file("src/gleam/bit_array.gleam", 170). +?DOC(" Decodes a base 16 encoded string into a `BitArray`.\n"). +-spec base16_decode(binary()) -> {ok, bitstring()} | {error, nil}. +base16_decode(Input) -> + gleam_stdlib:base16_decode(Input). + +-file("src/gleam/bit_array.gleam", 191). +-spec inspect_loop(bitstring(), binary()) -> binary(). +inspect_loop(Input, Accumulator) -> + case Input of + <<>> -> + Accumulator; + + <> -> + <<<>/binary, + ":size(1)"/utf8>>; + + <> -> + <<<>/binary, + ":size(2)"/utf8>>; + + <> -> + <<<>/binary, + ":size(3)"/utf8>>; + + <> -> + <<<>/binary, + ":size(4)"/utf8>>; + + <> -> + <<<>/binary, + ":size(5)"/utf8>>; + + <> -> + <<<>/binary, + ":size(6)"/utf8>>; + + <> -> + <<<>/binary, + ":size(7)"/utf8>>; + + <> -> + Suffix = case Rest of + <<>> -> + <<""/utf8>>; + + _ -> + <<", "/utf8>> + end, + Accumulator@1 = <<<>/binary, + Suffix/binary>>, + inspect_loop(Rest, Accumulator@1); + + _ -> + Accumulator + end. + +-file("src/gleam/bit_array.gleam", 187). +?DOC( + " Converts a bit array to a string containing the decimal value of each byte.\n" + "\n" + " Use this over `string.inspect` when you have a bit array you want printed\n" + " in the array syntax even if it is valid UTF-8.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " inspect(<<0, 20, 0x20, 255>>)\n" + " // -> \"<<0, 20, 32, 255>>\"\n" + "\n" + " inspect(<<100, 5:3>>)\n" + " // -> \"<<100, 5:size(3)>>\"\n" + " ```\n" +). +-spec inspect(bitstring()) -> binary(). +inspect(Input) -> + <<(inspect_loop(Input, <<"<<"/utf8>>))/binary, ">>"/utf8>>. + +-file("src/gleam/bit_array.gleam", 232). +?DOC( + " Compare two bit arrays as sequences of bytes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " compare(<<1>>, <<2>>)\n" + " // -> Lt\n" + "\n" + " compare(<<\"AB\":utf8>>, <<\"AA\":utf8>>)\n" + " // -> Gt\n" + "\n" + " compare(<<1, 2:size(2)>>, with: <<1, 2:size(2)>>)\n" + " // -> Eq\n" + " ```\n" +). +-spec compare(bitstring(), bitstring()) -> gleam@order:order(). +compare(A, B) -> + case {A, B} of + {<>, + <>} -> + case {First_byte, Second_byte} of + {F, S} when F > S -> + gt; + + {F@1, S@1} when F@1 < S@1 -> + lt; + + {_, _} -> + compare(First_rest, Second_rest) + end; + + {<<>>, <<>>} -> + eq; + + {_, <<>>} -> + gt; + + {<<>>, _} -> + lt; + + {First, Second} -> + case {gleam_stdlib:bit_array_to_int_and_size(First), + gleam_stdlib:bit_array_to_int_and_size(Second)} of + {{A@1, _}, {B@1, _}} when A@1 > B@1 -> + gt; + + {{A@2, _}, {B@2, _}} when A@2 < B@2 -> + lt; + + {{_, Size_a}, {_, Size_b}} when Size_a > Size_b -> + gt; + + {{_, Size_a@1}, {_, Size_b@1}} when Size_a@1 < Size_b@1 -> + lt; + + {_, _} -> + eq + end + end. + +-file("src/gleam/bit_array.gleam", 273). +?DOC( + " Checks whether the first `BitArray` starts with the second one.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " starts_with(<<1, 2, 3, 4>>, <<1, 2>>)\n" + " // -> True\n" + " ```\n" +). +-spec starts_with(bitstring(), bitstring()) -> boolean(). +starts_with(Bits, Prefix) -> + Prefix_size = erlang:bit_size(Prefix), + case Bits of + <> when Pref =:= Prefix -> + true; + + _ -> + false + end. diff --git a/build/packages/gleam_stdlib/src/gleam@bool.erl b/build/packages/gleam_stdlib/src/gleam@bool.erl new file mode 100644 index 0000000..01307b3 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@bool.erl @@ -0,0 +1,352 @@ +-module(gleam@bool). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/bool.gleam"). +-export(['and'/2, 'or'/2, negate/1, nor/2, nand/2, exclusive_or/2, exclusive_nor/2, to_string/1, guard/3, lazy_guard/3]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " A type with two possible values, `True` and `False`. Used to indicate whether\n" + " things are... true or false!\n" + "\n" + " Often is it clearer and offers more type safety to define a custom type\n" + " than to use `Bool`. For example, rather than having a `is_teacher: Bool`\n" + " field consider having a `role: SchoolRole` field where `SchoolRole` is a custom\n" + " type that can be either `Student` or `Teacher`.\n" +). + +-file("src/gleam/bool.gleam", 31). +?DOC( + " Returns the and of two bools, but it evaluates both arguments.\n" + "\n" + " It's the function equivalent of the `&&` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " and(True, True)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " and(False, True)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " False |> and(True)\n" + " // -> False\n" + " ```\n" +). +-spec 'and'(boolean(), boolean()) -> boolean(). +'and'(A, B) -> + A andalso B. + +-file("src/gleam/bool.gleam", 57). +?DOC( + " Returns the or of two bools, but it evaluates both arguments.\n" + "\n" + " It's the function equivalent of the `||` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " or(True, True)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " or(False, True)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " False |> or(True)\n" + " // -> True\n" + " ```\n" +). +-spec 'or'(boolean(), boolean()) -> boolean(). +'or'(A, B) -> + A orelse B. + +-file("src/gleam/bool.gleam", 77). +?DOC( + " Returns the opposite bool value.\n" + "\n" + " This is the same as the `!` or `not` operators in some other languages.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " negate(True)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " negate(False)\n" + " // -> True\n" + " ```\n" +). +-spec negate(boolean()) -> boolean(). +negate(Bool) -> + not Bool. + +-file("src/gleam/bool.gleam", 105). +?DOC( + " Returns the nor of two bools.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " nor(False, False)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " nor(False, True)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " nor(True, False)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " nor(True, True)\n" + " // -> False\n" + " ```\n" +). +-spec nor(boolean(), boolean()) -> boolean(). +nor(A, B) -> + not (A orelse B). + +-file("src/gleam/bool.gleam", 133). +?DOC( + " Returns the nand of two bools.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " nand(False, False)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " nand(False, True)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " nand(True, False)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " nand(True, True)\n" + " // -> False\n" + " ```\n" +). +-spec nand(boolean(), boolean()) -> boolean(). +nand(A, B) -> + not (A andalso B). + +-file("src/gleam/bool.gleam", 161). +?DOC( + " Returns the exclusive or of two bools.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " exclusive_or(False, False)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_or(False, True)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_or(True, False)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_or(True, True)\n" + " // -> False\n" + " ```\n" +). +-spec exclusive_or(boolean(), boolean()) -> boolean(). +exclusive_or(A, B) -> + A /= B. + +-file("src/gleam/bool.gleam", 189). +?DOC( + " Returns the exclusive nor of two bools.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " exclusive_nor(False, False)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_nor(False, True)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_nor(True, False)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " exclusive_nor(True, True)\n" + " // -> True\n" + " ```\n" +). +-spec exclusive_nor(boolean(), boolean()) -> boolean(). +exclusive_nor(A, B) -> + A =:= B. + +-file("src/gleam/bool.gleam", 207). +?DOC( + " Returns a string representation of the given bool.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_string(True)\n" + " // -> \"True\"\n" + " ```\n" + "\n" + " ```gleam\n" + " to_string(False)\n" + " // -> \"False\"\n" + " ```\n" +). +-spec to_string(boolean()) -> binary(). +to_string(Bool) -> + case Bool of + false -> + <<"False"/utf8>>; + + true -> + <<"True"/utf8>> + end. + +-file("src/gleam/bool.gleam", 266). +?DOC( + " Run a callback function if the given bool is `False`, otherwise return a\n" + " default value.\n" + "\n" + " With a `use` expression this function can simulate the early-return pattern\n" + " found in some other programming languages.\n" + "\n" + " In a procedural language:\n" + "\n" + " ```js\n" + " if (predicate) return value;\n" + " // ...\n" + " ```\n" + "\n" + " In Gleam with a `use` expression:\n" + "\n" + " ```gleam\n" + " use <- guard(when: predicate, return: value)\n" + " // ...\n" + " ```\n" + "\n" + " Like everything in Gleam `use` is an expression, so it short circuits the\n" + " current block, not the entire function. As a result you can assign the value\n" + " to a variable:\n" + "\n" + " ```gleam\n" + " let x = {\n" + " use <- guard(when: predicate, return: value)\n" + " // ...\n" + " }\n" + " ```\n" + "\n" + " Note that unlike in procedural languages the `return` value is evaluated\n" + " even when the predicate is `False`, so it is advisable not to perform\n" + " expensive computation nor side-effects there.\n" + "\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let name = \"\"\n" + " use <- guard(when: name == \"\", return: \"Welcome!\")\n" + " \"Hello, \" <> name\n" + " // -> \"Welcome!\"\n" + " ```\n" + "\n" + " ```gleam\n" + " let name = \"Kamaka\"\n" + " use <- guard(when: name == \"\", return: \"Welcome!\")\n" + " \"Hello, \" <> name\n" + " // -> \"Hello, Kamaka\"\n" + " ```\n" +). +-spec guard(boolean(), BSY, fun(() -> BSY)) -> BSY. +guard(Requirement, Consequence, Alternative) -> + case Requirement of + true -> + Consequence; + + false -> + Alternative() + end. + +-file("src/gleam/bool.gleam", 307). +?DOC( + " Runs a callback function if the given bool is `True`, otherwise runs an\n" + " alternative callback function.\n" + "\n" + " Useful when further computation should be delayed regardless of the given\n" + " bool's value.\n" + "\n" + " See [`guard`](#guard) for more info.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let name = \"Kamaka\"\n" + " let inquiry = fn() { \"How may we address you?\" }\n" + " use <- lazy_guard(when: name == \"\", return: inquiry)\n" + " \"Hello, \" <> name\n" + " // -> \"Hello, Kamaka\"\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " let name = \"\"\n" + " let greeting = fn() { \"Hello, \" <> name }\n" + " use <- lazy_guard(when: name == \"\", otherwise: greeting)\n" + " let number = int.random(99)\n" + " let name = \"User \" <> int.to_string(number)\n" + " \"Welcome, \" <> name\n" + " // -> \"Welcome, User 54\"\n" + " ```\n" +). +-spec lazy_guard(boolean(), fun(() -> BSZ), fun(() -> BSZ)) -> BSZ. +lazy_guard(Requirement, Consequence, Alternative) -> + case Requirement of + true -> + Consequence(); + + false -> + Alternative() + end. diff --git a/build/packages/gleam_stdlib/src/gleam@bytes_tree.erl b/build/packages/gleam_stdlib/src/gleam@bytes_tree.erl new file mode 100644 index 0000000..a96eaa2 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@bytes_tree.erl @@ -0,0 +1,211 @@ +-module(gleam@bytes_tree). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/bytes_tree.gleam"). +-export([append_tree/2, prepend_tree/2, concat/1, new/0, from_string/1, prepend_string/2, append_string/2, from_string_tree/1, from_bit_array/1, prepend/2, append/2, concat_bit_arrays/1, to_bit_array/1, byte_size/1]). +-export_type([bytes_tree/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " `BytesTree` is a type used for efficiently building binary content to be\n" + " written to a file or a socket. Internally it is represented as tree so to\n" + " append or prepend to a bytes tree is a constant time operation that\n" + " allocates a new node in the tree without copying any of the content. When\n" + " writing to an output stream the tree is traversed and the content is sent\n" + " directly rather than copying it into a single buffer beforehand.\n" + "\n" + " If we append one bit array to another the bit arrays must be copied to a\n" + " new location in memory so that they can sit together. This behaviour\n" + " enables efficient reading of the data but copying can be expensive,\n" + " especially if we want to join many bit arrays together.\n" + "\n" + " BytesTree is different in that it can be joined together in constant\n" + " time using minimal memory, and then can be efficiently converted to a\n" + " bit array using the `to_bit_array` function.\n" + "\n" + " Byte trees are always byte aligned, so that a number of bits that is not\n" + " divisible by 8 will be padded with 0s.\n" + "\n" + " On Erlang this type is compatible with Erlang's iolists.\n" +). + +-opaque bytes_tree() :: {bytes, bitstring()} | + {text, gleam@string_tree:string_tree()} | + {many, list(bytes_tree())}. + +-file("src/gleam/bytes_tree.gleam", 68). +?DOC( + " Appends a bytes tree onto the end of another.\n" + "\n" + " Runs in constant time.\n" +). +-spec append_tree(bytes_tree(), bytes_tree()) -> bytes_tree(). +append_tree(First, Second) -> + gleam_stdlib:iodata_append(First, Second). + +-file("src/gleam/bytes_tree.gleam", 59). +?DOC( + " Prepends a bytes tree onto the start of another.\n" + "\n" + " Runs in constant time.\n" +). +-spec prepend_tree(bytes_tree(), bytes_tree()) -> bytes_tree(). +prepend_tree(Second, First) -> + gleam_stdlib:iodata_append(First, Second). + +-file("src/gleam/bytes_tree.gleam", 98). +?DOC( + " Joins a list of bytes trees into a single one.\n" + "\n" + " Runs in constant time.\n" +). +-spec concat(list(bytes_tree())) -> bytes_tree(). +concat(Trees) -> + gleam_stdlib:identity(Trees). + +-file("src/gleam/bytes_tree.gleam", 35). +?DOC( + " Create an empty `BytesTree`. Useful as the start of a pipe chaining many\n" + " trees together.\n" +). +-spec new() -> bytes_tree(). +new() -> + gleam_stdlib:identity([]). + +-file("src/gleam/bytes_tree.gleam", 118). +?DOC( + " Creates a new bytes tree from a string.\n" + "\n" + " Runs in constant time when running on Erlang.\n" + " Runs in linear time otherwise.\n" +). +-spec from_string(binary()) -> bytes_tree(). +from_string(String) -> + gleam_stdlib:wrap_list(String). + +-file("src/gleam/bytes_tree.gleam", 80). +?DOC( + " Prepends a string onto the start of a bytes tree.\n" + "\n" + " Runs in constant time when running on Erlang.\n" + " Runs in linear time with the length of the string otherwise.\n" +). +-spec prepend_string(bytes_tree(), binary()) -> bytes_tree(). +prepend_string(Second, First) -> + gleam_stdlib:iodata_append(gleam_stdlib:wrap_list(First), Second). + +-file("src/gleam/bytes_tree.gleam", 89). +?DOC( + " Appends a string onto the end of a bytes tree.\n" + "\n" + " Runs in constant time when running on Erlang.\n" + " Runs in linear time with the length of the string otherwise.\n" +). +-spec append_string(bytes_tree(), binary()) -> bytes_tree(). +append_string(First, Second) -> + gleam_stdlib:iodata_append(First, gleam_stdlib:wrap_list(Second)). + +-file("src/gleam/bytes_tree.gleam", 128). +?DOC( + " Creates a new bytes tree from a string tree.\n" + "\n" + " Runs in constant time when running on Erlang.\n" + " Runs in linear time otherwise.\n" +). +-spec from_string_tree(gleam@string_tree:string_tree()) -> bytes_tree(). +from_string_tree(Tree) -> + gleam_stdlib:wrap_list(Tree). + +-file("src/gleam/bytes_tree.gleam", 136). +?DOC( + " Creates a new bytes tree from a bit array.\n" + "\n" + " Runs in constant time.\n" +). +-spec from_bit_array(bitstring()) -> bytes_tree(). +from_bit_array(Bits) -> + _pipe = Bits, + _pipe@1 = gleam_stdlib:bit_array_pad_to_bytes(_pipe), + gleam_stdlib:wrap_list(_pipe@1). + +-file("src/gleam/bytes_tree.gleam", 43). +?DOC( + " Prepends a bit array to the start of a bytes tree.\n" + "\n" + " Runs in constant time.\n" +). +-spec prepend(bytes_tree(), bitstring()) -> bytes_tree(). +prepend(Second, First) -> + gleam_stdlib:iodata_append(from_bit_array(First), Second). + +-file("src/gleam/bytes_tree.gleam", 51). +?DOC( + " Appends a bit array to the end of a bytes tree.\n" + "\n" + " Runs in constant time.\n" +). +-spec append(bytes_tree(), bitstring()) -> bytes_tree(). +append(First, Second) -> + gleam_stdlib:iodata_append(First, from_bit_array(Second)). + +-file("src/gleam/bytes_tree.gleam", 106). +?DOC( + " Joins a list of bit arrays into a single bytes tree.\n" + "\n" + " Runs in constant time.\n" +). +-spec concat_bit_arrays(list(bitstring())) -> bytes_tree(). +concat_bit_arrays(Bits) -> + _pipe = Bits, + _pipe@1 = gleam@list:map(_pipe, fun from_bit_array/1), + gleam_stdlib:identity(_pipe@1). + +-file("src/gleam/bytes_tree.gleam", 162). +-spec to_list(list(list(bytes_tree())), list(bitstring())) -> list(bitstring()). +to_list(Stack, Acc) -> + case Stack of + [] -> + Acc; + + [[] | Remaining_stack] -> + to_list(Remaining_stack, Acc); + + [[{bytes, Bits} | Rest] | Remaining_stack@1] -> + to_list([Rest | Remaining_stack@1], [Bits | Acc]); + + [[{text, Tree} | Rest@1] | Remaining_stack@2] -> + Bits@1 = gleam_stdlib:identity(unicode:characters_to_binary(Tree)), + to_list([Rest@1 | Remaining_stack@2], [Bits@1 | Acc]); + + [[{many, Trees} | Rest@2] | Remaining_stack@3] -> + to_list([Trees, Rest@2 | Remaining_stack@3], Acc) + end. + +-file("src/gleam/bytes_tree.gleam", 155). +?DOC( + " Turns a bytes tree into a bit array.\n" + "\n" + " Runs in linear time.\n" + "\n" + " When running on Erlang this function is implemented natively by the\n" + " virtual machine and is highly optimised.\n" +). +-spec to_bit_array(bytes_tree()) -> bitstring(). +to_bit_array(Tree) -> + erlang:list_to_bitstring(Tree). + +-file("src/gleam/bytes_tree.gleam", 186). +?DOC( + " Returns the size of the bytes tree's content in bytes.\n" + "\n" + " Runs in linear time.\n" +). +-spec byte_size(bytes_tree()) -> integer(). +byte_size(Tree) -> + erlang:iolist_size(Tree). diff --git a/build/packages/gleam_stdlib/src/gleam@dict.erl b/build/packages/gleam_stdlib/src/gleam@dict.erl new file mode 100644 index 0000000..d496afb --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@dict.erl @@ -0,0 +1,561 @@ +-module(gleam@dict). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/dict.gleam"). +-export([size/1, is_empty/1, to_list/1, new/0, get/2, has_key/2, insert/3, from_list/1, keys/1, values/1, take/2, merge/2, delete/2, drop/2, upsert/3, fold/3, map_values/2, filter/2, each/2, combine/3]). +-export_type([dict/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type dict(KG, KH) :: any() | {gleam_phantom, KG, KH}. + +-file("src/gleam/dict.gleam", 36). +?DOC( + " Determines the number of key-value pairs in the dict.\n" + " This function runs in constant time and does not need to iterate the dict.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> size\n" + " // -> 0\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(\"key\", \"value\") |> size\n" + " // -> 1\n" + " ```\n" +). +-spec size(dict(any(), any())) -> integer(). +size(Dict) -> + maps:size(Dict). + +-file("src/gleam/dict.gleam", 52). +?DOC( + " Determines whether or not the dict is empty.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> is_empty\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(\"b\", 1) |> is_empty\n" + " // -> False\n" + " ```\n" +). +-spec is_empty(dict(any(), any())) -> boolean(). +is_empty(Dict) -> + maps:size(Dict) =:= 0. + +-file("src/gleam/dict.gleam", 80). +?DOC( + " Converts the dict to a list of 2-element tuples `#(key, value)`, one for\n" + " each key-value pair in the dict.\n" + "\n" + " The tuples in the list have no specific order.\n" + "\n" + " ## Examples\n" + "\n" + " Calling `to_list` on an empty `dict` returns an empty list.\n" + "\n" + " ```gleam\n" + " new() |> to_list\n" + " // -> []\n" + " ```\n" + "\n" + " The ordering of elements in the resulting list is an implementation detail\n" + " that should not be relied upon.\n" + "\n" + " ```gleam\n" + " new() |> insert(\"b\", 1) |> insert(\"a\", 0) |> insert(\"c\", 2) |> to_list\n" + " // -> [#(\"a\", 0), #(\"b\", 1), #(\"c\", 2)]\n" + " ```\n" +). +-spec to_list(dict(KQ, KR)) -> list({KQ, KR}). +to_list(Dict) -> + maps:to_list(Dict). + +-file("src/gleam/dict.gleam", 129). +?DOC(" Creates a fresh dict that contains no values.\n"). +-spec new() -> dict(any(), any()). +new() -> + maps:new(). + +-file("src/gleam/dict.gleam", 150). +?DOC( + " Fetches a value from a dict for a given key.\n" + "\n" + " The dict may not have a value for the key, so the value is wrapped in a\n" + " `Result`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0) |> get(\"a\")\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0) |> get(\"b\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec get(dict(LT, LU), LT) -> {ok, LU} | {error, nil}. +get(From, Get) -> + gleam_stdlib:map_get(From, Get). + +-file("src/gleam/dict.gleam", 116). +?DOC( + " Determines whether or not a value present in the dict for a given key.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0) |> has_key(\"a\")\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0) |> has_key(\"b\")\n" + " // -> False\n" + " ```\n" +). +-spec has_key(dict(LH, any()), LH) -> boolean(). +has_key(Dict, Key) -> + maps:is_key(Key, Dict). + +-file("src/gleam/dict.gleam", 169). +?DOC( + " Inserts a value into the dict with the given key.\n" + "\n" + " If the dict already has a value for the given key then the value is\n" + " replaced with the new value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0)\n" + " // -> from_list([#(\"a\", 0)])\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(\"a\", 0) |> insert(\"a\", 5)\n" + " // -> from_list([#(\"a\", 5)])\n" + " ```\n" +). +-spec insert(dict(LZ, MA), LZ, MA) -> dict(LZ, MA). +insert(Dict, Key, Value) -> + maps:put(Key, Value, Dict). + +-file("src/gleam/dict.gleam", 92). +-spec from_list_loop(list({LA, LB}), dict(LA, LB)) -> dict(LA, LB). +from_list_loop(List, Initial) -> + case List of + [] -> + Initial; + + [{Key, Value} | Rest] -> + from_list_loop(Rest, insert(Initial, Key, Value)) + end. + +-file("src/gleam/dict.gleam", 88). +?DOC( + " Converts a list of 2-element tuples `#(key, value)` to a dict.\n" + "\n" + " If two tuples have the same key the last one in the list will be the one\n" + " that is present in the dict.\n" +). +-spec from_list(list({KV, KW})) -> dict(KV, KW). +from_list(List) -> + maps:from_list(List). + +-file("src/gleam/dict.gleam", 223). +-spec reverse_and_concat(list(NJ), list(NJ)) -> list(NJ). +reverse_and_concat(Remaining, Accumulator) -> + case Remaining of + [] -> + Accumulator; + + [First | Rest] -> + reverse_and_concat(Rest, [First | Accumulator]) + end. + +-file("src/gleam/dict.gleam", 216). +-spec do_keys_loop(list({NE, any()}), list(NE)) -> list(NE). +do_keys_loop(List, Acc) -> + case List of + [] -> + reverse_and_concat(Acc, []); + + [{Key, _} | Rest] -> + do_keys_loop(Rest, [Key | Acc]) + end. + +-file("src/gleam/dict.gleam", 212). +?DOC( + " Gets a list of all keys in a given dict.\n" + "\n" + " Dicts are not ordered so the keys are not returned in any specific order. Do\n" + " not write code that relies on the order keys are returned by this function\n" + " as it may change in later versions of Gleam or Erlang.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> keys\n" + " // -> [\"a\", \"b\"]\n" + " ```\n" +). +-spec keys(dict(MZ, any())) -> list(MZ). +keys(Dict) -> + maps:keys(Dict). + +-file("src/gleam/dict.gleam", 249). +-spec do_values_loop(list({any(), NT}), list(NT)) -> list(NT). +do_values_loop(List, Acc) -> + case List of + [] -> + reverse_and_concat(Acc, []); + + [{_, Value} | Rest] -> + do_values_loop(Rest, [Value | Acc]) + end. + +-file("src/gleam/dict.gleam", 244). +?DOC( + " Gets a list of all values in a given dict.\n" + "\n" + " Dicts are not ordered so the values are not returned in any specific order. Do\n" + " not write code that relies on the order values are returned by this function\n" + " as it may change in later versions of Gleam or Erlang.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> values\n" + " // -> [0, 1]\n" + " ```\n" +). +-spec values(dict(any(), NO)) -> list(NO). +values(Dict) -> + maps:values(Dict). + +-file("src/gleam/dict.gleam", 318). +-spec do_take_loop(dict(OX, OY), list(OX), dict(OX, OY)) -> dict(OX, OY). +do_take_loop(Dict, Desired_keys, Acc) -> + Insert = fun(Taken, Key) -> case gleam_stdlib:map_get(Dict, Key) of + {ok, Value} -> + insert(Taken, Key, Value); + + {error, _} -> + Taken + end end, + case Desired_keys of + [] -> + Acc; + + [First | Rest] -> + do_take_loop(Dict, Rest, Insert(Acc, First)) + end. + +-file("src/gleam/dict.gleam", 309). +?DOC( + " Creates a new dict from a given dict, only including any entries for which the\n" + " keys are in a given list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " |> take([\"b\"])\n" + " // -> from_list([#(\"b\", 1)])\n" + " ```\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " |> take([\"a\", \"b\", \"c\"])\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " ```\n" +). +-spec take(dict(OJ, OK), list(OJ)) -> dict(OJ, OK). +take(Dict, Desired_keys) -> + maps:with(Desired_keys, Dict). + +-file("src/gleam/dict.gleam", 363). +-spec insert_pair(dict(PV, PW), {PV, PW}) -> dict(PV, PW). +insert_pair(Dict, Pair) -> + insert(Dict, erlang:element(1, Pair), erlang:element(2, Pair)). + +-file("src/gleam/dict.gleam", 356). +-spec fold_inserts(list({PO, PP}), dict(PO, PP)) -> dict(PO, PP). +fold_inserts(New_entries, Dict) -> + case New_entries of + [] -> + Dict; + + [First | Rest] -> + fold_inserts(Rest, insert_pair(Dict, First)) + end. + +-file("src/gleam/dict.gleam", 350). +?DOC( + " Creates a new dict from a pair of given dicts by combining their entries.\n" + "\n" + " If there are entries with the same keys in both dicts the entry from the\n" + " second dict takes precedence.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let a = from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " let b = from_list([#(\"b\", 2), #(\"c\", 3)])\n" + " merge(a, b)\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 2), #(\"c\", 3)])\n" + " ```\n" +). +-spec merge(dict(PG, PH), dict(PG, PH)) -> dict(PG, PH). +merge(Dict, New_entries) -> + maps:merge(Dict, New_entries). + +-file("src/gleam/dict.gleam", 382). +?DOC( + " Creates a new dict from a given dict with all the same entries except for the\n" + " one with a given key, if it exists.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> delete(\"a\")\n" + " // -> from_list([#(\"b\", 1)])\n" + " ```\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> delete(\"c\")\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " ```\n" +). +-spec delete(dict(QB, QC), QB) -> dict(QB, QC). +delete(Dict, Key) -> + maps:remove(Key, Dict). + +-file("src/gleam/dict.gleam", 410). +?DOC( + " Creates a new dict from a given dict with all the same entries except any with\n" + " keys found in a given list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> drop([\"a\"])\n" + " // -> from_list([#(\"b\", 1)])\n" + " ```\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> drop([\"c\"])\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " ```\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)]) |> drop([\"a\", \"b\", \"c\"])\n" + " // -> from_list([])\n" + " ```\n" +). +-spec drop(dict(QN, QO), list(QN)) -> dict(QN, QO). +drop(Dict, Disallowed_keys) -> + case Disallowed_keys of + [] -> + Dict; + + [First | Rest] -> + drop(delete(Dict, First), Rest) + end. + +-file("src/gleam/dict.gleam", 440). +?DOC( + " Creates a new dict with one entry inserted or updated using a given function.\n" + "\n" + " If there was not an entry in the dict for the given key then the function\n" + " gets `None` as its argument, otherwise it gets `Some(value)`.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " let dict = from_list([#(\"a\", 0)])\n" + " let increment = fn(x) {\n" + " case x {\n" + " Some(i) -> i + 1\n" + " None -> 0\n" + " }\n" + " }\n" + "\n" + " upsert(dict, \"a\", increment)\n" + " // -> from_list([#(\"a\", 1)])\n" + "\n" + " upsert(dict, \"b\", increment)\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 0)])\n" + " ```\n" +). +-spec upsert(dict(QU, QV), QU, fun((gleam@option:option(QV)) -> QV)) -> dict(QU, QV). +upsert(Dict, Key, Fun) -> + case gleam_stdlib:map_get(Dict, Key) of + {ok, Value} -> + insert(Dict, Key, Fun({some, Value})); + + {error, _} -> + insert(Dict, Key, Fun(none)) + end. + +-file("src/gleam/dict.gleam", 484). +-spec fold_loop(list({RG, RH}), RJ, fun((RJ, RG, RH) -> RJ)) -> RJ. +fold_loop(List, Initial, Fun) -> + case List of + [] -> + Initial; + + [{K, V} | Rest] -> + fold_loop(Rest, Fun(Initial, K, V), Fun) + end. + +-file("src/gleam/dict.gleam", 476). +?DOC( + " Combines all entries into a single value by calling a given function on each\n" + " one.\n" + "\n" + " Dicts are not ordered so the values are not returned in any specific order. Do\n" + " not write code that relies on the order entries are used by this function\n" + " as it may change in later versions of Gleam or Erlang.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let dict = from_list([#(\"a\", 1), #(\"b\", 3), #(\"c\", 9)])\n" + " fold(dict, 0, fn(accumulator, key, value) { accumulator + value })\n" + " // -> 13\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/string\n" + "\n" + " let dict = from_list([#(\"a\", 1), #(\"b\", 3), #(\"c\", 9)])\n" + " fold(dict, \"\", fn(accumulator, key, value) {\n" + " string.append(accumulator, key)\n" + " })\n" + " // -> \"abc\"\n" + " ```\n" +). +-spec fold(dict(RB, RC), RF, fun((RF, RB, RC) -> RF)) -> RF. +fold(Dict, Initial, Fun) -> + fold_loop(maps:to_list(Dict), Initial, Fun). + +-file("src/gleam/dict.gleam", 188). +?DOC( + " Updates all values in a given dict by calling a given function on each key\n" + " and value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(3, 3), #(2, 4)])\n" + " |> map_values(fn(key, value) { key * value })\n" + " // -> from_list([#(3, 9), #(2, 8)])\n" + " ```\n" +). +-spec map_values(dict(ML, MM), fun((ML, MM) -> MP)) -> dict(ML, MP). +map_values(Dict, Fun) -> + maps:map(Fun, Dict). + +-file("src/gleam/dict.gleam", 273). +?DOC( + " Creates a new dict from a given dict, minus any entries that a given function\n" + " returns `False` for.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " |> filter(fn(key, value) { value != 0 })\n" + " // -> from_list([#(\"b\", 1)])\n" + " ```\n" + "\n" + " ```gleam\n" + " from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " |> filter(fn(key, value) { True })\n" + " // -> from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " ```\n" +). +-spec filter(dict(NX, NY), fun((NX, NY) -> boolean())) -> dict(NX, NY). +filter(Dict, Predicate) -> + maps:filter(Predicate, Dict). + +-file("src/gleam/dict.gleam", 517). +?DOC( + " Calls a function for each key and value in a dict, discarding the return\n" + " value.\n" + "\n" + " Useful for producing a side effect for every item of a dict.\n" + "\n" + " ```gleam\n" + " import gleam/io\n" + "\n" + " let dict = from_list([#(\"a\", \"apple\"), #(\"b\", \"banana\"), #(\"c\", \"cherry\")])\n" + "\n" + " each(dict, fn(k, v) {\n" + " io.println(key <> \" => \" <> value)\n" + " })\n" + " // -> Nil\n" + " // a => apple\n" + " // b => banana\n" + " // c => cherry\n" + " ```\n" + "\n" + " The order of elements in the iteration is an implementation detail that\n" + " should not be relied upon.\n" +). +-spec each(dict(RK, RL), fun((RK, RL) -> any())) -> nil. +each(Dict, Fun) -> + fold( + Dict, + nil, + fun(Nil, K, V) -> + Fun(K, V), + Nil + end + ). + +-file("src/gleam/dict.gleam", 538). +?DOC( + " Creates a new dict from a pair of given dicts by combining their entries.\n" + "\n" + " If there are entries with the same keys in both dicts the given function is\n" + " used to determine the new value to use in the resulting dict.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let a = from_list([#(\"a\", 0), #(\"b\", 1)])\n" + " let b = from_list([#(\"a\", 2), #(\"c\", 3)])\n" + " combine(a, b, fn(one, other) { one + other })\n" + " // -> from_list([#(\"a\", 2), #(\"b\", 1), #(\"c\", 3)])\n" + " ```\n" +). +-spec combine(dict(RP, RQ), dict(RP, RQ), fun((RQ, RQ) -> RQ)) -> dict(RP, RQ). +combine(Dict, Other, Fun) -> + fold( + Dict, + Other, + fun(Acc, Key, Value) -> case gleam_stdlib:map_get(Acc, Key) of + {ok, Other_value} -> + insert(Acc, Key, Fun(Value, Other_value)); + + {error, _} -> + insert(Acc, Key, Value) + end end + ). diff --git a/build/packages/gleam_stdlib/src/gleam@dynamic.erl b/build/packages/gleam_stdlib/src/gleam@dynamic.erl new file mode 100644 index 0000000..f057ca2 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@dynamic.erl @@ -0,0 +1,106 @@ +-module(gleam@dynamic). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/dynamic.gleam"). +-export([classify/1, bool/1, string/1, float/1, int/1, bit_array/1, list/1, array/1, properties/1, nil/0]). +-export_type([dynamic_/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type dynamic_() :: any(). + +-file("src/gleam/dynamic.gleam", 30). +?DOC( + " Return a string indicating the type of the dynamic value.\n" + "\n" + " This function may be useful for constructing error messages or logs. If you\n" + " want to turn dynamic data into well typed data then you want the\n" + " `gleam/dynamic/decode` module.\n" + "\n" + " ```gleam\n" + " classify(string(\"Hello\"))\n" + " // -> \"String\"\n" + " ```\n" +). +-spec classify(dynamic_()) -> binary(). +classify(Data) -> + gleam_stdlib:classify_dynamic(Data). + +-file("src/gleam/dynamic.gleam", 36). +?DOC(" Create a dynamic value from a bool.\n"). +-spec bool(boolean()) -> dynamic_(). +bool(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 44). +?DOC( + " Create a dynamic value from a string.\n" + "\n" + " On Erlang this will be a binary string rather than a character list.\n" +). +-spec string(binary()) -> dynamic_(). +string(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 50). +?DOC(" Create a dynamic value from a float.\n"). +-spec float(float()) -> dynamic_(). +float(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 56). +?DOC(" Create a dynamic value from an int.\n"). +-spec int(integer()) -> dynamic_(). +int(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 62). +?DOC(" Create a dynamic value from a bit array.\n"). +-spec bit_array(bitstring()) -> dynamic_(). +bit_array(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 68). +?DOC(" Create a dynamic value from a list.\n"). +-spec list(list(dynamic_())) -> dynamic_(). +list(A) -> + gleam_stdlib:identity(A). + +-file("src/gleam/dynamic.gleam", 77). +?DOC( + " Create a dynamic value from a list, converting it to a sequential runtime\n" + " format rather than the regular list format.\n" + "\n" + " On Erlang this will be a tuple, on JavaScript this will be an array.\n" +). +-spec array(list(dynamic_())) -> dynamic_(). +array(A) -> + erlang:list_to_tuple(A). + +-file("src/gleam/dynamic.gleam", 85). +?DOC( + " Create a dynamic value made an unordered series of keys and values, where\n" + " the keys are unique.\n" + "\n" + " On Erlang this will be a map, on JavaScript this will be a Gleam dict\n" + " object.\n" +). +-spec properties(list({dynamic_(), dynamic_()})) -> dynamic_(). +properties(Entries) -> + gleam_stdlib:identity(maps:from_list(Entries)). + +-file("src/gleam/dynamic.gleam", 94). +?DOC( + " A dynamic value representing nothing.\n" + "\n" + " On Erlang this will be the atom `nil`, on JavaScript this will be\n" + " `undefined`.\n" +). +-spec nil() -> dynamic_(). +nil() -> + gleam_stdlib:identity(nil). diff --git a/build/packages/gleam_stdlib/src/gleam@dynamic@decode.erl b/build/packages/gleam_stdlib/src/gleam@dynamic@decode.erl new file mode 100644 index 0000000..bf4b951 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@dynamic@decode.erl @@ -0,0 +1,1088 @@ +-module(gleam@dynamic@decode). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/dynamic/decode.gleam"). +-export([run/2, success/1, decode_dynamic/1, map/2, map_errors/2, then/2, one_of/2, recursive/1, optional/1, decode_error/2, decode_bool/1, decode_int/1, decode_float/1, decode_bit_array/1, collapse_errors/2, failure/2, new_primitive_decoder/2, decode_string/1, dict/2, list/1, subfield/3, at/2, field/3, optional_field/4, optionally_at/3]). +-export_type([decode_error/0, decoder/1]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " The `Dynamic` type is used to represent dynamically typed data. That is, data\n" + " that we don't know the precise type of yet, so we need to introspect the data to\n" + " see if it is of the desired type before we can use it. Typically data like this\n" + " would come from user input or from untyped languages such as Erlang or JavaScript.\n" + "\n" + " This module provides the `Decoder` type and associated functions, which provides\n" + " a type-safe and composable way to convert dynamic data into some desired type,\n" + " or into errors if the data doesn't have the desired structure.\n" + "\n" + " The `Decoder` type is generic and has 1 type parameter, which is the type that\n" + " it attempts to decode. A `Decoder(String)` can be used to decode strings, and a\n" + " `Decoder(Option(Int))` can be used to decode `Option(Int)`s\n" + "\n" + " Decoders work using _runtime reflection_ and the data structures of the target\n" + " platform. Differences between Erlang and JavaScript data structures may impact\n" + " your decoders, so it is important to test your decoders on all supported\n" + " platforms.\n" + "\n" + " The decoding technique used by this module was inspired by Juraj Petráš'\n" + " [Toy](https://github.com/Hackder/toy), Go's `encoding/json`, and Elm's\n" + " `Json.Decode`. Thank you to them!\n" + "\n" + " # Examples\n" + "\n" + " Dynamic data may come from various sources and so many different syntaxes could\n" + " be used to describe or construct them. In these examples a pseudocode\n" + " syntax is used to describe the data.\n" + "\n" + " ## Simple types\n" + "\n" + " This module defines decoders for simple data types such as [`string`](#string),\n" + " [`int`](#int), [`float`](#float), [`bit_array`](#bit_array), and [`bool`](#bool).\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // \"Hello, Joe!\"\n" + "\n" + " let result = decode.run(data, decode.string)\n" + " assert result == Ok(\"Hello, Joe!\")\n" + " ```\n" + "\n" + " ## Lists\n" + "\n" + " The [`list`](#list) decoder decodes `List`s. To use it you must construct it by\n" + " passing in another decoder into the `list` function, which is the decoder that\n" + " is to be used for the elements of the list, type checking both the list and its\n" + " elements.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // [1, 2, 3, 4]\n" + "\n" + " let result = decode.run(data, decode.list(decode.int))\n" + " assert result == Ok([1, 2, 3, 4])\n" + " ```\n" + "\n" + " On Erlang this decoder can decode from lists, and on JavaScript it can\n" + " decode from lists as well as JavaScript arrays.\n" + "\n" + " ## Options\n" + "\n" + " The [`optional`](#optional) decoder is used to decode values that may or may not\n" + " be present. In other environment these might be called \"nullable\" values.\n" + "\n" + " Like the `list` decoder, the `optional` decoder takes another decoder,\n" + " which is used to decode the value if it is present.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // 12.45\n" + "\n" + " let result = decode.run(data, decode.optional(decode.float))\n" + " assert result == Ok(option.Some(12.45))\n" + " ```\n" + " ```gleam\n" + " // Data:\n" + " // null\n" + "\n" + " let result = decode.run(data, decode.optional(decode.int))\n" + " assert result == Ok(option.None)\n" + " ```\n" + "\n" + " This decoder knows how to handle multiple different runtime representations of\n" + " absent values, including `Nil`, `None`, `null`, and `undefined`.\n" + "\n" + " ## Dicts\n" + "\n" + " The [`dict`](#dict) decoder decodes `Dicts` and contains two other decoders, one\n" + " for the keys, one for the values.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // { \"Lucy\" -> 10, \"Nubi\" -> 20 }\n" + "\n" + " let result = decode.run(data, decode.dict(decode.string, decode.int))\n" + " assert result == Ok(dict.from_list([\n" + " #(\"Lucy\", 10),\n" + " #(\"Nubi\", 20),\n" + " ]))\n" + " ```\n" + "\n" + " ## Indexing objects\n" + "\n" + " The [`at`](#at) decoder can be used to decode a value that is nested within\n" + " key-value containers such as Gleam dicts, Erlang maps, or JavaScript objects.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // { \"one\" -> { \"two\" -> 123 } }\n" + "\n" + " let result = decode.run(data, decode.at([\"one\", \"two\"], decode.int))\n" + " assert result == Ok(123)\n" + " ```\n" + "\n" + " ## Indexing arrays\n" + "\n" + " If you use ints as keys then the [`at`](#at) decoder can be used to index into\n" + " array-like containers such as Gleam or Erlang tuples, or JavaScript arrays.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // [\"one\", \"two\", \"three\"]\n" + "\n" + " let result = decode.run(data, decode.at([1], decode.string))\n" + " assert result == Ok(\"two\")\n" + " ```\n" + "\n" + " ## Records\n" + "\n" + " Decoding records from dynamic data is more complex and requires combining a\n" + " decoder for each field and a special constructor that builds your records with\n" + " the decoded field values.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // {\n" + " // \"score\" -> 180,\n" + " // \"name\" -> \"Mel Smith\",\n" + " // \"is-admin\" -> false,\n" + " // \"enrolled\" -> true,\n" + " // \"colour\" -> \"Red\",\n" + " // }\n" + "\n" + " let decoder = {\n" + " use name <- decode.field(\"name\", decode.string)\n" + " use score <- decode.field(\"score\", decode.int)\n" + " use colour <- decode.field(\"colour\", decode.string)\n" + " use enrolled <- decode.field(\"enrolled\", decode.bool)\n" + " decode.success(Player(name:, score:, colour:, enrolled:))\n" + " }\n" + "\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(Player(\"Mel Smith\", 180, \"Red\", True))\n" + " ```\n" + "\n" + " ## Enum variants\n" + "\n" + " Imagine you have a custom type where all the variants do not contain any values.\n" + "\n" + " ```gleam\n" + " pub type PocketMonsterType {\n" + " Fire\n" + " Water\n" + " Grass\n" + " Electric\n" + " }\n" + " ```\n" + "\n" + " You might choose to encode these variants as strings, `\"fire\"` for `Fire`,\n" + " `\"water\"` for `Water`, and so on. To decode them you'll need to decode the dynamic\n" + " data as a string, but then you'll need to decode it further still as not all\n" + " strings are valid values for the enum. This can be done with the `then`\n" + " function, which enables running a second decoder after the first one\n" + " succeeds.\n" + "\n" + " ```gleam\n" + " let decoder = {\n" + " use decoded_string <- decode.then(decode.string)\n" + " case decoded_string {\n" + " // Return succeeding decoders for valid strings\n" + " \"fire\" -> decode.success(Fire)\n" + " \"water\" -> decode.success(Water)\n" + " \"grass\" -> decode.success(Grass)\n" + " \"electric\" -> decode.success(Electric)\n" + " // Return a failing decoder for any other strings\n" + " _ -> decode.failure(Fire, \"PocketMonsterType\")\n" + " }\n" + " }\n" + "\n" + " let result = decode.run(dynamic.string(\"water\"), decoder)\n" + " assert result == Ok(Water)\n" + "\n" + " let result = decode.run(dynamic.string(\"wobble\"), decoder)\n" + " assert result == Error([DecodeError(\"PocketMonsterType\", \"String\", [])])\n" + " ```\n" + "\n" + " ## Record variants\n" + "\n" + " Decoding type variants that contain other values is done by combining the\n" + " techniques from the \"enum variants\" and \"records\" examples. Imagine you have\n" + " this custom type that you want to decode:\n" + "\n" + " ```gleam\n" + " pub type PocketMonsterPerson {\n" + " Trainer(name: String, badge_count: Int)\n" + " GymLeader(name: String, speciality: PocketMonsterType)\n" + " }\n" + " ```\n" + " And you would like to be able to decode these from dynamic data like this:\n" + " ```erlang\n" + " {\n" + " \"type\" -> \"trainer\",\n" + " \"name\" -> \"Ash\",\n" + " \"badge-count\" -> 1,\n" + " }\n" + " ```\n" + " ```erlang\n" + " {\n" + " \"type\" -> \"gym-leader\",\n" + " \"name\" -> \"Misty\",\n" + " \"speciality\" -> \"water\",\n" + " }\n" + " ```\n" + "\n" + " Notice how both documents have a `\"type\"` field, which is used to indicate which\n" + " variant the data is for.\n" + "\n" + " First, define decoders for each of the variants:\n" + "\n" + " ```gleam\n" + " let trainer_decoder = {\n" + " use name <- decode.field(\"name\", decode.string)\n" + " use badge_count <- decode.field(\"badge-count\", decode.int)\n" + " decode.success(Trainer(name, badge_count))\n" + " }\n" + "\n" + " let gym_leader_decoder = {\n" + " use name <- decode.field(\"name\", decode.string)\n" + " use speciality <- decode.field(\"speciality\", pocket_monster_type_decoder)\n" + " decode.success(GymLeader(name, speciality))\n" + " }\n" + " ```\n" + "\n" + " A third decoder can be used to extract and decode the `\"type\"` field, and the\n" + " expression can evaluate to whichever decoder is suitable for the document.\n" + "\n" + " ```gleam\n" + " // Data:\n" + " // {\n" + " // \"type\" -> \"gym-leader\",\n" + " // \"name\" -> \"Misty\",\n" + " // \"speciality\" -> \"water\",\n" + " // }\n" + "\n" + " let decoder = {\n" + " use tag <- decode.field(\"type\", decode.string)\n" + " case tag {\n" + " \"gym-leader\" -> gym_leader_decoder\n" + " _ -> trainer_decoder\n" + " }\n" + " }\n" + "\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(GymLeader(\"Misty\", Water))\n" + " ```\n" +). + +-type decode_error() :: {decode_error, binary(), binary(), list(binary())}. + +-opaque decoder(BUW) :: {decoder, + fun((gleam@dynamic:dynamic_()) -> {BUW, list(decode_error())})}. + +-file("src/gleam/dynamic/decode.gleam", 356). +?DOC( + " Run a decoder on a `Dynamic` value, decoding the value if it is of the\n" + " desired type, or returning errors.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = {\n" + " use name <- decode.field(\"email\", decode.string)\n" + " use email <- decode.field(\"password\", decode.string)\n" + " decode.success(SignUp(name: name, email: email))\n" + " }\n" + "\n" + " decode.run(data, decoder)\n" + " ```\n" +). +-spec run(gleam@dynamic:dynamic_(), decoder(BVE)) -> {ok, BVE} | + {error, list(decode_error())}. +run(Data, Decoder) -> + {Maybe_invalid_data, Errors} = (erlang:element(2, Decoder))(Data), + case Errors of + [] -> + {ok, Maybe_invalid_data}; + + [_ | _] -> + {error, Errors} + end. + +-file("src/gleam/dynamic/decode.gleam", 479). +?DOC( + " Finalise a decoder having successfully extracted a value.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"email\"), dynamic.string(\"lucy@example.com\")),\n" + " #(dynamic.string(\"name\"), dynamic.string(\"Lucy\")),\n" + " ]))\n" + "\n" + " let decoder = {\n" + " use name <- decode.field(\"name\", string)\n" + " use email <- decode.field(\"email\", string)\n" + " decode.success(SignUp(name: name, email: email))\n" + " }\n" + "\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(SignUp(name: \"Lucy\", email: \"lucy@example.com\"))\n" + " ```\n" +). +-spec success(BWF) -> decoder(BWF). +success(Data) -> + {decoder, fun(_) -> {Data, []} end}. + +-file("src/gleam/dynamic/decode.gleam", 718). +-spec decode_dynamic(gleam@dynamic:dynamic_()) -> {gleam@dynamic:dynamic_(), + list(decode_error())}. +decode_dynamic(Data) -> + {Data, []}. + +-file("src/gleam/dynamic/decode.gleam", 875). +?DOC( + " Apply a transformation function to any value decoded by the decoder.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = decode.int |> decode.map(int.to_string)\n" + " let result = decode.run(dynamic.int(1000), decoder)\n" + " assert result == Ok(\"1000\")\n" + " ```\n" +). +-spec map(decoder(BZC), fun((BZC) -> BZE)) -> decoder(BZE). +map(Decoder, Transformer) -> + {decoder, + fun(D) -> + {Data, Errors} = (erlang:element(2, Decoder))(D), + {Transformer(Data), Errors} + end}. + +-file("src/gleam/dynamic/decode.gleam", 884). +?DOC(" Apply a transformation function to any errors returned by the decoder.\n"). +-spec map_errors( + decoder(BZG), + fun((list(decode_error())) -> list(decode_error())) +) -> decoder(BZG). +map_errors(Decoder, Transformer) -> + {decoder, + fun(D) -> + {Data, Errors} = (erlang:element(2, Decoder))(D), + {Data, Transformer(Errors)} + end}. + +-file("src/gleam/dynamic/decode.gleam", 922). +?DOC( + " Create a new decoder based upon the value of a previous decoder.\n" + "\n" + " This may be useful to run one previous decoder to use in further decoding.\n" +). +-spec then(decoder(BZO), fun((BZO) -> decoder(BZQ))) -> decoder(BZQ). +then(Decoder, Next) -> + {decoder, + fun(Dynamic_data) -> + {Data, Errors} = (erlang:element(2, Decoder))(Dynamic_data), + Decoder@1 = Next(Data), + {Data@1, _} = Layer = (erlang:element(2, Decoder@1))(Dynamic_data), + case Errors of + [] -> + Layer; + + [_ | _] -> + {Data@1, Errors} + end + end}. + +-file("src/gleam/dynamic/decode.gleam", 965). +-spec run_decoders( + gleam@dynamic:dynamic_(), + {BZY, list(decode_error())}, + list(decoder(BZY)) +) -> {BZY, list(decode_error())}. +run_decoders(Data, Failure, Decoders) -> + case Decoders of + [] -> + Failure; + + [Decoder | Decoders@1] -> + {_, Errors} = Layer = (erlang:element(2, Decoder))(Data), + case Errors of + [] -> + Layer; + + [_ | _] -> + run_decoders(Data, Failure, Decoders@1) + end + end. + +-file("src/gleam/dynamic/decode.gleam", 952). +?DOC( + " Create a new decoder from several other decoders. Each of the inner\n" + " decoders is run in turn, and the value from the first to succeed is used.\n" + "\n" + " If no decoder succeeds then the errors from the first decoder is used.\n" + " If you wish for different errors then you may wish to use the\n" + " `collapse_errors` or `map_errors` functions.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = decode.one_of(decode.string, or: [\n" + " decode.int |> decode.map(int.to_string),\n" + " decode.float |> decode.map(float.to_string),\n" + " ])\n" + " decode.run(dynamic.int(1000), decoder)\n" + " // -> Ok(\"1000\")\n" + " ```\n" +). +-spec one_of(decoder(BZT), list(decoder(BZT))) -> decoder(BZT). +one_of(First, Alternatives) -> + {decoder, + fun(Dynamic_data) -> + {_, Errors} = Layer = (erlang:element(2, First))(Dynamic_data), + case Errors of + [] -> + Layer; + + [_ | _] -> + run_decoders(Dynamic_data, Layer, Alternatives) + end + end}. + +-file("src/gleam/dynamic/decode.gleam", 1048). +?DOC( + " Create a decoder that can refer to itself, useful for decoding deeply\n" + " nested data.\n" + "\n" + " Attempting to create a recursive decoder without this function could result\n" + " in an infinite loop. If you are using `field` or other `use`able functions\n" + " then you may not need to use this function.\n" + "\n" + " ```gleam\n" + " type Nested {\n" + " Nested(List(Nested))\n" + " Value(String)\n" + " }\n" + "\n" + " fn nested_decoder() -> decode.Decoder(Nested) {\n" + " use <- decode.recursive\n" + " decode.one_of(decode.string |> decode.map(Value), [\n" + " decode.list(nested_decoder()) |> decode.map(Nested),\n" + " ])\n" + " }\n" + " ```\n" +). +-spec recursive(fun(() -> decoder(CAJ))) -> decoder(CAJ). +recursive(Inner) -> + {decoder, + fun(Data) -> + Decoder = Inner(), + (erlang:element(2, Decoder))(Data) + end}. + +-file("src/gleam/dynamic/decode.gleam", 853). +?DOC( + " A decoder that decodes nullable values of a type decoded by with a given\n" + " decoder.\n" + "\n" + " This function can handle common representations of null on all runtimes, such as\n" + " `nil`, `null`, and `undefined` on Erlang, and `undefined` and `null` on\n" + " JavaScript.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let result = decode.run(dynamic.int(100), decode.optional(decode.int))\n" + " assert result == Ok(option.Some(100))\n" + " ```\n" + "\n" + " ```gleam\n" + " let result = decode.run(dynamic.nil(), decode.optional(decode.int))\n" + " assert result == Ok(option.None)\n" + " ```\n" +). +-spec optional(decoder(BYY)) -> decoder(gleam@option:option(BYY)). +optional(Inner) -> + {decoder, fun(Data) -> case gleam_stdlib:is_null(Data) of + true -> + {none, []}; + + false -> + {Data@1, Errors} = (erlang:element(2, Inner))(Data), + {{some, Data@1}, Errors} + end end}. + +-file("src/gleam/dynamic/decode.gleam", 485). +?DOC(" Construct a decode error for some unexpected dynamic data.\n"). +-spec decode_error(binary(), gleam@dynamic:dynamic_()) -> list(decode_error()). +decode_error(Expected, Found) -> + [{decode_error, Expected, gleam_stdlib:classify_dynamic(Found), []}]. + +-file("src/gleam/dynamic/decode.gleam", 609). +-spec run_dynamic_function( + gleam@dynamic:dynamic_(), + binary(), + fun((gleam@dynamic:dynamic_()) -> {ok, BWZ} | {error, BWZ}) +) -> {BWZ, list(decode_error())}. +run_dynamic_function(Data, Name, F) -> + case F(Data) of + {ok, Data@1} -> + {Data@1, []}; + + {error, Zero} -> + {Zero, + [{decode_error, Name, gleam_stdlib:classify_dynamic(Data), []}]} + end. + +-file("src/gleam/dynamic/decode.gleam", 658). +-spec decode_bool(gleam@dynamic:dynamic_()) -> {boolean(), list(decode_error())}. +decode_bool(Data) -> + case gleam_stdlib:identity(true) =:= Data of + true -> + {true, []}; + + false -> + case gleam_stdlib:identity(false) =:= Data of + true -> + {false, []}; + + false -> + {false, decode_error(<<"Bool"/utf8>>, Data)} + end + end. + +-file("src/gleam/dynamic/decode.gleam", 680). +-spec decode_int(gleam@dynamic:dynamic_()) -> {integer(), list(decode_error())}. +decode_int(Data) -> + run_dynamic_function(Data, <<"Int"/utf8>>, fun gleam_stdlib:int/1). + +-file("src/gleam/dynamic/decode.gleam", 699). +-spec decode_float(gleam@dynamic:dynamic_()) -> {float(), list(decode_error())}. +decode_float(Data) -> + run_dynamic_function(Data, <<"Float"/utf8>>, fun gleam_stdlib:float/1). + +-file("src/gleam/dynamic/decode.gleam", 733). +-spec decode_bit_array(gleam@dynamic:dynamic_()) -> {bitstring(), + list(decode_error())}. +decode_bit_array(Data) -> + run_dynamic_function( + Data, + <<"BitArray"/utf8>>, + fun gleam_stdlib:bit_array/1 + ). + +-file("src/gleam/dynamic/decode.gleam", 908). +?DOC( + " Replace all errors produced by a decoder with one single error for a named\n" + " expected type.\n" + "\n" + " This function may be useful if you wish to simplify errors before\n" + " presenting them to a user, particularly when using the `one_of` function.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = decode.string |> decode.collapse_errors(\"MyThing\")\n" + " let result = decode.run(dynamic.int(1000), decoder)\n" + " assert result == Error([DecodeError(\"MyThing\", \"Int\", [])])\n" + " ```\n" +). +-spec collapse_errors(decoder(BZL), binary()) -> decoder(BZL). +collapse_errors(Decoder, Name) -> + {decoder, + fun(Dynamic_data) -> + {Data, Errors} = Layer = (erlang:element(2, Decoder))(Dynamic_data), + case Errors of + [] -> + Layer; + + [_ | _] -> + {Data, decode_error(Name, Dynamic_data)} + end + end}. + +-file("src/gleam/dynamic/decode.gleam", 986). +?DOC( + " Define a decoder that always fails. The parameter for this function is the\n" + " name of the type that has failed to decode.\n" +). +-spec failure(CAD, binary()) -> decoder(CAD). +failure(Zero, Expected) -> + {decoder, fun(D) -> {Zero, decode_error(Expected, D)} end}. + +-file("src/gleam/dynamic/decode.gleam", 1015). +?DOC( + " Create a decoder for a new data type from a decoding function.\n" + "\n" + " This function is used for new primitive types. For example, you might\n" + " define a decoder for Erlang's pid type.\n" + "\n" + " A default \"zero\" value is also required to make a decoder. When this\n" + " decoder is used as part of a larger decoder this zero value used as\n" + " a placeholder so that the rest of the decoder can continue to run and\n" + " collect all decoding errors.\n" + "\n" + " If you were to make a decoder for the `String` type (rather than using the\n" + " build-in `string` decoder) you would define it like so:\n" + "\n" + " ```gleam\n" + " pub fn string_decoder() -> decode.Decoder(String) {\n" + " let default = \"\"\n" + " decode.new_primitive_decoder(\"String\", fn(data) {\n" + " case dynamic.string(data) {\n" + " Ok(x) -> Ok(x)\n" + " Error(_) -> Error(default)\n" + " }\n" + " })\n" + " }\n" + " ```\n" +). +-spec new_primitive_decoder( + binary(), + fun((gleam@dynamic:dynamic_()) -> {ok, CAF} | {error, CAF}) +) -> decoder(CAF). +new_primitive_decoder(Name, Decoding_function) -> + {decoder, fun(D) -> case Decoding_function(D) of + {ok, T} -> + {T, []}; + + {error, Zero} -> + {Zero, + [{decode_error, + Name, + gleam_stdlib:classify_dynamic(D), + []}]} + end end}. + +-file("src/gleam/dynamic/decode.gleam", 636). +-spec dynamic_string(gleam@dynamic:dynamic_()) -> {ok, binary()} | + {error, binary()}. +dynamic_string(Data) -> + case gleam_stdlib:bit_array(Data) of + {ok, Data@1} -> + case gleam@bit_array:to_string(Data@1) of + {ok, String} -> + {ok, String}; + + {error, _} -> + {error, <<""/utf8>>} + end; + + {error, _} -> + {error, <<""/utf8>>} + end. + +-file("src/gleam/dynamic/decode.gleam", 631). +-spec decode_string(gleam@dynamic:dynamic_()) -> {binary(), + list(decode_error())}. +decode_string(Data) -> + run_dynamic_function(Data, <<"String"/utf8>>, fun dynamic_string/1). + +-file("src/gleam/dynamic/decode.gleam", 807). +-spec fold_dict( + {gleam@dict:dict(BYK, BYL), list(decode_error())}, + gleam@dynamic:dynamic_(), + gleam@dynamic:dynamic_(), + fun((gleam@dynamic:dynamic_()) -> {BYK, list(decode_error())}), + fun((gleam@dynamic:dynamic_()) -> {BYL, list(decode_error())}) +) -> {gleam@dict:dict(BYK, BYL), list(decode_error())}. +fold_dict(Acc, Key, Value, Key_decoder, Value_decoder) -> + case Key_decoder(Key) of + {Key@1, []} -> + case Value_decoder(Value) of + {Value@1, []} -> + Dict = gleam@dict:insert( + erlang:element(1, Acc), + Key@1, + Value@1 + ), + {Dict, erlang:element(2, Acc)}; + + {_, Errors} -> + push_path({maps:new(), Errors}, [<<"values"/utf8>>]) + end; + + {_, Errors@1} -> + push_path({maps:new(), Errors@1}, [<<"keys"/utf8>>]) + end. + +-file("src/gleam/dynamic/decode.gleam", 787). +?DOC( + " A decoder that decodes dicts where all keys and vales are decoded with\n" + " given decoders.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let values = dynamic.properties([\n" + " #(dynamic.string(\"one\"), dynamic.int(1)),\n" + " #(dynamic.string(\"two\"), dynamic.int(2)),\n" + " ])\n" + "\n" + " let result =\n" + " decode.run(values, decode.dict(decode.string, decode.int))\n" + " assert result == Ok(values)\n" + " ```\n" +). +-spec dict(decoder(BYD), decoder(BYF)) -> decoder(gleam@dict:dict(BYD, BYF)). +dict(Key, Value) -> + {decoder, fun(Data) -> case gleam_stdlib:dict(Data) of + {error, _} -> + {maps:new(), decode_error(<<"Dict"/utf8>>, Data)}; + + {ok, Dict} -> + gleam@dict:fold( + Dict, + {maps:new(), []}, + fun(A, K, V) -> case erlang:element(2, A) of + [] -> + fold_dict( + A, + K, + V, + erlang:element(2, Key), + erlang:element(2, Value) + ); + + [_ | _] -> + A + end end + ) + end end}. + +-file("src/gleam/dynamic/decode.gleam", 755). +?DOC( + " A decoder that decodes lists where all elements are decoded with a given\n" + " decoder.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let result =\n" + " [1, 2, 3]\n" + " |> list.map(dynamic.int)\n" + " |> dynamic.list\n" + " |> decode.run(decode.list(of: decode.int))\n" + " assert result == Ok([1, 2, 3])\n" + " ```\n" +). +-spec list(decoder(BXR)) -> decoder(list(BXR)). +list(Inner) -> + {decoder, + fun(Data) -> + gleam_stdlib:list( + Data, + erlang:element(2, Inner), + fun(P, K) -> push_path(P, [K]) end, + 0, + [] + ) + end}. + +-file("src/gleam/dynamic/decode.gleam", 439). +-spec push_path({BWA, list(decode_error())}, list(any())) -> {BWA, + list(decode_error())}. +push_path(Layer, Path) -> + Decoder = one_of( + {decoder, fun decode_string/1}, + [begin + _pipe = {decoder, fun decode_int/1}, + map(_pipe, fun erlang:integer_to_binary/1) + end] + ), + Path@1 = gleam@list:map( + Path, + fun(Key) -> + Key@1 = gleam_stdlib:identity(Key), + case run(Key@1, Decoder) of + {ok, Key@2} -> + Key@2; + + {error, _} -> + <<<<"<"/utf8, + (gleam_stdlib:classify_dynamic(Key@1))/binary>>/binary, + ">"/utf8>> + end + end + ), + Errors = gleam@list:map( + erlang:element(2, Layer), + fun(Error) -> + {decode_error, + erlang:element(2, Error), + erlang:element(3, Error), + lists:append(Path@1, erlang:element(4, Error))} + end + ), + {erlang:element(1, Layer), Errors}. + +-file("src/gleam/dynamic/decode.gleam", 403). +-spec index( + list(BVO), + list(BVO), + fun((gleam@dynamic:dynamic_()) -> {BVR, list(decode_error())}), + gleam@dynamic:dynamic_(), + fun((gleam@dynamic:dynamic_(), list(BVO)) -> {BVR, list(decode_error())}) +) -> {BVR, list(decode_error())}. +index(Path, Position, Inner, Data, Handle_miss) -> + case Path of + [] -> + _pipe = Data, + _pipe@1 = Inner(_pipe), + push_path(_pipe@1, lists:reverse(Position)); + + [Key | Path@1] -> + case gleam_stdlib:index(Data, Key) of + {ok, {some, Data@1}} -> + index(Path@1, [Key | Position], Inner, Data@1, Handle_miss); + + {ok, none} -> + Handle_miss(Data, [Key | Position]); + + {error, Kind} -> + {Default, _} = Inner(Data), + _pipe@2 = {Default, + [{decode_error, + Kind, + gleam_stdlib:classify_dynamic(Data), + []}]}, + push_path(_pipe@2, lists:reverse(Position)) + end + end. + +-file("src/gleam/dynamic/decode.gleam", 324). +?DOC( + " The same as [`field`](#field), except taking a path to the value rather\n" + " than a field name.\n" + "\n" + " This function will index into dictionaries with any key type, and if the key is\n" + " an int then it'll also index into Erlang tuples and JavaScript arrays, and\n" + " the first eight elements of Gleam lists.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"data\"), dynamic.properties([\n" + " #(dynamic.string(\"email\"), dynamic.string(\"lucy@example.com\")),\n" + " #(dynamic.string(\"name\"), dynamic.string(\"Lucy\")),\n" + " ])\n" + " ]))\n" + "\n" + " let decoder = {\n" + " use name <- decode.subfield([\"data\", \"name\"], decode.string)\n" + " use email <- decode.subfield([\"data\", \"email\"], decode.string)\n" + " decode.success(SignUp(name: name, email: email))\n" + " }\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(SignUp(name: \"Lucy\", email: \"lucy@example.com\"))\n" + " ```\n" +). +-spec subfield(list(any()), decoder(BUZ), fun((BUZ) -> decoder(BVB))) -> decoder(BVB). +subfield(Field_path, Field_decoder, Next) -> + {decoder, + fun(Data) -> + {Out, Errors1} = index( + Field_path, + [], + erlang:element(2, Field_decoder), + Data, + fun(Data@1, Position) -> + {Default, _} = (erlang:element(2, Field_decoder))(Data@1), + _pipe = {Default, + [{decode_error, + <<"Field"/utf8>>, + <<"Nothing"/utf8>>, + []}]}, + push_path(_pipe, lists:reverse(Position)) + end + ), + {Out@1, Errors2} = (erlang:element(2, Next(Out)))(Data), + {Out@1, lists:append(Errors1, Errors2)} + end}. + +-file("src/gleam/dynamic/decode.gleam", 393). +?DOC( + " A decoder that decodes a value that is nested within other values. For\n" + " example, decoding a value that is within some deeply nested JSON objects.\n" + "\n" + " This function will index into dictionaries with any key type, and if the key is\n" + " an int then it'll also index into Erlang tuples and JavaScript arrays, and\n" + " the first eight elements of Gleam lists.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = decode.at([\"one\", \"two\"], decode.int)\n" + "\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"one\"), dynamic.properties([\n" + " #(dynamic.string(\"two\"), dynamic.int(1000)),\n" + " ])),\n" + " ]))\n" + "\n" + "\n" + " decode.run(data, decoder)\n" + " // -> Ok(1000)\n" + " ```\n" + "\n" + " ```gleam\n" + " dynamic.nil()\n" + " |> decode.run(decode.optional(decode.int))\n" + " // -> Ok(option.None)\n" + " ```\n" +). +-spec at(list(any()), decoder(BVL)) -> decoder(BVL). +at(Path, Inner) -> + {decoder, + fun(Data) -> + index( + Path, + [], + erlang:element(2, Inner), + Data, + fun(Data@1, Position) -> + {Default, _} = (erlang:element(2, Inner))(Data@1), + _pipe = {Default, + [{decode_error, + <<"Field"/utf8>>, + <<"Nothing"/utf8>>, + []}]}, + push_path(_pipe, lists:reverse(Position)) + end + ) + end}. + +-file("src/gleam/dynamic/decode.gleam", 524). +?DOC( + " Run a decoder on a field of a `Dynamic` value, decoding the value if it is\n" + " of the desired type, or returning errors. An error is returned if there is\n" + " no field for the specified key.\n" + "\n" + " This function will index into dictionaries with any key type, and if the key is\n" + " an int then it'll also index into Erlang tuples and JavaScript arrays, and\n" + " the first eight elements of Gleam lists.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"email\"), dynamic.string(\"lucy@example.com\")),\n" + " #(dynamic.string(\"name\"), dynamic.string(\"Lucy\")),\n" + " ]))\n" + "\n" + " let decoder = {\n" + " use name <- decode.field(\"name\", string)\n" + " use email <- decode.field(\"email\", string)\n" + " decode.success(SignUp(name: name, email: email))\n" + " }\n" + "\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(SignUp(name: \"Lucy\", email: \"lucy@example.com\"))\n" + " ```\n" + "\n" + " If you wish to decode a value that is more deeply nested within the dynamic\n" + " data, see [`subfield`](#subfield) and [`at`](#at).\n" + "\n" + " If you wish to return a default in the event that a field is not present,\n" + " see [`optional_field`](#optional_field) and / [`optionally_at`](#optionally_at).\n" +). +-spec field(any(), decoder(BWJ), fun((BWJ) -> decoder(BWL))) -> decoder(BWL). +field(Field_name, Field_decoder, Next) -> + subfield([Field_name], Field_decoder, Next). + +-file("src/gleam/dynamic/decode.gleam", 557). +?DOC( + " Run a decoder on a field of a `Dynamic` value, decoding the value if it is\n" + " of the desired type, or returning errors. The given default value is\n" + " returned if there is no field for the specified key.\n" + "\n" + " This function will index into dictionaries with any key type, and if the key is\n" + " an int then it'll also index into Erlang tuples and JavaScript arrays, and\n" + " the first eight elements of Gleam lists.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"name\"), dynamic.string(\"Lucy\")),\n" + " ]))\n" + "\n" + " let decoder = {\n" + " use name <- decode.field(\"name\", string)\n" + " use email <- decode.optional_field(\"email\", \"n/a\", string)\n" + " decode.success(SignUp(name: name, email: email))\n" + " }\n" + "\n" + " let result = decode.run(data, decoder)\n" + " assert result == Ok(SignUp(name: \"Lucy\", email: \"n/a\"))\n" + " ```\n" +). +-spec optional_field(any(), BWP, decoder(BWP), fun((BWP) -> decoder(BWR))) -> decoder(BWR). +optional_field(Key, Default, Field_decoder, Next) -> + {decoder, + fun(Data) -> + {Out, Errors1} = begin + _pipe = case gleam_stdlib:index(Data, Key) of + {ok, {some, Data@1}} -> + (erlang:element(2, Field_decoder))(Data@1); + + {ok, none} -> + {Default, []}; + + {error, Kind} -> + {Default, + [{decode_error, + Kind, + gleam_stdlib:classify_dynamic(Data), + []}]} + end, + push_path(_pipe, [Key]) + end, + {Out@1, Errors2} = (erlang:element(2, Next(Out)))(Data), + {Out@1, lists:append(Errors1, Errors2)} + end}. + +-file("src/gleam/dynamic/decode.gleam", 599). +?DOC( + " A decoder that decodes a value that is nested within other values. For\n" + " example, decoding a value that is within some deeply nested JSON objects.\n" + "\n" + " This function will index into dictionaries with any key type, and if the key is\n" + " an int then it'll also index into Erlang tuples and JavaScript arrays, and\n" + " the first eight elements of Gleam lists.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " let decoder = decode.optionally_at([\"one\", \"two\"], 100, decode.int)\n" + "\n" + " let data = dynamic.properties([\n" + " #(dynamic.string(\"one\"), dynamic.properties([])),\n" + " ]))\n" + "\n" + "\n" + " decode.run(data, decoder)\n" + " // -> Ok(100)\n" + " ```\n" +). +-spec optionally_at(list(any()), BWW, decoder(BWW)) -> decoder(BWW). +optionally_at(Path, Default, Inner) -> + {decoder, + fun(Data) -> + index( + Path, + [], + erlang:element(2, Inner), + Data, + fun(_, _) -> {Default, []} end + ) + end}. diff --git a/build/packages/gleam_stdlib/src/gleam@float.erl b/build/packages/gleam_stdlib/src/gleam@float.erl new file mode 100644 index 0000000..6b55b47 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@float.erl @@ -0,0 +1,744 @@ +-module(gleam@float). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/float.gleam"). +-export([parse/1, to_string/1, compare/2, min/2, max/2, clamp/3, ceiling/1, floor/1, truncate/1, absolute_value/1, loosely_compare/3, loosely_equals/3, power/2, square_root/1, negate/1, round/1, to_precision/2, sum/1, product/1, random/0, modulo/2, divide/2, add/2, multiply/2, subtract/2, logarithm/1, exponential/1]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Functions for working with floats.\n" + "\n" + " ## Float representation\n" + "\n" + " Floats are represented as 64 bit floating point numbers on both the Erlang\n" + " and JavaScript runtimes. The floating point behaviour is native to their\n" + " respective runtimes, so their exact behaviour will be slightly different on\n" + " the two runtimes.\n" + "\n" + " ### Infinity and NaN\n" + "\n" + " Under the JavaScript runtime, exceeding the maximum (or minimum)\n" + " representable value for a floating point value will result in Infinity (or\n" + " -Infinity). Should you try to divide two infinities you will get NaN as a\n" + " result.\n" + "\n" + " When running on BEAM, exceeding the maximum (or minimum) representable\n" + " value for a floating point value will raise an error.\n" + "\n" + " ## Division by zero\n" + "\n" + " Gleam runs on the Erlang virtual machine, which does not follow the IEEE\n" + " 754 standard for floating point arithmetic and does not have an `Infinity`\n" + " value. In Erlang division by zero results in a crash, however Gleam does\n" + " not have partial functions and operators in core so instead division by zero\n" + " returns zero, a behaviour taken from Pony, Coq, and Lean.\n" + "\n" + " This may seem unexpected at first, but it is no less mathematically valid\n" + " than crashing or returning a special value. Division by zero is undefined\n" + " in mathematics.\n" +). + +-file("src/gleam/float.gleam", 51). +?DOC( + " Attempts to parse a string as a `Float`, returning `Error(Nil)` if it was\n" + " not possible.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " parse(\"2.3\")\n" + " // -> Ok(2.3)\n" + " ```\n" + "\n" + " ```gleam\n" + " parse(\"ABC\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec parse(binary()) -> {ok, float()} | {error, nil}. +parse(String) -> + gleam_stdlib:parse_float(String). + +-file("src/gleam/float.gleam", 64). +?DOC( + " Returns the string representation of the provided `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_string(2.3)\n" + " // -> \"2.3\"\n" + " ```\n" +). +-spec to_string(float()) -> binary(). +to_string(X) -> + gleam_stdlib:float_to_string(X). + +-file("src/gleam/float.gleam", 95). +?DOC( + " Compares two `Float`s, returning an `Order`:\n" + " `Lt` for lower than, `Eq` for equals, or `Gt` for greater than.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " compare(2.0, 2.3)\n" + " // -> Lt\n" + " ```\n" + "\n" + " To handle\n" + " [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems)\n" + " you may use [`loosely_compare`](#loosely_compare) instead.\n" +). +-spec compare(float(), float()) -> gleam@order:order(). +compare(A, B) -> + case A =:= B of + true -> + eq; + + false -> + case A < B of + true -> + lt; + + false -> + gt + end + end. + +-file("src/gleam/float.gleam", 176). +?DOC( + " Compares two `Float`s, returning the smaller of the two.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " min(2.0, 2.3)\n" + " // -> 2.0\n" + " ```\n" +). +-spec min(float(), float()) -> float(). +min(A, B) -> + case A < B of + true -> + A; + + false -> + B + end. + +-file("src/gleam/float.gleam", 192). +?DOC( + " Compares two `Float`s, returning the larger of the two.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " max(2.0, 2.3)\n" + " // -> 2.3\n" + " ```\n" +). +-spec max(float(), float()) -> float(). +max(A, B) -> + case A > B of + true -> + A; + + false -> + B + end. + +-file("src/gleam/float.gleam", 75). +?DOC( + " Restricts a `Float` between a lower and upper bound.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " clamp(1.2, min: 1.4, max: 1.6)\n" + " // -> 1.4\n" + " ```\n" +). +-spec clamp(float(), float(), float()) -> float(). +clamp(X, Min_bound, Max_bound) -> + _pipe = X, + _pipe@1 = min(_pipe, Max_bound), + max(_pipe@1, Min_bound). + +-file("src/gleam/float.gleam", 210). +?DOC( + " Rounds the value to the next highest whole number as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " ceiling(2.3)\n" + " // -> 3.0\n" + " ```\n" +). +-spec ceiling(float()) -> float(). +ceiling(X) -> + math:ceil(X). + +-file("src/gleam/float.gleam", 223). +?DOC( + " Rounds the value to the next lowest whole number as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " floor(2.3)\n" + " // -> 2.0\n" + " ```\n" +). +-spec floor(float()) -> float(). +floor(X) -> + math:floor(X). + +-file("src/gleam/float.gleam", 261). +?DOC( + " Returns the value as an `Int`, truncating all decimal digits.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " truncate(2.4343434847383438)\n" + " // -> 2\n" + " ```\n" +). +-spec truncate(float()) -> integer(). +truncate(X) -> + erlang:trunc(X). + +-file("src/gleam/float.gleam", 311). +?DOC( + " Returns the absolute value of the input as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " absolute_value(-12.5)\n" + " // -> 12.5\n" + " ```\n" + "\n" + " ```gleam\n" + " absolute_value(10.2)\n" + " // -> 10.2\n" + " ```\n" +). +-spec absolute_value(float()) -> float(). +absolute_value(X) -> + case X >= +0.0 of + true -> + X; + + false -> + +0.0 - X + end. + +-file("src/gleam/float.gleam", 125). +?DOC( + " Compares two `Float`s within a tolerance, returning an `Order`:\n" + " `Lt` for lower than, `Eq` for equals, or `Gt` for greater than.\n" + "\n" + " This function allows Float comparison while handling\n" + " [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems).\n" + "\n" + " Notice: For `Float`s the tolerance won't be exact:\n" + " `5.3 - 5.0` is not exactly `0.3`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " loosely_compare(5.0, with: 5.3, tolerating: 0.5)\n" + " // -> Eq\n" + " ```\n" + "\n" + " If you want to check only for equality you may use\n" + " [`loosely_equals`](#loosely_equals) instead.\n" +). +-spec loosely_compare(float(), float(), float()) -> gleam@order:order(). +loosely_compare(A, B, Tolerance) -> + Difference = absolute_value(A - B), + case Difference =< Tolerance of + true -> + eq; + + false -> + compare(A, B) + end. + +-file("src/gleam/float.gleam", 158). +?DOC( + " Checks for equality of two `Float`s within a tolerance,\n" + " returning an `Bool`.\n" + "\n" + " This function allows Float comparison while handling\n" + " [Floating Point Imprecision](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems).\n" + "\n" + " Notice: For `Float`s the tolerance won't be exact:\n" + " `5.3 - 5.0` is not exactly `0.3`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " loosely_equals(5.0, with: 5.3, tolerating: 0.5)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " loosely_equals(5.0, with: 5.1, tolerating: 0.1)\n" + " // -> False\n" + " ```\n" +). +-spec loosely_equals(float(), float(), float()) -> boolean(). +loosely_equals(A, B, Tolerance) -> + Difference = absolute_value(A - B), + Difference =< Tolerance. + +-file("src/gleam/float.gleam", 348). +?DOC( + " Returns the results of the base being raised to the power of the\n" + " exponent, as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " power(2.0, -1.0)\n" + " // -> Ok(0.5)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(2.0, 2.0)\n" + " // -> Ok(4.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(8.0, 1.5)\n" + " // -> Ok(22.627416997969522)\n" + " ```\n" + "\n" + " ```gleam\n" + " 4.0 |> power(of: 2.0)\n" + " // -> Ok(16.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(-1.0, 0.5)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec power(float(), float()) -> {ok, float()} | {error, nil}. +power(Base, Exponent) -> + Fractional = (math:ceil(Exponent) - Exponent) > +0.0, + case ((Base < +0.0) andalso Fractional) orelse ((Base =:= +0.0) andalso (Exponent + < +0.0)) of + true -> + {error, nil}; + + false -> + {ok, math:pow(Base, Exponent)} + end. + +-file("src/gleam/float.gleam", 380). +?DOC( + " Returns the square root of the input as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " square_root(4.0)\n" + " // -> Ok(2.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " square_root(-16.0)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec square_root(float()) -> {ok, float()} | {error, nil}. +square_root(X) -> + power(X, 0.5). + +-file("src/gleam/float.gleam", 393). +?DOC( + " Returns the negative of the value provided.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " negate(1.0)\n" + " // -> -1.0\n" + " ```\n" +). +-spec negate(float()) -> float(). +negate(X) -> + -1.0 * X. + +-file("src/gleam/float.gleam", 240). +?DOC( + " Rounds the value to the nearest whole number as an `Int`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " round(2.3)\n" + " // -> 2\n" + " ```\n" + "\n" + " ```gleam\n" + " round(2.5)\n" + " // -> 3\n" + " ```\n" +). +-spec round(float()) -> integer(). +round(X) -> + erlang:round(X). + +-file("src/gleam/float.gleam", 280). +?DOC( + " Converts the value to a given precision as a `Float`.\n" + " The precision is the number of allowed decimal places.\n" + " Negative precisions are allowed and force rounding\n" + " to the nearest tenth, hundredth, thousandth etc.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_precision(2.43434348473, precision: 2)\n" + " // -> 2.43\n" + " ```\n" + "\n" + " ```gleam\n" + " to_precision(547890.453444, precision: -3)\n" + " // -> 548000.0\n" + " ```\n" +). +-spec to_precision(float(), integer()) -> float(). +to_precision(X, Precision) -> + case Precision =< 0 of + true -> + Factor = math:pow(10.0, erlang:float(- Precision)), + erlang:float(erlang:round(case Factor of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> X / Gleam@denominator + end)) * Factor; + + false -> + Factor@1 = math:pow(10.0, erlang:float(Precision)), + case Factor@1 of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator@1 -> erlang:float(erlang:round(X * Factor@1)) + / Gleam@denominator@1 + end + end. + +-file("src/gleam/float.gleam", 410). +-spec sum_loop(list(float()), float()) -> float(). +sum_loop(Numbers, Initial) -> + case Numbers of + [First | Rest] -> + sum_loop(Rest, First + Initial); + + [] -> + Initial + end. + +-file("src/gleam/float.gleam", 406). +?DOC( + " Sums a list of `Float`s.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " sum([1.0, 2.2, 3.3])\n" + " // -> 6.5\n" + " ```\n" +). +-spec sum(list(float())) -> float(). +sum(Numbers) -> + sum_loop(Numbers, +0.0). + +-file("src/gleam/float.gleam", 430). +-spec product_loop(list(float()), float()) -> float(). +product_loop(Numbers, Initial) -> + case Numbers of + [First | Rest] -> + product_loop(Rest, First * Initial); + + [] -> + Initial + end. + +-file("src/gleam/float.gleam", 426). +?DOC( + " Multiplies a list of `Float`s and returns the product.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " product([2.5, 3.2, 4.2])\n" + " // -> 33.6\n" + " ```\n" +). +-spec product(list(float())) -> float(). +product(Numbers) -> + product_loop(Numbers, 1.0). + +-file("src/gleam/float.gleam", 452). +?DOC( + " Generates a random float between the given zero (inclusive) and one\n" + " (exclusive).\n" + "\n" + " On Erlang this updates the random state in the process dictionary.\n" + " See: \n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " random()\n" + " // -> 0.646355926896028\n" + " ```\n" +). +-spec random() -> float(). +random() -> + rand:uniform(). + +-file("src/gleam/float.gleam", 481). +?DOC( + " Computes the modulo of an float division of inputs as a `Result`.\n" + "\n" + " Returns division of the inputs as a `Result`: If the given divisor equals\n" + " `0`, this function returns an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " modulo(13.3, by: 3.3)\n" + " // -> Ok(0.1)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(-13.3, by: 3.3)\n" + " // -> Ok(3.2)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(13.3, by: -3.3)\n" + " // -> Ok(-3.2)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(-13.3, by: -3.3)\n" + " // -> Ok(-0.1)\n" + " ```\n" +). +-spec modulo(float(), float()) -> {ok, float()} | {error, nil}. +modulo(Dividend, Divisor) -> + case Divisor of + +0.0 -> + {error, nil}; + + _ -> + {ok, Dividend - (math:floor(case Divisor of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> Dividend / Gleam@denominator + end) * Divisor)} + end. + +-file("src/gleam/float.gleam", 502). +?DOC( + " Returns division of the inputs as a `Result`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " divide(0.0, 1.0)\n" + " // -> Ok(0.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " divide(1.0, 0.0)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec divide(float(), float()) -> {ok, float()} | {error, nil}. +divide(A, B) -> + case B of + +0.0 -> + {error, nil}; + + B@1 -> + {ok, case B@1 of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> A / Gleam@denominator + end} + end. + +-file("src/gleam/float.gleam", 533). +?DOC( + " Adds two floats together.\n" + "\n" + " It's the function equivalent of the `+.` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " add(1.0, 2.0)\n" + " // -> 3.0\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + "\n" + " list.fold([1.0, 2.0, 3.0], 0.0, add)\n" + " // -> 6.0\n" + " ```\n" + "\n" + " ```gleam\n" + " 3.0 |> add(2.0)\n" + " // -> 5.0\n" + " ```\n" +). +-spec add(float(), float()) -> float(). +add(A, B) -> + A + B. + +-file("src/gleam/float.gleam", 561). +?DOC( + " Multiplies two floats together.\n" + "\n" + " It's the function equivalent of the `*.` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " multiply(2.0, 4.0)\n" + " // -> 8.0\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + "\n" + " list.fold([2.0, 3.0, 4.0], 1.0, multiply)\n" + " // -> 24.0\n" + " ```\n" + "\n" + " ```gleam\n" + " 3.0 |> multiply(2.0)\n" + " // -> 6.0\n" + " ```\n" +). +-spec multiply(float(), float()) -> float(). +multiply(A, B) -> + A * B. + +-file("src/gleam/float.gleam", 594). +?DOC( + " Subtracts one float from another.\n" + "\n" + " It's the function equivalent of the `-.` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " subtract(3.0, 1.0)\n" + " // -> 2.0\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + "\n" + " list.fold([1.0, 2.0, 3.0], 10.0, subtract)\n" + " // -> 4.0\n" + " ```\n" + "\n" + " ```gleam\n" + " 3.0 |> subtract(_, 2.0)\n" + " // -> 1.0\n" + " ```\n" + "\n" + " ```gleam\n" + " 3.0 |> subtract(2.0, _)\n" + " // -> -1.0\n" + " ```\n" +). +-spec subtract(float(), float()) -> float(). +subtract(A, B) -> + A - B. + +-file("src/gleam/float.gleam", 623). +?DOC( + " Returns the natural logarithm (base e) of the given as a `Result`. If the\n" + " input is less than or equal to 0, returns `Error(Nil)`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " logarithm(1.0)\n" + " // -> Ok(0.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " logarithm(2.718281828459045) // e\n" + " // -> Ok(1.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " logarithm(0.0)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " logarithm(-1.0)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec logarithm(float()) -> {ok, float()} | {error, nil}. +logarithm(X) -> + case X =< +0.0 of + true -> + {error, nil}; + + false -> + {ok, math:log(X)} + end. + +-file("src/gleam/float.gleam", 661). +?DOC( + " Returns e (Euler's number) raised to the power of the given exponent, as\n" + " a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " exponential(0.0)\n" + " // -> Ok(1.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " exponential(1.0)\n" + " // -> Ok(2.718281828459045)\n" + " ```\n" + "\n" + " ```gleam\n" + " exponential(-1.0)\n" + " // -> Ok(0.36787944117144233)\n" + " ```\n" +). +-spec exponential(float()) -> float(). +exponential(X) -> + math:exp(X). diff --git a/build/packages/gleam_stdlib/src/gleam@function.erl b/build/packages/gleam_stdlib/src/gleam@function.erl new file mode 100644 index 0000000..a6dec81 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@function.erl @@ -0,0 +1,30 @@ +-module(gleam@function). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/function.gleam"). +-export([identity/1, tap/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-file("src/gleam/function.gleam", 3). +?DOC(" Takes a single argument and always returns its input value.\n"). +-spec identity(CLA) -> CLA. +identity(X) -> + X. + +-file("src/gleam/function.gleam", 12). +?DOC( + " Takes an argument and a single function, calls that function with that\n" + " argument and returns that argument instead of the function return value.\n" + "\n" + " Useful for running synchronous side effects in a pipeline.\n" +). +-spec tap(CLB, fun((CLB) -> any())) -> CLB. +tap(Arg, Effect) -> + Effect(Arg), + Arg. diff --git a/build/packages/gleam_stdlib/src/gleam@int.erl b/build/packages/gleam_stdlib/src/gleam@int.erl new file mode 100644 index 0000000..5e298da --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@int.erl @@ -0,0 +1,986 @@ +-module(gleam@int). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/int.gleam"). +-export([absolute_value/1, parse/1, base_parse/2, to_string/1, to_base_string/2, to_base2/1, to_base8/1, to_base16/1, to_base36/1, to_float/1, power/2, square_root/1, compare/2, min/2, max/2, clamp/3, is_even/1, is_odd/1, negate/1, sum/1, product/1, digits/2, undigits/2, random/1, divide/2, remainder/2, modulo/2, floor_divide/2, add/2, multiply/2, subtract/2, bitwise_and/2, bitwise_not/1, bitwise_or/2, bitwise_exclusive_or/2, bitwise_shift_left/2, bitwise_shift_right/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Functions for working with integers.\n" + "\n" + " ## Division by zero\n" + "\n" + " In Erlang division by zero results in a crash, however Gleam does not have\n" + " partial functions and operators in core so instead division by zero returns\n" + " zero, a behaviour taken from Pony, Coq, and Lean.\n" + "\n" + " This may seem unexpected at first, but it is no less mathematically valid\n" + " than crashing or returning a special value. Division by zero is undefined\n" + " in mathematics.\n" +). + +-file("src/gleam/int.gleam", 30). +?DOC( + " Returns the absolute value of the input.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " absolute_value(-12)\n" + " // -> 12\n" + " ```\n" + "\n" + " ```gleam\n" + " absolute_value(10)\n" + " // -> 10\n" + " ```\n" +). +-spec absolute_value(integer()) -> integer(). +absolute_value(X) -> + case X >= 0 of + true -> + X; + + false -> + X * -1 + end. + +-file("src/gleam/int.gleam", 109). +?DOC( + " Parses a given string as an int if possible.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " parse(\"2\")\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " parse(\"ABC\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec parse(binary()) -> {ok, integer()} | {error, nil}. +parse(String) -> + gleam_stdlib:parse_int(String). + +-file("src/gleam/int.gleam", 141). +?DOC( + " Parses a given string as an int in a given base if possible.\n" + " Supports only bases 2 to 36, for values outside of which this function returns an `Error(Nil)`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " base_parse(\"10\", 2)\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " base_parse(\"30\", 16)\n" + " // -> Ok(48)\n" + " ```\n" + "\n" + " ```gleam\n" + " base_parse(\"1C\", 36)\n" + " // -> Ok(48)\n" + " ```\n" + "\n" + " ```gleam\n" + " base_parse(\"48\", 1)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " base_parse(\"48\", 37)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec base_parse(binary(), integer()) -> {ok, integer()} | {error, nil}. +base_parse(String, Base) -> + case (Base >= 2) andalso (Base =< 36) of + true -> + gleam_stdlib:int_from_base_string(String, Base); + + false -> + {error, nil} + end. + +-file("src/gleam/int.gleam", 163). +?DOC( + " Prints a given int to a string.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_string(2)\n" + " // -> \"2\"\n" + " ```\n" +). +-spec to_string(integer()) -> binary(). +to_string(X) -> + erlang:integer_to_binary(X). + +-file("src/gleam/int.gleam", 196). +?DOC( + " Prints a given int to a string using the base number provided.\n" + " Supports only bases 2 to 36, for values outside of which this function returns an `Error(Nil)`.\n" + " For common bases (2, 8, 16, 36), use the `to_baseN` functions.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_base_string(2, 2)\n" + " // -> Ok(\"10\")\n" + " ```\n" + "\n" + " ```gleam\n" + " to_base_string(48, 16)\n" + " // -> Ok(\"30\")\n" + " ```\n" + "\n" + " ```gleam\n" + " to_base_string(48, 36)\n" + " // -> Ok(\"1C\")\n" + " ```\n" + "\n" + " ```gleam\n" + " to_base_string(48, 1)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " to_base_string(48, 37)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec to_base_string(integer(), integer()) -> {ok, binary()} | {error, nil}. +to_base_string(X, Base) -> + case (Base >= 2) andalso (Base =< 36) of + true -> + {ok, erlang:integer_to_binary(X, Base)}; + + false -> + {error, nil} + end. + +-file("src/gleam/int.gleam", 216). +?DOC( + " Prints a given int to a string using base-2.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_base2(2)\n" + " // -> \"10\"\n" + " ```\n" +). +-spec to_base2(integer()) -> binary(). +to_base2(X) -> + erlang:integer_to_binary(X, 2). + +-file("src/gleam/int.gleam", 229). +?DOC( + " Prints a given int to a string using base-8.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_base8(15)\n" + " // -> \"17\"\n" + " ```\n" +). +-spec to_base8(integer()) -> binary(). +to_base8(X) -> + erlang:integer_to_binary(X, 8). + +-file("src/gleam/int.gleam", 242). +?DOC( + " Prints a given int to a string using base-16.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_base16(48)\n" + " // -> \"30\"\n" + " ```\n" +). +-spec to_base16(integer()) -> binary(). +to_base16(X) -> + erlang:integer_to_binary(X, 16). + +-file("src/gleam/int.gleam", 255). +?DOC( + " Prints a given int to a string using base-36.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_base36(48)\n" + " // -> \"1C\"\n" + " ```\n" +). +-spec to_base36(integer()) -> binary(). +to_base36(X) -> + erlang:integer_to_binary(X, 36). + +-file("src/gleam/int.gleam", 280). +?DOC( + " Takes an int and returns its value as a float.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_float(5)\n" + " // -> 5.0\n" + " ```\n" + "\n" + " ```gleam\n" + " to_float(0)\n" + " // -> 0.0\n" + " ```\n" + "\n" + " ```gleam\n" + " to_float(-3)\n" + " // -> -3.0\n" + " ```\n" +). +-spec to_float(integer()) -> float(). +to_float(X) -> + erlang:float(X). + +-file("src/gleam/int.gleam", 67). +?DOC( + " Returns the results of the base being raised to the power of the\n" + " exponent, as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " power(2, -1.0)\n" + " // -> Ok(0.5)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(2, 2.0)\n" + " // -> Ok(4.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(8, 1.5)\n" + " // -> Ok(22.627416997969522)\n" + " ```\n" + "\n" + " ```gleam\n" + " 4 |> power(of: 2.0)\n" + " // -> Ok(16.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " power(-1, 0.5)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec power(integer(), float()) -> {ok, float()} | {error, nil}. +power(Base, Exponent) -> + _pipe = Base, + _pipe@1 = erlang:float(_pipe), + gleam@float:power(_pipe@1, Exponent). + +-file("src/gleam/int.gleam", 87). +?DOC( + " Returns the square root of the input as a `Float`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " square_root(4)\n" + " // -> Ok(2.0)\n" + " ```\n" + "\n" + " ```gleam\n" + " square_root(-16)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec square_root(integer()) -> {ok, float()} | {error, nil}. +square_root(X) -> + _pipe = X, + _pipe@1 = erlang:float(_pipe), + gleam@float:square_root(_pipe@1). + +-file("src/gleam/int.gleam", 316). +?DOC( + " Compares two ints, returning an order.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " compare(2, 3)\n" + " // -> Lt\n" + " ```\n" + "\n" + " ```gleam\n" + " compare(4, 3)\n" + " // -> Gt\n" + " ```\n" + "\n" + " ```gleam\n" + " compare(3, 3)\n" + " // -> Eq\n" + " ```\n" +). +-spec compare(integer(), integer()) -> gleam@order:order(). +compare(A, B) -> + case A =:= B of + true -> + eq; + + false -> + case A < B of + true -> + lt; + + false -> + gt + end + end. + +-file("src/gleam/int.gleam", 336). +?DOC( + " Compares two ints, returning the smaller of the two.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " min(2, 3)\n" + " // -> 2\n" + " ```\n" +). +-spec min(integer(), integer()) -> integer(). +min(A, B) -> + case A < B of + true -> + A; + + false -> + B + end. + +-file("src/gleam/int.gleam", 352). +?DOC( + " Compares two ints, returning the larger of the two.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " max(2, 3)\n" + " // -> 3\n" + " ```\n" +). +-spec max(integer(), integer()) -> integer(). +max(A, B) -> + case A > B of + true -> + A; + + false -> + B + end. + +-file("src/gleam/int.gleam", 291). +?DOC( + " Restricts an int between a lower and upper bound.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " clamp(40, min: 50, max: 60)\n" + " // -> 50\n" + " ```\n" +). +-spec clamp(integer(), integer(), integer()) -> integer(). +clamp(X, Min_bound, Max_bound) -> + _pipe = X, + _pipe@1 = min(_pipe, Max_bound), + max(_pipe@1, Min_bound). + +-file("src/gleam/int.gleam", 373). +?DOC( + " Returns whether the value provided is even.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_even(2)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_even(3)\n" + " // -> False\n" + " ```\n" +). +-spec is_even(integer()) -> boolean(). +is_even(X) -> + (X rem 2) =:= 0. + +-file("src/gleam/int.gleam", 391). +?DOC( + " Returns whether the value provided is odd.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_odd(3)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_odd(2)\n" + " // -> False\n" + " ```\n" +). +-spec is_odd(integer()) -> boolean(). +is_odd(X) -> + (X rem 2) /= 0. + +-file("src/gleam/int.gleam", 404). +?DOC( + " Returns the negative of the value provided.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " negate(1)\n" + " // -> -1\n" + " ```\n" +). +-spec negate(integer()) -> integer(). +negate(X) -> + -1 * X. + +-file("src/gleam/int.gleam", 421). +-spec sum_loop(list(integer()), integer()) -> integer(). +sum_loop(Numbers, Initial) -> + case Numbers of + [First | Rest] -> + sum_loop(Rest, First + Initial); + + [] -> + Initial + end. + +-file("src/gleam/int.gleam", 417). +?DOC( + " Sums a list of ints.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " sum([1, 2, 3])\n" + " // -> 6\n" + " ```\n" +). +-spec sum(list(integer())) -> integer(). +sum(Numbers) -> + sum_loop(Numbers, 0). + +-file("src/gleam/int.gleam", 441). +-spec product_loop(list(integer()), integer()) -> integer(). +product_loop(Numbers, Initial) -> + case Numbers of + [First | Rest] -> + product_loop(Rest, First * Initial); + + [] -> + Initial + end. + +-file("src/gleam/int.gleam", 437). +?DOC( + " Multiplies a list of ints and returns the product.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " product([2, 3, 4])\n" + " // -> 24\n" + " ```\n" +). +-spec product(list(integer())) -> integer(). +product(Numbers) -> + product_loop(Numbers, 1). + +-file("src/gleam/int.gleam", 456). +-spec digits_loop(integer(), integer(), list(integer())) -> list(integer()). +digits_loop(X, Base, Acc) -> + case absolute_value(X) < Base of + true -> + [X | Acc]; + + false -> + digits_loop(case Base of + 0 -> 0; + Gleam@denominator -> X div Gleam@denominator + end, Base, [case Base of + 0 -> 0; + Gleam@denominator@1 -> X rem Gleam@denominator@1 + end | Acc]) + end. + +-file("src/gleam/int.gleam", 449). +-spec digits(integer(), integer()) -> {ok, list(integer())} | {error, nil}. +digits(X, Base) -> + case Base < 2 of + true -> + {error, nil}; + + false -> + {ok, digits_loop(X, Base, [])} + end. + +-file("src/gleam/int.gleam", 471). +-spec undigits_loop(list(integer()), integer(), integer()) -> {ok, integer()} | + {error, nil}. +undigits_loop(Numbers, Base, Acc) -> + case Numbers of + [] -> + {ok, Acc}; + + [Digit | _] when Digit >= Base -> + {error, nil}; + + [Digit@1 | Rest] -> + undigits_loop(Rest, Base, (Acc * Base) + Digit@1) + end. + +-file("src/gleam/int.gleam", 464). +-spec undigits(list(integer()), integer()) -> {ok, integer()} | {error, nil}. +undigits(Numbers, Base) -> + case Base < 2 of + true -> + {error, nil}; + + false -> + undigits_loop(Numbers, Base, 0) + end. + +-file("src/gleam/int.gleam", 500). +?DOC( + " Generates a random int between zero and the given maximum.\n" + "\n" + " The lower number is inclusive, the upper number is exclusive.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " random(10)\n" + " // -> 4\n" + " ```\n" + "\n" + " ```gleam\n" + " random(1)\n" + " // -> 0\n" + " ```\n" + "\n" + " ```gleam\n" + " random(-1)\n" + " // -> -1\n" + " ```\n" +). +-spec random(integer()) -> integer(). +random(Max) -> + _pipe = (rand:uniform() * erlang:float(Max)), + _pipe@1 = math:floor(_pipe), + erlang:round(_pipe@1). + +-file("src/gleam/int.gleam", 533). +?DOC( + " Performs a truncated integer division.\n" + "\n" + " Returns division of the inputs as a `Result`: If the given divisor equals\n" + " `0`, this function returns an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " divide(0, 1)\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " divide(1, 0)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " divide(5, 2)\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " divide(-99, 2)\n" + " // -> Ok(-49)\n" + " ```\n" +). +-spec divide(integer(), integer()) -> {ok, integer()} | {error, nil}. +divide(Dividend, Divisor) -> + case Divisor of + 0 -> + {error, nil}; + + Divisor@1 -> + {ok, case Divisor@1 of + 0 -> 0; + Gleam@denominator -> Dividend div Gleam@denominator + end} + end. + +-file("src/gleam/int.gleam", 585). +?DOC( + " Computes the remainder of an integer division of inputs as a `Result`.\n" + "\n" + " Returns division of the inputs as a `Result`: If the given divisor equals\n" + " `0`, this function returns an `Error`.\n" + "\n" + " Most the time you will want to use the `%` operator instead of this\n" + " function.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " remainder(3, 2)\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(1, 0)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(10, -1)\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(13, by: 3)\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(-13, by: 3)\n" + " // -> Ok(-1)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(13, by: -3)\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " remainder(-13, by: -3)\n" + " // -> Ok(-1)\n" + " ```\n" +). +-spec remainder(integer(), integer()) -> {ok, integer()} | {error, nil}. +remainder(Dividend, Divisor) -> + case Divisor of + 0 -> + {error, nil}; + + Divisor@1 -> + {ok, case Divisor@1 of + 0 -> 0; + Gleam@denominator -> Dividend rem Gleam@denominator + end} + end. + +-file("src/gleam/int.gleam", 627). +?DOC( + " Computes the modulo of an integer division of inputs as a `Result`.\n" + "\n" + " Returns division of the inputs as a `Result`: If the given divisor equals\n" + " `0`, this function returns an `Error`.\n" + "\n" + " Most the time you will want to use the `%` operator instead of this\n" + " function.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " modulo(3, 2)\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(1, 0)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(10, -1)\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(13, by: 3)\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " modulo(-13, by: 3)\n" + " // -> Ok(2)\n" + " ```\n" +). +-spec modulo(integer(), integer()) -> {ok, integer()} | {error, nil}. +modulo(Dividend, Divisor) -> + case Divisor of + 0 -> + {error, nil}; + + _ -> + Remainder = case Divisor of + 0 -> 0; + Gleam@denominator -> Dividend rem Gleam@denominator + end, + case (Remainder * Divisor) < 0 of + true -> + {ok, Remainder + Divisor}; + + false -> + {ok, Remainder} + end + end. + +-file("src/gleam/int.gleam", 671). +?DOC( + " Performs a *floored* integer division, which means that the result will\n" + " always be rounded towards negative infinity.\n" + "\n" + " If you want to perform truncated integer division (rounding towards zero),\n" + " use `int.divide()` or the `/` operator instead.\n" + "\n" + " Returns division of the inputs as a `Result`: If the given divisor equals\n" + " `0`, this function returns an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " floor_divide(1, 0)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " floor_divide(5, 2)\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " floor_divide(6, -4)\n" + " // -> Ok(-2)\n" + " ```\n" + "\n" + " ```gleam\n" + " floor_divide(-99, 2)\n" + " // -> Ok(-50)\n" + " ```\n" +). +-spec floor_divide(integer(), integer()) -> {ok, integer()} | {error, nil}. +floor_divide(Dividend, Divisor) -> + case Divisor of + 0 -> + {error, nil}; + + Divisor@1 -> + case ((Dividend * Divisor@1) < 0) andalso ((case Divisor@1 of + 0 -> 0; + Gleam@denominator -> Dividend rem Gleam@denominator + end) /= 0) of + true -> + {ok, (case Divisor@1 of + 0 -> 0; + Gleam@denominator@1 -> Dividend div Gleam@denominator@1 + end) - 1}; + + false -> + {ok, case Divisor@1 of + 0 -> 0; + Gleam@denominator@2 -> Dividend div Gleam@denominator@2 + end} + end + end. + +-file("src/gleam/int.gleam", 705). +?DOC( + " Adds two integers together.\n" + "\n" + " It's the function equivalent of the `+` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " add(1, 2)\n" + " // -> 3\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + " list.fold([1, 2, 3], 0, add)\n" + " // -> 6\n" + " ```\n" + "\n" + " ```gleam\n" + " 3 |> add(2)\n" + " // -> 5\n" + " ```\n" +). +-spec add(integer(), integer()) -> integer(). +add(A, B) -> + A + B. + +-file("src/gleam/int.gleam", 733). +?DOC( + " Multiplies two integers together.\n" + "\n" + " It's the function equivalent of the `*` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " multiply(2, 4)\n" + " // -> 8\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + "\n" + " list.fold([2, 3, 4], 1, multiply)\n" + " // -> 24\n" + " ```\n" + "\n" + " ```gleam\n" + " 3 |> multiply(2)\n" + " // -> 6\n" + " ```\n" +). +-spec multiply(integer(), integer()) -> integer(). +multiply(A, B) -> + A * B. + +-file("src/gleam/int.gleam", 766). +?DOC( + " Subtracts one int from another.\n" + "\n" + " It's the function equivalent of the `-` operator.\n" + " This function is useful in higher order functions or pipes.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " subtract(3, 1)\n" + " // -> 2\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/list\n" + "\n" + " list.fold([1, 2, 3], 10, subtract)\n" + " // -> 4\n" + " ```\n" + "\n" + " ```gleam\n" + " 3 |> subtract(2)\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " 3 |> subtract(2, _)\n" + " // -> -1\n" + " ```\n" +). +-spec subtract(integer(), integer()) -> integer(). +subtract(A, B) -> + A - B. + +-file("src/gleam/int.gleam", 778). +?DOC( + " Calculates the bitwise AND of its arguments.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_and(integer(), integer()) -> integer(). +bitwise_and(X, Y) -> + erlang:'band'(X, Y). + +-file("src/gleam/int.gleam", 788). +?DOC( + " Calculates the bitwise NOT of its argument.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_not(integer()) -> integer(). +bitwise_not(X) -> + erlang:'bnot'(X). + +-file("src/gleam/int.gleam", 798). +?DOC( + " Calculates the bitwise OR of its arguments.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_or(integer(), integer()) -> integer(). +bitwise_or(X, Y) -> + erlang:'bor'(X, Y). + +-file("src/gleam/int.gleam", 808). +?DOC( + " Calculates the bitwise XOR of its arguments.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_exclusive_or(integer(), integer()) -> integer(). +bitwise_exclusive_or(X, Y) -> + erlang:'bxor'(X, Y). + +-file("src/gleam/int.gleam", 818). +?DOC( + " Calculates the result of an arithmetic left bitshift.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_shift_left(integer(), integer()) -> integer(). +bitwise_shift_left(X, Y) -> + erlang:'bsl'(X, Y). + +-file("src/gleam/int.gleam", 828). +?DOC( + " Calculates the result of an arithmetic right bitshift.\n" + "\n" + " The exact behaviour of this function depends on the target platform.\n" + " On Erlang it is equivalent to bitwise operations on ints, on JavaScript it\n" + " is equivalent to bitwise operations on big-ints.\n" +). +-spec bitwise_shift_right(integer(), integer()) -> integer(). +bitwise_shift_right(X, Y) -> + erlang:'bsr'(X, Y). diff --git a/build/packages/gleam_stdlib/src/gleam@io.erl b/build/packages/gleam_stdlib/src/gleam@io.erl new file mode 100644 index 0000000..e60295e --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@io.erl @@ -0,0 +1,80 @@ +-module(gleam@io). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/io.gleam"). +-export([print/1, print_error/1, println/1, println_error/1]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-file("src/gleam/io.gleam", 15). +?DOC( + " Writes a string to standard output (stdout).\n" + "\n" + " If you want your output to be printed on its own line see `println`.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " io.print(\"Hi mum\")\n" + " // -> Nil\n" + " // Hi mum\n" + " ```\n" +). +-spec print(binary()) -> nil. +print(String) -> + gleam_stdlib:print(String). + +-file("src/gleam/io.gleam", 31). +?DOC( + " Writes a string to standard error (stderr).\n" + "\n" + " If you want your output to be printed on its own line see `println_error`.\n" + "\n" + " ## Example\n" + "\n" + " ```\n" + " io.print_error(\"Hi pop\")\n" + " // -> Nil\n" + " // Hi pop\n" + " ```\n" +). +-spec print_error(binary()) -> nil. +print_error(String) -> + gleam_stdlib:print_error(String). + +-file("src/gleam/io.gleam", 45). +?DOC( + " Writes a string to standard output (stdout), appending a newline to the end.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " io.println(\"Hi mum\")\n" + " // -> Nil\n" + " // Hi mum\n" + " ```\n" +). +-spec println(binary()) -> nil. +println(String) -> + gleam_stdlib:println(String). + +-file("src/gleam/io.gleam", 59). +?DOC( + " Writes a string to standard error (stderr), appending a newline to the end.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " io.println_error(\"Hi pop\")\n" + " // -> Nil\n" + " // Hi pop\n" + " ```\n" +). +-spec println_error(binary()) -> nil. +println_error(String) -> + gleam_stdlib:println_error(String). diff --git a/build/packages/gleam_stdlib/src/gleam@list.erl b/build/packages/gleam_stdlib/src/gleam@list.erl new file mode 100644 index 0000000..0c96ecb --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@list.erl @@ -0,0 +1,2873 @@ +-module(gleam@list). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/list.gleam"). +-export([length/1, count/2, reverse/1, is_empty/1, contains/2, first/1, rest/1, group/2, filter/2, filter_map/2, map/2, map2/3, map_fold/3, index_map/2, try_map/2, drop/2, take/2, new/0, wrap/1, append/2, prepend/2, flatten/1, flat_map/2, fold/3, fold_right/3, index_fold/3, try_fold/3, fold_until/3, find/2, find_map/2, all/2, any/2, zip/2, strict_zip/2, unzip/1, intersperse/2, unique/1, sort/2, range/2, repeat/2, split/2, split_while/2, key_find/2, key_filter/2, key_pop/2, key_set/3, each/2, try_each/2, partition/2, window/2, window_by_2/1, drop_while/2, take_while/2, chunk/2, sized_chunk/2, reduce/2, scan/3, last/1, combinations/2, combination_pairs/1, transpose/1, interleave/1, shuffle/1, max/2, sample/2, permutations/1]). +-export_type([continue_or_stop/1, sorting/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Lists are an ordered sequence of elements and are one of the most common\n" + " data types in Gleam.\n" + "\n" + " New elements can be added and removed from the front of a list in\n" + " constant time, while adding and removing from the end requires traversing\n" + " and copying the whole list, so keep this in mind when designing your\n" + " programs.\n" + "\n" + " There is a dedicated syntax for prefixing to a list:\n" + "\n" + " ```gleam\n" + " let new_list = [1, 2, ..existing_list]\n" + " ```\n" + "\n" + " And a matching syntax for getting the first elements of a list:\n" + "\n" + " ```gleam\n" + " case list {\n" + " [first_element, ..rest] -> first_element\n" + " _ -> \"this pattern matches when the list is empty\"\n" + " }\n" + " ```\n" + "\n" +). + +-type continue_or_stop(XG) :: {continue, XG} | {stop, XG}. + +-type sorting() :: ascending | descending. + +-file("src/gleam/list.gleam", 60). +-spec length_loop(list(any()), integer()) -> integer(). +length_loop(List, Count) -> + case List of + [_ | List@1] -> + length_loop(List@1, Count + 1); + + [] -> + Count + end. + +-file("src/gleam/list.gleam", 56). +?DOC( + " Counts the number of elements in a given list.\n" + "\n" + " This function has to traverse the list to determine the number of elements,\n" + " so it runs in linear time.\n" + "\n" + " This function is natively implemented by the virtual machine and is highly\n" + " optimised.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " length([])\n" + " // -> 0\n" + " ```\n" + "\n" + " ```gleam\n" + " length([1])\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " length([1, 2])\n" + " // -> 2\n" + " ```\n" +). +-spec length(list(any())) -> integer(). +length(List) -> + erlang:length(List). + +-file("src/gleam/list.gleam", 93). +-spec count_loop(list(XN), fun((XN) -> boolean()), integer()) -> integer(). +count_loop(List, Predicate, Acc) -> + case List of + [] -> + Acc; + + [First | Rest] -> + case Predicate(First) of + true -> + count_loop(Rest, Predicate, Acc + 1); + + false -> + count_loop(Rest, Predicate, Acc) + end + end. + +-file("src/gleam/list.gleam", 89). +?DOC( + " Counts the number of elements in a given list satisfying a given predicate.\n" + "\n" + " This function has to traverse the list to determine the number of elements,\n" + " so it runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " count([], fn(a) { a > 0 })\n" + " // -> 0\n" + " ```\n" + "\n" + " ```gleam\n" + " count([1], fn(a) { a > 0 })\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " count([1, 2, 3], int.is_odd)\n" + " // -> 2\n" + " ```\n" +). +-spec count(list(XL), fun((XL) -> boolean())) -> integer(). +count(List, Predicate) -> + count_loop(List, Predicate, 0). + +-file("src/gleam/list.gleam", 131). +?DOC( + " Creates a new list from a given list containing the same elements but in the\n" + " opposite order.\n" + "\n" + " This function has to traverse the list to create the new reversed list, so\n" + " it runs in linear time.\n" + "\n" + " This function is natively implemented by the virtual machine and is highly\n" + " optimised.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " reverse([])\n" + " // -> []\n" + " ```\n" + "\n" + " ```gleam\n" + " reverse([1])\n" + " // -> [1]\n" + " ```\n" + "\n" + " ```gleam\n" + " reverse([1, 2])\n" + " // -> [2, 1]\n" + " ```\n" +). +-spec reverse(list(XP)) -> list(XP). +reverse(List) -> + lists:reverse(List). + +-file("src/gleam/list.gleam", 168). +?DOC( + " Determines whether or not the list is empty.\n" + "\n" + " This function runs in constant time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_empty([])\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_empty([1])\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " is_empty([1, 1])\n" + " // -> False\n" + " ```\n" +). +-spec is_empty(list(any())) -> boolean(). +is_empty(List) -> + List =:= []. + +-file("src/gleam/list.gleam", 204). +?DOC( + " Determines whether or not a given element exists within a given list.\n" + "\n" + " This function traverses the list to find the element, so it runs in linear\n" + " time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [] |> contains(any: 0)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " [0] |> contains(any: 0)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " [1] |> contains(any: 0)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " [1, 1] |> contains(any: 0)\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " [1, 0] |> contains(any: 0)\n" + " // -> True\n" + " ```\n" +). +-spec contains(list(XY), XY) -> boolean(). +contains(List, Elem) -> + case List of + [] -> + false; + + [First | _] when First =:= Elem -> + true; + + [_ | Rest] -> + contains(Rest, Elem) + end. + +-file("src/gleam/list.gleam", 231). +?DOC( + " Gets the first element from the start of the list, if there is one.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " first([])\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " first([0])\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " first([1, 2])\n" + " // -> Ok(1)\n" + " ```\n" +). +-spec first(list(YA)) -> {ok, YA} | {error, nil}. +first(List) -> + case List of + [] -> + {error, nil}; + + [First | _] -> + {ok, First} + end. + +-file("src/gleam/list.gleam", 260). +?DOC( + " Returns the list minus the first element. If the list is empty, `Error(Nil)` is\n" + " returned.\n" + "\n" + " This function runs in constant time and does not make a copy of the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " rest([])\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " rest([0])\n" + " // -> Ok([])\n" + " ```\n" + "\n" + " ```gleam\n" + " rest([1, 2])\n" + " // -> Ok([2])\n" + " ```\n" +). +-spec rest(list(YE)) -> {ok, list(YE)} | {error, nil}. +rest(List) -> + case List of + [] -> + {error, nil}; + + [_ | Rest] -> + {ok, Rest} + end. + +-file("src/gleam/list.gleam", 302). +-spec group_loop(list(YP), fun((YP) -> YR), gleam@dict:dict(YR, list(YP))) -> gleam@dict:dict(YR, list(YP)). +group_loop(List, To_key, Groups) -> + case List of + [] -> + Groups; + + [First | Rest] -> + Key = To_key(First), + Groups@1 = case gleam_stdlib:map_get(Groups, Key) of + {error, _} -> + gleam@dict:insert(Groups, Key, [First]); + + {ok, Existing} -> + gleam@dict:insert(Groups, Key, [First | Existing]) + end, + group_loop(Rest, To_key, Groups@1) + end. + +-file("src/gleam/list.gleam", 298). +?DOC( + " Groups the elements from the given list by the given key function.\n" + "\n" + " Does not preserve the initial value order.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/dict\n" + "\n" + " [Ok(3), Error(\"Wrong\"), Ok(200), Ok(73)]\n" + " |> group(by: fn(i) {\n" + " case i {\n" + " Ok(_) -> \"Successful\"\n" + " Error(_) -> \"Failed\"\n" + " }\n" + " })\n" + " |> dict.to_list\n" + " // -> [\n" + " // #(\"Failed\", [Error(\"Wrong\")]),\n" + " // #(\"Successful\", [Ok(73), Ok(200), Ok(3)])\n" + " // ]\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/dict\n" + "\n" + " group([1,2,3,4,5], by: fn(i) { i - i / 3 * 3 })\n" + " |> dict.to_list\n" + " // -> [#(0, [3]), #(1, [4, 1]), #(2, [5, 2])]\n" + " ```\n" +). +-spec group(list(YJ), fun((YJ) -> YL)) -> gleam@dict:dict(YL, list(YJ)). +group(List, Key) -> + group_loop(List, Key, maps:new()). + +-file("src/gleam/list.gleam", 339). +-spec filter_loop(list(AAB), fun((AAB) -> boolean()), list(AAB)) -> list(AAB). +filter_loop(List, Fun, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + New_acc = case Fun(First) of + true -> + [First | Acc]; + + false -> + Acc + end, + filter_loop(Rest, Fun, New_acc) + end. + +-file("src/gleam/list.gleam", 335). +?DOC( + " Returns a new list containing only the elements from the first list for\n" + " which the given functions returns `True`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " filter([2, 4, 6, 1], fn(x) { x > 2 })\n" + " // -> [4, 6]\n" + " ```\n" + "\n" + " ```gleam\n" + " filter([2, 4, 6, 1], fn(x) { x > 6 })\n" + " // -> []\n" + " ```\n" +). +-spec filter(list(YY), fun((YY) -> boolean())) -> list(YY). +filter(List, Predicate) -> + filter_loop(List, Predicate, []). + +-file("src/gleam/list.gleam", 371). +-spec filter_map_loop( + list(AAM), + fun((AAM) -> {ok, AAO} | {error, any()}), + list(AAO) +) -> list(AAO). +filter_map_loop(List, Fun, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + New_acc = case Fun(First) of + {ok, First@1} -> + [First@1 | Acc]; + + {error, _} -> + Acc + end, + filter_map_loop(Rest, Fun, New_acc) + end. + +-file("src/gleam/list.gleam", 367). +?DOC( + " Returns a new list containing only the elements from the first list for\n" + " which the given functions returns `Ok(_)`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " filter_map([2, 4, 6, 1], Error)\n" + " // -> []\n" + " ```\n" + "\n" + " ```gleam\n" + " filter_map([2, 4, 6, 1], fn(x) { Ok(x + 1) })\n" + " // -> [3, 5, 7, 2]\n" + " ```\n" +). +-spec filter_map(list(AAF), fun((AAF) -> {ok, AAH} | {error, any()})) -> list(AAH). +filter_map(List, Fun) -> + filter_map_loop(List, Fun, []). + +-file("src/gleam/list.gleam", 402). +-spec map_loop(list(AAY), fun((AAY) -> ABA), list(ABA)) -> list(ABA). +map_loop(List, Fun, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + map_loop(Rest, Fun, [Fun(First) | Acc]) + end. + +-file("src/gleam/list.gleam", 398). +?DOC( + " Returns a new list containing only the elements of the first list after the\n" + " function has been applied to each one.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map([2, 4, 6], fn(x) { x * 2 })\n" + " // -> [4, 8, 12]\n" + " ```\n" +). +-spec map(list(AAU), fun((AAU) -> AAW)) -> list(AAW). +map(List, Fun) -> + map_loop(List, Fun, []). + +-file("src/gleam/list.gleam", 429). +-spec map2_loop(list(ABJ), list(ABL), fun((ABJ, ABL) -> ABN), list(ABN)) -> list(ABN). +map2_loop(List1, List2, Fun, Acc) -> + case {List1, List2} of + {[], _} -> + lists:reverse(Acc); + + {_, []} -> + lists:reverse(Acc); + + {[A | As_], [B | Bs]} -> + map2_loop(As_, Bs, Fun, [Fun(A, B) | Acc]) + end. + +-file("src/gleam/list.gleam", 425). +?DOC( + " Combines two lists into a single list using the given function.\n" + "\n" + " If a list is longer than the other the extra elements are dropped.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map2([1, 2, 3], [4, 5, 6], fn(x, y) { x + y })\n" + " // -> [5, 7, 9]\n" + " ```\n" + "\n" + " ```gleam\n" + " map2([1, 2], [\"a\", \"b\", \"c\"], fn(i, x) { #(i, x) })\n" + " // -> [#(1, \"a\"), #(2, \"b\")]\n" + " ```\n" +). +-spec map2(list(ABD), list(ABF), fun((ABD, ABF) -> ABH)) -> list(ABH). +map2(List1, List2, Fun) -> + map2_loop(List1, List2, Fun, []). + +-file("src/gleam/list.gleam", 462). +-spec map_fold_loop(list(ABV), fun((ABX, ABV) -> {ABX, ABY}), ABX, list(ABY)) -> {ABX, + list(ABY)}. +map_fold_loop(List, Fun, Acc, List_acc) -> + case List of + [] -> + {Acc, lists:reverse(List_acc)}; + + [First | Rest] -> + {Acc@1, First@1} = Fun(Acc, First), + map_fold_loop(Rest, Fun, Acc@1, [First@1 | List_acc]) + end. + +-file("src/gleam/list.gleam", 454). +?DOC( + " Similar to `map` but also lets you pass around an accumulated value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map_fold(\n" + " over: [1, 2, 3],\n" + " from: 100,\n" + " with: fn(memo, i) { #(memo + i, i * 2) }\n" + " )\n" + " // -> #(106, [2, 4, 6])\n" + " ```\n" +). +-spec map_fold(list(ABQ), ABS, fun((ABS, ABQ) -> {ABS, ABT})) -> {ABS, + list(ABT)}. +map_fold(List, Initial, Fun) -> + map_fold_loop(List, Fun, Initial, []). + +-file("src/gleam/list.gleam", 494). +-spec index_map_loop( + list(ACF), + fun((ACF, integer()) -> ACH), + integer(), + list(ACH) +) -> list(ACH). +index_map_loop(List, Fun, Index, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + Acc@1 = [Fun(First, Index) | Acc], + index_map_loop(Rest, Fun, Index + 1, Acc@1) + end. + +-file("src/gleam/list.gleam", 490). +?DOC( + " Returns a new list containing only the elements of the first list after the\n" + " function has been applied to each one and their index.\n" + "\n" + " The index starts at 0, so the first element is 0, the second is 1, and so\n" + " on.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " index_map([\"a\", \"b\"], fn(x, i) { #(i, x) })\n" + " // -> [#(0, \"a\"), #(1, \"b\")]\n" + " ```\n" +). +-spec index_map(list(ACB), fun((ACB, integer()) -> ACD)) -> list(ACD). +index_map(List, Fun) -> + index_map_loop(List, Fun, 0, []). + +-file("src/gleam/list.gleam", 548). +-spec try_map_loop(list(ACT), fun((ACT) -> {ok, ACV} | {error, ACW}), list(ACV)) -> {ok, + list(ACV)} | + {error, ACW}. +try_map_loop(List, Fun, Acc) -> + case List of + [] -> + {ok, lists:reverse(Acc)}; + + [First | Rest] -> + case Fun(First) of + {ok, First@1} -> + try_map_loop(Rest, Fun, [First@1 | Acc]); + + {error, Error} -> + {error, Error} + end + end. + +-file("src/gleam/list.gleam", 541). +?DOC( + " Takes a function that returns a `Result` and applies it to each element in a\n" + " given list in turn.\n" + "\n" + " If the function returns `Ok(new_value)` for all elements in the list then a\n" + " list of the new values is returned.\n" + "\n" + " If the function returns `Error(reason)` for any of the elements then it is\n" + " returned immediately. None of the elements in the list are processed after\n" + " one returns an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " try_map([1, 2, 3], fn(x) { Ok(x + 2) })\n" + " // -> Ok([3, 4, 5])\n" + " ```\n" + "\n" + " ```gleam\n" + " try_map([1, 2, 3], fn(_) { Error(0) })\n" + " // -> Error(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " try_map([[1], [2, 3]], first)\n" + " // -> Ok([1, 2])\n" + " ```\n" + "\n" + " ```gleam\n" + " try_map([[1], [], [2]], first)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec try_map(list(ACK), fun((ACK) -> {ok, ACM} | {error, ACN})) -> {ok, + list(ACM)} | + {error, ACN}. +try_map(List, Fun) -> + try_map_loop(List, Fun, []). + +-file("src/gleam/list.gleam", 583). +?DOC( + " Returns a list that is the given list with up to the given number of\n" + " elements removed from the front of the list.\n" + "\n" + " If the element has less than the number of elements an empty list is\n" + " returned.\n" + "\n" + " This function runs in linear time but does not copy the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " drop([1, 2, 3, 4], 2)\n" + " // -> [3, 4]\n" + " ```\n" + "\n" + " ```gleam\n" + " drop([1, 2, 3, 4], 9)\n" + " // -> []\n" + " ```\n" +). +-spec drop(list(ADD), integer()) -> list(ADD). +drop(List, N) -> + case N =< 0 of + true -> + List; + + false -> + case List of + [] -> + []; + + [_ | Rest] -> + drop(Rest, N - 1) + end + end. + +-file("src/gleam/list.gleam", 618). +-spec take_loop(list(ADJ), integer(), list(ADJ)) -> list(ADJ). +take_loop(List, N, Acc) -> + case N =< 0 of + true -> + lists:reverse(Acc); + + false -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + take_loop(Rest, N - 1, [First | Acc]) + end + end. + +-file("src/gleam/list.gleam", 614). +?DOC( + " Returns a list containing the first given number of elements from the given\n" + " list.\n" + "\n" + " If the element has less than the number of elements then the full list is\n" + " returned.\n" + "\n" + " This function runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " take([1, 2, 3, 4], 2)\n" + " // -> [1, 2]\n" + " ```\n" + "\n" + " ```gleam\n" + " take([1, 2, 3, 4], 9)\n" + " // -> [1, 2, 3, 4]\n" + " ```\n" +). +-spec take(list(ADG), integer()) -> list(ADG). +take(List, N) -> + take_loop(List, N, []). + +-file("src/gleam/list.gleam", 638). +?DOC( + " Returns a new empty list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new()\n" + " // -> []\n" + " ```\n" +). +-spec new() -> list(any()). +new() -> + []. + +-file("src/gleam/list.gleam", 658). +?DOC( + " Returns the given item wrapped in a list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " wrap(1)\n" + " // -> [1]\n" + "\n" + " wrap([\"a\", \"b\", \"c\"])\n" + " // -> [[\"a\", \"b\", \"c\"]]\n" + "\n" + " wrap([[]])\n" + " // -> [[[]]]\n" + " ```\n" +). +-spec wrap(ADP) -> list(ADP). +wrap(Item) -> + [Item]. + +-file("src/gleam/list.gleam", 679). +-spec append_loop(list(ADV), list(ADV)) -> list(ADV). +append_loop(First, Second) -> + case First of + [] -> + Second; + + [First@1 | Rest] -> + append_loop(Rest, [First@1 | Second]) + end. + +-file("src/gleam/list.gleam", 675). +?DOC( + " Joins one list onto the end of another.\n" + "\n" + " This function runs in linear time, and it traverses and copies the first\n" + " list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " append([1, 2], [3])\n" + " // -> [1, 2, 3]\n" + " ```\n" +). +-spec append(list(ADR), list(ADR)) -> list(ADR). +append(First, Second) -> + lists:append(First, Second). + +-file("src/gleam/list.gleam", 699). +?DOC( + " Prefixes an item to a list. This can also be done using the dedicated\n" + " syntax instead\n" + "\n" + " ```gleam\n" + " let existing_list = [2, 3, 4]\n" + "\n" + " [1, ..existing_list]\n" + " // -> [1, 2, 3, 4]\n" + "\n" + " prepend(to: existing_list, this: 1)\n" + " // -> [1, 2, 3, 4]\n" + " ```\n" +). +-spec prepend(list(ADZ), ADZ) -> list(ADZ). +prepend(List, Item) -> + [Item | List]. + +-file("src/gleam/list.gleam", 720). +-spec flatten_loop(list(list(AEG)), list(AEG)) -> list(AEG). +flatten_loop(Lists, Acc) -> + case Lists of + [] -> + lists:reverse(Acc); + + [List | Further_lists] -> + flatten_loop(Further_lists, lists:reverse(List, Acc)) + end. + +-file("src/gleam/list.gleam", 716). +?DOC( + " Joins a list of lists into a single list.\n" + "\n" + " This function traverses all elements twice on the JavaScript target.\n" + " This function traverses all elements once on the Erlang target.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " flatten([[1], [2, 3], []])\n" + " // -> [1, 2, 3]\n" + " ```\n" +). +-spec flatten(list(list(AEC))) -> list(AEC). +flatten(Lists) -> + lists:append(Lists). + +-file("src/gleam/list.gleam", 737). +?DOC( + " Maps the list with the given function into a list of lists, and then flattens it.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " flat_map([2, 4, 6], fn(x) { [x, x + 1] })\n" + " // -> [2, 3, 4, 5, 6, 7]\n" + " ```\n" +). +-spec flat_map(list(AEL), fun((AEL) -> list(AEN))) -> list(AEN). +flat_map(List, Fun) -> + lists:append(map(List, Fun)). + +-file("src/gleam/list.gleam", 749). +?DOC( + " Reduces a list of elements into a single value by calling a given function\n" + " on each element, going from left to right.\n" + "\n" + " `fold([1, 2, 3], 0, add)` is the equivalent of\n" + " `add(add(add(0, 1), 2), 3)`.\n" + "\n" + " This function runs in linear time.\n" +). +-spec fold(list(AEQ), AES, fun((AES, AEQ) -> AES)) -> AES. +fold(List, Initial, Fun) -> + case List of + [] -> + Initial; + + [First | Rest] -> + fold(Rest, Fun(Initial, First), Fun) + end. + +-file("src/gleam/list.gleam", 771). +?DOC( + " Reduces a list of elements into a single value by calling a given function\n" + " on each element, going from right to left.\n" + "\n" + " `fold_right([1, 2, 3], 0, add)` is the equivalent of\n" + " `add(add(add(0, 3), 2), 1)`.\n" + "\n" + " This function runs in linear time.\n" + "\n" + " Unlike `fold` this function is not tail recursive. Where possible use\n" + " `fold` instead as it will use less memory.\n" +). +-spec fold_right(list(AET), AEV, fun((AEV, AET) -> AEV)) -> AEV. +fold_right(List, Initial, Fun) -> + case List of + [] -> + Initial; + + [First | Rest] -> + Fun(fold_right(Rest, Initial, Fun), First) + end. + +-file("src/gleam/list.gleam", 808). +-spec index_fold_loop( + list(AEZ), + AFB, + fun((AFB, AEZ, integer()) -> AFB), + integer() +) -> AFB. +index_fold_loop(Over, Acc, With, Index) -> + case Over of + [] -> + Acc; + + [First | Rest] -> + index_fold_loop(Rest, With(Acc, First, Index), With, Index + 1) + end. + +-file("src/gleam/list.gleam", 800). +?DOC( + " Like fold but the folding function also receives the index of the current element.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [\"a\", \"b\", \"c\"]\n" + " |> index_fold(\"\", fn(acc, item, index) {\n" + " acc <> int.to_string(index) <> \":\" <> item <> \" \"\n" + " })\n" + " // -> \"0:a 1:b 2:c\"\n" + " ```\n" + "\n" + " ```gleam\n" + " [10, 20, 30]\n" + " |> index_fold(0, fn(acc, item, index) { acc + item * index })\n" + " // -> 80\n" + " ```\n" +). +-spec index_fold(list(AEW), AEY, fun((AEY, AEW, integer()) -> AEY)) -> AEY. +index_fold(List, Initial, Fun) -> + index_fold_loop(List, Initial, Fun, 0). + +-file("src/gleam/list.gleam", 840). +?DOC( + " A variant of fold that might fail.\n" + "\n" + " The folding function should return `Result(accumulator, error)`.\n" + " If the returned value is `Ok(accumulator)` try_fold will try the next value in the list.\n" + " If the returned value is `Error(error)` try_fold will stop and return that error.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [1, 2, 3, 4]\n" + " |> try_fold(0, fn(acc, i) {\n" + " case i < 3 {\n" + " True -> Ok(acc + i)\n" + " False -> Error(Nil)\n" + " }\n" + " })\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec try_fold(list(AFC), AFE, fun((AFE, AFC) -> {ok, AFE} | {error, AFF})) -> {ok, + AFE} | + {error, AFF}. +try_fold(List, Initial, Fun) -> + case List of + [] -> + {ok, Initial}; + + [First | Rest] -> + case Fun(Initial, First) of + {ok, Result} -> + try_fold(Rest, Result, Fun); + + {error, _} = Error -> + Error + end + end. + +-file("src/gleam/list.gleam", 879). +?DOC( + " A variant of fold that allows to stop folding earlier.\n" + "\n" + " The folding function should return `ContinueOrStop(accumulator)`.\n" + " If the returned value is `Continue(accumulator)` fold_until will try the next value in the list.\n" + " If the returned value is `Stop(accumulator)` fold_until will stop and return that accumulator.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [1, 2, 3, 4]\n" + " |> fold_until(0, fn(acc, i) {\n" + " case i < 3 {\n" + " True -> Continue(acc + i)\n" + " False -> Stop(acc)\n" + " }\n" + " })\n" + " // -> 3\n" + " ```\n" +). +-spec fold_until(list(AFK), AFM, fun((AFM, AFK) -> continue_or_stop(AFM))) -> AFM. +fold_until(List, Initial, Fun) -> + case List of + [] -> + Initial; + + [First | Rest] -> + case Fun(Initial, First) of + {continue, Next_accumulator} -> + fold_until(Rest, Next_accumulator, Fun); + + {stop, B} -> + B + end + end. + +-file("src/gleam/list.gleam", 916). +?DOC( + " Finds the first element in a given list for which the given function returns\n" + " `True`.\n" + "\n" + " Returns `Error(Nil)` if no such element is found.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " find([1, 2, 3], fn(x) { x > 2 })\n" + " // -> Ok(3)\n" + " ```\n" + "\n" + " ```gleam\n" + " find([1, 2, 3], fn(x) { x > 4 })\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " find([], fn(_) { True })\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec find(list(AFO), fun((AFO) -> boolean())) -> {ok, AFO} | {error, nil}. +find(List, Is_desired) -> + case List of + [] -> + {error, nil}; + + [First | Rest] -> + case Is_desired(First) of + true -> + {ok, First}; + + false -> + find(Rest, Is_desired) + end + end. + +-file("src/gleam/list.gleam", 952). +?DOC( + " Finds the first element in a given list for which the given function returns\n" + " `Ok(new_value)`, then returns the wrapped `new_value`.\n" + "\n" + " Returns `Error(Nil)` if no such element is found.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " find_map([[], [2], [3]], first)\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " find_map([[], []], first)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " find_map([], first)\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec find_map(list(AFS), fun((AFS) -> {ok, AFU} | {error, any()})) -> {ok, AFU} | + {error, nil}. +find_map(List, Fun) -> + case List of + [] -> + {error, nil}; + + [First | Rest] -> + case Fun(First) of + {ok, First@1} -> + {ok, First@1}; + + {error, _} -> + find_map(Rest, Fun) + end + end. + +-file("src/gleam/list.gleam", 987). +?DOC( + " Returns `True` if the given function returns `True` for all the elements in\n" + " the given list. If the function returns `False` for any of the elements it\n" + " immediately returns `False` without checking the rest of the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " all([], fn(x) { x > 3 })\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " all([4, 5], fn(x) { x > 3 })\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " all([4, 3], fn(x) { x > 3 })\n" + " // -> False\n" + " ```\n" +). +-spec all(list(AGA), fun((AGA) -> boolean())) -> boolean(). +all(List, Predicate) -> + case List of + [] -> + true; + + [First | Rest] -> + case Predicate(First) of + true -> + all(Rest, Predicate); + + false -> + false + end + end. + +-file("src/gleam/list.gleam", 1024). +?DOC( + " Returns `True` if the given function returns `True` for any the elements in\n" + " the given list. If the function returns `True` for any of the elements it\n" + " immediately returns `True` without checking the rest of the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " any([], fn(x) { x > 3 })\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " any([4, 5], fn(x) { x > 3 })\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " any([4, 3], fn(x) { x > 4 })\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " any([3, 4], fn(x) { x > 3 })\n" + " // -> True\n" + " ```\n" +). +-spec any(list(AGC), fun((AGC) -> boolean())) -> boolean(). +any(List, Predicate) -> + case List of + [] -> + false; + + [First | Rest] -> + case Predicate(First) of + true -> + true; + + false -> + any(Rest, Predicate) + end + end. + +-file("src/gleam/list.gleam", 1066). +-spec zip_loop(list(AGJ), list(AGL), list({AGJ, AGL})) -> list({AGJ, AGL}). +zip_loop(One, Other, Acc) -> + case {One, Other} of + {[First_one | Rest_one], [First_other | Rest_other]} -> + zip_loop(Rest_one, Rest_other, [{First_one, First_other} | Acc]); + + {_, _} -> + lists:reverse(Acc) + end. + +-file("src/gleam/list.gleam", 1062). +?DOC( + " Takes two lists and returns a single list of 2-element tuples.\n" + "\n" + " If one of the lists is longer than the other, the remaining elements from\n" + " the longer list are not used.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " zip([], [])\n" + " // -> []\n" + " ```\n" + "\n" + " ```gleam\n" + " zip([1, 2], [3])\n" + " // -> [#(1, 3)]\n" + " ```\n" + "\n" + " ```gleam\n" + " zip([1], [3, 4])\n" + " // -> [#(1, 3)]\n" + " ```\n" + "\n" + " ```gleam\n" + " zip([1, 2], [3, 4])\n" + " // -> [#(1, 3), #(2, 4)]\n" + " ```\n" +). +-spec zip(list(AGE), list(AGG)) -> list({AGE, AGG}). +zip(List, Other) -> + zip_loop(List, Other, []). + +-file("src/gleam/list.gleam", 1107). +-spec strict_zip_loop(list(AGW), list(AGY), list({AGW, AGY})) -> {ok, + list({AGW, AGY})} | + {error, nil}. +strict_zip_loop(One, Other, Acc) -> + case {One, Other} of + {[], []} -> + {ok, lists:reverse(Acc)}; + + {[], _} -> + {error, nil}; + + {_, []} -> + {error, nil}; + + {[First_one | Rest_one], [First_other | Rest_other]} -> + strict_zip_loop( + Rest_one, + Rest_other, + [{First_one, First_other} | Acc] + ) + end. + +-file("src/gleam/list.gleam", 1100). +?DOC( + " Takes two lists and returns a single list of 2-element tuples.\n" + "\n" + " If one of the lists is longer than the other, an `Error` is returned.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " strict_zip([], [])\n" + " // -> Ok([])\n" + " ```\n" + "\n" + " ```gleam\n" + " strict_zip([1, 2], [3])\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " strict_zip([1], [3, 4])\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " strict_zip([1, 2], [3, 4])\n" + " // -> Ok([#(1, 3), #(2, 4)])\n" + " ```\n" +). +-spec strict_zip(list(AGP), list(AGR)) -> {ok, list({AGP, AGR})} | {error, nil}. +strict_zip(List, Other) -> + strict_zip_loop(List, Other, []). + +-file("src/gleam/list.gleam", 1138). +-spec unzip_loop(list({AHJ, AHK}), list(AHJ), list(AHK)) -> {list(AHJ), + list(AHK)}. +unzip_loop(Input, One, Other) -> + case Input of + [] -> + {lists:reverse(One), lists:reverse(Other)}; + + [{First_one, First_other} | Rest] -> + unzip_loop(Rest, [First_one | One], [First_other | Other]) + end. + +-file("src/gleam/list.gleam", 1134). +?DOC( + " Takes a single list of 2-element tuples and returns two lists.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " unzip([#(1, 2), #(3, 4)])\n" + " // -> #([1, 3], [2, 4])\n" + " ```\n" + "\n" + " ```gleam\n" + " unzip([])\n" + " // -> #([], [])\n" + " ```\n" +). +-spec unzip(list({AHE, AHF})) -> {list(AHE), list(AHF)}. +unzip(Input) -> + unzip_loop(Input, [], []). + +-file("src/gleam/list.gleam", 1173). +-spec intersperse_loop(list(AHT), AHT, list(AHT)) -> list(AHT). +intersperse_loop(List, Separator, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + intersperse_loop(Rest, Separator, [First, Separator | Acc]) + end. + +-file("src/gleam/list.gleam", 1166). +?DOC( + " Inserts a given value between each existing element in a given list.\n" + "\n" + " This function runs in linear time and copies the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " intersperse([1, 1, 1], 2)\n" + " // -> [1, 2, 1, 2, 1]\n" + " ```\n" + "\n" + " ```gleam\n" + " intersperse([], 2)\n" + " // -> []\n" + " ```\n" +). +-spec intersperse(list(AHQ), AHQ) -> list(AHQ). +intersperse(List, Elem) -> + case List of + [] -> + List; + + [_] -> + List; + + [First | Rest] -> + intersperse_loop(Rest, Elem, [First]) + end. + +-file("src/gleam/list.gleam", 1196). +-spec unique_loop(list(AIA), gleam@dict:dict(AIA, nil), list(AIA)) -> list(AIA). +unique_loop(List, Seen, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + case gleam@dict:has_key(Seen, First) of + true -> + unique_loop(Rest, Seen, Acc); + + false -> + unique_loop( + Rest, + gleam@dict:insert(Seen, First, nil), + [First | Acc] + ) + end + end. + +-file("src/gleam/list.gleam", 1192). +?DOC( + " Removes any duplicate elements from a given list.\n" + "\n" + " This function returns in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " unique([1, 1, 1, 4, 7, 3, 3, 4])\n" + " // -> [1, 4, 7, 3]\n" + " ```\n" +). +-spec unique(list(AHX)) -> list(AHX). +unique(List) -> + unique_loop(List, maps:new(), []). + +-file("src/gleam/list.gleam", 1282). +?DOC( + " Given a list it returns slices of it that are locally sorted in ascending\n" + " order.\n" + "\n" + " Imagine you have this list:\n" + "\n" + " ```\n" + " [1, 2, 3, 2, 1, 0]\n" + " ^^^^^^^ ^^^^^^^ This is a slice in descending order\n" + " |\n" + " | This is a slice that is sorted in ascending order\n" + " ```\n" + "\n" + " So the produced result will contain these two slices, each one sorted in\n" + " ascending order: `[[1, 2, 3], [0, 1, 2]]`.\n" + "\n" + " - `growing` is an accumulator with the current slice being grown\n" + " - `direction` is the growing direction of the slice being grown, it could\n" + " either be ascending or strictly descending\n" + " - `prev` is the previous element that needs to be added to the growing slice\n" + " it is carried around to check whether we have to keep growing the current\n" + " slice or not\n" + " - `acc` is the accumulator containing the slices sorted in ascending order\n" +). +-spec sequences( + list(AIJ), + fun((AIJ, AIJ) -> gleam@order:order()), + list(AIJ), + sorting(), + AIJ, + list(list(AIJ)) +) -> list(list(AIJ)). +sequences(List, Compare, Growing, Direction, Prev, Acc) -> + Growing@1 = [Prev | Growing], + case List of + [] -> + case Direction of + ascending -> + [lists:reverse(Growing@1) | Acc]; + + descending -> + [Growing@1 | Acc] + end; + + [New | Rest] -> + case {Compare(Prev, New), Direction} of + {gt, descending} -> + sequences(Rest, Compare, Growing@1, Direction, New, Acc); + + {lt, ascending} -> + sequences(Rest, Compare, Growing@1, Direction, New, Acc); + + {eq, ascending} -> + sequences(Rest, Compare, Growing@1, Direction, New, Acc); + + {gt, ascending} -> + Acc@1 = case Direction of + ascending -> + [lists:reverse(Growing@1) | Acc]; + + descending -> + [Growing@1 | Acc] + end, + case Rest of + [] -> + [[New] | Acc@1]; + + [Next | Rest@1] -> + Direction@1 = case Compare(New, Next) of + lt -> + ascending; + + eq -> + ascending; + + gt -> + descending + end, + sequences( + Rest@1, + Compare, + [New], + Direction@1, + Next, + Acc@1 + ) + end; + + {lt, descending} -> + Acc@1 = case Direction of + ascending -> + [lists:reverse(Growing@1) | Acc]; + + descending -> + [Growing@1 | Acc] + end, + case Rest of + [] -> + [[New] | Acc@1]; + + [Next | Rest@1] -> + Direction@1 = case Compare(New, Next) of + lt -> + ascending; + + eq -> + ascending; + + gt -> + descending + end, + sequences( + Rest@1, + Compare, + [New], + Direction@1, + Next, + Acc@1 + ) + end; + + {eq, descending} -> + Acc@1 = case Direction of + ascending -> + [lists:reverse(Growing@1) | Acc]; + + descending -> + [Growing@1 | Acc] + end, + case Rest of + [] -> + [[New] | Acc@1]; + + [Next | Rest@1] -> + Direction@1 = case Compare(New, Next) of + lt -> + ascending; + + eq -> + ascending; + + gt -> + descending + end, + sequences( + Rest@1, + Compare, + [New], + Direction@1, + Next, + Acc@1 + ) + end + end + end. + +-file("src/gleam/list.gleam", 1430). +?DOC( + " Merges two lists sorted in ascending order into a single list sorted in\n" + " descending order according to the given comparator function.\n" + "\n" + " This reversing of the sort order is not avoidable if we want to implement\n" + " merge as a tail recursive function. We could reverse the accumulator before\n" + " returning it but that would end up being less efficient; so the merging\n" + " algorithm has to play around this.\n" +). +-spec merge_ascendings( + list(AJG), + list(AJG), + fun((AJG, AJG) -> gleam@order:order()), + list(AJG) +) -> list(AJG). +merge_ascendings(List1, List2, Compare, Acc) -> + case {List1, List2} of + {[], List} -> + lists:reverse(List, Acc); + + {List, []} -> + lists:reverse(List, Acc); + + {[First1 | Rest1], [First2 | Rest2]} -> + case Compare(First1, First2) of + lt -> + merge_ascendings(Rest1, List2, Compare, [First1 | Acc]); + + gt -> + merge_ascendings(List1, Rest2, Compare, [First2 | Acc]); + + eq -> + merge_ascendings(List1, Rest2, Compare, [First2 | Acc]) + end + end. + +-file("src/gleam/list.gleam", 1383). +?DOC( + " Given a list of ascending lists, it merges adjacent pairs into a single\n" + " descending list, halving their number.\n" + " It returns a list of the remaining descending lists.\n" +). +-spec merge_ascending_pairs( + list(list(AIU)), + fun((AIU, AIU) -> gleam@order:order()), + list(list(AIU)) +) -> list(list(AIU)). +merge_ascending_pairs(Sequences, Compare, Acc) -> + case Sequences of + [] -> + lists:reverse(Acc); + + [Sequence] -> + lists:reverse([lists:reverse(Sequence) | Acc]); + + [Ascending1, Ascending2 | Rest] -> + Descending = merge_ascendings(Ascending1, Ascending2, Compare, []), + merge_ascending_pairs(Rest, Compare, [Descending | Acc]) + end. + +-file("src/gleam/list.gleam", 1457). +?DOC( + " This is exactly the same as merge_ascendings but mirrored: it merges two\n" + " lists sorted in descending order into a single list sorted in ascending\n" + " order according to the given comparator function.\n" + "\n" + " This reversing of the sort order is not avoidable if we want to implement\n" + " merge as a tail recursive function. We could reverse the accumulator before\n" + " returning it but that would end up being less efficient; so the merging\n" + " algorithm has to play around this.\n" +). +-spec merge_descendings( + list(AJL), + list(AJL), + fun((AJL, AJL) -> gleam@order:order()), + list(AJL) +) -> list(AJL). +merge_descendings(List1, List2, Compare, Acc) -> + case {List1, List2} of + {[], List} -> + lists:reverse(List, Acc); + + {List, []} -> + lists:reverse(List, Acc); + + {[First1 | Rest1], [First2 | Rest2]} -> + case Compare(First1, First2) of + lt -> + merge_descendings(List1, Rest2, Compare, [First2 | Acc]); + + gt -> + merge_descendings(Rest1, List2, Compare, [First1 | Acc]); + + eq -> + merge_descendings(Rest1, List2, Compare, [First1 | Acc]) + end + end. + +-file("src/gleam/list.gleam", 1405). +?DOC(" This is the same as merge_ascending_pairs but flipped for descending lists.\n"). +-spec merge_descending_pairs( + list(list(AJA)), + fun((AJA, AJA) -> gleam@order:order()), + list(list(AJA)) +) -> list(list(AJA)). +merge_descending_pairs(Sequences, Compare, Acc) -> + case Sequences of + [] -> + lists:reverse(Acc); + + [Sequence] -> + lists:reverse([lists:reverse(Sequence) | Acc]); + + [Descending1, Descending2 | Rest] -> + Ascending = merge_descendings(Descending1, Descending2, Compare, []), + merge_descending_pairs(Rest, Compare, [Ascending | Acc]) + end. + +-file("src/gleam/list.gleam", 1349). +?DOC( + " Given some some sorted sequences (assumed to be sorted in `direction`) it\n" + " merges them all together until we're left with just a list sorted in\n" + " ascending order.\n" +). +-spec merge_all( + list(list(AIQ)), + sorting(), + fun((AIQ, AIQ) -> gleam@order:order()) +) -> list(AIQ). +merge_all(Sequences, Direction, Compare) -> + case {Sequences, Direction} of + {[], _} -> + []; + + {[Sequence], ascending} -> + Sequence; + + {[Sequence@1], descending} -> + lists:reverse(Sequence@1); + + {_, ascending} -> + Sequences@1 = merge_ascending_pairs(Sequences, Compare, []), + merge_all(Sequences@1, descending, Compare); + + {_, descending} -> + Sequences@2 = merge_descending_pairs(Sequences, Compare, []), + merge_all(Sequences@2, ascending, Compare) + end. + +-file("src/gleam/list.gleam", 1220). +?DOC( + " Sorts from smallest to largest based upon the ordering specified by a given\n" + " function.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " sort([4, 3, 6, 5, 4, 1, 2], by: int.compare)\n" + " // -> [1, 2, 3, 4, 4, 5, 6]\n" + " ```\n" +). +-spec sort(list(AIG), fun((AIG, AIG) -> gleam@order:order())) -> list(AIG). +sort(List, Compare) -> + case List of + [] -> + []; + + [X] -> + [X]; + + [X@1, Y | Rest] -> + Direction = case Compare(X@1, Y) of + lt -> + ascending; + + eq -> + ascending; + + gt -> + descending + end, + Sequences = sequences(Rest, Compare, [X@1], Direction, Y, []), + merge_all(Sequences, ascending, Compare) + end. + +-file("src/gleam/list.gleam", 1497). +-spec range_loop(integer(), integer(), list(integer())) -> list(integer()). +range_loop(Start, Stop, Acc) -> + case gleam@int:compare(Start, Stop) of + eq -> + [Stop | Acc]; + + gt -> + range_loop(Start, Stop + 1, [Stop | Acc]); + + lt -> + range_loop(Start, Stop - 1, [Stop | Acc]) + end. + +-file("src/gleam/list.gleam", 1493). +?DOC( + " Creates a list of ints ranging from a given start and finish.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " range(0, 0)\n" + " // -> [0]\n" + " ```\n" + "\n" + " ```gleam\n" + " range(0, 5)\n" + " // -> [0, 1, 2, 3, 4, 5]\n" + " ```\n" + "\n" + " ```gleam\n" + " range(1, -5)\n" + " // -> [1, 0, -1, -2, -3, -4, -5]\n" + " ```\n" +). +-spec range(integer(), integer()) -> list(integer()). +range(Start, Stop) -> + range_loop(Start, Stop, []). + +-file("src/gleam/list.gleam", 1523). +-spec repeat_loop(AJV, integer(), list(AJV)) -> list(AJV). +repeat_loop(Item, Times, Acc) -> + case Times =< 0 of + true -> + Acc; + + false -> + repeat_loop(Item, Times - 1, [Item | Acc]) + end. + +-file("src/gleam/list.gleam", 1519). +?DOC( + " Builds a list of a given value a given number of times.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " repeat(\"a\", times: 0)\n" + " // -> []\n" + " ```\n" + "\n" + " ```gleam\n" + " repeat(\"a\", times: 5)\n" + " // -> [\"a\", \"a\", \"a\", \"a\", \"a\"]\n" + " ```\n" +). +-spec repeat(AJT, integer()) -> list(AJT). +repeat(A, Times) -> + repeat_loop(A, Times, []). + +-file("src/gleam/list.gleam", 1556). +-spec split_loop(list(AKC), integer(), list(AKC)) -> {list(AKC), list(AKC)}. +split_loop(List, N, Taken) -> + case N =< 0 of + true -> + {lists:reverse(Taken), List}; + + false -> + case List of + [] -> + {lists:reverse(Taken), []}; + + [First | Rest] -> + split_loop(Rest, N - 1, [First | Taken]) + end + end. + +-file("src/gleam/list.gleam", 1552). +?DOC( + " Splits a list in two before the given index.\n" + "\n" + " If the list is not long enough to have the given index the before list will\n" + " be the input list, and the after list will be empty.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " split([6, 7, 8, 9], 0)\n" + " // -> #([], [6, 7, 8, 9])\n" + " ```\n" + "\n" + " ```gleam\n" + " split([6, 7, 8, 9], 2)\n" + " // -> #([6, 7], [8, 9])\n" + " ```\n" + "\n" + " ```gleam\n" + " split([6, 7, 8, 9], 4)\n" + " // -> #([6, 7, 8, 9], [])\n" + " ```\n" +). +-spec split(list(AJY), integer()) -> {list(AJY), list(AJY)}. +split(List, Index) -> + split_loop(List, Index, []). + +-file("src/gleam/list.gleam", 1592). +-spec split_while_loop(list(AKL), fun((AKL) -> boolean()), list(AKL)) -> {list(AKL), + list(AKL)}. +split_while_loop(List, F, Acc) -> + case List of + [] -> + {lists:reverse(Acc), []}; + + [First | Rest] -> + case F(First) of + true -> + split_while_loop(Rest, F, [First | Acc]); + + false -> + {lists:reverse(Acc), List} + end + end. + +-file("src/gleam/list.gleam", 1585). +?DOC( + " Splits a list in two before the first element that a given function returns\n" + " `False` for.\n" + "\n" + " If the function returns `True` for all elements the first list will be the\n" + " input list, and the second list will be empty.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " split_while([1, 2, 3, 4, 5], fn(x) { x <= 3 })\n" + " // -> #([1, 2, 3], [4, 5])\n" + " ```\n" + "\n" + " ```gleam\n" + " split_while([1, 2, 3, 4, 5], fn(x) { x <= 5 })\n" + " // -> #([1, 2, 3, 4, 5], [])\n" + " ```\n" +). +-spec split_while(list(AKH), fun((AKH) -> boolean())) -> {list(AKH), list(AKH)}. +split_while(List, Predicate) -> + split_while_loop(List, Predicate, []). + +-file("src/gleam/list.gleam", 1632). +?DOC( + " Given a list of 2-element tuples, finds the first tuple that has a given\n" + " key as the first element and returns the second element.\n" + "\n" + " If no tuple is found with the given key then `Error(Nil)` is returned.\n" + "\n" + " This function may be useful for interacting with Erlang code where lists of\n" + " tuples are common.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " key_find([#(\"a\", 0), #(\"b\", 1)], \"a\")\n" + " // -> Ok(0)\n" + " ```\n" + "\n" + " ```gleam\n" + " key_find([#(\"a\", 0), #(\"b\", 1)], \"b\")\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " key_find([#(\"a\", 0), #(\"b\", 1)], \"c\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec key_find(list({AKQ, AKR}), AKQ) -> {ok, AKR} | {error, nil}. +key_find(Keyword_list, Desired_key) -> + find_map( + Keyword_list, + fun(Keyword) -> + {Key, Value} = Keyword, + case Key =:= Desired_key of + true -> + {ok, Value}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/list.gleam", 1663). +?DOC( + " Given a list of 2-element tuples, finds all tuples that have a given\n" + " key as the first element and returns the second element.\n" + "\n" + " This function may be useful for interacting with Erlang code where lists of\n" + " tuples are common.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " key_filter([#(\"a\", 0), #(\"b\", 1), #(\"a\", 2)], \"a\")\n" + " // -> [0, 2]\n" + " ```\n" + "\n" + " ```gleam\n" + " key_filter([#(\"a\", 0), #(\"b\", 1)], \"c\")\n" + " // -> []\n" + " ```\n" +). +-spec key_filter(list({AKV, AKW}), AKV) -> list(AKW). +key_filter(Keyword_list, Desired_key) -> + filter_map( + Keyword_list, + fun(Keyword) -> + {Key, Value} = Keyword, + case Key =:= Desired_key of + true -> + {ok, Value}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/list.gleam", 1703). +-spec key_pop_loop(list({ALF, ALG}), ALF, list({ALF, ALG})) -> {ok, + {ALG, list({ALF, ALG})}} | + {error, nil}. +key_pop_loop(List, Key, Checked) -> + case List of + [] -> + {error, nil}; + + [{K, V} | Rest] when K =:= Key -> + {ok, {V, lists:reverse(Checked, Rest)}}; + + [First | Rest@1] -> + key_pop_loop(Rest@1, Key, [First | Checked]) + end. + +-file("src/gleam/list.gleam", 1699). +?DOC( + " Given a list of 2-element tuples, finds the first tuple that has a given\n" + " key as the first element. This function will return the second element\n" + " of the found tuple and list with tuple removed.\n" + "\n" + " If no tuple is found with the given key then `Error(Nil)` is returned.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " key_pop([#(\"a\", 0), #(\"b\", 1)], \"a\")\n" + " // -> Ok(#(0, [#(\"b\", 1)]))\n" + " ```\n" + "\n" + " ```gleam\n" + " key_pop([#(\"a\", 0), #(\"b\", 1)], \"b\")\n" + " // -> Ok(#(1, [#(\"a\", 0)]))\n" + " ```\n" + "\n" + " ```gleam\n" + " key_pop([#(\"a\", 0), #(\"b\", 1)], \"c\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec key_pop(list({AKZ, ALA}), AKZ) -> {ok, {ALA, list({AKZ, ALA})}} | + {error, nil}. +key_pop(List, Key) -> + key_pop_loop(List, Key, []). + +-file("src/gleam/list.gleam", 1737). +-spec key_set_loop(list({ALQ, ALR}), ALQ, ALR, list({ALQ, ALR})) -> list({ALQ, + ALR}). +key_set_loop(List, Key, Value, Inspected) -> + case List of + [{K, _} | Rest] when K =:= Key -> + lists:reverse(Inspected, [{K, Value} | Rest]); + + [First | Rest@1] -> + key_set_loop(Rest@1, Key, Value, [First | Inspected]); + + [] -> + lists:reverse([{Key, Value} | Inspected]) + end. + +-file("src/gleam/list.gleam", 1733). +?DOC( + " Given a list of 2-element tuples, inserts a key and value into the list.\n" + "\n" + " If there was already a tuple with the key then it is replaced, otherwise it\n" + " is added to the end of the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " key_set([#(5, 0), #(4, 1)], 4, 100)\n" + " // -> [#(5, 0), #(4, 100)]\n" + " ```\n" + "\n" + " ```gleam\n" + " key_set([#(5, 0), #(4, 1)], 1, 100)\n" + " // -> [#(5, 0), #(4, 1), #(1, 100)]\n" + " ```\n" +). +-spec key_set(list({ALM, ALN}), ALM, ALN) -> list({ALM, ALN}). +key_set(List, Key, Value) -> + key_set_loop(List, Key, Value, []). + +-file("src/gleam/list.gleam", 1765). +?DOC( + " Calls a function for each element in a list, discarding the return value.\n" + "\n" + " Useful for calling a side effect for every item of a list.\n" + "\n" + " ```gleam\n" + " import gleam/io\n" + "\n" + " each([\"1\", \"2\", \"3\"], io.println)\n" + " // -> Nil\n" + " // 1\n" + " // 2\n" + " // 3\n" + " ```\n" +). +-spec each(list(ALV), fun((ALV) -> any())) -> nil. +each(List, F) -> + case List of + [] -> + nil; + + [First | Rest] -> + F(First), + each(Rest, F) + end. + +-file("src/gleam/list.gleam", 1791). +?DOC( + " Calls a `Result` returning function for each element in a list, discarding\n" + " the return value. If the function returns `Error` then the iteration is\n" + " stopped and the error is returned.\n" + "\n" + " Useful for calling a side effect for every item of a list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " try_each(\n" + " over: [1, 2, 3],\n" + " with: function_that_might_fail,\n" + " )\n" + " // -> Ok(Nil)\n" + " ```\n" +). +-spec try_each(list(ALY), fun((ALY) -> {ok, any()} | {error, AMB})) -> {ok, nil} | + {error, AMB}. +try_each(List, Fun) -> + case List of + [] -> + {ok, nil}; + + [First | Rest] -> + case Fun(First) of + {ok, _} -> + try_each(Rest, Fun); + + {error, E} -> + {error, E} + end + end. + +-file("src/gleam/list.gleam", 1824). +-spec partition_loop(list(BFR), fun((BFR) -> boolean()), list(BFR), list(BFR)) -> {list(BFR), + list(BFR)}. +partition_loop(List, Categorise, Trues, Falses) -> + case List of + [] -> + {lists:reverse(Trues), lists:reverse(Falses)}; + + [First | Rest] -> + case Categorise(First) of + true -> + partition_loop(Rest, Categorise, [First | Trues], Falses); + + false -> + partition_loop(Rest, Categorise, Trues, [First | Falses]) + end + end. + +-file("src/gleam/list.gleam", 1817). +?DOC( + " Partitions a list into a tuple/pair of lists\n" + " by a given categorisation function.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " [1, 2, 3, 4, 5] |> partition(int.is_odd)\n" + " // -> #([1, 3, 5], [2, 4])\n" + " ```\n" +). +-spec partition(list(AMG), fun((AMG) -> boolean())) -> {list(AMG), list(AMG)}. +partition(List, Categorise) -> + partition_loop(List, Categorise, [], []). + +-file("src/gleam/list.gleam", 1904). +-spec window_loop(list(list(ANN)), list(ANN), integer()) -> list(list(ANN)). +window_loop(Acc, List, N) -> + Window = take(List, N), + case erlang:length(Window) =:= N of + true -> + window_loop([Window | Acc], drop(List, 1), N); + + false -> + lists:reverse(Acc) + end. + +-file("src/gleam/list.gleam", 1897). +?DOC( + " Returns a list of sliding windows.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " window([1,2,3,4,5], 3)\n" + " // -> [[1, 2, 3], [2, 3, 4], [3, 4, 5]]\n" + " ```\n" + "\n" + " ```gleam\n" + " window([1, 2], 4)\n" + " // -> []\n" + " ```\n" +). +-spec window(list(ANJ), integer()) -> list(list(ANJ)). +window(List, N) -> + case N =< 0 of + true -> + []; + + false -> + window_loop([], List, N) + end. + +-file("src/gleam/list.gleam", 1927). +?DOC( + " Returns a list of tuples containing two contiguous elements.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " window_by_2([1,2,3,4])\n" + " // -> [#(1, 2), #(2, 3), #(3, 4)]\n" + " ```\n" + "\n" + " ```gleam\n" + " window_by_2([1])\n" + " // -> []\n" + " ```\n" +). +-spec window_by_2(list(ANT)) -> list({ANT, ANT}). +window_by_2(List) -> + zip(List, drop(List, 1)). + +-file("src/gleam/list.gleam", 1940). +?DOC( + " Drops the first elements in a given list for which the predicate function returns `True`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " drop_while([1, 2, 3, 4], fn (x) { x < 3 })\n" + " // -> [3, 4]\n" + " ```\n" +). +-spec drop_while(list(ANW), fun((ANW) -> boolean())) -> list(ANW). +drop_while(List, Predicate) -> + case List of + [] -> + []; + + [First | Rest] -> + case Predicate(First) of + true -> + drop_while(Rest, Predicate); + + false -> + [First | Rest] + end + end. + +-file("src/gleam/list.gleam", 1970). +-spec take_while_loop(list(AOC), fun((AOC) -> boolean()), list(AOC)) -> list(AOC). +take_while_loop(List, Predicate, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + case Predicate(First) of + true -> + take_while_loop(Rest, Predicate, [First | Acc]); + + false -> + lists:reverse(Acc) + end + end. + +-file("src/gleam/list.gleam", 1963). +?DOC( + " Takes the first elements in a given list for which the predicate function returns `True`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " take_while([1, 2, 3, 2, 4], fn (x) { x < 3 })\n" + " // -> [1, 2]\n" + " ```\n" +). +-spec take_while(list(ANZ), fun((ANZ) -> boolean())) -> list(ANZ). +take_while(List, Predicate) -> + take_while_loop(List, Predicate, []). + +-file("src/gleam/list.gleam", 2002). +-spec chunk_loop(list(AOL), fun((AOL) -> AON), AON, list(AOL), list(list(AOL))) -> list(list(AOL)). +chunk_loop(List, F, Previous_key, Current_chunk, Acc) -> + case List of + [First | Rest] -> + Key = F(First), + case Key =:= Previous_key of + true -> + chunk_loop(Rest, F, Key, [First | Current_chunk], Acc); + + false -> + New_acc = [lists:reverse(Current_chunk) | Acc], + chunk_loop(Rest, F, Key, [First], New_acc) + end; + + [] -> + lists:reverse([lists:reverse(Current_chunk) | Acc]) + end. + +-file("src/gleam/list.gleam", 1995). +?DOC( + " Returns a list of chunks in which\n" + " the return value of calling `f` on each element is the same.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [1, 2, 2, 3, 4, 4, 6, 7, 7] |> chunk(by: fn(n) { n % 2 })\n" + " // -> [[1], [2, 2], [3], [4, 4, 6], [7, 7]]\n" + " ```\n" +). +-spec chunk(list(AOG), fun((AOG) -> any())) -> list(list(AOG)). +chunk(List, F) -> + case List of + [] -> + []; + + [First | Rest] -> + chunk_loop(Rest, F, F(First), [First], []) + end. + +-file("src/gleam/list.gleam", 2047). +-spec sized_chunk_loop( + list(AOX), + integer(), + integer(), + list(AOX), + list(list(AOX)) +) -> list(list(AOX)). +sized_chunk_loop(List, Count, Left, Current_chunk, Acc) -> + case List of + [] -> + case Current_chunk of + [] -> + lists:reverse(Acc); + + Remaining -> + lists:reverse([lists:reverse(Remaining) | Acc]) + end; + + [First | Rest] -> + Chunk = [First | Current_chunk], + case Left > 1 of + true -> + sized_chunk_loop(Rest, Count, Left - 1, Chunk, Acc); + + false -> + sized_chunk_loop( + Rest, + Count, + Count, + [], + [lists:reverse(Chunk) | Acc] + ) + end + end. + +-file("src/gleam/list.gleam", 2043). +?DOC( + " Returns a list of chunks containing `count` elements each.\n" + "\n" + " If the last chunk does not have `count` elements, it is instead\n" + " a partial chunk, with less than `count` elements.\n" + "\n" + " For any `count` less than 1 this function behaves as if it was set to 1.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [1, 2, 3, 4, 5, 6] |> sized_chunk(into: 2)\n" + " // -> [[1, 2], [3, 4], [5, 6]]\n" + " ```\n" + "\n" + " ```gleam\n" + " [1, 2, 3, 4, 5, 6, 7, 8] |> sized_chunk(into: 3)\n" + " // -> [[1, 2, 3], [4, 5, 6], [7, 8]]\n" + " ```\n" +). +-spec sized_chunk(list(AOT), integer()) -> list(list(AOT)). +sized_chunk(List, Count) -> + sized_chunk_loop(List, Count, Count, [], []). + +-file("src/gleam/list.gleam", 2091). +?DOC( + " This function acts similar to fold, but does not take an initial state.\n" + " Instead, it starts from the first element in the list\n" + " and combines it with each subsequent element in turn using the given\n" + " function. The function is called as `fun(accumulator, current_element)`.\n" + "\n" + " Returns `Ok` to indicate a successful run, and `Error` if called on an\n" + " empty list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " [] |> reduce(fn(acc, x) { acc + x })\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " [1, 2, 3, 4, 5] |> reduce(fn(acc, x) { acc + x })\n" + " // -> Ok(15)\n" + " ```\n" +). +-spec reduce(list(APE), fun((APE, APE) -> APE)) -> {ok, APE} | {error, nil}. +reduce(List, Fun) -> + case List of + [] -> + {error, nil}; + + [First | Rest] -> + {ok, fold(Rest, First, Fun)} + end. + +-file("src/gleam/list.gleam", 2115). +-spec scan_loop(list(APM), APO, list(APO), fun((APO, APM) -> APO)) -> list(APO). +scan_loop(List, Accumulator, Accumulated, Fun) -> + case List of + [] -> + lists:reverse(Accumulated); + + [First | Rest] -> + Next = Fun(Accumulator, First), + scan_loop(Rest, Next, [Next | Accumulated], Fun) + end. + +-file("src/gleam/list.gleam", 2107). +?DOC( + " Similar to `fold`, but yields the state of the accumulator at each stage.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " scan(over: [1, 2, 3], from: 100, with: fn(acc, i) { acc + i })\n" + " // -> [101, 103, 106]\n" + " ```\n" +). +-spec scan(list(API), APK, fun((APK, API) -> APK)) -> list(APK). +scan(List, Initial, Fun) -> + scan_loop(List, Initial, [], Fun). + +-file("src/gleam/list.gleam", 2148). +?DOC( + " Returns the last element in the given list.\n" + "\n" + " Returns `Error(Nil)` if the list is empty.\n" + "\n" + " This function runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " last([])\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " last([1, 2, 3, 4, 5])\n" + " // -> Ok(5)\n" + " ```\n" +). +-spec last(list(APR)) -> {ok, APR} | {error, nil}. +last(List) -> + case List of + [] -> + {error, nil}; + + [Last] -> + {ok, Last}; + + [_ | Rest] -> + last(Rest) + end. + +-file("src/gleam/list.gleam", 2170). +?DOC( + " Return unique combinations of elements in the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " combinations([1, 2, 3], 2)\n" + " // -> [[1, 2], [1, 3], [2, 3]]\n" + " ```\n" + "\n" + " ```gleam\n" + " combinations([1, 2, 3, 4], 3)\n" + " // -> [[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]]\n" + " ```\n" +). +-spec combinations(list(APV), integer()) -> list(list(APV)). +combinations(Items, N) -> + case {N, Items} of + {0, _} -> + [[]]; + + {_, []} -> + []; + + {_, [First | Rest]} -> + _pipe = Rest, + _pipe@1 = combinations(_pipe, N - 1), + _pipe@2 = map( + _pipe@1, + fun(Combination) -> [First | Combination] end + ), + _pipe@3 = lists:reverse(_pipe@2), + fold(_pipe@3, combinations(Rest, N), fun(Acc, C) -> [C | Acc] end) + end. + +-file("src/gleam/list.gleam", 2196). +-spec combination_pairs_loop(list(AQC), list({AQC, AQC})) -> list({AQC, AQC}). +combination_pairs_loop(Items, Acc) -> + case Items of + [] -> + lists:reverse(Acc); + + [First | Rest] -> + First_combinations = map(Rest, fun(Other) -> {First, Other} end), + Acc@1 = lists:reverse(First_combinations, Acc), + combination_pairs_loop(Rest, Acc@1) + end. + +-file("src/gleam/list.gleam", 2192). +?DOC( + " Return unique pair combinations of elements in the list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " combination_pairs([1, 2, 3])\n" + " // -> [#(1, 2), #(1, 3), #(2, 3)]\n" + " ```\n" +). +-spec combination_pairs(list(APZ)) -> list({APZ, APZ}). +combination_pairs(Items) -> + combination_pairs_loop(Items, []). + +-file("src/gleam/list.gleam", 2252). +-spec take_firsts(list(list(AQW)), list(AQW), list(list(AQW))) -> {list(AQW), + list(list(AQW))}. +take_firsts(Rows, Column, Remaining_rows) -> + case Rows of + [] -> + {lists:reverse(Column), lists:reverse(Remaining_rows)}; + + [[] | Rest] -> + take_firsts(Rest, Column, Remaining_rows); + + [[First | Remaining_row] | Rest_rows] -> + Remaining_rows@1 = [Remaining_row | Remaining_rows], + take_firsts(Rest_rows, [First | Column], Remaining_rows@1) + end. + +-file("src/gleam/list.gleam", 2239). +-spec transpose_loop(list(list(AQP)), list(list(AQP))) -> list(list(AQP)). +transpose_loop(Rows, Columns) -> + case Rows of + [] -> + lists:reverse(Columns); + + _ -> + {Column, Rest} = take_firsts(Rows, [], []), + case Column of + [_ | _] -> + transpose_loop(Rest, [Column | Columns]); + + [] -> + transpose_loop(Rest, Columns) + end + end. + +-file("src/gleam/list.gleam", 2235). +?DOC( + " Transpose rows and columns of the list of lists.\n" + "\n" + " Notice: This function is not tail recursive,\n" + " and thus may exceed stack size if called,\n" + " with large lists (on the JavaScript target).\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " transpose([[1, 2, 3], [101, 102, 103]])\n" + " // -> [[1, 101], [2, 102], [3, 103]]\n" + " ```\n" +). +-spec transpose(list(list(AQK))) -> list(list(AQK)). +transpose(List_of_lists) -> + transpose_loop(List_of_lists, []). + +-file("src/gleam/list.gleam", 2216). +?DOC( + " Make a list alternating the elements from the given lists\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " interleave([[1, 2], [101, 102], [201, 202]])\n" + " // -> [1, 101, 201, 2, 102, 202]\n" + " ```\n" +). +-spec interleave(list(list(AQG))) -> list(AQG). +interleave(List) -> + _pipe = List, + _pipe@1 = transpose(_pipe), + lists:append(_pipe@1). + +-file("src/gleam/list.gleam", 2285). +-spec shuffle_pair_unwrap_loop(list({float(), ARI}), list(ARI)) -> list(ARI). +shuffle_pair_unwrap_loop(List, Acc) -> + case List of + [] -> + Acc; + + [Elem_pair | Enumerable] -> + shuffle_pair_unwrap_loop( + Enumerable, + [erlang:element(2, Elem_pair) | Acc] + ) + end. + +-file("src/gleam/list.gleam", 2293). +-spec do_shuffle_by_pair_indexes(list({float(), ARM})) -> list({float(), ARM}). +do_shuffle_by_pair_indexes(List_of_pairs) -> + sort( + List_of_pairs, + fun(A_pair, B_pair) -> + gleam@float:compare( + erlang:element(1, A_pair), + erlang:element(1, B_pair) + ) + end + ). + +-file("src/gleam/list.gleam", 2278). +?DOC( + " Takes a list, randomly sorts all items and returns the shuffled list.\n" + "\n" + " This function uses `float.random` to decide the order of the elements.\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " range(1, 10) |> shuffle()\n" + " // -> [1, 6, 9, 10, 3, 8, 4, 2, 7, 5]\n" + " ```\n" +). +-spec shuffle(list(ARF)) -> list(ARF). +shuffle(List) -> + _pipe = List, + _pipe@1 = fold(_pipe, [], fun(Acc, A) -> [{rand:uniform(), A} | Acc] end), + _pipe@2 = do_shuffle_by_pair_indexes(_pipe@1), + shuffle_pair_unwrap_loop(_pipe@2, []). + +-file("src/gleam/list.gleam", 2325). +-spec max_loop(list(ARW), fun((ARW, ARW) -> gleam@order:order()), ARW) -> ARW. +max_loop(List, Compare, Max) -> + case List of + [] -> + Max; + + [First | Rest] -> + case Compare(First, Max) of + gt -> + max_loop(Rest, Compare, First); + + lt -> + max_loop(Rest, Compare, Max); + + eq -> + max_loop(Rest, Compare, Max) + end + end. + +-file("src/gleam/list.gleam", 2315). +?DOC( + " Takes a list and a comparator, and returns the maximum element in the list\n" + "\n" + "\n" + " ## Example\n" + "\n" + " ```gleam\n" + " range(1, 10) |> list.max(int.compare)\n" + " // -> Ok(10)\n" + " ```\n" + "\n" + " ```gleam\n" + " [\"a\", \"c\", \"b\"] |> list.max(string.compare)\n" + " // -> Ok(\"c\")\n" + " ```\n" +). +-spec max(list(ARP), fun((ARP, ARP) -> gleam@order:order())) -> {ok, ARP} | + {error, nil}. +max(List, Compare) -> + case List of + [] -> + {error, nil}; + + [First | Rest] -> + {ok, max_loop(Rest, Compare, First)} + end. + +-file("src/gleam/list.gleam", 2406). +-spec build_reservoir_loop( + list(ASL), + integer(), + gleam@dict:dict(integer(), ASL) +) -> {gleam@dict:dict(integer(), ASL), list(ASL)}. +build_reservoir_loop(List, Size, Reservoir) -> + Reservoir_size = maps:size(Reservoir), + case Reservoir_size >= Size of + true -> + {Reservoir, List}; + + false -> + case List of + [] -> + {Reservoir, []}; + + [First | Rest] -> + Reservoir@1 = gleam@dict:insert( + Reservoir, + Reservoir_size, + First + ), + build_reservoir_loop(Rest, Size, Reservoir@1) + end + end. + +-file("src/gleam/list.gleam", 2402). +?DOC( + " Builds the initial reservoir used by Algorithm L.\n" + " This is a dictionary with keys ranging from `0` up to `n - 1` where each\n" + " value is the corresponding element at that position in `list`.\n" + "\n" + " This also returns the remaining elements of `list` that didn't end up in\n" + " the reservoir.\n" +). +-spec build_reservoir(list(ASG), integer()) -> {gleam@dict:dict(integer(), ASG), + list(ASG)}. +build_reservoir(List, N) -> + build_reservoir_loop(List, N, maps:new()). + +-file("src/gleam/list.gleam", 2390). +-spec log_random() -> float(). +log_random() -> + Random@1 = case gleam@float:logarithm( + rand:uniform() + 2.2250738585072014e-308 + ) of + {ok, Random} -> Random; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam/list"/utf8>>, + function => <<"log_random"/utf8>>, + line => 2391, + value => _assert_fail, + start => 56078, + 'end' => 56149, + pattern_start => 56089, + pattern_end => 56099}) + end, + Random@1. + +-file("src/gleam/list.gleam", 2367). +-spec sample_loop( + list(ASA), + gleam@dict:dict(integer(), ASA), + integer(), + float() +) -> gleam@dict:dict(integer(), ASA). +sample_loop(List, Reservoir, N, W) -> + Skip = begin + Log@1 = case gleam@float:logarithm(1.0 - W) of + {ok, Log} -> Log; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Pattern match failed, no pattern matched the value."/utf8>>, + file => <>, + module => <<"gleam/list"/utf8>>, + function => <<"sample_loop"/utf8>>, + line => 2374, + value => _assert_fail, + start => 55639, + 'end' => 55685, + pattern_start => 55650, + pattern_end => 55657}) + end, + erlang:round(math:floor(case Log@1 of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> log_random() / Gleam@denominator + end)) + end, + case drop(List, Skip) of + [] -> + Reservoir; + + [First | Rest] -> + Reservoir@1 = gleam@dict:insert( + Reservoir, + gleam@int:random(N), + First + ), + W@1 = W * math:exp(case erlang:float(N) of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator@1 -> log_random() / Gleam@denominator@1 + end), + sample_loop(Rest, Reservoir@1, N, W@1) + end. + +-file("src/gleam/list.gleam", 2349). +?DOC( + " Returns a random sample of up to n elements from a list using reservoir\n" + " sampling via [Algorithm L](https://en.wikipedia.org/wiki/Reservoir_sampling#Optimal:_Algorithm_L).\n" + " Returns an empty list if the sample size is less than or equal to 0.\n" + "\n" + " Order is not random, only selection is.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " reservoir_sample([1, 2, 3, 4, 5], 3)\n" + " // -> [2, 4, 5] // A random sample of 3 items\n" + " ```\n" +). +-spec sample(list(ARX), integer()) -> list(ARX). +sample(List, N) -> + {Reservoir, Rest} = build_reservoir(List, N), + case gleam@dict:is_empty(Reservoir) of + true -> + []; + + false -> + W = math:exp(case erlang:float(N) of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> log_random() / Gleam@denominator + end), + maps:values(sample_loop(Rest, Reservoir, N, W)) + end. + +-file("src/gleam/list.gleam", 1851). +-spec permutation_zip(list(AMT), list(AMT), list(list(AMT))) -> list(list(AMT)). +permutation_zip(List, Rest, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [Head | Tail] -> + permutation_prepend( + Head, + permutations(lists:reverse(Rest, Tail)), + Tail, + [Head | Rest], + Acc + ) + end. + +-file("src/gleam/list.gleam", 1869). +-spec permutation_prepend( + ANA, + list(list(ANA)), + list(ANA), + list(ANA), + list(list(ANA)) +) -> list(list(ANA)). +permutation_prepend(El, Permutations, List_1, List_2, Acc) -> + case Permutations of + [] -> + permutation_zip(List_1, List_2, Acc); + + [Head | Tail] -> + permutation_prepend(El, Tail, List_1, List_2, [[El | Head] | Acc]) + end. + +-file("src/gleam/list.gleam", 1844). +?DOC( + " Returns all the permutations of a list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " permutations([1, 2])\n" + " // -> [[1, 2], [2, 1]]\n" + " ```\n" +). +-spec permutations(list(AMP)) -> list(list(AMP)). +permutations(List) -> + case List of + [] -> + [[]]; + + L -> + permutation_zip(L, [], []) + end. diff --git a/build/packages/gleam_stdlib/src/gleam@option.erl b/build/packages/gleam_stdlib/src/gleam@option.erl new file mode 100644 index 0000000..8e86a8e --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@option.erl @@ -0,0 +1,413 @@ +-module(gleam@option). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/option.gleam"). +-export([all/1, is_some/1, is_none/1, to_result/2, from_result/1, unwrap/2, lazy_unwrap/2, map/2, flatten/1, then/2, 'or'/2, lazy_or/2, values/1]). +-export_type([option/1]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type option(FG) :: {some, FG} | none. + +-file("src/gleam/option.gleam", 59). +-spec reverse_and_prepend(list(FV), list(FV)) -> list(FV). +reverse_and_prepend(Prefix, Suffix) -> + case Prefix of + [] -> + Suffix; + + [First | Rest] -> + reverse_and_prepend(Rest, [First | Suffix]) + end. + +-file("src/gleam/option.gleam", 44). +-spec all_loop(list(option(FM)), list(FM)) -> option(list(FM)). +all_loop(List, Acc) -> + case List of + [] -> + {some, lists:reverse(Acc)}; + + [none | _] -> + none; + + [{some, First} | Rest] -> + all_loop(Rest, [First | Acc]) + end. + +-file("src/gleam/option.gleam", 40). +?DOC( + " Combines a list of `Option`s into a single `Option`.\n" + " If all elements in the list are `Some` then returns a `Some` holding the list of values.\n" + " If any element is `None` then returns`None`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " all([Some(1), Some(2)])\n" + " // -> Some([1, 2])\n" + " ```\n" + "\n" + " ```gleam\n" + " all([Some(1), None])\n" + " // -> None\n" + " ```\n" +). +-spec all(list(option(FH))) -> option(list(FH)). +all(List) -> + all_loop(List, []). + +-file("src/gleam/option.gleam", 80). +?DOC( + " Checks whether the `Option` is a `Some` value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_some(Some(1))\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_some(None)\n" + " // -> False\n" + " ```\n" +). +-spec is_some(option(any())) -> boolean(). +is_some(Option) -> + Option /= none. + +-file("src/gleam/option.gleam", 98). +?DOC( + " Checks whether the `Option` is a `None` value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_none(Some(1))\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " is_none(None)\n" + " // -> True\n" + " ```\n" +). +-spec is_none(option(any())) -> boolean(). +is_none(Option) -> + Option =:= none. + +-file("src/gleam/option.gleam", 116). +?DOC( + " Converts an `Option` type to a `Result` type.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_result(Some(1), \"some_error\")\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " to_result(None, \"some_error\")\n" + " // -> Error(\"some_error\")\n" + " ```\n" +). +-spec to_result(option(GD), GG) -> {ok, GD} | {error, GG}. +to_result(Option, E) -> + case Option of + {some, A} -> + {ok, A}; + + none -> + {error, E} + end. + +-file("src/gleam/option.gleam", 137). +?DOC( + " Converts a `Result` type to an `Option` type.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_result(Ok(1))\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " from_result(Error(\"some_error\"))\n" + " // -> None\n" + " ```\n" +). +-spec from_result({ok, GJ} | {error, any()}) -> option(GJ). +from_result(Result) -> + case Result of + {ok, A} -> + {some, A}; + + {error, _} -> + none + end. + +-file("src/gleam/option.gleam", 158). +?DOC( + " Extracts the value from an `Option`, returning a default value if there is none.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " unwrap(Some(1), 0)\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " unwrap(None, 0)\n" + " // -> 0\n" + " ```\n" +). +-spec unwrap(option(GO), GO) -> GO. +unwrap(Option, Default) -> + case Option of + {some, X} -> + X; + + none -> + Default + end. + +-file("src/gleam/option.gleam", 179). +?DOC( + " Extracts the value from an `Option`, evaluating the default function if the option is `None`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " lazy_unwrap(Some(1), fn() { 0 })\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_unwrap(None, fn() { 0 })\n" + " // -> 0\n" + " ```\n" +). +-spec lazy_unwrap(option(GQ), fun(() -> GQ)) -> GQ. +lazy_unwrap(Option, Default) -> + case Option of + {some, X} -> + X; + + none -> + Default() + end. + +-file("src/gleam/option.gleam", 204). +?DOC( + " Updates a value held within the `Some` of an `Option` by calling a given function\n" + " on it.\n" + "\n" + " If the `Option` is a `None` rather than `Some`, the function is not called and the\n" + " `Option` stays the same.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map(over: Some(1), with: fn(x) { x + 1 })\n" + " // -> Some(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " map(over: None, with: fn(x) { x + 1 })\n" + " // -> None\n" + " ```\n" +). +-spec map(option(GS), fun((GS) -> GU)) -> option(GU). +map(Option, Fun) -> + case Option of + {some, X} -> + {some, Fun(X)}; + + none -> + none + end. + +-file("src/gleam/option.gleam", 230). +?DOC( + " Merges a nested `Option` into a single layer.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " flatten(Some(Some(1)))\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " flatten(Some(None))\n" + " // -> None\n" + " ```\n" + "\n" + " ```gleam\n" + " flatten(None)\n" + " // -> None\n" + " ```\n" +). +-spec flatten(option(option(GW))) -> option(GW). +flatten(Option) -> + case Option of + {some, X} -> + X; + + none -> + none + end. + +-file("src/gleam/option.gleam", 269). +?DOC( + " Updates a value held within the `Some` of an `Option` by calling a given function\n" + " on it, where the given function also returns an `Option`. The two options are\n" + " then merged together into one `Option`.\n" + "\n" + " If the `Option` is a `None` rather than `Some` the function is not called and the\n" + " option stays the same.\n" + "\n" + " This function is the equivalent of calling `map` followed by `flatten`, and\n" + " it is useful for chaining together multiple functions that return `Option`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " then(Some(1), fn(x) { Some(x + 1) })\n" + " // -> Some(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " then(Some(1), fn(x) { Some(#(\"a\", x)) })\n" + " // -> Some(#(\"a\", 1))\n" + " ```\n" + "\n" + " ```gleam\n" + " then(Some(1), fn(_) { None })\n" + " // -> None\n" + " ```\n" + "\n" + " ```gleam\n" + " then(None, fn(x) { Some(x + 1) })\n" + " // -> None\n" + " ```\n" +). +-spec then(option(HA), fun((HA) -> option(HC))) -> option(HC). +then(Option, Fun) -> + case Option of + {some, X} -> + Fun(X); + + none -> + none + end. + +-file("src/gleam/option.gleam", 300). +?DOC( + " Returns the first value if it is `Some`, otherwise returns the second value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " or(Some(1), Some(2))\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(Some(1), None)\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(None, Some(2))\n" + " // -> Some(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(None, None)\n" + " // -> None\n" + " ```\n" +). +-spec 'or'(option(HF), option(HF)) -> option(HF). +'or'(First, Second) -> + case First of + {some, _} -> + First; + + none -> + Second + end. + +-file("src/gleam/option.gleam", 331). +?DOC( + " Returns the first value if it is `Some`, otherwise evaluates the given function for a fallback value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " lazy_or(Some(1), fn() { Some(2) })\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(Some(1), fn() { None })\n" + " // -> Some(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(None, fn() { Some(2) })\n" + " // -> Some(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(None, fn() { None })\n" + " // -> None\n" + " ```\n" +). +-spec lazy_or(option(HJ), fun(() -> option(HJ))) -> option(HJ). +lazy_or(First, Second) -> + case First of + {some, _} -> + First; + + none -> + Second() + end. + +-file("src/gleam/option.gleam", 352). +-spec values_loop(list(option(HR)), list(HR)) -> list(HR). +values_loop(List, Acc) -> + case List of + [] -> + lists:reverse(Acc); + + [none | Rest] -> + values_loop(Rest, Acc); + + [{some, First} | Rest@1] -> + values_loop(Rest@1, [First | Acc]) + end. + +-file("src/gleam/option.gleam", 348). +?DOC( + " Given a list of `Option`s,\n" + " returns only the values inside `Some`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " values([Some(1), None, Some(3)])\n" + " // -> [1, 3]\n" + " ```\n" +). +-spec values(list(option(HN))) -> list(HN). +values(Options) -> + values_loop(Options, []). diff --git a/build/packages/gleam_stdlib/src/gleam@order.erl b/build/packages/gleam_stdlib/src/gleam@order.erl new file mode 100644 index 0000000..ec2bb84 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@order.erl @@ -0,0 +1,200 @@ +-module(gleam@order). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/order.gleam"). +-export([negate/1, to_int/1, compare/2, reverse/1, break_tie/2, lazy_break_tie/2]). +-export_type([order/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type order() :: lt | eq | gt. + +-file("src/gleam/order.gleam", 35). +?DOC( + " Inverts an order, so less-than becomes greater-than and greater-than\n" + " becomes less-than.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " negate(Lt)\n" + " // -> Gt\n" + " ```\n" + "\n" + " ```gleam\n" + " negate(Eq)\n" + " // -> Eq\n" + " ```\n" + "\n" + " ```gleam\n" + " negate(Gt)\n" + " // -> Lt\n" + " ```\n" +). +-spec negate(order()) -> order(). +negate(Order) -> + case Order of + lt -> + gt; + + eq -> + eq; + + gt -> + lt + end. + +-file("src/gleam/order.gleam", 62). +?DOC( + " Produces a numeric representation of the order.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_int(Lt)\n" + " // -> -1\n" + " ```\n" + "\n" + " ```gleam\n" + " to_int(Eq)\n" + " // -> 0\n" + " ```\n" + "\n" + " ```gleam\n" + " to_int(Gt)\n" + " // -> 1\n" + " ```\n" +). +-spec to_int(order()) -> integer(). +to_int(Order) -> + case Order of + lt -> + -1; + + eq -> + 0; + + gt -> + 1 + end. + +-file("src/gleam/order.gleam", 79). +?DOC( + " Compares two `Order` values to one another, producing a new `Order`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " compare(Eq, with: Lt)\n" + " // -> Gt\n" + " ```\n" +). +-spec compare(order(), order()) -> order(). +compare(A, B) -> + case {A, B} of + {X, Y} when X =:= Y -> + eq; + + {lt, _} -> + lt; + + {eq, gt} -> + lt; + + {_, _} -> + gt + end. + +-file("src/gleam/order.gleam", 100). +?DOC( + " Inverts an ordering function, so less-than becomes greater-than and greater-than\n" + " becomes less-than.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + " import gleam/list\n" + "\n" + " list.sort([1, 5, 4], by: reverse(int.compare))\n" + " // -> [5, 4, 1]\n" + " ```\n" +). +-spec reverse(fun((I, I) -> order())) -> fun((I, I) -> order()). +reverse(Orderer) -> + fun(A, B) -> Orderer(B, A) end. + +-file("src/gleam/order.gleam", 122). +?DOC( + " Return a fallback `Order` in case the first argument is `Eq`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " break_tie(in: int.compare(1, 1), with: Lt)\n" + " // -> Lt\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " break_tie(in: int.compare(1, 0), with: Eq)\n" + " // -> Gt\n" + " ```\n" +). +-spec break_tie(order(), order()) -> order(). +break_tie(Order, Other) -> + case Order of + lt -> + Order; + + gt -> + Order; + + eq -> + Other + end. + +-file("src/gleam/order.gleam", 151). +?DOC( + " Invokes a fallback function returning an `Order` in case the first argument\n" + " is `Eq`.\n" + "\n" + " This can be useful when the fallback comparison might be expensive and it\n" + " needs to be delayed until strictly necessary.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " lazy_break_tie(in: int.compare(1, 1), with: fn() { Lt })\n" + " // -> Lt\n" + " ```\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " lazy_break_tie(in: int.compare(1, 0), with: fn() { Eq })\n" + " // -> Gt\n" + " ```\n" +). +-spec lazy_break_tie(order(), fun(() -> order())) -> order(). +lazy_break_tie(Order, Comparison) -> + case Order of + lt -> + Order; + + gt -> + Order; + + eq -> + Comparison() + end. diff --git a/build/packages/gleam_stdlib/src/gleam@pair.erl b/build/packages/gleam_stdlib/src/gleam@pair.erl new file mode 100644 index 0000000..cb18264 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@pair.erl @@ -0,0 +1,110 @@ +-module(gleam@pair). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/pair.gleam"). +-export([first/1, second/1, swap/1, map_first/2, map_second/2, new/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-file("src/gleam/pair.gleam", 10). +?DOC( + " Returns the first element in a pair.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " first(#(1, 2))\n" + " // -> 1\n" + " ```\n" +). +-spec first({CLF, any()}) -> CLF. +first(Pair) -> + {A, _} = Pair, + A. + +-file("src/gleam/pair.gleam", 24). +?DOC( + " Returns the second element in a pair.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " second(#(1, 2))\n" + " // -> 2\n" + " ```\n" +). +-spec second({any(), CLI}) -> CLI. +second(Pair) -> + {_, A} = Pair, + A. + +-file("src/gleam/pair.gleam", 38). +?DOC( + " Returns a new pair with the elements swapped.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " swap(#(1, 2))\n" + " // -> #(2, 1)\n" + " ```\n" +). +-spec swap({CLJ, CLK}) -> {CLK, CLJ}. +swap(Pair) -> + {A, B} = Pair, + {B, A}. + +-file("src/gleam/pair.gleam", 53). +?DOC( + " Returns a new pair with the first element having had `with` applied to\n" + " it.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " #(1, 2) |> map_first(fn(n) { n * 2 })\n" + " // -> #(2, 2)\n" + " ```\n" +). +-spec map_first({CLL, CLM}, fun((CLL) -> CLN)) -> {CLN, CLM}. +map_first(Pair, Fun) -> + {A, B} = Pair, + {Fun(A), B}. + +-file("src/gleam/pair.gleam", 68). +?DOC( + " Returns a new pair with the second element having had `with` applied to\n" + " it.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " #(1, 2) |> map_second(fn(n) { n * 2 })\n" + " // -> #(1, 4)\n" + " ```\n" +). +-spec map_second({CLO, CLP}, fun((CLP) -> CLQ)) -> {CLO, CLQ}. +map_second(Pair, Fun) -> + {A, B} = Pair, + {A, Fun(B)}. + +-file("src/gleam/pair.gleam", 83). +?DOC( + " Returns a new pair with the given elements. This can also be done using the dedicated\n" + " syntax instead: `new(1, 2) == #(1, 2)`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new(1, 2)\n" + " // -> #(1, 2)\n" + " ```\n" +). +-spec new(CLR, CLS) -> {CLR, CLS}. +new(First, Second) -> + {First, Second}. diff --git a/build/packages/gleam_stdlib/src/gleam@result.erl b/build/packages/gleam_stdlib/src/gleam@result.erl new file mode 100644 index 0000000..9d89ff7 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@result.erl @@ -0,0 +1,550 @@ +-module(gleam@result). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/result.gleam"). +-export([is_ok/1, is_error/1, map/2, map_error/2, flatten/1, 'try'/2, then/2, unwrap/2, lazy_unwrap/2, unwrap_error/2, unwrap_both/1, 'or'/2, lazy_or/2, all/1, partition/1, replace/2, replace_error/2, values/1, try_recover/2]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Result represents the result of something that may succeed or not.\n" + " `Ok` means it was successful, `Error` means it was not successful.\n" +). + +-file("src/gleam/result.gleam", 20). +?DOC( + " Checks whether the result is an `Ok` value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_ok(Ok(1))\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_ok(Error(Nil))\n" + " // -> False\n" + " ```\n" +). +-spec is_ok({ok, any()} | {error, any()}) -> boolean(). +is_ok(Result) -> + case Result of + {error, _} -> + false; + + {ok, _} -> + true + end. + +-file("src/gleam/result.gleam", 41). +?DOC( + " Checks whether the result is an `Error` value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_error(Ok(1))\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " is_error(Error(Nil))\n" + " // -> True\n" + " ```\n" +). +-spec is_error({ok, any()} | {error, any()}) -> boolean(). +is_error(Result) -> + case Result of + {ok, _} -> + false; + + {error, _} -> + true + end. + +-file("src/gleam/result.gleam", 66). +?DOC( + " Updates a value held within the `Ok` of a result by calling a given function\n" + " on it.\n" + "\n" + " If the result is an `Error` rather than `Ok` the function is not called and the\n" + " result stays the same.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map(over: Ok(1), with: fn(x) { x + 1 })\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " map(over: Error(1), with: fn(x) { x + 1 })\n" + " // -> Error(1)\n" + " ```\n" +). +-spec map({ok, CMC} | {error, CMD}, fun((CMC) -> CMG)) -> {ok, CMG} | + {error, CMD}. +map(Result, Fun) -> + case Result of + {ok, X} -> + {ok, Fun(X)}; + + {error, E} -> + {error, E} + end. + +-file("src/gleam/result.gleam", 91). +?DOC( + " Updates a value held within the `Error` of a result by calling a given function\n" + " on it.\n" + "\n" + " If the result is `Ok` rather than `Error` the function is not called and the\n" + " result stays the same.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " map_error(over: Error(1), with: fn(x) { x + 1 })\n" + " // -> Error(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " map_error(over: Ok(1), with: fn(x) { x + 1 })\n" + " // -> Ok(1)\n" + " ```\n" +). +-spec map_error({ok, CMJ} | {error, CMK}, fun((CMK) -> CMN)) -> {ok, CMJ} | + {error, CMN}. +map_error(Result, Fun) -> + case Result of + {ok, X} -> + {ok, X}; + + {error, Error} -> + {error, Fun(Error)} + end. + +-file("src/gleam/result.gleam", 120). +?DOC( + " Merges a nested `Result` into a single layer.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " flatten(Ok(Ok(1)))\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " flatten(Ok(Error(\"\")))\n" + " // -> Error(\"\")\n" + " ```\n" + "\n" + " ```gleam\n" + " flatten(Error(Nil))\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec flatten({ok, {ok, CMQ} | {error, CMR}} | {error, CMR}) -> {ok, CMQ} | + {error, CMR}. +flatten(Result) -> + case Result of + {ok, X} -> + X; + + {error, Error} -> + {error, Error} + end. + +-file("src/gleam/result.gleam", 158). +?DOC( + " \"Updates\" an `Ok` result by passing its value to a function that yields a result,\n" + " and returning the yielded result. (This may \"replace\" the `Ok` with an `Error`.)\n" + "\n" + " If the input is an `Error` rather than an `Ok`, the function is not called and\n" + " the original `Error` is returned.\n" + "\n" + " This function is the equivalent of calling `map` followed by `flatten`, and\n" + " it is useful for chaining together multiple functions that may fail.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " try(Ok(1), fn(x) { Ok(x + 1) })\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " try(Ok(1), fn(x) { Ok(#(\"a\", x)) })\n" + " // -> Ok(#(\"a\", 1))\n" + " ```\n" + "\n" + " ```gleam\n" + " try(Ok(1), fn(_) { Error(\"Oh no\") })\n" + " // -> Error(\"Oh no\")\n" + " ```\n" + "\n" + " ```gleam\n" + " try(Error(Nil), fn(x) { Ok(x + 1) })\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec 'try'({ok, CMY} | {error, CMZ}, fun((CMY) -> {ok, CNC} | {error, CMZ})) -> {ok, + CNC} | + {error, CMZ}. +'try'(Result, Fun) -> + case Result of + {ok, X} -> + Fun(X); + + {error, E} -> + {error, E} + end. + +-file("src/gleam/result.gleam", 169). +-spec then({ok, CNH} | {error, CNI}, fun((CNH) -> {ok, CNL} | {error, CNI})) -> {ok, + CNL} | + {error, CNI}. +then(Result, Fun) -> + 'try'(Result, Fun). + +-file("src/gleam/result.gleam", 191). +?DOC( + " Extracts the `Ok` value from a result, returning a default value if the result\n" + " is an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " unwrap(Ok(1), 0)\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " unwrap(Error(\"\"), 0)\n" + " // -> 0\n" + " ```\n" +). +-spec unwrap({ok, CNQ} | {error, any()}, CNQ) -> CNQ. +unwrap(Result, Default) -> + case Result of + {ok, V} -> + V; + + {error, _} -> + Default + end. + +-file("src/gleam/result.gleam", 213). +?DOC( + " Extracts the `Ok` value from a result, evaluating the default function if the result\n" + " is an `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " lazy_unwrap(Ok(1), fn() { 0 })\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_unwrap(Error(\"\"), fn() { 0 })\n" + " // -> 0\n" + " ```\n" +). +-spec lazy_unwrap({ok, CNU} | {error, any()}, fun(() -> CNU)) -> CNU. +lazy_unwrap(Result, Default) -> + case Result of + {ok, V} -> + V; + + {error, _} -> + Default() + end. + +-file("src/gleam/result.gleam", 235). +?DOC( + " Extracts the `Error` value from a result, returning a default value if the result\n" + " is an `Ok`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " unwrap_error(Error(1), 0)\n" + " // -> 1\n" + " ```\n" + "\n" + " ```gleam\n" + " unwrap_error(Ok(\"\"), 0)\n" + " // -> 0\n" + " ```\n" +). +-spec unwrap_error({ok, any()} | {error, CNZ}, CNZ) -> CNZ. +unwrap_error(Result, Default) -> + case Result of + {ok, _} -> + Default; + + {error, E} -> + E + end. + +-file("src/gleam/result.gleam", 243). +-spec unwrap_both({ok, COC} | {error, COC}) -> COC. +unwrap_both(Result) -> + case Result of + {ok, A} -> + A; + + {error, A@1} -> + A@1 + end. + +-file("src/gleam/result.gleam", 274). +?DOC( + " Returns the first value if it is `Ok`, otherwise returns the second value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " or(Ok(1), Ok(2))\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(Ok(1), Error(\"Error 2\"))\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(Error(\"Error 1\"), Ok(2))\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " or(Error(\"Error 1\"), Error(\"Error 2\"))\n" + " // -> Error(\"Error 2\")\n" + " ```\n" +). +-spec 'or'({ok, COF} | {error, COG}, {ok, COF} | {error, COG}) -> {ok, COF} | + {error, COG}. +'or'(First, Second) -> + case First of + {ok, _} -> + First; + + {error, _} -> + Second + end. + +-file("src/gleam/result.gleam", 307). +?DOC( + " Returns the first value if it is `Ok`, otherwise evaluates the given function for a fallback value.\n" + "\n" + " If you need access to the initial error value, use `result.try_recover`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " lazy_or(Ok(1), fn() { Ok(2) })\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(Ok(1), fn() { Error(\"Error 2\") })\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(Error(\"Error 1\"), fn() { Ok(2) })\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " lazy_or(Error(\"Error 1\"), fn() { Error(\"Error 2\") })\n" + " // -> Error(\"Error 2\")\n" + " ```\n" +). +-spec lazy_or({ok, CON} | {error, COO}, fun(() -> {ok, CON} | {error, COO})) -> {ok, + CON} | + {error, COO}. +lazy_or(First, Second) -> + case First of + {ok, _} -> + First; + + {error, _} -> + Second() + end. + +-file("src/gleam/result.gleam", 333). +?DOC( + " Combines a list of results into a single result.\n" + " If all elements in the list are `Ok` then returns an `Ok` holding the list of values.\n" + " If any element is `Error` then returns the first error.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " all([Ok(1), Ok(2)])\n" + " // -> Ok([1, 2])\n" + " ```\n" + "\n" + " ```gleam\n" + " all([Ok(1), Error(\"e\")])\n" + " // -> Error(\"e\")\n" + " ```\n" +). +-spec all(list({ok, COV} | {error, COW})) -> {ok, list(COV)} | {error, COW}. +all(Results) -> + gleam@list:try_map(Results, fun(Result) -> Result end). + +-file("src/gleam/result.gleam", 353). +-spec partition_loop(list({ok, CPK} | {error, CPL}), list(CPK), list(CPL)) -> {list(CPK), + list(CPL)}. +partition_loop(Results, Oks, Errors) -> + case Results of + [] -> + {Oks, Errors}; + + [{ok, A} | Rest] -> + partition_loop(Rest, [A | Oks], Errors); + + [{error, E} | Rest@1] -> + partition_loop(Rest@1, Oks, [E | Errors]) + end. + +-file("src/gleam/result.gleam", 349). +?DOC( + " Given a list of results, returns a pair where the first element is a list\n" + " of all the values inside `Ok` and the second element is a list with all the\n" + " values inside `Error`. The values in both lists appear in reverse order with\n" + " respect to their position in the original list of results.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " partition([Ok(1), Error(\"a\"), Error(\"b\"), Ok(2)])\n" + " // -> #([2, 1], [\"b\", \"a\"])\n" + " ```\n" +). +-spec partition(list({ok, CPD} | {error, CPE})) -> {list(CPD), list(CPE)}. +partition(Results) -> + partition_loop(Results, [], []). + +-file("src/gleam/result.gleam", 375). +?DOC( + " Replace the value within a result\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " replace(Ok(1), Nil)\n" + " // -> Ok(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " replace(Error(1), Nil)\n" + " // -> Error(1)\n" + " ```\n" +). +-spec replace({ok, any()} | {error, CPT}, CPW) -> {ok, CPW} | {error, CPT}. +replace(Result, Value) -> + case Result of + {ok, _} -> + {ok, Value}; + + {error, Error} -> + {error, Error} + end. + +-file("src/gleam/result.gleam", 396). +?DOC( + " Replace the error within a result\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " replace_error(Error(1), Nil)\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " replace_error(Ok(1), Nil)\n" + " // -> Ok(1)\n" + " ```\n" +). +-spec replace_error({ok, CPZ} | {error, any()}, CQD) -> {ok, CPZ} | {error, CQD}. +replace_error(Result, Error) -> + case Result of + {ok, X} -> + {ok, X}; + + {error, _} -> + {error, Error} + end. + +-file("src/gleam/result.gleam", 412). +?DOC( + " Given a list of results, returns only the values inside `Ok`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " values([Ok(1), Error(\"a\"), Ok(3)])\n" + " // -> [1, 3]\n" + " ```\n" +). +-spec values(list({ok, CQG} | {error, any()})) -> list(CQG). +values(Results) -> + gleam@list:filter_map(Results, fun(Result) -> Result end). + +-file("src/gleam/result.gleam", 445). +?DOC( + " Updates a value held within the `Error` of a result by calling a given function\n" + " on it, where the given function also returns a result. The two results are\n" + " then merged together into one result.\n" + "\n" + " If the result is an `Ok` rather than `Error` the function is not called and the\n" + " result stays the same.\n" + "\n" + " This function is useful for chaining together computations that may fail\n" + " and trying to recover from possible errors.\n" + "\n" + " If you do not need access to the initial error value, use `result.lazy_or`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " Ok(1) |> try_recover(with: fn(_) { Error(\"failed to recover\") })\n" + " // -> Ok(1)\n" + " ```\n" + "\n" + " ```gleam\n" + " Error(1) |> try_recover(with: fn(error) { Ok(error + 1) })\n" + " // -> Ok(2)\n" + " ```\n" + "\n" + " ```gleam\n" + " Error(1) |> try_recover(with: fn(error) { Error(\"failed to recover\") })\n" + " // -> Error(\"failed to recover\")\n" + " ```\n" +). +-spec try_recover( + {ok, CQM} | {error, CQN}, + fun((CQN) -> {ok, CQM} | {error, CQQ}) +) -> {ok, CQM} | {error, CQQ}. +try_recover(Result, Fun) -> + case Result of + {ok, Value} -> + {ok, Value}; + + {error, Error} -> + Fun(Error) + end. diff --git a/build/packages/gleam_stdlib/src/gleam@set.erl b/build/packages/gleam_stdlib/src/gleam@set.erl new file mode 100644 index 0000000..bb3c417 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@set.erl @@ -0,0 +1,429 @@ +-module(gleam@set). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/set.gleam"). +-export([new/0, size/1, is_empty/1, contains/2, delete/2, to_list/1, fold/3, filter/2, drop/2, take/2, intersection/2, difference/2, is_subset/2, is_disjoint/2, each/2, insert/2, from_list/1, map/2, union/2, symmetric_difference/2]). +-export_type([set/1]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-opaque set(CVL) :: {set, gleam@dict:dict(CVL, list(nil))}. + +-file("src/gleam/set.gleam", 32). +?DOC(" Creates a new empty set.\n"). +-spec new() -> set(any()). +new() -> + {set, maps:new()}. + +-file("src/gleam/set.gleam", 50). +?DOC( + " Gets the number of members in a set.\n" + "\n" + " This function runs in constant time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new()\n" + " |> insert(1)\n" + " |> insert(2)\n" + " |> size\n" + " // -> 2\n" + " ```\n" +). +-spec size(set(any())) -> integer(). +size(Set) -> + maps:size(erlang:element(2, Set)). + +-file("src/gleam/set.gleam", 68). +?DOC( + " Determines whether or not the set is empty.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> is_empty\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " new() |> insert(1) |> is_empty\n" + " // -> False\n" + " ```\n" +). +-spec is_empty(set(any())) -> boolean(). +is_empty(Set) -> + Set =:= new(). + +-file("src/gleam/set.gleam", 110). +?DOC( + " Checks whether a set contains a given member.\n" + "\n" + " This function runs in logarithmic time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new()\n" + " |> insert(2)\n" + " |> contains(2)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " new()\n" + " |> insert(2)\n" + " |> contains(1)\n" + " // -> False\n" + " ```\n" +). +-spec contains(set(CVW), CVW) -> boolean(). +contains(Set, Member) -> + _pipe = erlang:element(2, Set), + _pipe@1 = gleam_stdlib:map_get(_pipe, Member), + gleam@result:is_ok(_pipe@1). + +-file("src/gleam/set.gleam", 131). +?DOC( + " Removes a member from a set. If the set does not contain the member then\n" + " the set is returned unchanged.\n" + "\n" + " This function runs in logarithmic time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new()\n" + " |> insert(2)\n" + " |> delete(2)\n" + " |> contains(1)\n" + " // -> False\n" + " ```\n" +). +-spec delete(set(CVY), CVY) -> set(CVY). +delete(Set, Member) -> + {set, gleam@dict:delete(erlang:element(2, Set), Member)}. + +-file("src/gleam/set.gleam", 149). +?DOC( + " Converts the set into a list of the contained members.\n" + "\n" + " The list has no specific ordering, any unintentional ordering may change in\n" + " future versions of Gleam or Erlang.\n" + "\n" + " This function runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new() |> insert(2) |> to_list\n" + " // -> [2]\n" + " ```\n" +). +-spec to_list(set(CWB)) -> list(CWB). +to_list(Set) -> + maps:keys(erlang:element(2, Set)). + +-file("src/gleam/set.gleam", 190). +?DOC( + " Combines all entries into a single value by calling a given function on each\n" + " one.\n" + "\n" + " Sets are not ordered so the values are not returned in any specific order.\n" + " Do not write code that relies on the order entries are used by this\n" + " function as it may change in later versions of Gleam or Erlang.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " from_list([1, 3, 9])\n" + " |> fold(0, fn(accumulator, member) { accumulator + member })\n" + " // -> 13\n" + " ```\n" +). +-spec fold(set(CWH), CWJ, fun((CWJ, CWH) -> CWJ)) -> CWJ. +fold(Set, Initial, Reducer) -> + gleam@dict:fold( + erlang:element(2, Set), + Initial, + fun(A, K, _) -> Reducer(A, K) end + ). + +-file("src/gleam/set.gleam", 214). +?DOC( + " Creates a new set from an existing set, minus any members that a given\n" + " function returns `False` for.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + "\n" + " from_list([1, 4, 6, 3, 675, 44, 67])\n" + " |> filter(keeping: int.is_even)\n" + " |> to_list\n" + " // -> [4, 6, 44]\n" + " ```\n" +). +-spec filter(set(CWK), fun((CWK) -> boolean())) -> set(CWK). +filter(Set, Predicate) -> + {set, + gleam@dict:filter(erlang:element(2, Set), fun(M, _) -> Predicate(M) end)}. + +-file("src/gleam/set.gleam", 249). +?DOC( + " Creates a new set from a given set with all the same entries except any\n" + " entry found on the given list.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([1, 2, 3, 4])\n" + " |> drop([1, 3])\n" + " |> to_list\n" + " // -> [2, 4]\n" + " ```\n" +). +-spec drop(set(CWR), list(CWR)) -> set(CWR). +drop(Set, Disallowed) -> + gleam@list:fold(Disallowed, Set, fun delete/2). + +-file("src/gleam/set.gleam", 267). +?DOC( + " Creates a new set from a given set, only including any members which are in\n" + " a given list.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([1, 2, 3])\n" + " |> take([1, 3, 5])\n" + " |> to_list\n" + " // -> [1, 3]\n" + " ```\n" +). +-spec take(set(CWV), list(CWV)) -> set(CWV). +take(Set, Desired) -> + {set, gleam@dict:take(erlang:element(2, Set), Desired)}. + +-file("src/gleam/set.gleam", 287). +-spec order(set(CXD), set(CXD)) -> {set(CXD), set(CXD)}. +order(First, Second) -> + case maps:size(erlang:element(2, First)) > maps:size( + erlang:element(2, Second) + ) of + true -> + {First, Second}; + + false -> + {Second, First} + end. + +-file("src/gleam/set.gleam", 305). +?DOC( + " Creates a new set that contains members that are present in both given sets.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " intersection(from_list([1, 2]), from_list([2, 3])) |> to_list\n" + " // -> [2]\n" + " ```\n" +). +-spec intersection(set(CXI), set(CXI)) -> set(CXI). +intersection(First, Second) -> + {Larger, Smaller} = order(First, Second), + take(Larger, to_list(Smaller)). + +-file("src/gleam/set.gleam", 323). +?DOC( + " Creates a new set that contains members that are present in the first set\n" + " but not the second.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " difference(from_list([1, 2]), from_list([2, 3, 4])) |> to_list\n" + " // -> [1]\n" + " ```\n" +). +-spec difference(set(CXM), set(CXM)) -> set(CXM). +difference(First, Second) -> + drop(First, to_list(Second)). + +-file("src/gleam/set.gleam", 344). +?DOC( + " Determines if a set is fully contained by another.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_subset(from_list([1]), from_list([1, 2]))\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_subset(from_list([1, 2, 3]), from_list([3, 4, 5]))\n" + " // -> False\n" + " ```\n" +). +-spec is_subset(set(CXQ), set(CXQ)) -> boolean(). +is_subset(First, Second) -> + intersection(First, Second) =:= First. + +-file("src/gleam/set.gleam", 362). +?DOC( + " Determines if two sets contain no common members\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_disjoint(from_list([1, 2, 3]), from_list([4, 5, 6]))\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_disjoint(from_list([1, 2, 3]), from_list([3, 4, 5]))\n" + " // -> False\n" + " ```\n" +). +-spec is_disjoint(set(CXT), set(CXT)) -> boolean(). +is_disjoint(First, Second) -> + intersection(First, Second) =:= new(). + +-file("src/gleam/set.gleam", 402). +?DOC( + " Calls a function for each member in a set, discarding the return\n" + " value.\n" + "\n" + " Useful for producing a side effect for every item of a set.\n" + "\n" + " ```gleam\n" + " let set = from_list([\"apple\", \"banana\", \"cherry\"])\n" + "\n" + " each(set, io.println)\n" + " // -> Nil\n" + " // apple\n" + " // banana\n" + " // cherry\n" + " ```\n" + "\n" + " The order of elements in the iteration is an implementation detail that\n" + " should not be relied upon.\n" +). +-spec each(set(CYA), fun((CYA) -> any())) -> nil. +each(Set, Fun) -> + fold( + Set, + nil, + fun(Nil, Member) -> + Fun(Member), + Nil + end + ). + +-file("src/gleam/set.gleam", 86). +?DOC( + " Inserts an member into the set.\n" + "\n" + " This function runs in logarithmic time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " new()\n" + " |> insert(1)\n" + " |> insert(2)\n" + " |> size\n" + " // -> 2\n" + " ```\n" +). +-spec insert(set(CVT), CVT) -> set(CVT). +insert(Set, Member) -> + {set, gleam@dict:insert(erlang:element(2, Set), Member, [])}. + +-file("src/gleam/set.gleam", 167). +?DOC( + " Creates a new set of the members in a given list.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " import gleam/int\n" + " import gleam/list\n" + "\n" + " [1, 1, 2, 4, 3, 2] |> from_list |> to_list |> list.sort(by: int.compare)\n" + " // -> [1, 2, 3, 4]\n" + " ```\n" +). +-spec from_list(list(CWE)) -> set(CWE). +from_list(Members) -> + Dict = gleam@list:fold( + Members, + maps:new(), + fun(M, K) -> gleam@dict:insert(M, K, []) end + ), + {set, Dict}. + +-file("src/gleam/set.gleam", 232). +?DOC( + " Creates a new set from a given set with the result of applying the given\n" + " function to each member.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_list([1, 2, 3, 4])\n" + " |> map(with: fn(x) { x * 2 })\n" + " |> to_list\n" + " // -> [2, 4, 6, 8]\n" + " ```\n" +). +-spec map(set(CWN), fun((CWN) -> CWP)) -> set(CWP). +map(Set, Fun) -> + fold(Set, new(), fun(Acc, Member) -> insert(Acc, Fun(Member)) end). + +-file("src/gleam/set.gleam", 282). +?DOC( + " Creates a new set that contains all members of both given sets.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " union(from_list([1, 2]), from_list([2, 3])) |> to_list\n" + " // -> [1, 2, 3]\n" + " ```\n" +). +-spec union(set(CWZ), set(CWZ)) -> set(CWZ). +union(First, Second) -> + {Larger, Smaller} = order(First, Second), + fold(Smaller, Larger, fun insert/2). + +-file("src/gleam/set.gleam", 374). +?DOC( + " Creates a new set that contains members that are present in either set, but\n" + " not both.\n" + "\n" + " ```gleam\n" + " symmetric_difference(from_list([1, 2, 3]), from_list([3, 4])) |> to_list\n" + " // -> [1, 2, 4]\n" + " ```\n" +). +-spec symmetric_difference(set(CXW), set(CXW)) -> set(CXW). +symmetric_difference(First, Second) -> + difference(union(First, Second), intersection(First, Second)). diff --git a/build/packages/gleam_stdlib/src/gleam@string.erl b/build/packages/gleam_stdlib/src/gleam@string.erl new file mode 100644 index 0000000..63006b8 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@string.erl @@ -0,0 +1,1012 @@ +-module(gleam@string). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/string.gleam"). +-export([is_empty/1, length/1, reverse/1, replace/3, lowercase/1, uppercase/1, compare/2, slice/3, crop/2, drop_end/2, contains/2, starts_with/2, ends_with/2, split_once/2, append/2, concat/1, repeat/2, join/2, pad_start/3, pad_end/3, trim_start/1, trim_end/1, trim/1, pop_grapheme/1, to_graphemes/1, split/2, to_utf_codepoints/1, from_utf_codepoints/1, utf_codepoint/1, utf_codepoint_to_int/1, to_option/1, first/1, last/1, capitalise/1, inspect/1, byte_size/1, drop_start/2]). +-export_type([direction/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Strings in Gleam are UTF-8 binaries. They can be written in your code as\n" + " text surrounded by `\"double quotes\"`.\n" +). + +-type direction() :: leading | trailing. + +-file("src/gleam/string.gleam", 23). +?DOC( + " Determines if a `String` is empty.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " is_empty(\"\")\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_empty(\"the world\")\n" + " // -> False\n" + " ```\n" +). +-spec is_empty(binary()) -> boolean(). +is_empty(Str) -> + Str =:= <<""/utf8>>. + +-file("src/gleam/string.gleam", 51). +?DOC( + " Gets the number of grapheme clusters in a given `String`.\n" + "\n" + " This function has to iterate across the whole string to count the number of\n" + " graphemes, so it runs in linear time. Avoid using this in a loop.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " length(\"Gleam\")\n" + " // -> 5\n" + " ```\n" + "\n" + " ```gleam\n" + " length(\"ß↑e̊\")\n" + " // -> 3\n" + " ```\n" + "\n" + " ```gleam\n" + " length(\"\")\n" + " // -> 0\n" + " ```\n" +). +-spec length(binary()) -> integer(). +length(String) -> + string:length(String). + +-file("src/gleam/string.gleam", 65). +?DOC( + " Reverses a `String`.\n" + "\n" + " This function has to iterate across the whole `String` so it runs in linear\n" + " time. Avoid using this in a loop.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " reverse(\"stressed\")\n" + " // -> \"desserts\"\n" + " ```\n" +). +-spec reverse(binary()) -> binary(). +reverse(String) -> + _pipe = String, + _pipe@1 = gleam_stdlib:identity(_pipe), + _pipe@2 = string:reverse(_pipe@1), + unicode:characters_to_binary(_pipe@2). + +-file("src/gleam/string.gleam", 86). +?DOC( + " Creates a new `String` by replacing all occurrences of a given substring.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " replace(\"www.example.com\", each: \".\", with: \"-\")\n" + " // -> \"www-example-com\"\n" + " ```\n" + "\n" + " ```gleam\n" + " replace(\"a,b,c,d,e\", each: \",\", with: \"/\")\n" + " // -> \"a/b/c/d/e\"\n" + " ```\n" +). +-spec replace(binary(), binary(), binary()) -> binary(). +replace(String, Pattern, Substitute) -> + _pipe = String, + _pipe@1 = gleam_stdlib:identity(_pipe), + _pipe@2 = gleam_stdlib:string_replace(_pipe@1, Pattern, Substitute), + unicode:characters_to_binary(_pipe@2). + +-file("src/gleam/string.gleam", 111). +?DOC( + " Creates a new `String` with all the graphemes in the input `String` converted to\n" + " lowercase.\n" + "\n" + " Useful for case-insensitive comparisons.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " lowercase(\"X-FILES\")\n" + " // -> \"x-files\"\n" + " ```\n" +). +-spec lowercase(binary()) -> binary(). +lowercase(String) -> + string:lowercase(String). + +-file("src/gleam/string.gleam", 127). +?DOC( + " Creates a new `String` with all the graphemes in the input `String` converted to\n" + " uppercase.\n" + "\n" + " Useful for case-insensitive comparisons and VIRTUAL YELLING.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " uppercase(\"skinner\")\n" + " // -> \"SKINNER\"\n" + " ```\n" +). +-spec uppercase(binary()) -> binary(). +uppercase(String) -> + string:uppercase(String). + +-file("src/gleam/string.gleam", 145). +?DOC( + " Compares two `String`s to see which is \"larger\" by comparing their graphemes.\n" + "\n" + " This does not compare the size or length of the given `String`s.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " compare(\"Anthony\", \"Anthony\")\n" + " // -> order.Eq\n" + " ```\n" + "\n" + " ```gleam\n" + " compare(\"A\", \"B\")\n" + " // -> order.Lt\n" + " ```\n" +). +-spec compare(binary(), binary()) -> gleam@order:order(). +compare(A, B) -> + case A =:= B of + true -> + eq; + + _ -> + case gleam_stdlib:less_than(A, B) of + true -> + lt; + + false -> + gt + end + end. + +-file("src/gleam/string.gleam", 194). +?DOC( + " Takes a substring given a start grapheme index and a length. Negative indexes\n" + " are taken starting from the *end* of the list.\n" + "\n" + " This function runs in linear time with the size of the index and the\n" + " length. Negative indexes are linear with the size of the input string in\n" + " addition to the other costs.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " slice(from: \"gleam\", at_index: 1, length: 2)\n" + " // -> \"le\"\n" + " ```\n" + "\n" + " ```gleam\n" + " slice(from: \"gleam\", at_index: 1, length: 10)\n" + " // -> \"leam\"\n" + " ```\n" + "\n" + " ```gleam\n" + " slice(from: \"gleam\", at_index: 10, length: 3)\n" + " // -> \"\"\n" + " ```\n" + "\n" + " ```gleam\n" + " slice(from: \"gleam\", at_index: -2, length: 2)\n" + " // -> \"am\"\n" + " ```\n" + "\n" + " ```gleam\n" + " slice(from: \"gleam\", at_index: -12, length: 2)\n" + " // -> \"\"\n" + " ```\n" +). +-spec slice(binary(), integer(), integer()) -> binary(). +slice(String, Idx, Len) -> + case Len =< 0 of + true -> + <<""/utf8>>; + + false -> + case Idx < 0 of + true -> + Translated_idx = string:length(String) + Idx, + case Translated_idx < 0 of + true -> + <<""/utf8>>; + + false -> + gleam_stdlib:slice(String, Translated_idx, Len) + end; + + false -> + gleam_stdlib:slice(String, Idx, Len) + end + end. + +-file("src/gleam/string.gleam", 232). +?DOC( + " Drops contents of the first `String` that occur before the second `String`.\n" + " If the `from` string does not contain the `before` string, `from` is\n" + " returned unchanged.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " crop(from: \"The Lone Gunmen\", before: \"Lone\")\n" + " // -> \"Lone Gunmen\"\n" + " ```\n" +). +-spec crop(binary(), binary()) -> binary(). +crop(String, Substring) -> + gleam_stdlib:crop_string(String, Substring). + +-file("src/gleam/string.gleam", 268). +?DOC( + " Drops *n* graphemes from the end of a `String`.\n" + "\n" + " This function traverses the full string, so it runs in linear time with the\n" + " size of the string. Avoid using this in a loop.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " drop_end(from: \"Cigarette Smoking Man\", up_to: 2)\n" + " // -> \"Cigarette Smoking M\"\n" + " ```\n" +). +-spec drop_end(binary(), integer()) -> binary(). +drop_end(String, Num_graphemes) -> + case Num_graphemes =< 0 of + true -> + String; + + false -> + slice(String, 0, string:length(String) - Num_graphemes) + end. + +-file("src/gleam/string.gleam", 296). +?DOC( + " Checks if the first `String` contains the second.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " contains(does: \"theory\", contain: \"ory\")\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " contains(does: \"theory\", contain: \"the\")\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " contains(does: \"theory\", contain: \"THE\")\n" + " // -> False\n" + " ```\n" +). +-spec contains(binary(), binary()) -> boolean(). +contains(Haystack, Needle) -> + gleam_stdlib:contains_string(Haystack, Needle). + +-file("src/gleam/string.gleam", 309). +?DOC( + " Checks whether the first `String` starts with the second one.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " starts_with(\"theory\", \"ory\")\n" + " // -> False\n" + " ```\n" +). +-spec starts_with(binary(), binary()) -> boolean(). +starts_with(String, Prefix) -> + gleam_stdlib:string_starts_with(String, Prefix). + +-file("src/gleam/string.gleam", 322). +?DOC( + " Checks whether the first `String` ends with the second one.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " ends_with(\"theory\", \"ory\")\n" + " // -> True\n" + " ```\n" +). +-spec ends_with(binary(), binary()) -> boolean(). +ends_with(String, Suffix) -> + gleam_stdlib:string_ends_with(String, Suffix). + +-file("src/gleam/string.gleam", 361). +?DOC( + " Splits a `String` a single time on the given substring.\n" + "\n" + " Returns an `Error` if substring not present.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " split_once(\"home/gleam/desktop/\", on: \"/\")\n" + " // -> Ok(#(\"home\", \"gleam/desktop/\"))\n" + " ```\n" + "\n" + " ```gleam\n" + " split_once(\"home/gleam/desktop/\", on: \"?\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec split_once(binary(), binary()) -> {ok, {binary(), binary()}} | + {error, nil}. +split_once(String, Substring) -> + case string:split(String, Substring) of + [First, Rest] -> + {ok, {First, Rest}}; + + _ -> + {error, nil} + end. + +-file("src/gleam/string.gleam", 392). +?DOC( + " Creates a new `String` by joining two `String`s together.\n" + "\n" + " This function typically copies both `String`s and runs in linear time, but\n" + " the exact behaviour will depend on how the runtime you are using optimises\n" + " your code. Benchmark and profile your code if you need to understand its\n" + " performance better.\n" + "\n" + " If you are joining together large string and want to avoid copying any data\n" + " you may want to investigate using the [`string_tree`](../gleam/string_tree.html)\n" + " module.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " append(to: \"butter\", suffix: \"fly\")\n" + " // -> \"butterfly\"\n" + " ```\n" +). +-spec append(binary(), binary()) -> binary(). +append(First, Second) -> + <>. + +-file("src/gleam/string.gleam", 412). +-spec concat_loop(list(binary()), binary()) -> binary(). +concat_loop(Strings, Accumulator) -> + case Strings of + [String | Strings@1] -> + concat_loop(Strings@1, <>); + + [] -> + Accumulator + end. + +-file("src/gleam/string.gleam", 408). +?DOC( + " Creates a new `String` by joining many `String`s together.\n" + "\n" + " This function copies all the `String`s and runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " concat([\"never\", \"the\", \"less\"])\n" + " // -> \"nevertheless\"\n" + " ```\n" +). +-spec concat(list(binary())) -> binary(). +concat(Strings) -> + erlang:list_to_binary(Strings). + +-file("src/gleam/string.gleam", 437). +-spec repeat_loop(integer(), binary(), binary()) -> binary(). +repeat_loop(Times, Doubling_acc, Acc) -> + Acc@1 = case Times rem 2 of + 0 -> + Acc; + + _ -> + <> + end, + Times@1 = Times div 2, + case Times@1 =< 0 of + true -> + Acc@1; + + false -> + repeat_loop( + Times@1, + <>, + Acc@1 + ) + end. + +-file("src/gleam/string.gleam", 430). +?DOC( + " Creates a new `String` by repeating a `String` a given number of times.\n" + "\n" + " This function runs in loglinear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " repeat(\"ha\", times: 3)\n" + " // -> \"hahaha\"\n" + " ```\n" +). +-spec repeat(binary(), integer()) -> binary(). +repeat(String, Times) -> + case Times =< 0 of + true -> + <<""/utf8>>; + + false -> + repeat_loop(Times, String, <<""/utf8>>) + end. + +-file("src/gleam/string.gleam", 467). +-spec join_loop(list(binary()), binary(), binary()) -> binary(). +join_loop(Strings, Separator, Accumulator) -> + case Strings of + [] -> + Accumulator; + + [String | Strings@1] -> + join_loop( + Strings@1, + Separator, + <<<>/binary, + String/binary>> + ) + end. + +-file("src/gleam/string.gleam", 460). +?DOC( + " Joins many `String`s together with a given separator.\n" + "\n" + " This function runs in linear time.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " join([\"home\",\"evan\",\"Desktop\"], with: \"/\")\n" + " // -> \"home/evan/Desktop\"\n" + " ```\n" +). +-spec join(list(binary()), binary()) -> binary(). +join(Strings, Separator) -> + case Strings of + [] -> + <<""/utf8>>; + + [First | Rest] -> + join_loop(Rest, Separator, First) + end. + +-file("src/gleam/string.gleam", 545). +-spec padding(integer(), binary()) -> binary(). +padding(Size, Pad_string) -> + Pad_string_length = string:length(Pad_string), + Num_pads = case Pad_string_length of + 0 -> 0; + Gleam@denominator -> Size div Gleam@denominator + end, + Extra = case Pad_string_length of + 0 -> 0; + Gleam@denominator@1 -> Size rem Gleam@denominator@1 + end, + <<(repeat(Pad_string, Num_pads))/binary, + (slice(Pad_string, 0, Extra))/binary>>. + +-file("src/gleam/string.gleam", 498). +?DOC( + " Pads the start of a `String` until it has a given length.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " pad_start(\"121\", to: 5, with: \".\")\n" + " // -> \"..121\"\n" + " ```\n" + "\n" + " ```gleam\n" + " pad_start(\"121\", to: 3, with: \".\")\n" + " // -> \"121\"\n" + " ```\n" + "\n" + " ```gleam\n" + " pad_start(\"121\", to: 2, with: \".\")\n" + " // -> \"121\"\n" + " ```\n" +). +-spec pad_start(binary(), integer(), binary()) -> binary(). +pad_start(String, Desired_length, Pad_string) -> + Current_length = string:length(String), + To_pad_length = Desired_length - Current_length, + case To_pad_length =< 0 of + true -> + String; + + false -> + <<(padding(To_pad_length, Pad_string))/binary, String/binary>> + end. + +-file("src/gleam/string.gleam", 531). +?DOC( + " Pads the end of a `String` until it has a given length.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " pad_end(\"123\", to: 5, with: \".\")\n" + " // -> \"123..\"\n" + " ```\n" + "\n" + " ```gleam\n" + " pad_end(\"123\", to: 3, with: \".\")\n" + " // -> \"123\"\n" + " ```\n" + "\n" + " ```gleam\n" + " pad_end(\"123\", to: 2, with: \".\")\n" + " // -> \"123\"\n" + " ```\n" +). +-spec pad_end(binary(), integer(), binary()) -> binary(). +pad_end(String, Desired_length, Pad_string) -> + Current_length = string:length(String), + To_pad_length = Desired_length - Current_length, + case To_pad_length =< 0 of + true -> + String; + + false -> + <> + end. + +-file("src/gleam/string.gleam", 589). +?DOC( + " Removes whitespace at the start of a `String`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " trim_start(\" hats \\n\")\n" + " // -> \"hats \\n\"\n" + " ```\n" +). +-spec trim_start(binary()) -> binary(). +trim_start(String) -> + string:trim(String, leading). + +-file("src/gleam/string.gleam", 603). +?DOC( + " Removes whitespace at the end of a `String`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " trim_end(\" hats \\n\")\n" + " // -> \" hats\"\n" + " ```\n" +). +-spec trim_end(binary()) -> binary(). +trim_end(String) -> + string:trim(String, trailing). + +-file("src/gleam/string.gleam", 567). +?DOC( + " Removes whitespace on both sides of a `String`.\n" + "\n" + " Whitespace in this function is the set of nonbreakable whitespace\n" + " codepoints, defined as Pattern_White_Space in [Unicode Standard Annex #31][1].\n" + "\n" + " [1]: https://unicode.org/reports/tr31/\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " trim(\" hats \\n\")\n" + " // -> \"hats\"\n" + " ```\n" +). +-spec trim(binary()) -> binary(). +trim(String) -> + _pipe = String, + _pipe@1 = trim_start(_pipe), + trim_end(_pipe@1). + +-file("src/gleam/string.gleam", 630). +?DOC( + " Splits a non-empty `String` into its first element (head) and rest (tail).\n" + " This lets you pattern match on `String`s exactly as you would with lists.\n" + "\n" + " ## Performance\n" + "\n" + " There is a notable overhead to using this function, so you may not want to\n" + " use it in a tight loop. If you wish to efficiently parse a string you may\n" + " want to use alternatives such as the [splitter package](https://hex.pm/packages/splitter).\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " pop_grapheme(\"gleam\")\n" + " // -> Ok(#(\"g\", \"leam\"))\n" + " ```\n" + "\n" + " ```gleam\n" + " pop_grapheme(\"\")\n" + " // -> Error(Nil)\n" + " ```\n" +). +-spec pop_grapheme(binary()) -> {ok, {binary(), binary()}} | {error, nil}. +pop_grapheme(String) -> + gleam_stdlib:string_pop_grapheme(String). + +-file("src/gleam/string.gleam", 647). +-spec to_graphemes_loop(binary(), list(binary())) -> list(binary()). +to_graphemes_loop(String, Acc) -> + case gleam_stdlib:string_pop_grapheme(String) of + {ok, {Grapheme, Rest}} -> + to_graphemes_loop(Rest, [Grapheme | Acc]); + + {error, _} -> + Acc + end. + +-file("src/gleam/string.gleam", 641). +?DOC( + " Converts a `String` to a list of\n" + " [graphemes](https://en.wikipedia.org/wiki/Grapheme).\n" + "\n" + " ```gleam\n" + " to_graphemes(\"abc\")\n" + " // -> [\"a\", \"b\", \"c\"]\n" + " ```\n" +). +-spec to_graphemes(binary()) -> list(binary()). +to_graphemes(String) -> + _pipe = String, + _pipe@1 = to_graphemes_loop(_pipe, []), + lists:reverse(_pipe@1). + +-file("src/gleam/string.gleam", 333). +?DOC( + " Creates a list of `String`s by splitting a given string on a given substring.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " split(\"home/gleam/desktop/\", on: \"/\")\n" + " // -> [\"home\", \"gleam\", \"desktop\", \"\"]\n" + " ```\n" +). +-spec split(binary(), binary()) -> list(binary()). +split(X, Substring) -> + case Substring of + <<""/utf8>> -> + to_graphemes(X); + + _ -> + _pipe = X, + _pipe@1 = gleam_stdlib:identity(_pipe), + _pipe@2 = gleam@string_tree:split(_pipe@1, Substring), + gleam@list:map(_pipe@2, fun unicode:characters_to_binary/1) + end. + +-file("src/gleam/string.gleam", 694). +-spec to_utf_codepoints_loop(bitstring(), list(integer())) -> list(integer()). +to_utf_codepoints_loop(Bit_array, Acc) -> + case Bit_array of + <> -> + to_utf_codepoints_loop(Rest, [First | Acc]); + + _ -> + lists:reverse(Acc) + end. + +-file("src/gleam/string.gleam", 689). +-spec do_to_utf_codepoints(binary()) -> list(integer()). +do_to_utf_codepoints(String) -> + to_utf_codepoints_loop(<>, []). + +-file("src/gleam/string.gleam", 684). +?DOC( + " Converts a `String` to a `List` of `UtfCodepoint`.\n" + "\n" + " See and\n" + " for an\n" + " explanation on code points.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " \"a\" |> to_utf_codepoints\n" + " // -> [UtfCodepoint(97)]\n" + " ```\n" + "\n" + " ```gleam\n" + " // Semantically the same as:\n" + " // [\"🏳\", \"️\", \"‍\", \"🌈\"] or:\n" + " // [waving_white_flag, variant_selector_16, zero_width_joiner, rainbow]\n" + " \"🏳️‍🌈\" |> to_utf_codepoints\n" + " // -> [\n" + " // UtfCodepoint(127987),\n" + " // UtfCodepoint(65039),\n" + " // UtfCodepoint(8205),\n" + " // UtfCodepoint(127752),\n" + " // ]\n" + " ```\n" +). +-spec to_utf_codepoints(binary()) -> list(integer()). +to_utf_codepoints(String) -> + do_to_utf_codepoints(String). + +-file("src/gleam/string.gleam", 734). +?DOC( + " Converts a `List` of `UtfCodepoint`s to a `String`.\n" + "\n" + " See and\n" + " for an\n" + " explanation on code points.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let assert Ok(a) = utf_codepoint(97)\n" + " let assert Ok(b) = utf_codepoint(98)\n" + " let assert Ok(c) = utf_codepoint(99)\n" + " from_utf_codepoints([a, b, c])\n" + " // -> \"abc\"\n" + " ```\n" +). +-spec from_utf_codepoints(list(integer())) -> binary(). +from_utf_codepoints(Utf_codepoints) -> + gleam_stdlib:utf_codepoint_list_to_string(Utf_codepoints). + +-file("src/gleam/string.gleam", 740). +?DOC( + " Converts an integer to a `UtfCodepoint`.\n" + "\n" + " Returns an `Error` if the integer does not represent a valid UTF codepoint.\n" +). +-spec utf_codepoint(integer()) -> {ok, integer()} | {error, nil}. +utf_codepoint(Value) -> + case Value of + I when I > 1114111 -> + {error, nil}; + + I@1 when (I@1 >= 55296) andalso (I@1 =< 57343) -> + {error, nil}; + + I@2 when I@2 < 0 -> + {error, nil}; + + I@3 -> + {ok, gleam_stdlib:identity(I@3)} + end. + +-file("src/gleam/string.gleam", 761). +?DOC( + " Converts an UtfCodepoint to its ordinal code point value.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let assert [utf_codepoint, ..] = to_utf_codepoints(\"💜\")\n" + " utf_codepoint_to_int(utf_codepoint)\n" + " // -> 128156\n" + " ```\n" +). +-spec utf_codepoint_to_int(integer()) -> integer(). +utf_codepoint_to_int(Cp) -> + gleam_stdlib:identity(Cp). + +-file("src/gleam/string.gleam", 778). +?DOC( + " Converts a `String` into `Option(String)` where an empty `String` becomes\n" + " `None`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " to_option(\"\")\n" + " // -> None\n" + " ```\n" + "\n" + " ```gleam\n" + " to_option(\"hats\")\n" + " // -> Some(\"hats\")\n" + " ```\n" +). +-spec to_option(binary()) -> gleam@option:option(binary()). +to_option(String) -> + case String of + <<""/utf8>> -> + none; + + _ -> + {some, String} + end. + +-file("src/gleam/string.gleam", 801). +?DOC( + " Returns the first grapheme cluster in a given `String` and wraps it in a\n" + " `Result(String, Nil)`. If the `String` is empty, it returns `Error(Nil)`.\n" + " Otherwise, it returns `Ok(String)`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " first(\"\")\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " first(\"icecream\")\n" + " // -> Ok(\"i\")\n" + " ```\n" +). +-spec first(binary()) -> {ok, binary()} | {error, nil}. +first(String) -> + case gleam_stdlib:string_pop_grapheme(String) of + {ok, {First, _}} -> + {ok, First}; + + {error, E} -> + {error, E} + end. + +-file("src/gleam/string.gleam", 827). +?DOC( + " Returns the last grapheme cluster in a given `String` and wraps it in a\n" + " `Result(String, Nil)`. If the `String` is empty, it returns `Error(Nil)`.\n" + " Otherwise, it returns `Ok(String)`.\n" + "\n" + " This function traverses the full string, so it runs in linear time with the\n" + " length of the string. Avoid using this in a loop.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " last(\"\")\n" + " // -> Error(Nil)\n" + " ```\n" + "\n" + " ```gleam\n" + " last(\"icecream\")\n" + " // -> Ok(\"m\")\n" + " ```\n" +). +-spec last(binary()) -> {ok, binary()} | {error, nil}. +last(String) -> + case gleam_stdlib:string_pop_grapheme(String) of + {ok, {First, <<""/utf8>>}} -> + {ok, First}; + + {ok, {_, Rest}} -> + {ok, slice(Rest, -1, 1)}; + + {error, E} -> + {error, E} + end. + +-file("src/gleam/string.gleam", 845). +?DOC( + " Creates a new `String` with the first grapheme in the input `String`\n" + " converted to uppercase and the remaining graphemes to lowercase.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " capitalise(\"mamouna\")\n" + " // -> \"Mamouna\"\n" + " ```\n" +). +-spec capitalise(binary()) -> binary(). +capitalise(String) -> + case gleam_stdlib:string_pop_grapheme(String) of + {ok, {First, Rest}} -> + append(string:uppercase(First), string:lowercase(Rest)); + + {error, _} -> + <<""/utf8>> + end. + +-file("src/gleam/string.gleam", 876). +?DOC( + " Returns a `String` representation of a term in Gleam syntax.\n" + "\n" + " This may be occasionally useful for quick-and-dirty printing of values in\n" + " scripts. For error reporting and other uses prefer constructing strings by\n" + " pattern matching on the values.\n" + "\n" + " ## Limitations\n" + "\n" + " The output format of this function is not stable and could change at any\n" + " time. The output is not suitable for parsing.\n" + "\n" + " This function works using runtime reflection, so the output may not be\n" + " perfectly accurate for data structures where the runtime structure doesn't\n" + " hold enough information to determine the original syntax. For example,\n" + " tuples with an Erlang atom in the first position will be mistaken for Gleam\n" + " records.\n" + "\n" + " ## Security and safety\n" + "\n" + " There is no limit to how large the strings that this function can produce.\n" + " Be careful not to call this function with large data structures or you\n" + " could use very large amounts of memory, potentially causing runtime\n" + " problems.\n" +). +-spec inspect(any()) -> binary(). +inspect(Term) -> + _pipe = Term, + _pipe@1 = gleam_stdlib:inspect(_pipe), + unicode:characters_to_binary(_pipe@1). + +-file("src/gleam/string.gleam", 900). +?DOC( + " Returns the number of bytes in a `String`.\n" + "\n" + " This function runs in constant time on Erlang and in linear time on\n" + " JavaScript.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " byte_size(\"🏳️‍⚧️🏳️‍🌈👩🏾‍❤️‍👨🏻\")\n" + " // -> 58\n" + " ```\n" +). +-spec byte_size(binary()) -> integer(). +byte_size(String) -> + erlang:byte_size(String). + +-file("src/gleam/string.gleam", 245). +?DOC( + " Drops *n* graphemes from the start of a `String`.\n" + "\n" + " This function runs in linear time with the number of graphemes to drop.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " drop_start(from: \"The Lone Gunmen\", up_to: 2)\n" + " // -> \"e Lone Gunmen\"\n" + " ```\n" +). +-spec drop_start(binary(), integer()) -> binary(). +drop_start(String, Num_graphemes) -> + case Num_graphemes =< 0 of + true -> + String; + + false -> + Prefix = gleam_stdlib:slice(String, 0, Num_graphemes), + Prefix_size = erlang:byte_size(Prefix), + binary:part( + String, + Prefix_size, + erlang:byte_size(String) - Prefix_size + ) + end. diff --git a/build/packages/gleam_stdlib/src/gleam@string_tree.erl b/build/packages/gleam_stdlib/src/gleam@string_tree.erl new file mode 100644 index 0000000..0d72b4d --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@string_tree.erl @@ -0,0 +1,207 @@ +-module(gleam@string_tree). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/string_tree.gleam"). +-export([append_tree/2, prepend_tree/2, from_strings/1, new/0, concat/1, from_string/1, prepend/2, append/2, to_string/1, byte_size/1, join/2, lowercase/1, uppercase/1, reverse/1, split/2, replace/3, is_equal/2, is_empty/1]). +-export_type([string_tree/0, direction/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type string_tree() :: any(). + +-type direction() :: all. + +-file("src/gleam/string_tree.gleam", 61). +?DOC( + " Appends some `StringTree` onto the end of another.\n" + "\n" + " Runs in constant time.\n" +). +-spec append_tree(string_tree(), string_tree()) -> string_tree(). +append_tree(Tree, Suffix) -> + gleam_stdlib:iodata_append(Tree, Suffix). + +-file("src/gleam/string_tree.gleam", 48). +?DOC( + " Prepends some `StringTree` onto the start of another.\n" + "\n" + " Runs in constant time.\n" +). +-spec prepend_tree(string_tree(), string_tree()) -> string_tree(). +prepend_tree(Tree, Prefix) -> + gleam_stdlib:iodata_append(Prefix, Tree). + +-file("src/gleam/string_tree.gleam", 69). +?DOC( + " Converts a list of strings into a `StringTree`.\n" + "\n" + " Runs in constant time.\n" +). +-spec from_strings(list(binary())) -> string_tree(). +from_strings(Strings) -> + gleam_stdlib:identity(Strings). + +-file("src/gleam/string_tree.gleam", 24). +?DOC( + " Create an empty `StringTree`. Useful as the start of a pipe chaining many\n" + " trees together.\n" +). +-spec new() -> string_tree(). +new() -> + gleam_stdlib:identity([]). + +-file("src/gleam/string_tree.gleam", 77). +?DOC( + " Joins a list of trees into a single tree.\n" + "\n" + " Runs in constant time.\n" +). +-spec concat(list(string_tree())) -> string_tree(). +concat(Trees) -> + gleam_stdlib:identity(Trees). + +-file("src/gleam/string_tree.gleam", 85). +?DOC( + " Converts a string into a `StringTree`.\n" + "\n" + " Runs in constant time.\n" +). +-spec from_string(binary()) -> string_tree(). +from_string(String) -> + gleam_stdlib:identity(String). + +-file("src/gleam/string_tree.gleam", 32). +?DOC( + " Prepends a `String` onto the start of some `StringTree`.\n" + "\n" + " Runs in constant time.\n" +). +-spec prepend(string_tree(), binary()) -> string_tree(). +prepend(Tree, Prefix) -> + gleam_stdlib:iodata_append(gleam_stdlib:identity(Prefix), Tree). + +-file("src/gleam/string_tree.gleam", 40). +?DOC( + " Appends a `String` onto the end of some `StringTree`.\n" + "\n" + " Runs in constant time.\n" +). +-spec append(string_tree(), binary()) -> string_tree(). +append(Tree, Second) -> + gleam_stdlib:iodata_append(Tree, gleam_stdlib:identity(Second)). + +-file("src/gleam/string_tree.gleam", 94). +?DOC( + " Turns a `StringTree` into a `String`\n" + "\n" + " This function is implemented natively by the virtual machine and is highly\n" + " optimised.\n" +). +-spec to_string(string_tree()) -> binary(). +to_string(Tree) -> + unicode:characters_to_binary(Tree). + +-file("src/gleam/string_tree.gleam", 100). +?DOC(" Returns the size of the `StringTree` in bytes.\n"). +-spec byte_size(string_tree()) -> integer(). +byte_size(Tree) -> + erlang:iolist_size(Tree). + +-file("src/gleam/string_tree.gleam", 104). +?DOC(" Joins the given trees into a new tree separated with the given string.\n"). +-spec join(list(string_tree()), binary()) -> string_tree(). +join(Trees, Sep) -> + _pipe = Trees, + _pipe@1 = gleam@list:intersperse(_pipe, gleam_stdlib:identity(Sep)), + gleam_stdlib:identity(_pipe@1). + +-file("src/gleam/string_tree.gleam", 115). +?DOC( + " Converts a `StringTree` to a new one where the contents have been\n" + " lowercased.\n" +). +-spec lowercase(string_tree()) -> string_tree(). +lowercase(Tree) -> + string:lowercase(Tree). + +-file("src/gleam/string_tree.gleam", 122). +?DOC( + " Converts a `StringTree` to a new one where the contents have been\n" + " uppercased.\n" +). +-spec uppercase(string_tree()) -> string_tree(). +uppercase(Tree) -> + string:uppercase(Tree). + +-file("src/gleam/string_tree.gleam", 127). +?DOC(" Converts a `StringTree` to a new one with the contents reversed.\n"). +-spec reverse(string_tree()) -> string_tree(). +reverse(Tree) -> + string:reverse(Tree). + +-file("src/gleam/string_tree.gleam", 145). +?DOC(" Splits a `StringTree` on a given pattern into a list of trees.\n"). +-spec split(string_tree(), binary()) -> list(string_tree()). +split(Tree, Pattern) -> + string:split(Tree, Pattern, all). + +-file("src/gleam/string_tree.gleam", 156). +?DOC(" Replaces all instances of a pattern with a given string substitute.\n"). +-spec replace(string_tree(), binary(), binary()) -> string_tree(). +replace(Tree, Pattern, Substitute) -> + gleam_stdlib:string_replace(Tree, Pattern, Substitute). + +-file("src/gleam/string_tree.gleam", 182). +?DOC( + " Compares two string trees to determine if they have the same textual\n" + " content.\n" + "\n" + " Comparing two string trees using the `==` operator may return `False` even\n" + " if they have the same content as they may have been build in different ways,\n" + " so using this function is often preferred.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_strings([\"a\", \"b\"]) == from_string(\"ab\")\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " is_equal(from_strings([\"a\", \"b\"]), from_string(\"ab\"))\n" + " // -> True\n" + " ```\n" +). +-spec is_equal(string_tree(), string_tree()) -> boolean(). +is_equal(A, B) -> + string:equal(A, B). + +-file("src/gleam/string_tree.gleam", 206). +?DOC( + " Inspects a `StringTree` to determine if it is equivalent to an empty string.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " from_string(\"ok\") |> is_empty\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " from_string(\"\") |> is_empty\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " from_strings([]) |> is_empty\n" + " // -> True\n" + " ```\n" +). +-spec is_empty(string_tree()) -> boolean(). +is_empty(Tree) -> + string:is_empty(Tree). diff --git a/build/packages/gleam_stdlib/src/gleam@uri.erl b/build/packages/gleam_stdlib/src/gleam@uri.erl new file mode 100644 index 0000000..0819463 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam@uri.erl @@ -0,0 +1,1044 @@ +-module(gleam@uri). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/uri.gleam"). +-export([parse_query/1, percent_encode/1, query_to_string/1, percent_decode/1, path_segments/1, to_string/1, origin/1, merge/2, parse/1]). +-export_type([uri/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Utilities for working with URIs\n" + "\n" + " This module provides functions for working with URIs (for example, parsing\n" + " URIs or encoding query strings). The functions in this module are implemented\n" + " according to [RFC 3986](https://tools.ietf.org/html/rfc3986).\n" + "\n" + " Query encoding (Form encoding) is defined in the\n" + " [W3C specification](https://www.w3.org/TR/html52/sec-forms.html#urlencoded-form-data).\n" +). + +-type uri() :: {uri, + gleam@option:option(binary()), + gleam@option:option(binary()), + gleam@option:option(binary()), + gleam@option:option(integer()), + binary(), + gleam@option:option(binary()), + gleam@option:option(binary())}. + +-file("src/gleam/uri.gleam", 289). +-spec is_valid_host_within_brackets_char(integer()) -> boolean(). +is_valid_host_within_brackets_char(Char) -> + (((((48 >= Char) andalso (Char =< 57)) orelse ((65 >= Char) andalso (Char =< 90))) + orelse ((97 >= Char) andalso (Char =< 122))) + orelse (Char =:= 58)) + orelse (Char =:= 46). + +-file("src/gleam/uri.gleam", 506). +-spec parse_fragment(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_fragment(Rest, Pieces) -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + {some, Rest}}}. + +-file("src/gleam/uri.gleam", 478). +-spec parse_query_with_question_mark_loop(binary(), binary(), uri(), integer()) -> {ok, + uri()} | + {error, nil}. +parse_query_with_question_mark_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<"#"/utf8, Rest/binary>> when Size =:= 0 -> + parse_fragment(Rest, Pieces); + + <<"#"/utf8, Rest@1/binary>> -> + Query = binary:part(Original, 0, Size), + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + {some, Query}, + erlang:element(8, Pieces)}, + parse_fragment(Rest@1, Pieces@1); + + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + {some, Original}, + erlang:element(8, Pieces)}}; + + _ -> + {_, Rest@2} = gleam_stdlib:string_pop_codeunit(Uri_string), + parse_query_with_question_mark_loop( + Original, + Rest@2, + Pieces, + Size + 1 + ) + end. + +-file("src/gleam/uri.gleam", 471). +-spec parse_query_with_question_mark(binary(), uri()) -> {ok, uri()} | + {error, nil}. +parse_query_with_question_mark(Uri_string, Pieces) -> + parse_query_with_question_mark_loop(Uri_string, Uri_string, Pieces, 0). + +-file("src/gleam/uri.gleam", 437). +-spec parse_path_loop(binary(), binary(), uri(), integer()) -> {ok, uri()} | + {error, nil}. +parse_path_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<"?"/utf8, Rest/binary>> -> + Path = binary:part(Original, 0, Size), + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + Path, + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_query_with_question_mark(Rest, Pieces@1); + + <<"#"/utf8, Rest@1/binary>> -> + Path@1 = binary:part(Original, 0, Size), + Pieces@2 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + Path@1, + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_fragment(Rest@1, Pieces@2); + + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + Original, + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + _ -> + {_, Rest@2} = gleam_stdlib:string_pop_codeunit(Uri_string), + parse_path_loop(Original, Rest@2, Pieces, Size + 1) + end. + +-file("src/gleam/uri.gleam", 433). +-spec parse_path(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_path(Uri_string, Pieces) -> + parse_path_loop(Uri_string, Uri_string, Pieces, 0). + +-file("src/gleam/uri.gleam", 388). +-spec parse_port_loop(binary(), uri(), integer()) -> {ok, uri()} | {error, nil}. +parse_port_loop(Uri_string, Pieces, Port) -> + case Uri_string of + <<"0"/utf8, Rest/binary>> -> + parse_port_loop(Rest, Pieces, Port * 10); + + <<"1"/utf8, Rest@1/binary>> -> + parse_port_loop(Rest@1, Pieces, (Port * 10) + 1); + + <<"2"/utf8, Rest@2/binary>> -> + parse_port_loop(Rest@2, Pieces, (Port * 10) + 2); + + <<"3"/utf8, Rest@3/binary>> -> + parse_port_loop(Rest@3, Pieces, (Port * 10) + 3); + + <<"4"/utf8, Rest@4/binary>> -> + parse_port_loop(Rest@4, Pieces, (Port * 10) + 4); + + <<"5"/utf8, Rest@5/binary>> -> + parse_port_loop(Rest@5, Pieces, (Port * 10) + 5); + + <<"6"/utf8, Rest@6/binary>> -> + parse_port_loop(Rest@6, Pieces, (Port * 10) + 6); + + <<"7"/utf8, Rest@7/binary>> -> + parse_port_loop(Rest@7, Pieces, (Port * 10) + 7); + + <<"8"/utf8, Rest@8/binary>> -> + parse_port_loop(Rest@8, Pieces, (Port * 10) + 8); + + <<"9"/utf8, Rest@9/binary>> -> + parse_port_loop(Rest@9, Pieces, (Port * 10) + 9); + + <<"?"/utf8, Rest@10/binary>> -> + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + {some, Port}, + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_query_with_question_mark(Rest@10, Pieces@1); + + <<"#"/utf8, Rest@11/binary>> -> + Pieces@2 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + {some, Port}, + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_fragment(Rest@11, Pieces@2); + + <<"/"/utf8, _/binary>> -> + Pieces@3 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + {some, Port}, + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_path(Uri_string, Pieces@3); + + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + {some, Port}, + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + _ -> + {error, nil} + end. + +-file("src/gleam/uri.gleam", 353). +-spec parse_port(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_port(Uri_string, Pieces) -> + case Uri_string of + <<":0"/utf8, Rest/binary>> -> + parse_port_loop(Rest, Pieces, 0); + + <<":1"/utf8, Rest@1/binary>> -> + parse_port_loop(Rest@1, Pieces, 1); + + <<":2"/utf8, Rest@2/binary>> -> + parse_port_loop(Rest@2, Pieces, 2); + + <<":3"/utf8, Rest@3/binary>> -> + parse_port_loop(Rest@3, Pieces, 3); + + <<":4"/utf8, Rest@4/binary>> -> + parse_port_loop(Rest@4, Pieces, 4); + + <<":5"/utf8, Rest@5/binary>> -> + parse_port_loop(Rest@5, Pieces, 5); + + <<":6"/utf8, Rest@6/binary>> -> + parse_port_loop(Rest@6, Pieces, 6); + + <<":7"/utf8, Rest@7/binary>> -> + parse_port_loop(Rest@7, Pieces, 7); + + <<":8"/utf8, Rest@8/binary>> -> + parse_port_loop(Rest@8, Pieces, 8); + + <<":9"/utf8, Rest@9/binary>> -> + parse_port_loop(Rest@9, Pieces, 9); + + <<":"/utf8>> -> + {ok, Pieces}; + + <<""/utf8>> -> + {ok, Pieces}; + + <<"?"/utf8, Rest@10/binary>> -> + parse_query_with_question_mark(Rest@10, Pieces); + + <<":?"/utf8, Rest@10/binary>> -> + parse_query_with_question_mark(Rest@10, Pieces); + + <<"#"/utf8, Rest@11/binary>> -> + parse_fragment(Rest@11, Pieces); + + <<":#"/utf8, Rest@11/binary>> -> + parse_fragment(Rest@11, Pieces); + + <<"/"/utf8, _/binary>> -> + parse_path(Uri_string, Pieces); + + <<":"/utf8, Rest@12/binary>> -> + case Rest@12 of + <<"/"/utf8, _/binary>> -> + parse_path(Rest@12, Pieces); + + _ -> + {error, nil} + end; + + _ -> + {error, nil} + end. + +-file("src/gleam/uri.gleam", 309). +-spec parse_host_outside_of_brackets_loop(binary(), binary(), uri(), integer()) -> {ok, + uri()} | + {error, nil}. +parse_host_outside_of_brackets_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Original}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + <<":"/utf8, _/binary>> -> + Host = binary:part(Original, 0, Size), + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_port(Uri_string, Pieces@1); + + <<"/"/utf8, _/binary>> -> + Host@1 = binary:part(Original, 0, Size), + Pieces@2 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@1}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_path(Uri_string, Pieces@2); + + <<"?"/utf8, Rest/binary>> -> + Host@2 = binary:part(Original, 0, Size), + Pieces@3 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@2}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_query_with_question_mark(Rest, Pieces@3); + + <<"#"/utf8, Rest@1/binary>> -> + Host@3 = binary:part(Original, 0, Size), + Pieces@4 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@3}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_fragment(Rest@1, Pieces@4); + + _ -> + {_, Rest@2} = gleam_stdlib:string_pop_codeunit(Uri_string), + parse_host_outside_of_brackets_loop( + Original, + Rest@2, + Pieces, + Size + 1 + ) + end. + +-file("src/gleam/uri.gleam", 229). +-spec parse_host_within_brackets_loop(binary(), binary(), uri(), integer()) -> {ok, + uri()} | + {error, nil}. +parse_host_within_brackets_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Uri_string}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + <<"]"/utf8, Rest/binary>> when Size =:= 0 -> + parse_port(Rest, Pieces); + + <<"]"/utf8, Rest@1/binary>> -> + Host = binary:part(Original, 0, Size + 1), + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_port(Rest@1, Pieces@1); + + <<"/"/utf8, _/binary>> when Size =:= 0 -> + parse_path(Uri_string, Pieces); + + <<"/"/utf8, _/binary>> -> + Host@1 = binary:part(Original, 0, Size), + Pieces@2 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@1}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_path(Uri_string, Pieces@2); + + <<"?"/utf8, Rest@2/binary>> when Size =:= 0 -> + parse_query_with_question_mark(Rest@2, Pieces); + + <<"?"/utf8, Rest@3/binary>> -> + Host@2 = binary:part(Original, 0, Size), + Pieces@3 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@2}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_query_with_question_mark(Rest@3, Pieces@3); + + <<"#"/utf8, Rest@4/binary>> when Size =:= 0 -> + parse_fragment(Rest@4, Pieces); + + <<"#"/utf8, Rest@5/binary>> -> + Host@3 = binary:part(Original, 0, Size), + Pieces@4 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, Host@3}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_fragment(Rest@5, Pieces@4); + + _ -> + {Char, Rest@6} = gleam_stdlib:string_pop_codeunit(Uri_string), + case is_valid_host_within_brackets_char(Char) of + true -> + parse_host_within_brackets_loop( + Original, + Rest@6, + Pieces, + Size + 1 + ); + + false -> + parse_host_outside_of_brackets_loop( + Original, + Original, + Pieces, + 0 + ) + end + end. + +-file("src/gleam/uri.gleam", 222). +-spec parse_host_within_brackets(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_host_within_brackets(Uri_string, Pieces) -> + parse_host_within_brackets_loop(Uri_string, Uri_string, Pieces, 0). + +-file("src/gleam/uri.gleam", 302). +-spec parse_host_outside_of_brackets(binary(), uri()) -> {ok, uri()} | + {error, nil}. +parse_host_outside_of_brackets(Uri_string, Pieces) -> + parse_host_outside_of_brackets_loop(Uri_string, Uri_string, Pieces, 0). + +-file("src/gleam/uri.gleam", 199). +-spec parse_host(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_host(Uri_string, Pieces) -> + case Uri_string of + <<"["/utf8, _/binary>> -> + parse_host_within_brackets(Uri_string, Pieces); + + <<":"/utf8, _/binary>> -> + Pieces@1 = {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, <<""/utf8>>}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_port(Uri_string, Pieces@1); + + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, <<""/utf8>>}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + _ -> + parse_host_outside_of_brackets(Uri_string, Pieces) + end. + +-file("src/gleam/uri.gleam", 167). +-spec parse_userinfo_loop(binary(), binary(), uri(), integer()) -> {ok, uri()} | + {error, nil}. +parse_userinfo_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<"@"/utf8, Rest/binary>> when Size =:= 0 -> + parse_host(Rest, Pieces); + + <<"@"/utf8, Rest@1/binary>> -> + Userinfo = binary:part(Original, 0, Size), + Pieces@1 = {uri, + erlang:element(2, Pieces), + {some, Userinfo}, + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_host(Rest@1, Pieces@1); + + <<""/utf8>> -> + parse_host(Original, Pieces); + + <<"/"/utf8, _/binary>> -> + parse_host(Original, Pieces); + + <<"?"/utf8, _/binary>> -> + parse_host(Original, Pieces); + + <<"#"/utf8, _/binary>> -> + parse_host(Original, Pieces); + + _ -> + {_, Rest@2} = gleam_stdlib:string_pop_codeunit(Uri_string), + parse_userinfo_loop(Original, Rest@2, Pieces, Size + 1) + end. + +-file("src/gleam/uri.gleam", 163). +-spec parse_authority_pieces(binary(), uri()) -> {ok, uri()} | {error, nil}. +parse_authority_pieces(String, Pieces) -> + parse_userinfo_loop(String, String, Pieces, 0). + +-file("src/gleam/uri.gleam", 150). +-spec parse_authority_with_slashes(binary(), uri()) -> {ok, uri()} | + {error, nil}. +parse_authority_with_slashes(Uri_string, Pieces) -> + case Uri_string of + <<"//"/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + {some, <<""/utf8>>}, + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + <<"//"/utf8, Rest/binary>> -> + parse_authority_pieces(Rest, Pieces); + + _ -> + parse_path(Uri_string, Pieces) + end. + +-file("src/gleam/uri.gleam", 91). +-spec parse_scheme_loop(binary(), binary(), uri(), integer()) -> {ok, uri()} | + {error, nil}. +parse_scheme_loop(Original, Uri_string, Pieces, Size) -> + case Uri_string of + <<"/"/utf8, _/binary>> when Size =:= 0 -> + parse_authority_with_slashes(Uri_string, Pieces); + + <<"/"/utf8, _/binary>> -> + Scheme = binary:part(Original, 0, Size), + Pieces@1 = {uri, + {some, string:lowercase(Scheme)}, + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_authority_with_slashes(Uri_string, Pieces@1); + + <<"?"/utf8, Rest/binary>> when Size =:= 0 -> + parse_query_with_question_mark(Rest, Pieces); + + <<"?"/utf8, Rest@1/binary>> -> + Scheme@1 = binary:part(Original, 0, Size), + Pieces@2 = {uri, + {some, string:lowercase(Scheme@1)}, + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_query_with_question_mark(Rest@1, Pieces@2); + + <<"#"/utf8, Rest@2/binary>> when Size =:= 0 -> + parse_fragment(Rest@2, Pieces); + + <<"#"/utf8, Rest@3/binary>> -> + Scheme@2 = binary:part(Original, 0, Size), + Pieces@3 = {uri, + {some, string:lowercase(Scheme@2)}, + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_fragment(Rest@3, Pieces@3); + + <<":"/utf8, _/binary>> when Size =:= 0 -> + {error, nil}; + + <<":"/utf8, Rest@4/binary>> -> + Scheme@3 = binary:part(Original, 0, Size), + Pieces@4 = {uri, + {some, string:lowercase(Scheme@3)}, + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + erlang:element(6, Pieces), + erlang:element(7, Pieces), + erlang:element(8, Pieces)}, + parse_authority_with_slashes(Rest@4, Pieces@4); + + <<""/utf8>> -> + {ok, + {uri, + erlang:element(2, Pieces), + erlang:element(3, Pieces), + erlang:element(4, Pieces), + erlang:element(5, Pieces), + Original, + erlang:element(7, Pieces), + erlang:element(8, Pieces)}}; + + _ -> + {_, Rest@5} = gleam_stdlib:string_pop_codeunit(Uri_string), + parse_scheme_loop(Original, Rest@5, Pieces, Size + 1) + end. + +-file("src/gleam/uri.gleam", 537). +?DOC( + " Parses an urlencoded query string into a list of key value pairs.\n" + " Returns an error for invalid encoding.\n" + "\n" + " The opposite operation is `uri.query_to_string`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " parse_query(\"a=1&b=2\")\n" + " // -> Ok([#(\"a\", \"1\"), #(\"b\", \"2\")])\n" + " ```\n" +). +-spec parse_query(binary()) -> {ok, list({binary(), binary()})} | {error, nil}. +parse_query(Query) -> + gleam_stdlib:parse_query(Query). + +-file("src/gleam/uri.gleam", 573). +?DOC( + " Encodes a string into a percent encoded representation.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " percent_encode(\"100% great\")\n" + " // -> \"100%25%20great\"\n" + " ```\n" +). +-spec percent_encode(binary()) -> binary(). +percent_encode(Value) -> + gleam_stdlib:percent_encode(Value). + +-file("src/gleam/uri.gleam", 558). +-spec query_pair({binary(), binary()}) -> gleam@string_tree:string_tree(). +query_pair(Pair) -> + gleam_stdlib:identity( + [gleam_stdlib:percent_encode(erlang:element(1, Pair)), + <<"="/utf8>>, + gleam_stdlib:percent_encode(erlang:element(2, Pair))] + ). + +-file("src/gleam/uri.gleam", 550). +?DOC( + " Encodes a list of key value pairs as a URI query string.\n" + "\n" + " The opposite operation is `uri.parse_query`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " query_to_string([#(\"a\", \"1\"), #(\"b\", \"2\")])\n" + " // -> \"a=1&b=2\"\n" + " ```\n" +). +-spec query_to_string(list({binary(), binary()})) -> binary(). +query_to_string(Query) -> + _pipe = Query, + _pipe@1 = gleam@list:map(_pipe, fun query_pair/1), + _pipe@2 = gleam@list:intersperse( + _pipe@1, + gleam_stdlib:identity(<<"&"/utf8>>) + ), + _pipe@3 = gleam_stdlib:identity(_pipe@2), + unicode:characters_to_binary(_pipe@3). + +-file("src/gleam/uri.gleam", 586). +?DOC( + " Decodes a percent encoded string.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " percent_decode(\"100%25%20great+fun\")\n" + " // -> Ok(\"100% great+fun\")\n" + " ```\n" +). +-spec percent_decode(binary()) -> {ok, binary()} | {error, nil}. +percent_decode(Value) -> + gleam_stdlib:percent_decode(Value). + +-file("src/gleam/uri.gleam", 608). +-spec remove_dot_segments_loop(list(binary()), list(binary())) -> list(binary()). +remove_dot_segments_loop(Input, Accumulator) -> + case Input of + [] -> + lists:reverse(Accumulator); + + [Segment | Rest] -> + Accumulator@5 = case {Segment, Accumulator} of + {<<""/utf8>>, Accumulator@1} -> + Accumulator@1; + + {<<"."/utf8>>, Accumulator@2} -> + Accumulator@2; + + {<<".."/utf8>>, []} -> + []; + + {<<".."/utf8>>, [_ | Accumulator@3]} -> + Accumulator@3; + + {Segment@1, Accumulator@4} -> + [Segment@1 | Accumulator@4] + end, + remove_dot_segments_loop(Rest, Accumulator@5) + end. + +-file("src/gleam/uri.gleam", 604). +-spec remove_dot_segments(list(binary())) -> list(binary()). +remove_dot_segments(Input) -> + remove_dot_segments_loop(Input, []). + +-file("src/gleam/uri.gleam", 600). +?DOC( + " Splits the path section of a URI into it's constituent segments.\n" + "\n" + " Removes empty segments and resolves dot-segments as specified in\n" + " [section 5.2](https://www.ietf.org/rfc/rfc3986.html#section-5.2) of the RFC.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " path_segments(\"/users/1\")\n" + " // -> [\"users\" ,\"1\"]\n" + " ```\n" +). +-spec path_segments(binary()) -> list(binary()). +path_segments(Path) -> + remove_dot_segments(gleam@string:split(Path, <<"/"/utf8>>)). + +-file("src/gleam/uri.gleam", 639). +?DOC( + " Encodes a `Uri` value as a URI string.\n" + "\n" + " The opposite operation is `uri.parse`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let uri = Uri(..empty, scheme: Some(\"https\"), host: Some(\"example.com\"))\n" + " to_string(uri)\n" + " // -> \"https://example.com\"\n" + " ```\n" +). +-spec to_string(uri()) -> binary(). +to_string(Uri) -> + Parts = case erlang:element(8, Uri) of + {some, Fragment} -> + [<<"#"/utf8>>, Fragment]; + + none -> + [] + end, + Parts@1 = case erlang:element(7, Uri) of + {some, Query} -> + [<<"?"/utf8>>, Query | Parts]; + + none -> + Parts + end, + Parts@2 = [erlang:element(6, Uri) | Parts@1], + Parts@3 = case {erlang:element(4, Uri), + gleam_stdlib:string_starts_with(erlang:element(6, Uri), <<"/"/utf8>>)} of + {{some, Host}, false} when Host =/= <<""/utf8>> -> + [<<"/"/utf8>> | Parts@2]; + + {_, _} -> + Parts@2 + end, + Parts@4 = case {erlang:element(4, Uri), erlang:element(5, Uri)} of + {{some, _}, {some, Port}} -> + [<<":"/utf8>>, erlang:integer_to_binary(Port) | Parts@3]; + + {_, _} -> + Parts@3 + end, + Parts@5 = case {erlang:element(2, Uri), + erlang:element(3, Uri), + erlang:element(4, Uri)} of + {{some, S}, {some, U}, {some, H}} -> + [S, <<"://"/utf8>>, U, <<"@"/utf8>>, H | Parts@4]; + + {{some, S@1}, none, {some, H@1}} -> + [S@1, <<"://"/utf8>>, H@1 | Parts@4]; + + {{some, S@2}, {some, _}, none} -> + [S@2, <<":"/utf8>> | Parts@4]; + + {{some, S@2}, none, none} -> + [S@2, <<":"/utf8>> | Parts@4]; + + {none, none, {some, H@2}} -> + [<<"//"/utf8>>, H@2 | Parts@4]; + + {_, _, _} -> + Parts@4 + end, + erlang:list_to_binary(Parts@5). + +-file("src/gleam/uri.gleam", 683). +?DOC( + " Fetches the origin of a URI.\n" + "\n" + " Returns the origin of a uri as defined in\n" + " [RFC 6454](https://tools.ietf.org/html/rfc6454)\n" + "\n" + " The supported URI schemes are `http` and `https`.\n" + " URLs without a scheme will return `Error`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " let assert Ok(uri) = parse(\"https://example.com/path?foo#bar\")\n" + " origin(uri)\n" + " // -> Ok(\"https://example.com\")\n" + " ```\n" +). +-spec origin(uri()) -> {ok, binary()} | {error, nil}. +origin(Uri) -> + {uri, Scheme, _, Host, Port, _, _, _} = Uri, + case {Host, Scheme} of + {{some, H}, {some, <<"https"/utf8>>}} when Port =:= {some, 443} -> + {ok, erlang:list_to_binary([<<"https://"/utf8>>, H])}; + + {{some, H@1}, {some, <<"http"/utf8>>}} when Port =:= {some, 80} -> + {ok, erlang:list_to_binary([<<"http://"/utf8>>, H@1])}; + + {{some, H@2}, {some, S}} when (S =:= <<"http"/utf8>>) orelse (S =:= <<"https"/utf8>>) -> + case Port of + {some, P} -> + {ok, + erlang:list_to_binary( + [S, + <<"://"/utf8>>, + H@2, + <<":"/utf8>>, + erlang:integer_to_binary(P)] + )}; + + none -> + {ok, erlang:list_to_binary([S, <<"://"/utf8>>, H@2])} + end; + + {_, _} -> + {error, nil} + end. + +-file("src/gleam/uri.gleam", 764). +-spec drop_last(list(DDO)) -> list(DDO). +drop_last(Elements) -> + gleam@list:take(Elements, erlang:length(Elements) - 1). + +-file("src/gleam/uri.gleam", 768). +-spec join_segments(list(binary())) -> binary(). +join_segments(Segments) -> + gleam@string:join([<<""/utf8>> | Segments], <<"/"/utf8>>). + +-file("src/gleam/uri.gleam", 706). +?DOC( + " Resolves a URI with respect to the given base URI.\n" + "\n" + " The base URI must be an absolute URI or this function will return an error.\n" + " The algorithm for merging uris is described in\n" + " [RFC 3986](https://tools.ietf.org/html/rfc3986#section-5.2).\n" +). +-spec merge(uri(), uri()) -> {ok, uri()} | {error, nil}. +merge(Base, Relative) -> + case Base of + {uri, {some, _}, _, {some, _}, _, _, _, _} -> + case Relative of + {uri, _, _, {some, _}, _, _, _, _} -> + Path = begin + _pipe = erlang:element(6, Relative), + _pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>), + _pipe@2 = remove_dot_segments(_pipe@1), + join_segments(_pipe@2) + end, + Resolved = {uri, + gleam@option:'or'( + erlang:element(2, Relative), + erlang:element(2, Base) + ), + none, + erlang:element(4, Relative), + gleam@option:'or'( + erlang:element(5, Relative), + erlang:element(5, Base) + ), + Path, + erlang:element(7, Relative), + erlang:element(8, Relative)}, + {ok, Resolved}; + + _ -> + {New_path, New_query} = case erlang:element(6, Relative) of + <<""/utf8>> -> + {erlang:element(6, Base), + gleam@option:'or'( + erlang:element(7, Relative), + erlang:element(7, Base) + )}; + + _ -> + Path_segments = case gleam_stdlib:string_starts_with( + erlang:element(6, Relative), + <<"/"/utf8>> + ) of + true -> + gleam@string:split( + erlang:element(6, Relative), + <<"/"/utf8>> + ); + + false -> + _pipe@3 = erlang:element(6, Base), + _pipe@4 = gleam@string:split( + _pipe@3, + <<"/"/utf8>> + ), + _pipe@5 = drop_last(_pipe@4), + lists:append( + _pipe@5, + gleam@string:split( + erlang:element(6, Relative), + <<"/"/utf8>> + ) + ) + end, + Path@1 = begin + _pipe@6 = Path_segments, + _pipe@7 = remove_dot_segments(_pipe@6), + join_segments(_pipe@7) + end, + {Path@1, erlang:element(7, Relative)} + end, + Resolved@1 = {uri, + erlang:element(2, Base), + none, + erlang:element(4, Base), + erlang:element(5, Base), + New_path, + New_query, + erlang:element(8, Relative)}, + {ok, Resolved@1} + end; + + _ -> + {error, nil} + end. + +-file("src/gleam/uri.gleam", 81). +?DOC( + " Parses a compliant URI string into the `Uri` Type.\n" + " If the string is not a valid URI string then an error is returned.\n" + "\n" + " The opposite operation is `uri.to_string`.\n" + "\n" + " ## Examples\n" + "\n" + " ```gleam\n" + " parse(\"https://example.com:1234/a/b?query=true#fragment\")\n" + " // -> Ok(\n" + " // Uri(\n" + " // scheme: Some(\"https\"),\n" + " // userinfo: None,\n" + " // host: Some(\"example.com\"),\n" + " // port: Some(1234),\n" + " // path: \"/a/b\",\n" + " // query: Some(\"query=true\"),\n" + " // fragment: Some(\"fragment\")\n" + " // )\n" + " // )\n" + " ```\n" +). +-spec parse(binary()) -> {ok, uri()} | {error, nil}. +parse(Uri_string) -> + gleam_stdlib:uri_parse(Uri_string). diff --git a/build/packages/gleam_stdlib/src/gleam_stdlib.app.src b/build/packages/gleam_stdlib/src/gleam_stdlib.app.src new file mode 100644 index 0000000..761251e --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam_stdlib.app.src @@ -0,0 +1,31 @@ +{application, gleam_stdlib, [ + {vsn, "0.65.0"}, + {applications, []}, + {description, "A standard library for the Gleam programming language"}, + {modules, [gleam@bit_array, + gleam@bool, + gleam@bytes_tree, + gleam@dict, + gleam@dynamic, + gleam@dynamic@decode, + gleam@float, + gleam@function, + gleam@int, + gleam@io, + gleam@list, + gleam@option, + gleam@order, + gleam@pair, + gleam@result, + gleam@set, + gleam@string, + gleam@string_tree, + gleam@uri, + gleam_stdlib, + gleam_stdlib@@main, + gleam_stdlib_test_ffi, + gleeunit_ffi, + gleeunit_gleam_panic_ffi, + gleeunit_progress]}, + {registered, []} +]}. diff --git a/build/packages/gleam_stdlib/src/gleam_stdlib.erl b/build/packages/gleam_stdlib/src/gleam_stdlib.erl new file mode 100644 index 0000000..2c416f4 --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam_stdlib.erl @@ -0,0 +1,534 @@ +-module(gleam_stdlib). + +-export([ + map_get/2, iodata_append/2, identity/1, parse_int/1, parse_float/1, + less_than/2, string_pop_grapheme/1, string_pop_codeunit/1, + string_starts_with/2, wrap_list/1, string_ends_with/2, string_pad/4, + uri_parse/1, bit_array_slice/3, percent_encode/1, percent_decode/1, + base64_decode/1, parse_query/1, bit_array_concat/1, + base64_encode/2, tuple_get/2, classify_dynamic/1, print/1, + println/1, print_error/1, println_error/1, inspect/1, float_to_string/1, + int_from_base_string/2, utf_codepoint_list_to_string/1, contains_string/2, + crop_string/2, base16_encode/1, base16_decode/1, string_replace/3, slice/3, + bit_array_to_int_and_size/1, bit_array_pad_to_bytes/1, index/2, list/5, + dict/1, int/1, float/1, bit_array/1, is_null/1 +]). + +%% Taken from OTP's uri_string module +-define(DEC2HEX(X), + if ((X) >= 0) andalso ((X) =< 9) -> (X) + $0; + ((X) >= 10) andalso ((X) =< 15) -> (X) + $A - 10 + end). + +%% Taken from OTP's uri_string module +-define(HEX2DEC(X), + if ((X) >= $0) andalso ((X) =< $9) -> (X) - $0; + ((X) >= $A) andalso ((X) =< $F) -> (X) - $A + 10; + ((X) >= $a) andalso ((X) =< $f) -> (X) - $a + 10 + end). + +-define(is_lowercase_char(X), + (X > 96 andalso X < 123)). +-define(is_underscore_char(X), + (X == 95)). +-define(is_digit_char(X), + (X > 47 andalso X < 58)). +-define(is_ascii_character(X), + (erlang:is_integer(X) andalso X >= 32 andalso X =< 126)). + +uppercase(X) -> X - 32. + +map_get(Map, Key) -> + case maps:find(Key, Map) of + error -> {error, nil}; + OkFound -> OkFound + end. + +iodata_append(Iodata, String) -> [Iodata, String]. + +identity(X) -> X. + +classify_dynamic(nil) -> <<"Nil">>; +classify_dynamic(null) -> <<"Nil">>; +classify_dynamic(undefined) -> <<"Nil">>; +classify_dynamic(X) when is_boolean(X) -> <<"Bool">>; +classify_dynamic(X) when is_atom(X) -> <<"Atom">>; +classify_dynamic(X) when is_binary(X) -> <<"String">>; +classify_dynamic(X) when is_bitstring(X) -> <<"BitArray">>; +classify_dynamic(X) when is_integer(X) -> <<"Int">>; +classify_dynamic(X) when is_float(X) -> <<"Float">>; +classify_dynamic(X) when is_list(X) -> <<"List">>; +classify_dynamic(X) when is_map(X) -> <<"Dict">>; +classify_dynamic(X) when is_tuple(X) -> <<"Array">>; +classify_dynamic(X) when is_reference(X) -> <<"Reference">>; +classify_dynamic(X) when is_pid(X) -> <<"Pid">>; +classify_dynamic(X) when is_port(X) -> <<"Port">>; +classify_dynamic(X) when + is_function(X, 0) orelse is_function(X, 1) orelse is_function(X, 2) orelse + is_function(X, 3) orelse is_function(X, 4) orelse is_function(X, 5) orelse + is_function(X, 6) orelse is_function(X, 7) orelse is_function(X, 8) orelse + is_function(X, 9) orelse is_function(X, 10) orelse is_function(X, 11) orelse + is_function(X, 12) -> <<"Function">>; +classify_dynamic(_) -> <<"Unknown">>. + +tuple_get(_tup, Index) when Index < 0 -> {error, nil}; +tuple_get(Data, Index) when Index >= tuple_size(Data) -> {error, nil}; +tuple_get(Data, Index) -> {ok, element(Index + 1, Data)}. + +int_from_base_string(String, Base) -> + case catch binary_to_integer(String, Base) of + Int when is_integer(Int) -> {ok, Int}; + _ -> {error, nil} + end. + +parse_int(String) -> + case catch binary_to_integer(String) of + Int when is_integer(Int) -> {ok, Int}; + _ -> {error, nil} + end. + +parse_float(String) -> + case catch binary_to_float(String) of + Float when is_float(Float) -> {ok, Float}; + _ -> {error, nil} + end. + +less_than(Lhs, Rhs) -> + Lhs < Rhs. + +string_starts_with(_, <<>>) -> true; +string_starts_with(String, Prefix) when byte_size(Prefix) > byte_size(String) -> false; +string_starts_with(String, Prefix) -> + PrefixSize = byte_size(Prefix), + Prefix == binary_part(String, 0, PrefixSize). + +string_ends_with(_, <<>>) -> true; +string_ends_with(String, Suffix) when byte_size(Suffix) > byte_size(String) -> false; +string_ends_with(String, Suffix) -> + SuffixSize = byte_size(Suffix), + Suffix == binary_part(String, byte_size(String) - SuffixSize, SuffixSize). + +string_pad(String, Length, Dir, PadString) -> + Chars = string:pad(String, Length, Dir, binary_to_list(PadString)), + case unicode:characters_to_binary(Chars) of + Bin when is_binary(Bin) -> Bin; + Error -> erlang:error({gleam_error, {string_invalid_utf8, Error}}) + end. + +string_pop_grapheme(String) -> + case string:next_grapheme(String) of + [ Next | Rest ] when is_binary(Rest) -> + {ok, {unicode:characters_to_binary([Next]), Rest}}; + + [ Next | Rest ] -> + {ok, {unicode:characters_to_binary([Next]), unicode:characters_to_binary(Rest)}}; + + _ -> {error, nil} + end. + +string_pop_codeunit(<>) -> {Cp, Rest}; +string_pop_codeunit(Binary) -> {0, Binary}. + +bit_array_pad_to_bytes(Bin) -> + case erlang:bit_size(Bin) rem 8 of + 0 -> Bin; + TrailingBits -> + PaddingBits = 8 - TrailingBits, + <> + end. + +bit_array_concat(BitArrays) -> + list_to_bitstring(BitArrays). + +-if(?OTP_RELEASE >= 26). +base64_encode(Bin, Padding) -> + PaddedBin = bit_array_pad_to_bytes(Bin), + base64:encode(PaddedBin, #{padding => Padding}). +-else. +base64_encode(_Bin, _Padding) -> + erlang:error(<<"Erlang OTP/26 or higher is required to use base64:encode">>). +-endif. + +bit_array_slice(Bin, Pos, Len) -> + try {ok, binary:part(Bin, Pos, Len)} + catch error:badarg -> {error, nil} + end. + +base64_decode(S) -> + try {ok, base64:decode(S)} + catch error:_ -> {error, nil} + end. + +wrap_list(X) when is_list(X) -> X; +wrap_list(X) -> [X]. + +parse_query(Query) -> + case uri_string:dissect_query(Query) of + {error, _, _} -> {error, nil}; + Pairs -> + Pairs1 = lists:map(fun + ({K, true}) -> {K, <<"">>}; + (Pair) -> Pair + end, Pairs), + {ok, Pairs1} + end. + +percent_encode(B) -> percent_encode(B, <<>>). +percent_encode(<<>>, Acc) -> + Acc; +percent_encode(<>, Acc) -> + case percent_ok(H) of + true -> + percent_encode(T, <>); + false -> + <> = <>, + percent_encode(T, <>) + end. + +percent_decode(Cs) -> percent_decode(Cs, <<>>). +percent_decode(<<$%, C0, C1, Cs/binary>>, Acc) -> + case is_hex_digit(C0) andalso is_hex_digit(C1) of + true -> + B = ?HEX2DEC(C0)*16+?HEX2DEC(C1), + percent_decode(Cs, <>); + false -> + {error, nil} + end; +percent_decode(<>, Acc) -> + percent_decode(Cs, <>); +percent_decode(<<>>, Acc) -> + check_utf8(Acc). + +percent_ok($!) -> true; +percent_ok($$) -> true; +percent_ok($') -> true; +percent_ok($() -> true; +percent_ok($)) -> true; +percent_ok($*) -> true; +percent_ok($+) -> true; +percent_ok($-) -> true; +percent_ok($.) -> true; +percent_ok($_) -> true; +percent_ok($~) -> true; +percent_ok(C) when $0 =< C, C =< $9 -> true; +percent_ok(C) when $A =< C, C =< $Z -> true; +percent_ok(C) when $a =< C, C =< $z -> true; +percent_ok(_) -> false. + +is_hex_digit(C) -> + ($0 =< C andalso C =< $9) orelse ($a =< C andalso C =< $f) orelse ($A =< C andalso C =< $F). + +check_utf8(Cs) -> + case unicode:characters_to_list(Cs) of + {incomplete, _, _} -> {error, nil}; + {error, _, _} -> {error, nil}; + _ -> {ok, Cs} + end. + +uri_parse(String) -> + case uri_string:parse(String) of + {error, _, _} -> {error, nil}; + Uri -> + Port = + try maps:get(port, Uri) of + undefined -> none; + Value -> {some, Value} + catch _:_ -> none + end, + {ok, {uri, + maps_get_optional(Uri, scheme), + maps_get_optional(Uri, userinfo), + maps_get_optional(Uri, host), + Port, + maps_get_or(Uri, path, <<>>), + maps_get_optional(Uri, query), + maps_get_optional(Uri, fragment) + }} + end. + +maps_get_optional(Map, Key) -> + try {some, maps:get(Key, Map)} + catch _:_ -> none + end. + +maps_get_or(Map, Key, Default) -> + try maps:get(Key, Map) + catch _:_ -> Default + end. + +print(String) -> + io:put_chars(String), + nil. + +println(String) -> + io:put_chars([String, $\n]), + nil. + +print_error(String) -> + io:put_chars(standard_error, String), + nil. + +println_error(String) -> + io:put_chars(standard_error, [String, $\n]), + nil. + +inspect(true) -> + "True"; +inspect(false) -> + "False"; +inspect(nil) -> + "Nil"; +inspect(Data) when is_map(Data) -> + Fields = [ + [<<"#(">>, inspect(Key), <<", ">>, inspect(Value), <<")">>] + || {Key, Value} <- maps:to_list(Data) + ], + ["dict.from_list([", lists:join(", ", Fields), "])"]; +inspect(Atom) when is_atom(Atom) -> + erlang:element(2, inspect_atom(Atom)); +inspect(Any) when is_integer(Any) -> + erlang:integer_to_list(Any); +inspect(Any) when is_float(Any) -> + io_lib_format:fwrite_g(Any); +inspect(Binary) when is_binary(Binary) -> + case inspect_maybe_utf8_string(Binary, <<>>) of + {ok, InspectedUtf8String} -> InspectedUtf8String; + {error, not_a_utf8_string} -> + Segments = [erlang:integer_to_list(X) || <> <= Binary], + ["<<", lists:join(", ", Segments), ">>"] + end; +inspect(Bits) when is_bitstring(Bits) -> + inspect_bit_array(Bits); +inspect(List) when is_list(List) -> + case inspect_list(List, true) of + {charlist, _} -> ["charlist.from_string(\"", list_to_binary(List), "\")"]; + {proper, Elements} -> ["[", Elements, "]"]; + {improper, Elements} -> ["//erl([", Elements, "])"] + end; +inspect(Any) when is_tuple(Any) % Record constructors + andalso is_atom(element(1, Any)) + andalso element(1, Any) =/= false + andalso element(1, Any) =/= true + andalso element(1, Any) =/= nil +-> + [Atom | ArgsList] = erlang:tuple_to_list(Any), + InspectedArgs = lists:map(fun inspect/1, ArgsList), + case inspect_atom(Atom) of + {gleam_atom, GleamAtom} -> + Args = lists:join(<<", ">>, InspectedArgs), + [GleamAtom, "(", Args, ")"]; + {erlang_atom, ErlangAtom} -> + Args = lists:join(<<", ">>, [ErlangAtom | InspectedArgs]), + ["#(", Args, ")"] + end; +inspect(Tuple) when is_tuple(Tuple) -> + Elements = lists:map(fun inspect/1, erlang:tuple_to_list(Tuple)), + ["#(", lists:join(", ", Elements), ")"]; +inspect(Any) when is_function(Any) -> + {arity, Arity} = erlang:fun_info(Any, arity), + ArgsAsciiCodes = lists:seq($a, $a + Arity - 1), + Args = lists:join(<<", ">>, + lists:map(fun(Arg) -> <> end, ArgsAsciiCodes) + ), + ["//fn(", Args, ") { ... }"]; +inspect(Any) -> + ["//erl(", io_lib:format("~p", [Any]), ")"]. + +inspect_atom(Atom) -> + Binary = erlang:atom_to_binary(Atom), + case inspect_maybe_gleam_atom(Binary, none, <<>>) of + {ok, Inspected} -> {gleam_atom, Inspected}; + {error, _} -> {erlang_atom, ["atom.create(\"", Binary, "\")"]} + end. + +inspect_maybe_gleam_atom(<<>>, none, _) -> + {error, nil}; +inspect_maybe_gleam_atom(<>, none, _) when ?is_digit_char(First) -> + {error, nil}; +inspect_maybe_gleam_atom(<<"_", _Rest/binary>>, none, _) -> + {error, nil}; +inspect_maybe_gleam_atom(<<"_">>, _PrevChar, _Acc) -> + {error, nil}; +inspect_maybe_gleam_atom(<<"_", _Rest/binary>>, $_, _Acc) -> + {error, nil}; +inspect_maybe_gleam_atom(<>, _PrevChar, _Acc) + when not (?is_lowercase_char(First) orelse ?is_underscore_char(First) orelse ?is_digit_char(First)) -> + {error, nil}; +inspect_maybe_gleam_atom(<>, none, Acc) -> + inspect_maybe_gleam_atom(Rest, First, <>); +inspect_maybe_gleam_atom(<<"_", Rest/binary>>, _PrevChar, Acc) -> + inspect_maybe_gleam_atom(Rest, $_, Acc); +inspect_maybe_gleam_atom(<>, $_, Acc) -> + inspect_maybe_gleam_atom(Rest, First, <>); +inspect_maybe_gleam_atom(<>, _PrevChar, Acc) -> + inspect_maybe_gleam_atom(Rest, First, <>); +inspect_maybe_gleam_atom(<<>>, _PrevChar, Acc) -> + {ok, Acc}; +inspect_maybe_gleam_atom(A, B, C) -> + erlang:display({A, B, C}), + throw({gleam_error, A, B, C}). + +inspect_list([], _) -> + {proper, []}; +inspect_list([First], true) when ?is_ascii_character(First) -> + {charlist, nil}; +inspect_list([First], _) -> + {proper, [inspect(First)]}; +inspect_list([First | Rest], ValidCharlist) when is_list(Rest) -> + StillValidCharlist = ValidCharlist andalso ?is_ascii_character(First), + {Kind, Inspected} = inspect_list(Rest, StillValidCharlist), + {Kind, [inspect(First), <<", ">> | Inspected]}; +inspect_list([First | ImproperTail], _) -> + {improper, [inspect(First), <<" | ">>, inspect(ImproperTail)]}. + +inspect_bit_array(Bits) -> + Text = inspect_bit_array(Bits, <<"<<">>), + <>">>. + +inspect_bit_array(<<>>, Acc) -> + Acc; +inspect_bit_array(<>, Acc) -> + inspect_bit_array(Rest, append_segment(Acc, erlang:integer_to_binary(X))); +inspect_bit_array(Rest, Acc) -> + Size = bit_size(Rest), + <> = Rest, + X1 = erlang:integer_to_binary(X), + Size1 = erlang:integer_to_binary(Size), + Segment = <>, + inspect_bit_array(<<>>, append_segment(Acc, Segment)). + +bit_array_to_int_and_size(A) -> + Size = bit_size(A), + <> = A, + {A1, Size}. + +append_segment(<<"<<">>, Segment) -> + <<"<<", Segment/binary>>; +append_segment(Acc, Segment) -> + <>. + + +inspect_maybe_utf8_string(Binary, Acc) -> + case Binary of + <<>> -> {ok, <<$", Acc/binary, $">>}; + <> -> + Escaped = case First of + $" -> <<$\\, $">>; + $\\ -> <<$\\, $\\>>; + $\r -> <<$\\, $r>>; + $\n -> <<$\\, $n>>; + $\t -> <<$\\, $t>>; + $\f -> <<$\\, $f>>; + X when X > 126, X < 160 -> convert_to_u(X); + X when X < 32 -> convert_to_u(X); + Other -> <> + end, + inspect_maybe_utf8_string(Rest, <>); + _ -> {error, not_a_utf8_string} + end. + +convert_to_u(Code) -> + list_to_binary(io_lib:format("\\u{~4.16.0B}", [Code])). + +float_to_string(Float) when is_float(Float) -> + erlang:iolist_to_binary(io_lib_format:fwrite_g(Float)). + +utf_codepoint_list_to_string(List) -> + case unicode:characters_to_binary(List) of + {error, _} -> erlang:error({gleam_error, {string_invalid_utf8, List}}); + Binary -> Binary + end. + +crop_string(String, Prefix) -> + case string:find(String, Prefix) of + nomatch -> String; + New -> New + end. + +contains_string(String, Substring) -> + is_bitstring(string:find(String, Substring)). + +base16_encode(Bin) -> + PaddedBin = bit_array_pad_to_bytes(Bin), + binary:encode_hex(PaddedBin). + +base16_decode(String) -> + try + {ok, binary:decode_hex(String)} + catch + _:_ -> {error, nil} + end. + +string_replace(String, Pattern, Replacement) -> + string:replace(String, Pattern, Replacement, all). + +slice(String, Index, Length) -> + case string:slice(String, Index, Length) of + X when is_binary(X) -> X; + X when is_list(X) -> unicode:characters_to_binary(X) + end. + +index([X | _], 0) -> + {ok, {some, X}}; +index([_, X | _], 1) -> + {ok, {some, X}}; +index([_, _, X | _], 2) -> + {ok, {some, X}}; +index([_, _, _, X | _], 3) -> + {ok, {some, X}}; +index([_, _, _, _, X | _], 4) -> + {ok, {some, X}}; +index([_, _, _, _, _, X | _], 5) -> + {ok, {some, X}}; +index([_, _, _, _, _, _, X | _], 6) -> + {ok, {some, X}}; +index([_, _, _, _, _, _, _, X | _], 7) -> + {ok, {some, X}}; +index(Tuple, Index) when is_tuple(Tuple) andalso is_integer(Index) -> + {ok, try + {some, element(Index + 1, Tuple)} + catch _:_ -> + none + end}; +index(Map, Key) when is_map(Map) -> + {ok, try + {some, maps:get(Key, Map)} + catch _:_ -> + none + end}; +index(_, Index) when is_integer(Index) -> + {error, <<"Indexable">>}; +index(_, _) -> + {error, <<"Dict">>}. + +list(T, A, B, C, D) when is_tuple(T) -> + list(tuple_to_list(T), A, B, C, D); +list([], _, _, _, Acc) -> + {lists:reverse(Acc), []}; +list([X | Xs], Decode, PushPath, Index, Acc) -> + {Out, Errors} = Decode(X), + case Errors of + [] -> list(Xs, Decode, PushPath, Index + 1, [Out | Acc]); + _ -> PushPath({[], Errors}, integer_to_binary(Index)) + end; +list(Unexpected, _, _, _, []) -> + Found = gleam@dynamic:classify(Unexpected), + Error = {decode_error, <<"List"/utf8>>, Found, []}, + {[], [Error]}; +list(_, _, _, _, Acc) -> + {lists:reverse(Acc), []}. + +dict(#{} = Data) -> {ok, Data}; +dict(_) -> {error, nil}. + +int(I) when is_integer(I) -> {ok, I}; +int(_) -> {error, 0}. + +float(F) when is_float(F) -> {ok, F}; +float(_) -> {error, 0.0}. + +bit_array(B) when is_bitstring(B) -> {ok, B}; +bit_array(_) -> {error, <<>>}. + +is_null(X) -> + X =:= undefined orelse X =:= null orelse X =:= nil. diff --git a/build/packages/gleam_stdlib/src/gleam_stdlib.mjs b/build/packages/gleam_stdlib/src/gleam_stdlib.mjs new file mode 100644 index 0000000..ebac45f --- /dev/null +++ b/build/packages/gleam_stdlib/src/gleam_stdlib.mjs @@ -0,0 +1,1048 @@ +import { + BitArray, + Error, + List, + Ok, + Result, + UtfCodepoint, + stringBits, + toBitArray, + bitArraySlice, + NonEmpty, + Empty, + CustomType, +} from "./gleam.mjs"; +import { Some, None } from "./gleam/option.mjs"; +import Dict from "./dict.mjs"; +import { classify } from "./gleam/dynamic.mjs"; +import { DecodeError } from "./gleam/dynamic/decode.mjs"; + +const Nil = undefined; +const NOT_FOUND = {}; + +export function identity(x) { + return x; +} + +export function parse_int(value) { + if (/^[-+]?(\d+)$/.test(value)) { + return new Ok(parseInt(value)); + } else { + return new Error(Nil); + } +} + +export function parse_float(value) { + if (/^[-+]?(\d+)\.(\d+)([eE][-+]?\d+)?$/.test(value)) { + return new Ok(parseFloat(value)); + } else { + return new Error(Nil); + } +} + +export function to_string(term) { + return term.toString(); +} + +export function int_to_base_string(int, base) { + return int.toString(base).toUpperCase(); +} + +const int_base_patterns = { + 2: /[^0-1]/, + 3: /[^0-2]/, + 4: /[^0-3]/, + 5: /[^0-4]/, + 6: /[^0-5]/, + 7: /[^0-6]/, + 8: /[^0-7]/, + 9: /[^0-8]/, + 10: /[^0-9]/, + 11: /[^0-9a]/, + 12: /[^0-9a-b]/, + 13: /[^0-9a-c]/, + 14: /[^0-9a-d]/, + 15: /[^0-9a-e]/, + 16: /[^0-9a-f]/, + 17: /[^0-9a-g]/, + 18: /[^0-9a-h]/, + 19: /[^0-9a-i]/, + 20: /[^0-9a-j]/, + 21: /[^0-9a-k]/, + 22: /[^0-9a-l]/, + 23: /[^0-9a-m]/, + 24: /[^0-9a-n]/, + 25: /[^0-9a-o]/, + 26: /[^0-9a-p]/, + 27: /[^0-9a-q]/, + 28: /[^0-9a-r]/, + 29: /[^0-9a-s]/, + 30: /[^0-9a-t]/, + 31: /[^0-9a-u]/, + 32: /[^0-9a-v]/, + 33: /[^0-9a-w]/, + 34: /[^0-9a-x]/, + 35: /[^0-9a-y]/, + 36: /[^0-9a-z]/, +}; + +export function int_from_base_string(string, base) { + if (int_base_patterns[base].test(string.replace(/^-/, "").toLowerCase())) { + return new Error(Nil); + } + + const result = parseInt(string, base); + + if (isNaN(result)) { + return new Error(Nil); + } + + return new Ok(result); +} + +export function string_replace(string, target, substitute) { + return string.replaceAll(target, substitute); +} + +export function string_reverse(string) { + return [...string].reverse().join(""); +} + +export function string_length(string) { + if (string === "") { + return 0; + } + const iterator = graphemes_iterator(string); + if (iterator) { + let i = 0; + for (const _ of iterator) { + i++; + } + return i; + } else { + return string.match(/./gsu).length; + } +} + +export function graphemes(string) { + const iterator = graphemes_iterator(string); + if (iterator) { + return List.fromArray(Array.from(iterator).map((item) => item.segment)); + } else { + return List.fromArray(string.match(/./gsu)); + } +} + +let segmenter = undefined; + +function graphemes_iterator(string) { + if (globalThis.Intl && Intl.Segmenter) { + segmenter ||= new Intl.Segmenter(); + return segmenter.segment(string)[Symbol.iterator](); + } +} + +export function pop_grapheme(string) { + let first; + const iterator = graphemes_iterator(string); + if (iterator) { + first = iterator.next().value?.segment; + } else { + first = string.match(/./su)?.[0]; + } + if (first) { + return new Ok([first, string.slice(first.length)]); + } else { + return new Error(Nil); + } +} + +export function pop_codeunit(str) { + return [str.charCodeAt(0) | 0, str.slice(1)]; +} + +export function lowercase(string) { + return string.toLowerCase(); +} + +export function uppercase(string) { + return string.toUpperCase(); +} + +export function less_than(a, b) { + return a < b; +} + +export function add(a, b) { + return a + b; +} + +export function split(xs, pattern) { + return List.fromArray(xs.split(pattern)); +} + +export function concat(xs) { + let result = ""; + for (const x of xs) { + result = result + x; + } + return result; +} + +export function length(data) { + return data.length; +} + +export function string_byte_slice(string, index, length) { + return string.slice(index, index + length); +} + +export function string_grapheme_slice(string, idx, len) { + if (len <= 0 || idx >= string.length) { + return ""; + } + + const iterator = graphemes_iterator(string); + if (iterator) { + while (idx-- > 0) { + iterator.next(); + } + + let result = ""; + + while (len-- > 0) { + const v = iterator.next().value; + if (v === undefined) { + break; + } + + result += v.segment; + } + + return result; + } else { + return string + .match(/./gsu) + .slice(idx, idx + len) + .join(""); + } +} + +export function string_codeunit_slice(str, from, length) { + return str.slice(from, from + length); +} +export function crop_string(string, substring) { + return string.substring(string.indexOf(substring)); +} + +export function contains_string(haystack, needle) { + return haystack.indexOf(needle) >= 0; +} + +export function starts_with(haystack, needle) { + return haystack.startsWith(needle); +} + +export function ends_with(haystack, needle) { + return haystack.endsWith(needle); +} + +export function split_once(haystack, needle) { + const index = haystack.indexOf(needle); + if (index >= 0) { + const before = haystack.slice(0, index); + const after = haystack.slice(index + needle.length); + return new Ok([before, after]); + } else { + return new Error(Nil); + } +} + +const unicode_whitespaces = [ + "\u0020", // Space + "\u0009", // Horizontal tab + "\u000A", // Line feed + "\u000B", // Vertical tab + "\u000C", // Form feed + "\u000D", // Carriage return + "\u0085", // Next line + "\u2028", // Line separator + "\u2029", // Paragraph separator +].join(""); + +const trim_start_regex = /* @__PURE__ */ new RegExp( + `^[${unicode_whitespaces}]*`, +); +const trim_end_regex = /* @__PURE__ */ new RegExp(`[${unicode_whitespaces}]*$`); + +export function trim_start(string) { + return string.replace(trim_start_regex, ""); +} + +export function trim_end(string) { + return string.replace(trim_end_regex, ""); +} + +export function bit_array_from_string(string) { + return toBitArray([stringBits(string)]); +} + +export function bit_array_bit_size(bit_array) { + return bit_array.bitSize; +} + +export function bit_array_byte_size(bit_array) { + return bit_array.byteSize; +} + +export function bit_array_pad_to_bytes(bit_array) { + const trailingBitsCount = bit_array.bitSize % 8; + + // If the bit array is a whole number of bytes it can be returned unchanged + if (trailingBitsCount === 0) { + return bit_array; + } + + const finalByte = bit_array.byteAt(bit_array.byteSize - 1); + + // The required final byte has its unused trailing bits set to zero + const unusedBitsCount = 8 - trailingBitsCount; + const correctFinalByte = (finalByte >> unusedBitsCount) << unusedBitsCount; + + // If the unused bits in the final byte are already set to zero then the + // existing buffer can be re-used, avoiding a copy + if (finalByte === correctFinalByte) { + return new BitArray( + bit_array.rawBuffer, + bit_array.byteSize * 8, + bit_array.bitOffset, + ); + } + + // Copy the bit array into a new aligned buffer and set the correct final byte + const buffer = new Uint8Array(bit_array.byteSize); + for (let i = 0; i < buffer.length - 1; i++) { + buffer[i] = bit_array.byteAt(i); + } + buffer[buffer.length - 1] = correctFinalByte; + + return new BitArray(buffer); +} + +export function bit_array_concat(bit_arrays) { + return toBitArray(bit_arrays.toArray()); +} + +export function console_log(term) { + console.log(term); +} + +export function console_error(term) { + console.error(term); +} + +export function crash(message) { + throw new globalThis.Error(message); +} + +export function bit_array_to_string(bit_array) { + // If the bit array isn't a whole number of bytes then return an error + if (bit_array.bitSize % 8 !== 0) { + return new Error(Nil); + } + + try { + const decoder = new TextDecoder("utf-8", { fatal: true }); + + if (bit_array.bitOffset === 0) { + return new Ok(decoder.decode(bit_array.rawBuffer)); + } else { + // The input data isn't aligned, so copy it into a new aligned buffer so + // that TextDecoder can be used + const buffer = new Uint8Array(bit_array.byteSize); + for (let i = 0; i < buffer.length; i++) { + buffer[i] = bit_array.byteAt(i); + } + return new Ok(decoder.decode(buffer)); + } + } catch { + return new Error(Nil); + } +} + +export function print(string) { + if (typeof process === "object" && process.stdout?.write) { + process.stdout.write(string); // We can write without a trailing newline + } else if (typeof Deno === "object") { + Deno.stdout.writeSync(new TextEncoder().encode(string)); // We can write without a trailing newline + } else { + console.log(string); // We're in a browser. Newlines are mandated + } +} + +export function print_error(string) { + if (typeof process === "object" && process.stderr?.write) { + process.stderr.write(string); // We can write without a trailing newline + } else if (typeof Deno === "object") { + Deno.stderr.writeSync(new TextEncoder().encode(string)); // We can write without a trailing newline + } else { + console.error(string); // We're in a browser. Newlines are mandated + } +} + +export function print_debug(string) { + if (typeof process === "object" && process.stderr?.write) { + process.stderr.write(string + "\n"); // If we're in Node.js, use `stderr` + } else if (typeof Deno === "object") { + Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); // If we're in Deno, use `stderr` + } else { + console.log(string); // Otherwise, use `console.log` (so that it doesn't look like an error) + } +} + +export function ceiling(float) { + return Math.ceil(float); +} + +export function floor(float) { + return Math.floor(float); +} + +export function round(float) { + return Math.round(float); +} + +export function truncate(float) { + return Math.trunc(float); +} + +export function power(base, exponent) { + // It is checked in Gleam that: + // - The base is non-negative and that the exponent is not fractional. + // - The base is non-zero and the exponent is non-negative (otherwise + // the result will essentially be division by zero). + // It can thus be assumed that valid input is passed to the Math.pow + // function and a NaN or Infinity value will not be produced. + return Math.pow(base, exponent); +} + +export function random_uniform() { + const random_uniform_result = Math.random(); + // With round-to-nearest-even behavior, the ranges claimed for the functions below + // (excluding the one for Math.random() itself) aren't exact. + // If extremely large bounds are chosen (2^53 or higher), + // it's possible in extremely rare cases to calculate the usually-excluded upper bound. + // Note that as numbers in JavaScript are IEEE 754 floating point numbers + // See: + // Because of this, we just loop 'until' we get a valid result where 0.0 <= x < 1.0: + if (random_uniform_result === 1.0) { + return random_uniform(); + } + return random_uniform_result; +} + +export function bit_array_slice(bits, position, length) { + const start = Math.min(position, position + length); + const end = Math.max(position, position + length); + + if (start < 0 || end * 8 > bits.bitSize) { + return new Error(Nil); + } + + return new Ok(bitArraySlice(bits, start * 8, end * 8)); +} + +export function codepoint(int) { + return new UtfCodepoint(int); +} + +export function string_to_codepoint_integer_list(string) { + return List.fromArray(Array.from(string).map((item) => item.codePointAt(0))); +} + +export function utf_codepoint_list_to_string(utf_codepoint_integer_list) { + return utf_codepoint_integer_list + .toArray() + .map((x) => String.fromCodePoint(x.value)) + .join(""); +} + +export function utf_codepoint_to_int(utf_codepoint) { + return utf_codepoint.value; +} + +export function new_map() { + return Dict.new(); +} + +export function map_size(map) { + return map.size; +} + +export function map_to_list(map) { + return List.fromArray(map.entries()); +} + +export function map_remove(key, map) { + return map.delete(key); +} + +export function map_get(map, key) { + const value = map.get(key, NOT_FOUND); + if (value === NOT_FOUND) { + return new Error(Nil); + } + return new Ok(value); +} + +export function map_insert(key, value, map) { + return map.set(key, value); +} + +function unsafe_percent_decode(string) { + return decodeURIComponent(string || ""); +} + +function unsafe_percent_decode_query(string) { + return decodeURIComponent((string || "").replace("+", " ")); +} + +export function percent_decode(string) { + try { + return new Ok(unsafe_percent_decode(string)); + } catch { + return new Error(Nil); + } +} + +export function percent_encode(string) { + return encodeURIComponent(string).replace("%2B", "+"); +} + +export function parse_query(query) { + try { + const pairs = []; + for (const section of query.split("&")) { + const [key, value] = section.split("="); + if (!key) continue; + + const decodedKey = unsafe_percent_decode_query(key); + const decodedValue = unsafe_percent_decode_query(value); + pairs.push([decodedKey, decodedValue]); + } + return new Ok(List.fromArray(pairs)); + } catch { + return new Error(Nil); + } +} + +const b64EncodeLookup = [ + 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, + 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, + 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, + 122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 43, 47, +]; + +let b64TextDecoder; + +// Implementation based on https://github.com/mitschabaude/fast-base64/blob/main/js.js +export function base64_encode(bit_array, padding) { + b64TextDecoder ??= new TextDecoder(); + + bit_array = bit_array_pad_to_bytes(bit_array); + + const m = bit_array.byteSize; + const k = m % 3; + const n = Math.floor(m / 3) * 4 + (k && k + 1); + const N = Math.ceil(m / 3) * 4; + const encoded = new Uint8Array(N); + + for (let i = 0, j = 0; j < m; i += 4, j += 3) { + const y = + (bit_array.byteAt(j) << 16) + + (bit_array.byteAt(j + 1) << 8) + + (bit_array.byteAt(j + 2) | 0); + + encoded[i] = b64EncodeLookup[y >> 18]; + encoded[i + 1] = b64EncodeLookup[(y >> 12) & 0x3f]; + encoded[i + 2] = b64EncodeLookup[(y >> 6) & 0x3f]; + encoded[i + 3] = b64EncodeLookup[y & 0x3f]; + } + + let base64 = b64TextDecoder.decode(new Uint8Array(encoded.buffer, 0, n)); + + if (padding) { + if (k === 1) { + base64 += "=="; + } else if (k === 2) { + base64 += "="; + } + } + + return base64; +} + +// From https://developer.mozilla.org/en-US/docs/Glossary/Base64 +export function base64_decode(sBase64) { + try { + const binString = atob(sBase64); + const length = binString.length; + const array = new Uint8Array(length); + for (let i = 0; i < length; i++) { + array[i] = binString.charCodeAt(i); + } + return new Ok(new BitArray(array)); + } catch { + return new Error(Nil); + } +} + +export function classify_dynamic(data) { + if (typeof data === "string") { + return "String"; + } else if (typeof data === "boolean") { + return "Bool"; + } else if (data instanceof Result) { + return "Result"; + } else if (data instanceof List) { + return "List"; + } else if (data instanceof BitArray) { + return "BitArray"; + } else if (data instanceof Dict) { + return "Dict"; + } else if (Number.isInteger(data)) { + return "Int"; + } else if (Array.isArray(data)) { + return `Array`; + } else if (typeof data === "number") { + return "Float"; + } else if (data === null) { + return "Nil"; + } else if (data === undefined) { + return "Nil"; + } else { + const type = typeof data; + return type.charAt(0).toUpperCase() + type.slice(1); + } +} + +export function byte_size(string) { + return new TextEncoder().encode(string).length; +} + +// In JavaScript bitwise operations convert numbers to a sequence of 32 bits +// while Erlang uses arbitrary precision. +// To get around this problem and get consistent results use BigInt and then +// downcast the value back to a Number value. + +export function bitwise_and(x, y) { + return Number(BigInt(x) & BigInt(y)); +} + +export function bitwise_not(x) { + return Number(~BigInt(x)); +} + +export function bitwise_or(x, y) { + return Number(BigInt(x) | BigInt(y)); +} + +export function bitwise_exclusive_or(x, y) { + return Number(BigInt(x) ^ BigInt(y)); +} + +export function bitwise_shift_left(x, y) { + return Number(BigInt(x) << BigInt(y)); +} + +export function bitwise_shift_right(x, y) { + return Number(BigInt(x) >> BigInt(y)); +} + +export function inspect(v) { + return new Inspector().inspect(v); +} + +export function float_to_string(float) { + const string = float.toString().replace("+", ""); + if (string.indexOf(".") >= 0) { + return string; + } else { + const index = string.indexOf("e"); + if (index >= 0) { + return string.slice(0, index) + ".0" + string.slice(index); + } else { + return string + ".0"; + } + } +} + +class Inspector { + #references = new Set(); + + inspect(v) { + const t = typeof v; + if (v === true) return "True"; + if (v === false) return "False"; + if (v === null) return "//js(null)"; + if (v === undefined) return "Nil"; + if (t === "string") return this.#string(v); + if (t === "bigint" || Number.isInteger(v)) return v.toString(); + if (t === "number") return float_to_string(v); + if (v instanceof UtfCodepoint) return this.#utfCodepoint(v); + if (v instanceof BitArray) return this.#bit_array(v); + if (v instanceof RegExp) return `//js(${v})`; + if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; + if (v instanceof globalThis.Error) return `//js(${v.toString()})`; + if (v instanceof Function) { + const args = []; + for (const i of Array(v.length).keys()) + args.push(String.fromCharCode(i + 97)); + return `//fn(${args.join(", ")}) { ... }`; + } + + if (this.#references.size === this.#references.add(v).size) { + return "//js(circular reference)"; + } + + let printed; + if (Array.isArray(v)) { + printed = `#(${v.map((v) => this.inspect(v)).join(", ")})`; + } else if (v instanceof List) { + printed = this.#list(v); + } else if (v instanceof CustomType) { + printed = this.#customType(v); + } else if (v instanceof Dict) { + printed = this.#dict(v); + } else if (v instanceof Set) { + return `//js(Set(${[...v].map((v) => this.inspect(v)).join(", ")}))`; + } else { + printed = this.#object(v); + } + this.#references.delete(v); + return printed; + } + + #object(v) { + const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; + const props = []; + for (const k of Object.keys(v)) { + props.push(`${this.inspect(k)}: ${this.inspect(v[k])}`); + } + const body = props.length ? " " + props.join(", ") + " " : ""; + const head = name === "Object" ? "" : name + " "; + return `//js(${head}{${body}})`; + } + + #dict(map) { + let body = "dict.from_list(["; + let first = true; + map.forEach((value, key) => { + if (!first) body = body + ", "; + body = body + "#(" + this.inspect(key) + ", " + this.inspect(value) + ")"; + first = false; + }); + return body + "])"; + } + + #customType(record) { + const props = Object.keys(record) + .map((label) => { + const value = this.inspect(record[label]); + return isNaN(parseInt(label)) ? `${label}: ${value}` : value; + }) + .join(", "); + return props + ? `${record.constructor.name}(${props})` + : record.constructor.name; + } + + #list(list) { + if (list instanceof Empty) { + return "[]"; + } + + let char_out = 'charlist.from_string("'; + let list_out = "["; + + let current = list; + while (current instanceof NonEmpty) { + let element = current.head; + current = current.tail; + + if (list_out !== "[") { + list_out += ", "; + } + list_out += this.inspect(element); + + if (char_out) { + if (Number.isInteger(element) && element >= 32 && element <= 126) { + char_out += String.fromCharCode(element); + } else { + char_out = null; + } + } + } + + if (char_out) { + return char_out + '")'; + } else { + return list_out + "]"; + } + } + + #string(str) { + let new_str = '"'; + for (let i = 0; i < str.length; i++) { + const char = str[i]; + switch (char) { + case "\n": + new_str += "\\n"; + break; + case "\r": + new_str += "\\r"; + break; + case "\t": + new_str += "\\t"; + break; + case "\f": + new_str += "\\f"; + break; + case "\\": + new_str += "\\\\"; + break; + case '"': + new_str += '\\"'; + break; + default: + if (char < " " || (char > "~" && char < "\u{00A0}")) { + new_str += + "\\u{" + + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + + "}"; + } else { + new_str += char; + } + } + } + new_str += '"'; + return new_str; + } + + #utfCodepoint(codepoint) { + return `//utfcodepoint(${String.fromCodePoint(codepoint.value)})`; + } + + #bit_array(bits) { + if (bits.bitSize === 0) { + return "<<>>"; + } + + let acc = "<<"; + + for (let i = 0; i < bits.byteSize - 1; i++) { + acc += bits.byteAt(i).toString(); + acc += ", "; + } + + if (bits.byteSize * 8 === bits.bitSize) { + acc += bits.byteAt(bits.byteSize - 1).toString(); + } else { + const trailingBitsCount = bits.bitSize % 8; + acc += bits.byteAt(bits.byteSize - 1) >> (8 - trailingBitsCount); + acc += `:size(${trailingBitsCount})`; + } + + acc += ">>"; + return acc; + } +} + +export function base16_encode(bit_array) { + const trailingBitsCount = bit_array.bitSize % 8; + + let result = ""; + + for (let i = 0; i < bit_array.byteSize; i++) { + let byte = bit_array.byteAt(i); + + if (i === bit_array.byteSize - 1 && trailingBitsCount !== 0) { + const unusedBitsCount = 8 - trailingBitsCount; + byte = (byte >> unusedBitsCount) << unusedBitsCount; + } + + result += byte.toString(16).padStart(2, "0").toUpperCase(); + } + + return result; +} + +export function base16_decode(string) { + const bytes = new Uint8Array(string.length / 2); + for (let i = 0; i < string.length; i += 2) { + const a = parseInt(string[i], 16); + const b = parseInt(string[i + 1], 16); + if (isNaN(a) || isNaN(b)) return new Error(Nil); + bytes[i / 2] = a * 16 + b; + } + return new Ok(new BitArray(bytes)); +} + +export function bit_array_to_int_and_size(bits) { + const trailingBitsCount = bits.bitSize % 8; + const unusedBitsCount = trailingBitsCount === 0 ? 0 : 8 - trailingBitsCount; + + return [bits.byteAt(0) >> unusedBitsCount, bits.bitSize]; +} + +export function bit_array_starts_with(bits, prefix) { + if (prefix.bitSize > bits.bitSize) { + return false; + } + + // Check any whole bytes + const byteCount = Math.trunc(prefix.bitSize / 8); + for (let i = 0; i < byteCount; i++) { + if (bits.byteAt(i) !== prefix.byteAt(i)) { + return false; + } + } + + // Check any trailing bits at the end of the prefix + if (prefix.bitSize % 8 !== 0) { + const unusedBitsCount = 8 - (prefix.bitSize % 8); + if ( + bits.byteAt(byteCount) >> unusedBitsCount !== + prefix.byteAt(byteCount) >> unusedBitsCount + ) { + return false; + } + } + + return true; +} + +export function log(x) { + // It is checked in Gleam that: + // - The input is strictly positive (x > 0) + // - This ensures that Math.log will never return NaN or -Infinity + // The function can thus safely pass the input to Math.log + // and a valid finite float will always be produced. + return Math.log(x); +} + +export function exp(x) { + return Math.exp(x); +} + +export function list_to_array(list) { + let current = list; + let array = []; + while (current instanceof NonEmpty) { + array.push(current.head); + current = current.tail; + } + return array; +} + +export function index(data, key) { + // Dictionaries and dictionary-like objects can be indexed + if (data instanceof Dict || data instanceof WeakMap || data instanceof Map) { + const token = {}; + const entry = data.get(key, token); + if (entry === token) return new Ok(new None()); + return new Ok(new Some(entry)); + } + + const key_is_int = Number.isInteger(key); + + // Only elements 0-7 of lists can be indexed, negative indices are not allowed + if (key_is_int && key >= 0 && key < 8 && data instanceof List) { + let i = 0; + for (const value of data) { + if (i === key) return new Ok(new Some(value)); + i++; + } + return new Error("Indexable"); + } + + // Arrays and objects can be indexed + if ( + (key_is_int && Array.isArray(data)) || + (data && typeof data === "object") || + (data && Object.getPrototypeOf(data) === Object.prototype) + ) { + if (key in data) return new Ok(new Some(data[key])); + return new Ok(new None()); + } + + return new Error(key_is_int ? "Indexable" : "Dict"); +} + +export function list(data, decode, pushPath, index, emptyList) { + if (!(data instanceof List || Array.isArray(data))) { + const error = new DecodeError("List", classify(data), emptyList); + return [emptyList, List.fromArray([error])]; + } + + const decoded = []; + + for (const element of data) { + const layer = decode(element); + const [out, errors] = layer; + + if (errors instanceof NonEmpty) { + const [_, errors] = pushPath(layer, index.toString()); + return [emptyList, errors]; + } + decoded.push(out); + index++; + } + + return [List.fromArray(decoded), emptyList]; +} + +export function dict(data) { + if (data instanceof Dict) { + return new Ok(data); + } + if (data instanceof Map || data instanceof WeakMap) { + return new Ok(Dict.fromMap(data)); + } + if (data == null) { + return new Error("Dict"); + } + if (typeof data !== "object") { + return new Error("Dict"); + } + const proto = Object.getPrototypeOf(data); + if (proto === Object.prototype || proto === null) { + return new Ok(Dict.fromObject(data)); + } + return new Error("Dict"); +} + +export function bit_array(data) { + if (data instanceof BitArray) return new Ok(data); + if (data instanceof Uint8Array) return new Ok(new BitArray(data)); + return new Error(new BitArray(new Uint8Array())); +} + +export function float(data) { + if (typeof data === "number") return new Ok(data); + return new Error(0.0); +} + +export function int(data) { + if (Number.isInteger(data)) return new Ok(data); + return new Error(0); +} + +export function string(data) { + if (typeof data === "string") return new Ok(data); + return new Error(""); +} + +export function is_null(data) { + return data === null || data === undefined; +} diff --git a/build/packages/gleam_time/README.md b/build/packages/gleam_time/README.md new file mode 100644 index 0000000..4c4360f --- /dev/null +++ b/build/packages/gleam_time/README.md @@ -0,0 +1,101 @@ +# Time 🕰️ + +Work with time in Gleam! + +[![Package Version](https://img.shields.io/hexpm/v/gleam_time)](https://hex.pm/packages/gleam_time) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gleam_time/) + +```sh +gleam add gleam_time +``` + +This package is the foundation of all code that works with time in Gleam. If +your program uses time then you should be using the types in this package, and +you might choose to add other packages to provide additional functionality. + +## How not to have time related bugs + +Time is famously difficult to work with! It's a very complex area, and there's +many approaches that seem reasonable or obvious, but then commonly result in +bugs. This package is carefully designed to help you avoid these problems, so +it is wise to read this documentation before continuing. + +It is important to understand there are two main ways that time is represented: + +- **Calendar time**: This is how humans commonly think and communicate about + time. For example, "10pm on the 5th of January". This is easy for a human to + read, but is it typically ambiguous and hard to work with! 10pm in + Killorglin, Ireland is not the same point in time as 10pm in Harare, + Zimbabwe. The exact meaning of calendar time depends on daylight savings + time, leap years, leap seconds, and continuously changing national and + political declarations. To make calendar time unambiguous you will need to + know what time zone it is for, and to have an up-to-date time zone database. + +- **Epoch time**: Epoch time is defined as an exact amount of since some fixed + point in time. It is always unambiguous as is not impacted by geopolitics, + time zones, etc. It is efficient for computers to work with, and it is less + likely to result in buggy code. + +In this package epoch time is provided by the `gleam/time/timestamp` module, +and calendar time is provided by the `gleam/time/calendar` module. + +Time zone information has to be loaded from elsewhere, but which approch is +best will depend on your application. User interfaces may want to read current +time zone information from the user's web browser or operating system. Server +side applications may want to embed or downloads a full copy of the time zone +database and then ask clients which time zone they want to use. + +For an entertaining overview of some of the problems of calendar time view this +video: ["The Problem with Time & Timezones" by Computerphile](https://www.youtube.com/watch?v=-5wpm-gesOY). + +### Which time representation should you use? + +> **tldr**: Use `gleam/time/timestamp`. + +The longer, more detailed answer: + +- Default to `gleam/time/timestamp`, which is epoch time. It is + unambiguous, efficient, and significantly less likely to result in logic + bugs. + +- When writing time to a database or other data storage use epoch time, + using whatever epoch format it supports. For example, PostgreSQL + `timestamp` and `timestampz` are both epoch time, and `timestamp` is + preferred as it is more straightforward to use as your application is + also using epoch time. + +- When communicating with other computer systems continue to use epoch + time. For example, when sending times to another program you could + encode time as UNIX timestamps (seconds since 00:00:00 UTC on 1 January + 1970). + +- When communicating with humans use epoch time internally, and convert + to-and-from calendar time at the last moment, when iteracting with the + human user. It may also help the users to also show the time as a fuzzy + duration from the present time, such as "about 4 days ago". + +- When representing "fuzzy" human time concepts that don't exact periods + in time, such as "one month" (varies depending on which month, which + year, and in which time zone) and "Christmas Day" (varies depending on + which year and time zone) then use calendar time. + +Any time you do use calendar time you should be extra careful! It is very +easy to make mistake with. Avoid it where possible. + +## Special thanks + +This package was created with great help from several kind contributors. In +alphabetical order: + +- [Hayleigh Thompson](https://github.com/hayleigh-dot-dev) +- [John Strunk](https://github.com/jrstrunk) +- [Ryan Moore](https://github.com/mooreryan) +- [Shayan Javani](https://github.com/massivefermion) + +These non-Gleam projects where highly influential on the design of this +package: + +- Elm's `elm/time` package. +- Go's `time` module. +- Rust's `std::time` module. +- Elixir's standard library time modules and `timex` package. diff --git a/build/packages/gleam_time/gleam.toml b/build/packages/gleam_time/gleam.toml new file mode 100644 index 0000000..90f4a22 --- /dev/null +++ b/build/packages/gleam_time/gleam.toml @@ -0,0 +1,19 @@ +name = "gleam_time" +version = "1.6.0" +description = "Work with time in Gleam!" +gleam = ">= 1.11.0" +licences = ["Apache-2.0"] +repository = { type = "github", user = "gleam-lang", repo = "time" } +links = [ + { title = "Sponsor", href = "https://github.com/sponsors/lpil" } +] + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" +qcheck = ">= 1.0.0 and < 2.0.0" +simplifile = ">= 2.2.0 and < 3.0.0" +gleam_regexp = ">= 1.0.0 and < 2.0.0" +prng = ">= 4.0.1 and < 5.0.0" diff --git a/build/packages/gleam_time/include/gleam@time@calendar_Date.hrl b/build/packages/gleam_time/include/gleam@time@calendar_Date.hrl new file mode 100644 index 0000000..b746fad --- /dev/null +++ b/build/packages/gleam_time/include/gleam@time@calendar_Date.hrl @@ -0,0 +1,5 @@ +-record(date, { + year :: integer(), + month :: gleam@time@calendar:month(), + day :: integer() +}). diff --git a/build/packages/gleam_time/include/gleam@time@calendar_TimeOfDay.hrl b/build/packages/gleam_time/include/gleam@time@calendar_TimeOfDay.hrl new file mode 100644 index 0000000..b5a103d --- /dev/null +++ b/build/packages/gleam_time/include/gleam@time@calendar_TimeOfDay.hrl @@ -0,0 +1,6 @@ +-record(time_of_day, { + hours :: integer(), + minutes :: integer(), + seconds :: integer(), + nanoseconds :: integer() +}). diff --git a/build/packages/gleam_time/include/gleam@time@duration_Duration.hrl b/build/packages/gleam_time/include/gleam@time@duration_Duration.hrl new file mode 100644 index 0000000..5477733 --- /dev/null +++ b/build/packages/gleam_time/include/gleam@time@duration_Duration.hrl @@ -0,0 +1 @@ +-record(duration, {seconds :: integer(), nanoseconds :: integer()}). diff --git a/build/packages/gleam_time/include/gleam@time@timestamp_Timestamp.hrl b/build/packages/gleam_time/include/gleam@time@timestamp_Timestamp.hrl new file mode 100644 index 0000000..b05bafe --- /dev/null +++ b/build/packages/gleam_time/include/gleam@time@timestamp_Timestamp.hrl @@ -0,0 +1 @@ +-record(timestamp, {seconds :: integer(), nanoseconds :: integer()}). diff --git a/build/packages/gleam_time/src/gleam/time/calendar.gleam b/build/packages/gleam_time/src/gleam/time/calendar.gleam new file mode 100644 index 0000000..ac81c11 --- /dev/null +++ b/build/packages/gleam_time/src/gleam/time/calendar.gleam @@ -0,0 +1,346 @@ +//// This module is for working with the Gregorian calendar, established by +//// Pope Gregory XIII in 1582! +//// +//// ## When should you use this module? +//// +//// > **tldr:** You probably want to use [`gleam/time/timestamp`](./timestamp.html) +//// > instead! +//// +//// Calendar time is difficult to work with programmatically, it is the source +//// of most time-related bugs in software. Compared to _epoch time_, which the +//// `gleam/time/timestamp` module uses, there are many disadvantages to +//// calendar time: +//// +//// - They are ambiguous if you don't know what time-zone is being used. +//// +//// - A time-zone database is required to understand calendar time even when +//// you have the time zone. These are large and your program has to +//// continously be updated as new versions of the database are published. +//// +//// - The type permits invalid states. e.g. `days` could be set to the number +//// 32, but this should not be possible! +//// +//// - There is not a single unique canonical value for each point in time, +//// thanks to time zones. Two different `Date` + `TimeOfDay` value pairs +//// could represent the same point in time. This means that you can't check +//// for time equality with `==` when using calendar types. +//// +//// - They are computationally complex, using a more memory to represent and +//// requiring a lot more CPU time to manipulate. +//// +//// There are also advantages to calendar time: +//// +//// - Calendar time is how human's talk about time, so if you want to show a +//// time or take a time from a human user then calendar time will make it +//// easier for them. +//// +//// - They can represent more abstract time periods such as "New Year's Day". +//// This may seem like an exact window of time at first, but really the +//// definition of "New Year's Day" is more fuzzy than that. When it starts +//// and ends will depend where in the world you are, so if you want to refer +//// to a day as a global concept instead of a fixed window of time for that +//// day in a specific location, then calendar time can represent that. +//// +//// So when should you use calendar time? These are our recommendations: +//// +//// - Default to `gleam/time/timestamp`, which is epoch time. It is +//// unambiguous, efficient, and significantly less likely to result in logic +//// bugs. +//// +//// - When writing time to a database or other data storage use epoch time, +//// using whatever epoch format it supports. For example, PostgreSQL +//// `timestamp` and `timestampz` are both epoch time, and `timestamp` is +//// preferred as it is more straightforward to use as your application is +//// also using epoch time. +//// +//// - When communicating with other computer systems continue to use epoch +//// time. For example, when sending times to another program you could +//// encode time as UNIX timestamps (seconds since 00:00:00 UTC on 1 January +//// 1970). +//// +//// - When communicating with humans use epoch time internally, and convert +//// to-and-from calendar time at the last moment, when iteracting with the +//// human user. It may also help the users to also show the time as a fuzzy +//// duration from the present time, such as "about 4 days ago". +//// +//// - When representing "fuzzy" human time concepts that don't exact periods +//// in time, such as "one month" (varies depending on which month, which +//// year, and in which time zone) and "Christmas Day" (varies depending on +//// which year and time zone) then use calendar time. +//// +//// Any time you do use calendar time you should be extra careful! It is very +//// easy to make mistake with. Avoid it where possible. +//// +//// ## Time zone offsets +//// +//// This package includes the `utc_offset` value and the `local_offset` +//// function, which are the offset for the UTC time zone and get the time +//// offset the computer running the program is configured to respectively. +//// +//// If you need to use other offsets in your program then you will need to get +//// them from somewhere else, such as from a package which loads the +//// [IANA Time Zone Database](https://www.iana.org/time-zones), or from the +//// website visitor's web browser, which your frontend can send for you. +//// +//// ## Use in APIs +//// +//// If you are making an API such as a HTTP JSON API you are encouraged to use +//// Unix timestamps instead of calendar times. + +import gleam/int +import gleam/order.{type Order} +import gleam/time/duration + +/// The Gregorian calendar date. Ambiguous without a time zone. +/// +/// Prefer to represent your time using the `Timestamp` type, and convert it +/// only to calendar types when you need to display them. See the documentation +/// for this module for more information. +/// +pub type Date { + Date(year: Int, month: Month, day: Int) +} + +/// The time of day. Ambiguous without a date and time zone. +/// +pub type TimeOfDay { + TimeOfDay(hours: Int, minutes: Int, seconds: Int, nanoseconds: Int) +} + +/// The 12 months of the year. +pub type Month { + January + February + March + April + May + June + July + August + September + October + November + December +} + +/// The offset for the [Coordinated Universal Time (UTC)](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) +/// time zone. +/// +/// The utc zone has no time adjustments, it is always zero. It never observes +/// daylight-saving time and it never shifts around based on political +/// restructuring. +/// +pub const utc_offset = duration.empty + +/// Get the offset for the computer's currently configured time zone. +/// +/// Note this may not be the time zone that is correct to use for your user. +/// For example, if you are making a web application that runs on a server you +/// want _their_ computer's time zone, not yours. +/// +/// This is the _current local_ offset, not the current local time zone. This +/// means that while it will result in the expected outcome for the current +/// time, it may result in unexpected output if used with other timestamps. For +/// example: a timestamp that would locally be during daylight savings time if +/// is it not currently daylight savings time when this function is called. +/// +pub fn local_offset() -> duration.Duration { + duration.seconds(local_time_offset_seconds()) +} + +@external(erlang, "gleam_time_ffi", "local_time_offset_seconds") +@external(javascript, "../../gleam_time_ffi.mjs", "local_time_offset_seconds") +fn local_time_offset_seconds() -> Int + +/// Returns the English name for a month. +/// +/// # Examples +/// +/// ```gleam +/// month_to_string(April) +/// // -> "April" +/// ``` +pub fn month_to_string(month: Month) -> String { + case month { + January -> "January" + February -> "February" + March -> "March" + April -> "April" + May -> "May" + June -> "June" + July -> "July" + August -> "August" + September -> "September" + October -> "October" + November -> "November" + December -> "December" + } +} + +/// Returns the number for the month, where January is 1 and December is 12. +/// +/// # Examples +/// +/// ```gleam +/// month_to_int(January) +/// // -> 1 +/// ``` +pub fn month_to_int(month: Month) -> Int { + case month { + January -> 1 + February -> 2 + March -> 3 + April -> 4 + May -> 5 + June -> 6 + July -> 7 + August -> 8 + September -> 9 + October -> 10 + November -> 11 + December -> 12 + } +} + +/// Returns the month for a given number, where January is 1 and December is 12. +/// +/// # Examples +/// +/// ```gleam +/// month_from_int(1) +/// // -> Ok(January) +/// ``` +pub fn month_from_int(month: Int) -> Result(Month, Nil) { + case month { + 1 -> Ok(January) + 2 -> Ok(February) + 3 -> Ok(March) + 4 -> Ok(April) + 5 -> Ok(May) + 6 -> Ok(June) + 7 -> Ok(July) + 8 -> Ok(August) + 9 -> Ok(September) + 10 -> Ok(October) + 11 -> Ok(November) + 12 -> Ok(December) + _ -> Error(Nil) + } +} + +/// Checks if a given date is valid. +/// +/// This function properly accounts for leap years when validating February days. +/// A leap year occurs every 4 years, except for years divisible by 100, +/// unless they are also divisible by 400. +/// +/// # Examples +/// +/// ```gleam +/// is_valid_date(Date(2023, April, 15)) +/// // -> True +/// ``` +/// +/// ```gleam +/// is_valid_date(Date(2023, April, 31)) +/// // -> False +/// ``` +/// +/// ```gleam +/// is_valid_date(Date(2024, February, 29)) +/// // -> True (2024 is a leap year) +/// ``` +/// +pub fn is_valid_date(date: Date) -> Bool { + let Date(year:, month:, day:) = date + case day < 1 { + True -> False + False -> + case month { + January | March | May | July | August | October | December -> day <= 31 + April | June | September | November -> day <= 30 + February -> { + let max_february_days = case is_leap_year(year) { + True -> 29 + False -> 28 + } + day <= max_february_days + } + } + } +} + +/// Determines if a given year is a leap year. +/// +/// A leap year occurs every 4 years, except for years divisible by 100, +/// unless they are also divisible by 400. +/// +/// # Examples +/// +/// ```gleam +/// is_leap_year(2024) +/// // -> True +/// ``` +/// +/// ```gleam +/// is_leap_year(2023) +/// // -> False +/// ``` +/// +pub fn is_leap_year(year: Int) -> Bool { + case year % 400 == 0 { + True -> True + False -> + case year % 100 == 0 { + True -> False + False -> year % 4 == 0 + } + } +} + +/// Checks if a time of day is valid. +/// +/// Validates that hours are 0-23, minutes are 0-59, seconds are 0-59, +/// and nanoseconds are 0-999,999,999. +/// +/// # Examples +/// +/// ```gleam +/// is_valid_time_of_day(TimeOfDay(12, 30, 45, 123456789)) +/// // -> True +/// ``` +/// +pub fn is_valid_time_of_day(time: TimeOfDay) -> Bool { + let TimeOfDay(hours:, minutes:, seconds:, nanoseconds:) = time + + hours >= 0 + && hours <= 23 + && minutes >= 0 + && minutes <= 59 + && seconds >= 0 + && seconds <= 59 + && nanoseconds >= 0 + && nanoseconds <= 999_999_999 +} + +/// Naively compares two dates without any time zone information, returning an +/// order. +/// +/// ## Correctness +/// +/// This function compares dates without any time zone information, only using +/// the rules for the gregorian calendar. This is typically sufficient, but be +/// aware that in reality some time zones will change their calendar date +/// occasionally. This can result in days being skipped, out of order, or +/// happening multiple times. +/// +/// If you need real-world correct time ordering then use the +/// `gleam/time/timestamp` module instead. +/// +pub fn naive_date_compare(one: Date, other: Date) -> Order { + int.compare(one.year, other.year) + |> order.lazy_break_tie(fn() { + int.compare(month_to_int(one.month), month_to_int(other.month)) + }) + |> order.lazy_break_tie(fn() { int.compare(one.day, other.day) }) +} diff --git a/build/packages/gleam_time/src/gleam/time/duration.gleam b/build/packages/gleam_time/src/gleam/time/duration.gleam new file mode 100644 index 0000000..a3b7483 --- /dev/null +++ b/build/packages/gleam_time/src/gleam/time/duration.gleam @@ -0,0 +1,297 @@ +import gleam/bool +import gleam/int +import gleam/order +import gleam/string + +/// An amount of time, with up to nanosecond precision. +/// +/// This type does not represent calendar periods such as "1 month" or "2 +/// days". Those periods will be different lengths of time depending on which +/// month or day they apply to. For example, January is longer than February. +/// A different type should be used for calendar periods. +/// +pub opaque type Duration { + // When compiling to JavaScript ints have limited precision and size. This + // means that if we were to store the the timestamp in a single int the + // duration would not be able to represent very large or small durations. + // Durations are instead represented as a number of seconds and a number of + // nanoseconds. + // + // If you have manually adjusted the seconds and nanoseconds values the + // `normalise` function can be used to ensure the time is represented the + // intended way, with `nanoseconds` being positive and less than 1 second. + // + // The duration is the sum of the seconds and the nanoseconds. + Duration(seconds: Int, nanoseconds: Int) +} + +/// A division of time. +/// +/// Note that not all months and years are the same length, so a reasonable +/// average length is used by this module. +/// +pub type Unit { + Nanosecond + /// 1000 nanoseconds. + Microsecond + /// 1000 microseconds. + Millisecond + /// 1000 milliseconds. + Second + /// 60 seconds. + Minute + /// 60 minutes. + Hour + /// 24 hours. + Day + /// 7 days. + Week + /// About 30.4375 days. Real calendar months vary in length. + Month + /// About 365.25 days. Real calendar years vary in length. + Year +} + +/// Convert a duration to a number of the largest number of a unit, serving as +/// a rough description of the duration that a human can understand. +/// +/// The size used for each unit are described in the documentation for the +/// `Unit` type. +/// +/// ```gleam +/// seconds(125) +/// |> approximate +/// // -> #(2, Minute) +/// ``` +/// +/// This function rounds _towards zero_. This means that if a duration is just +/// short of 2 days then it will approximate to 1 day. +/// +/// ```gleam +/// hours(47) +/// |> approximate +/// // -> #(1, Day) +/// ``` +/// +pub fn approximate(duration: Duration) -> #(Int, Unit) { + let Duration(seconds: s, nanoseconds: ns) = duration + let minute = 60 + let hour = minute * 60 + let day = hour * 24 + let week = day * 7 + let year = day * 365 + hour * 6 + let month = year / 12 + let microsecond = 1000 + let millisecond = microsecond * 1000 + case Nil { + _ if s < 0 -> { + let #(amount, unit) = Duration(-s, -ns) |> normalise |> approximate + #(-amount, unit) + } + _ if s >= year -> #(s / year, Year) + _ if s >= month -> #(s / month, Month) + _ if s >= week -> #(s / week, Week) + _ if s >= day -> #(s / day, Day) + _ if s >= hour -> #(s / hour, Hour) + _ if s >= minute -> #(s / minute, Minute) + _ if s > 0 -> #(s, Second) + _ if ns >= millisecond -> #(ns / millisecond, Millisecond) + _ if ns >= microsecond -> #(ns / microsecond, Microsecond) + _ -> #(ns, Nanosecond) + } +} + +/// Ensure the duration is represented with `nanoseconds` being positive and +/// less than 1 second. +/// +/// This function does not change the amount of time that the duratoin refers +/// to, it only adjusts the values used to represent the time. +/// +fn normalise(duration: Duration) -> Duration { + let multiplier = 1_000_000_000 + let nanoseconds = duration.nanoseconds % multiplier + let overflow = duration.nanoseconds - nanoseconds + let seconds = duration.seconds + overflow / multiplier + case nanoseconds >= 0 { + True -> Duration(seconds, nanoseconds) + False -> Duration(seconds - 1, multiplier + nanoseconds) + } +} + +/// Compare one duration to another, indicating whether the first spans a +/// larger amount of time (and so is greater) or smaller amount of time (and so +/// is lesser) than the second. +/// +/// # Examples +/// +/// ```gleam +/// compare(seconds(1), seconds(2)) +/// // -> order.Lt +/// ``` +/// +/// Whether a duration is negative or positive doesn't matter for comparing +/// them, only the amount of time spanned matters. +/// +/// ```gleam +/// compare(seconds(-2), seconds(1)) +/// // -> order.Gt +/// ``` +/// +pub fn compare(left: Duration, right: Duration) -> order.Order { + let parts = fn(x: Duration) { + case x.seconds >= 0 { + True -> #(x.seconds, x.nanoseconds) + False -> #(x.seconds * -1 - 1, 1_000_000_000 - x.nanoseconds) + } + } + let #(ls, lns) = parts(left) + let #(rs, rns) = parts(right) + int.compare(ls, rs) + |> order.break_tie(int.compare(lns, rns)) +} + +/// Calculate the difference between two durations. +/// +/// This is effectively substracting the first duration from the second. +/// +/// # Examples +/// +/// ```gleam +/// difference(seconds(1), seconds(5)) +/// // -> seconds(4) +/// ``` +/// +pub fn difference(left: Duration, right: Duration) -> Duration { + Duration(right.seconds - left.seconds, right.nanoseconds - left.nanoseconds) + |> normalise +} + +/// Add two durations together. +/// +/// # Examples +/// +/// ```gleam +/// add(seconds(1), seconds(5)) +/// // -> seconds(6) +/// ``` +/// +pub fn add(left: Duration, right: Duration) -> Duration { + Duration(left.seconds + right.seconds, left.nanoseconds + right.nanoseconds) + |> normalise +} + +/// Convert the duration to an [ISO8601][1] formatted duration string. +/// +/// The ISO8601 duration format is ambiguous without context due to months and +/// years having different lengths, and because of leap seconds. This function +/// encodes the duration as days, hours, and seconds without any leap seconds. +/// Be sure to take this into account when using the duration strings. +/// +/// [1]: https://en.wikipedia.org/wiki/ISO_8601#Durations +/// +pub fn to_iso8601_string(duration: Duration) -> String { + use <- bool.guard(duration == empty, "PT0S") + let split = fn(total, limit) { + let amount = total % limit + let remainder = { total - amount } / limit + #(amount, remainder) + } + let #(seconds, rest) = split(duration.seconds, 60) + let #(minutes, rest) = split(rest, 60) + let #(hours, rest) = split(rest, 24) + let days = rest + let add = fn(out, value, unit) { + case value { + 0 -> out + _ -> out <> int.to_string(value) <> unit + } + } + let output = + "P" + |> add(days, "D") + |> string.append("T") + |> add(hours, "H") + |> add(minutes, "M") + case seconds, duration.nanoseconds { + 0, 0 -> output + _, 0 -> output <> int.to_string(seconds) <> "S" + _, _ -> { + let f = nanosecond_digits(duration.nanoseconds, 0, "") + output <> int.to_string(seconds) <> "." <> f <> "S" + } + } +} + +fn nanosecond_digits(n: Int, position: Int, acc: String) -> String { + case position { + 9 -> acc + _ if acc == "" && n % 10 == 0 -> { + nanosecond_digits(n / 10, position + 1, acc) + } + _ -> { + let acc = int.to_string(n % 10) <> acc + nanosecond_digits(n / 10, position + 1, acc) + } + } +} + +/// Create a duration of a number of seconds. +pub fn seconds(amount: Int) -> Duration { + Duration(amount, 0) +} + +/// Create a duration of a number of minutes. +pub fn minutes(amount: Int) -> Duration { + seconds(amount * 60) +} + +/// Create a duration of a number of hours. +pub fn hours(amount: Int) -> Duration { + seconds(amount * 60 * 60) +} + +/// Create a duration of a number of milliseconds. +pub fn milliseconds(amount: Int) -> Duration { + let remainder = amount % 1000 + let overflow = amount - remainder + let nanoseconds = remainder * 1_000_000 + let seconds = overflow / 1000 + Duration(seconds, nanoseconds) + |> normalise +} + +/// Create a duration of a number of nanoseconds. +/// +/// # JavaScript int limitations +/// +/// Remember that JavaScript can only perfectly represent ints between positive +/// and negative 9,007,199,254,740,991! If you use a single call to this +/// function to create durations larger than that number of nanoseconds then +/// you will likely not get exactly the value you expect. Use `seconds` and +/// `milliseconds` as much as possible for large durations. +/// +pub fn nanoseconds(amount: Int) -> Duration { + Duration(0, amount) + |> normalise +} + +/// Convert the duration to a number of seconds. +/// +/// There may be some small loss of precision due to `Duration` being +/// nanosecond accurate and `Float` not being able to represent this. +/// +pub fn to_seconds(duration: Duration) -> Float { + let seconds = int.to_float(duration.seconds) + let nanoseconds = int.to_float(duration.nanoseconds) + seconds +. { nanoseconds /. 1_000_000_000.0 } +} + +/// Convert the duration to a number of seconds and nanoseconds. There is no +/// loss of precision with this conversion on any target. +/// +pub fn to_seconds_and_nanoseconds(duration: Duration) -> #(Int, Int) { + #(duration.seconds, duration.nanoseconds) +} + +@internal +pub const empty = Duration(0, 0) diff --git a/build/packages/gleam_time/src/gleam/time/timestamp.gleam b/build/packages/gleam_time/src/gleam/time/timestamp.gleam new file mode 100644 index 0000000..02fa890 --- /dev/null +++ b/build/packages/gleam_time/src/gleam/time/timestamp.gleam @@ -0,0 +1,899 @@ +//// Welcome to the timestamp module! This module and its `Timestamp` type are +//// what you will be using most commonly when working with time in Gleam. +//// +//// A timestamp represents a moment in time, represented as an amount of time +//// since the calendar time 00:00:00 UTC on 1 January 1970, also known as the +//// _Unix epoch_. +//// +//// # Wall clock time and monotonicity +//// +//// Time is very complicated, especially on computers! While they generally do +//// a good job of keeping track of what the time is, computers can get +//// out-of-sync and start to report a time that is too late or too early. Most +//// computers use "network time protocol" to tell each other what they think +//// the time is, and computers that realise they are running too fast or too +//// slow will adjust their clock to correct it. When this happens it can seem +//// to your program that the current time has changed, and it may have even +//// jumped backwards in time! +//// +//// This measure of time is called _wall clock time_, and it is what people +//// commonly think of when they think of time. It is important to be aware that +//// it can go backwards, and your program must not rely on it only ever going +//// forwards at a steady rate. For example, for tracking what order events happen +//// in. +//// +//// This module uses wall clock time. If your program needs time values to always +//// increase you will need a _monotonic_ time instead. It's uncommon that you +//// would need monotonic time, one example might be if you're making a +//// benchmarking framework. +//// +//// The exact way that time works will depend on what runtime you use. The +//// Erlang documentation on time has a lot of detail about time generally as well +//// as how it works on the BEAM, it is worth reading. +//// . +//// +//// # Converting to local calendar time +//// +//// Timestamps don't take into account time zones, so a moment in time will +//// have the same timestamp value regardless of where you are in the world. To +//// convert them to local time you will need to know the offset for the time +//// zone you wish to use, likely from a time zone database. See the +//// `gleam/time/calendar` module for more information. +//// + +import gleam/bit_array +import gleam/float +import gleam/int +import gleam/list +import gleam/order +import gleam/result +import gleam/string +import gleam/time/calendar +import gleam/time/duration.{type Duration} + +const seconds_per_day: Int = 86_400 + +const seconds_per_hour: Int = 3600 + +const seconds_per_minute: Int = 60 + +const nanoseconds_per_second: Int = 1_000_000_000 + +/// The `:` character as a byte +const byte_colon: Int = 0x3A + +/// The `-` character as a byte +const byte_minus: Int = 0x2D + +/// The `0` character as a byte +const byte_zero: Int = 0x30 + +/// The `9` character as a byte +const byte_nine: Int = 0x39 + +/// The `t` character as a byte +const byte_t_lowercase: Int = 0x74 + +/// The `T` character as a byte +const byte_t_uppercase: Int = 0x54 + +/// The `T` character as a byte +const byte_space: Int = 0x20 + +/// The Julian seconds of the UNIX epoch (Julian day is 2_440_588) +const julian_seconds_unix_epoch: Int = 210_866_803_200 + +/// The main time type, which you should favour over other types such as +/// calendar time types. It is efficient, unambiguous, and it is not possible +/// to construct an invalid timestamp. +/// +/// The most common situation in which you may need a different time data +/// structure is when you need to display time to human for them to read. When +/// you need to do this convert the timestamp to calendar time when presenting +/// it, but internally always keep the time as a timestamp. +/// +pub opaque type Timestamp { + // When compiling to JavaScript ints have limited precision and size. This + // means that if we were to store the the timestamp in a single int the + // timestamp would not be able to represent times far in the future or in the + // past, or distinguish between two times that are close together. Timestamps + // are instead represented as a number of seconds and a number of nanoseconds. + // + // If you have manually adjusted the seconds and nanoseconds values the + // `normalise` function can be used to ensure the time is represented the + // intended way, with `nanoseconds` being positive and less than 1 second. + // + // The timestamp is the sum of the seconds and the nanoseconds. + Timestamp(seconds: Int, nanoseconds: Int) +} + +/// The epoch of Unix time, which is 00:00:00 UTC on 1 January 1970. +pub const unix_epoch = Timestamp(0, 0) + +/// Ensure the time is represented with `nanoseconds` being positive and less +/// than 1 second. +/// +/// This function does not change the time that the timestamp refers to, it +/// only adjusts the values used to represent the time. +/// +fn normalise(timestamp: Timestamp) -> Timestamp { + let multiplier = 1_000_000_000 + let nanoseconds = timestamp.nanoseconds % multiplier + let overflow = timestamp.nanoseconds - nanoseconds + let seconds = timestamp.seconds + overflow / multiplier + case nanoseconds >= 0 { + True -> Timestamp(seconds, nanoseconds) + False -> Timestamp(seconds - 1, multiplier + nanoseconds) + } +} + +/// Compare one timestamp to another, indicating whether the first is further +/// into the future (greater) or further into the past (lesser) than the +/// second. +/// +/// # Examples +/// +/// ```gleam +/// compare(from_unix_seconds(1), from_unix_seconds(2)) +/// // -> order.Lt +/// ``` +/// +pub fn compare(left: Timestamp, right: Timestamp) -> order.Order { + order.break_tie( + int.compare(left.seconds, right.seconds), + int.compare(left.nanoseconds, right.nanoseconds), + ) +} + +/// Get the current system time. +/// +/// Note this time is not unique or monotonic, it could change at any time or +/// even go backwards! The exact behaviour will depend on the runtime used. See +/// the module documentation for more information. +/// +/// On Erlang this uses [`erlang:system_time/1`][1]. On JavaScript this uses +/// [`Date.now`][2]. +/// +/// [1]: https://www.erlang.org/doc/apps/erts/erlang#system_time/1 +/// [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now +/// +pub fn system_time() -> Timestamp { + let #(seconds, nanoseconds) = get_system_time() + normalise(Timestamp(seconds, nanoseconds)) +} + +@external(erlang, "gleam_time_ffi", "system_time") +@external(javascript, "../../gleam_time_ffi.mjs", "system_time") +fn get_system_time() -> #(Int, Int) + +/// Calculate the difference between two timestamps. +/// +/// This is effectively substracting the first timestamp from the second. +/// +/// # Examples +/// +/// ```gleam +/// difference(from_unix_seconds(1), from_unix_seconds(5)) +/// // -> duration.seconds(4) +/// ``` +/// +pub fn difference(left: Timestamp, right: Timestamp) -> Duration { + let seconds = duration.seconds(right.seconds - left.seconds) + let nanoseconds = duration.nanoseconds(right.nanoseconds - left.nanoseconds) + duration.add(seconds, nanoseconds) +} + +/// Add a duration to a timestamp. +/// +/// # Examples +/// +/// ```gleam +/// add(from_unix_seconds(1000), duration.seconds(5)) +/// // -> from_unix_seconds(1005) +/// ``` +/// +pub fn add(timestamp: Timestamp, duration: Duration) -> Timestamp { + let #(seconds, nanoseconds) = duration.to_seconds_and_nanoseconds(duration) + Timestamp(timestamp.seconds + seconds, timestamp.nanoseconds + nanoseconds) + |> normalise +} + +/// Convert a timestamp to a RFC 3339 formatted time string, with an offset +/// supplied as an additional argument. +/// +/// The output of this function is also ISO 8601 compatible so long as the +/// offset not negative. Offsets have at-most minute precision, so an offset +/// with higher precision will be rounded to the nearest minute. +/// +/// If you are making an API such as a HTTP JSON API you are encouraged to use +/// Unix timestamps instead of this format or ISO 8601. Unix timestamps are a +/// better choice as they don't contain offset information. Consider: +/// +/// - UTC offsets are not time zones. This does not and cannot tell us the time +/// zone in which the date was recorded. So what are we supposed to do with +/// this information? +/// - Users typically want dates formatted according to their local time zone. +/// What if the provided UTC offset is different from the current user's time +/// zone? What are we supposed to do with it then? +/// - Despite it being useless (or worse, a source of bugs), the UTC offset +/// creates a larger payload to transfer. +/// +/// They also uses more memory than a unix timestamp. The way they are better +/// than Unix timestamp is that it is easier for a human to read them, but +/// this is a hinderance that tooling can remedy, and APIs are not primarily +/// for humans. +/// +/// # Examples +/// +/// ```gleam +/// timestamp.from_unix_seconds_and_nanoseconds(1000, 123_000_000) +/// |> to_rfc3339(calendar.utc_offset) +/// // -> "1970-01-01T00:16:40.123Z" +/// ``` +/// +/// ```gleam +/// timestamp.from_unix_seconds(1000) +/// |> to_rfc3339(duration.seconds(3600)) +/// // -> "1970-01-01T01:16:40+01:00" +/// ``` +/// +pub fn to_rfc3339(timestamp: Timestamp, offset: Duration) -> String { + let offset = duration_to_minutes(offset) + let #(years, months, days, hours, minutes, seconds) = + to_calendar_from_offset(timestamp, offset) + + let offset_minutes = modulo(offset, 60) + let offset_hours = int.absolute_value(floored_div(offset, 60.0)) + + let n2 = pad_digit(_, to: 2) + let n4 = pad_digit(_, to: 4) + let out = "" + let out = out <> n4(years) <> "-" <> n2(months) <> "-" <> n2(days) + let out = out <> "T" + let out = out <> n2(hours) <> ":" <> n2(minutes) <> ":" <> n2(seconds) + let out = out <> show_second_fraction(timestamp.nanoseconds) + case int.compare(offset, 0) { + order.Eq -> out <> "Z" + order.Gt -> out <> "+" <> n2(offset_hours) <> ":" <> n2(offset_minutes) + order.Lt -> out <> "-" <> n2(offset_hours) <> ":" <> n2(offset_minutes) + } +} + +fn pad_digit(digit: Int, to desired_length: Int) -> String { + int.to_string(digit) |> string.pad_start(desired_length, "0") +} + +/// Convert a `Timestamp` to calendar time, suitable for presenting to a human +/// to read. +/// +/// If you want a machine to use the time value then you should not use this +/// function and should instead keep it as a timestamp. See the documentation +/// for the `gleam/time/calendar` module for more information. +/// +/// # Examples +/// +/// ```gleam +/// timestamp.from_unix_seconds(0) +/// |> timestamp.to_calendar(calendar.utc_offset) +/// // -> #(Date(1970, January, 1), TimeOfDay(0, 0, 0, 0)) +/// ``` +/// +pub fn to_calendar( + timestamp: Timestamp, + offset: Duration, +) -> #(calendar.Date, calendar.TimeOfDay) { + let offset = duration_to_minutes(offset) + let #(year, month, day, hours, minutes, seconds) = + to_calendar_from_offset(timestamp, offset) + let month = case month { + 1 -> calendar.January + 2 -> calendar.February + 3 -> calendar.March + 4 -> calendar.April + 5 -> calendar.May + 6 -> calendar.June + 7 -> calendar.July + 8 -> calendar.August + 9 -> calendar.September + 10 -> calendar.October + 11 -> calendar.November + _ -> calendar.December + } + let nanoseconds = timestamp.nanoseconds + let date = calendar.Date(year:, month:, day:) + let time = calendar.TimeOfDay(hours:, minutes:, seconds:, nanoseconds:) + #(date, time) +} + +fn duration_to_minutes(duration: duration.Duration) -> Int { + float.round(duration.to_seconds(duration) /. 60.0) +} + +fn to_calendar_from_offset( + timestamp: Timestamp, + offset: Int, +) -> #(Int, Int, Int, Int, Int, Int) { + let total = timestamp.seconds + { offset * 60 } + let seconds = modulo(total, 60) + let total_minutes = floored_div(total, 60.0) + let minutes = modulo(total, 60 * 60) / 60 + let hours = modulo(total, 24 * 60 * 60) / { 60 * 60 } + let #(year, month, day) = to_civil(total_minutes) + #(year, month, day, hours, minutes, seconds) +} + +/// Create a `Timestamp` from a human-readable calendar time. +/// +/// # Examples +/// +/// ```gleam +/// timestamp.from_calendar( +/// date: calendar.Date(2024, calendar.December, 25), +/// time: calendar.TimeOfDay(12, 30, 50, 0), +/// offset: calendar.utc_offset, +/// ) +/// |> timestamp.to_rfc3339(calendar.utc_offset) +/// // -> "2024-12-25T12:30:50Z" +/// ``` +/// +pub fn from_calendar( + date date: calendar.Date, + time time: calendar.TimeOfDay, + offset offset: Duration, +) -> Timestamp { + let month = case date.month { + calendar.January -> 1 + calendar.February -> 2 + calendar.March -> 3 + calendar.April -> 4 + calendar.May -> 5 + calendar.June -> 6 + calendar.July -> 7 + calendar.August -> 8 + calendar.September -> 9 + calendar.October -> 10 + calendar.November -> 11 + calendar.December -> 12 + } + from_date_time( + year: date.year, + month:, + day: date.day, + hours: time.hours, + minutes: time.minutes, + seconds: time.seconds, + second_fraction_as_nanoseconds: time.nanoseconds, + offset_seconds: float.round(duration.to_seconds(offset)), + ) +} + +fn modulo(n: Int, m: Int) -> Int { + case int.modulo(n, m) { + Ok(n) -> n + Error(_) -> 0 + } +} + +fn floored_div(numerator: Int, denominator: Float) -> Int { + let n = int.to_float(numerator) /. denominator + float.round(float.floor(n)) +} + +// Adapted from Elm's Time module +fn to_civil(minutes: Int) -> #(Int, Int, Int) { + let raw_day = floored_div(minutes, { 60.0 *. 24.0 }) + 719_468 + let era = case raw_day >= 0 { + True -> raw_day / 146_097 + False -> { raw_day - 146_096 } / 146_097 + } + let day_of_era = raw_day - era * 146_097 + let year_of_era = + { + day_of_era + - { day_of_era / 1460 } + + { day_of_era / 36_524 } + - { day_of_era / 146_096 } + } + / 365 + let year = year_of_era + era * 400 + let day_of_year = + day_of_era + - { 365 * year_of_era + { year_of_era / 4 } - { year_of_era / 100 } } + let mp = { 5 * day_of_year + 2 } / 153 + let month = case mp < 10 { + True -> mp + 3 + False -> mp - 9 + } + let day = day_of_year - { 153 * mp + 2 } / 5 + 1 + let year = case month <= 2 { + True -> year + 1 + False -> year + } + #(year, month, day) +} + +/// Converts nanoseconds into a `String` representation of fractional seconds. +/// +/// Assumes that `nanoseconds < 1_000_000_000`, which will be true for any +/// normalised timestamp. +/// +fn show_second_fraction(nanoseconds: Int) -> String { + case int.compare(nanoseconds, 0) { + // Zero fractional seconds are not shown. + order.Lt | order.Eq -> "" + order.Gt -> { + let second_fraction_part = { + nanoseconds + |> get_zero_padded_digits + |> remove_trailing_zeros + |> list.map(int.to_string) + |> string.join("") + } + + "." <> second_fraction_part + } + } +} + +/// Given a list of digits, return new list with any trailing zeros removed. +/// +fn remove_trailing_zeros(digits: List(Int)) -> List(Int) { + let reversed_digits = list.reverse(digits) + + do_remove_trailing_zeros(reversed_digits) +} + +fn do_remove_trailing_zeros(reversed_digits) { + case reversed_digits { + [] -> [] + [digit, ..digits] if digit == 0 -> do_remove_trailing_zeros(digits) + reversed_digits -> list.reverse(reversed_digits) + } +} + +/// Returns the list of digits of `number`. If the number of digits is less +/// than 9, the result is zero-padded at the front. +/// +fn get_zero_padded_digits(number: Int) -> List(Int) { + do_get_zero_padded_digits(number, [], 0) +} + +fn do_get_zero_padded_digits( + number: Int, + digits: List(Int), + count: Int, +) -> List(Int) { + case number { + number if number <= 0 && count >= 9 -> digits + number if number <= 0 -> + // Zero-pad the digits at the front until we have at least 9 digits. + do_get_zero_padded_digits(number, [0, ..digits], count + 1) + number -> { + let digit = number % 10 + let number = floored_div(number, 10.0) + do_get_zero_padded_digits(number, [digit, ..digits], count + 1) + } + } +} + +/// Parses an [RFC 3339 formatted time string][spec] into a `Timestamp`. +/// +/// [spec]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6 +/// +/// # Examples +/// +/// ```gleam +/// let assert Ok(ts) = timestamp.parse_rfc3339("1970-01-01T00:00:01Z") +/// timestamp.to_unix_seconds_and_nanoseconds(ts) +/// // -> #(1, 0) +/// ``` +/// +/// Parsing an invalid timestamp returns an error. +/// +/// ```gleam +/// let assert Error(Nil) = timestamp.parse_rfc3339("1995-10-31") +/// ``` +/// +/// ## Time zones +/// +/// It may at first seem that the RFC 3339 format includes timezone +/// information, as it can specify an offset such as `Z` or `+3`, so why does +/// this function not return calendar time with a time zone? There are multiple +/// reasons: +/// +/// - RFC 3339's timestamp format is based on calendar time, but it is +/// unambigous, so it can be converted into epoch time when being parsed. It +/// is always better to internally use epoch time to represent unambiguous +/// points in time, so we perform that conversion as a convenience and to +/// ensure that programmers with less time experience don't accidentally use +/// a less suitable time representation. +/// +/// - RFC 3339's contains _calendar time offset_ information, not time zone +/// information. This is enough to convert it to an unambiguous timestamp, +/// but it is not enough information to reliably work with calendar time. +/// Without the time zone and the time zone database it's not possible to +/// know what time period that offset is valid for, so it cannot be used +/// without risk of bugs. +/// +/// ## Behaviour details +/// +/// - Follows the grammar specified in section 5.6 Internet Date/Time Format of +/// RFC 3339 . +/// - The `T` and `Z` characters may alternatively be lower case `t` or `z`, +/// respectively. +/// - Full dates and full times must be separated by `T` or `t`. A space is also +/// permitted. +/// - Leap seconds rules are not considered. That is, any timestamp may +/// specify digts `00` - `60` for the seconds. +/// - Any part of a fractional second that cannot be represented in the +/// nanosecond precision is tructated. That is, for the time string, +/// `"1970-01-01T00:00:00.1234567899Z"`, the fractional second `.1234567899` +/// will be represented as `123_456_789` in the `Timestamp`. +/// +pub fn parse_rfc3339(input: String) -> Result(Timestamp, Nil) { + let bytes = bit_array.from_string(input) + + // Date + use #(year, bytes) <- result.try(parse_year(from: bytes)) + use bytes <- result.try(accept_byte(from: bytes, value: byte_minus)) + use #(month, bytes) <- result.try(parse_month(from: bytes)) + use bytes <- result.try(accept_byte(from: bytes, value: byte_minus)) + use #(day, bytes) <- result.try(parse_day(from: bytes, year:, month:)) + + use bytes <- result.try(accept_date_time_separator(from: bytes)) + + // Time + use #(hours, bytes) <- result.try(parse_hours(from: bytes)) + use bytes <- result.try(accept_byte(from: bytes, value: byte_colon)) + use #(minutes, bytes) <- result.try(parse_minutes(from: bytes)) + use bytes <- result.try(accept_byte(from: bytes, value: byte_colon)) + use #(seconds, bytes) <- result.try(parse_seconds(from: bytes)) + use #(second_fraction_as_nanoseconds, bytes) <- result.try( + parse_second_fraction_as_nanoseconds(from: bytes), + ) + + // Offset + use #(offset_seconds, bytes) <- result.try(parse_offset(from: bytes)) + + // Done + use Nil <- result.try(accept_empty(bytes)) + + Ok(from_date_time( + year:, + month:, + day:, + hours:, + minutes:, + seconds:, + second_fraction_as_nanoseconds:, + offset_seconds:, + )) +} + +fn parse_year(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) { + parse_digits(from: bytes, count: 4) +} + +fn parse_month(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) { + use #(month, bytes) <- result.try(parse_digits(from: bytes, count: 2)) + case 1 <= month && month <= 12 { + True -> Ok(#(month, bytes)) + False -> Error(Nil) + } +} + +fn parse_day( + from bytes: BitArray, + year year, + month month, +) -> Result(#(Int, BitArray), Nil) { + use #(day, bytes) <- result.try(parse_digits(from: bytes, count: 2)) + + use max_day <- result.try(case month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 -> Ok(31) + 4 | 6 | 9 | 11 -> Ok(30) + 2 -> { + case is_leap_year(year) { + True -> Ok(29) + False -> Ok(28) + } + } + _ -> Error(Nil) + }) + + case 1 <= day && day <= max_day { + True -> Ok(#(day, bytes)) + False -> Error(Nil) + } +} + +// Implementation from RFC 3339 Appendix C +fn is_leap_year(year: Int) -> Bool { + year % 4 == 0 && { year % 100 != 0 || year % 400 == 0 } +} + +fn parse_hours(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) { + use #(hours, bytes) <- result.try(parse_digits(from: bytes, count: 2)) + case 0 <= hours && hours <= 23 { + True -> Ok(#(hours, bytes)) + False -> Error(Nil) + } +} + +fn parse_minutes(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) { + use #(minutes, bytes) <- result.try(parse_digits(from: bytes, count: 2)) + case 0 <= minutes && minutes <= 59 { + True -> Ok(#(minutes, bytes)) + False -> Error(Nil) + } +} + +fn parse_seconds(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) { + use #(seconds, bytes) <- result.try(parse_digits(from: bytes, count: 2)) + // Max of 60 for leap seconds. We don't bother to check if this leap second + // actually occurred in the past or not. + case 0 <= seconds && seconds <= 60 { + True -> Ok(#(seconds, bytes)) + False -> Error(Nil) + } +} + +// Truncates any part of the fraction that is beyond the nanosecond precision. +fn parse_second_fraction_as_nanoseconds(from bytes: BitArray) { + case bytes { + <<".", byte, remaining_bytes:bytes>> + if byte_zero <= byte && byte <= byte_nine + -> { + do_parse_second_fraction_as_nanoseconds( + from: <>, + acc: 0, + power: nanoseconds_per_second, + ) + } + // bytes starts with a ".", which should introduce a fraction, but it does + // not, and so it is an ill-formed input. + <<".", _:bytes>> -> Error(Nil) + // bytes does not start with a "." so there is no fraction. Call this 0 + // nanoseconds. + _ -> Ok(#(0, bytes)) + } +} + +fn do_parse_second_fraction_as_nanoseconds( + from bytes: BitArray, + acc acc: Int, + power power: Int, +) -> Result(#(Int, BitArray), a) { + // Each digit place to the left in the fractional second is 10x fewer + // nanoseconds. + let power = power / 10 + + case bytes { + <> + if byte_zero <= byte && byte <= byte_nine && power < 1 + -> { + // We already have the max precision for nanoseconds. Truncate any + // remaining digits. + do_parse_second_fraction_as_nanoseconds( + from: remaining_bytes, + acc:, + power:, + ) + } + <> if byte_zero <= byte && byte <= byte_nine -> { + // We have not yet reached the precision limit. Parse the next digit. + let digit = byte - 0x30 + do_parse_second_fraction_as_nanoseconds( + from: remaining_bytes, + acc: acc + digit * power, + power:, + ) + } + _ -> Ok(#(acc, bytes)) + } +} + +fn parse_offset(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) { + case bytes { + <<"Z", remaining_bytes:bytes>> | <<"z", remaining_bytes:bytes>> -> + Ok(#(0, remaining_bytes)) + _ -> parse_numeric_offset(bytes) + } +} + +fn parse_numeric_offset(from bytes: BitArray) -> Result(#(Int, BitArray), Nil) { + use #(sign, bytes) <- result.try(parse_sign(from: bytes)) + use #(hours, bytes) <- result.try(parse_hours(from: bytes)) + use bytes <- result.try(accept_byte(from: bytes, value: byte_colon)) + use #(minutes, bytes) <- result.try(parse_minutes(from: bytes)) + + let offset_seconds = offset_to_seconds(sign, hours:, minutes:) + + Ok(#(offset_seconds, bytes)) +} + +fn parse_sign(from bytes) { + case bytes { + <<"+", remaining_bytes:bytes>> -> Ok(#("+", remaining_bytes)) + <<"-", remaining_bytes:bytes>> -> Ok(#("-", remaining_bytes)) + _ -> Error(Nil) + } +} + +fn offset_to_seconds(sign, hours hours, minutes minutes) { + let abs_seconds = hours * seconds_per_hour + minutes * seconds_per_minute + + case sign { + "-" -> -abs_seconds + _ -> abs_seconds + } +} + +/// Parse and return the given number of digits from the given bytes. +/// +fn parse_digits( + from bytes: BitArray, + count count: Int, +) -> Result(#(Int, BitArray), Nil) { + do_parse_digits(from: bytes, count:, acc: 0, k: 0) +} + +fn do_parse_digits( + from bytes: BitArray, + count count: Int, + acc acc: Int, + k k: Int, +) -> Result(#(Int, BitArray), Nil) { + case bytes { + _ if k >= count -> Ok(#(acc, bytes)) + <> if byte_zero <= byte && byte <= byte_nine -> + do_parse_digits( + from: remaining_bytes, + count:, + acc: acc * 10 + { byte - 0x30 }, + k: k + 1, + ) + _ -> Error(Nil) + } +} + +/// Accept the given value from `bytes` and move past it if found. +/// +fn accept_byte(from bytes: BitArray, value value: Int) -> Result(BitArray, Nil) { + case bytes { + <> if byte == value -> Ok(remaining_bytes) + _ -> Error(Nil) + } +} + +fn accept_date_time_separator(from bytes: BitArray) -> Result(BitArray, Nil) { + case bytes { + <> + if byte == byte_t_uppercase + || byte == byte_t_lowercase + || byte == byte_space + -> Ok(remaining_bytes) + _ -> Error(Nil) + } +} + +fn accept_empty(from bytes: BitArray) -> Result(Nil, Nil) { + case bytes { + <<>> -> Ok(Nil) + _ -> Error(Nil) + } +} + +/// Note: The caller of this function must ensure that all inputs are valid. +/// +fn from_date_time( + year year: Int, + month month: Int, + day day: Int, + hours hours: Int, + minutes minutes: Int, + seconds seconds: Int, + second_fraction_as_nanoseconds second_fraction_as_nanoseconds: Int, + offset_seconds offset_seconds: Int, +) -> Timestamp { + let julian_seconds = + julian_seconds_from_parts(year:, month:, day:, hours:, minutes:, seconds:) + + let julian_seconds_since_epoch = julian_seconds - julian_seconds_unix_epoch + + Timestamp( + seconds: julian_seconds_since_epoch - offset_seconds, + nanoseconds: second_fraction_as_nanoseconds, + ) + |> normalise +} + +/// `julian_seconds_from_parts(year, month, day, hours, minutes, seconds)` +/// returns the number of Julian +/// seconds represented by the given arguments. +/// +/// Note: It is the callers responsibility to ensure the inputs are valid. +/// +/// See https://www.tondering.dk/claus/cal/julperiod.php#formula +/// +fn julian_seconds_from_parts( + year year: Int, + month month: Int, + day day: Int, + hours hours: Int, + minutes minutes: Int, + seconds seconds: Int, +) { + let julian_day_seconds = + julian_day_from_ymd(year:, month:, day:) * seconds_per_day + + julian_day_seconds + + { hours * seconds_per_hour } + + { minutes * seconds_per_minute } + + seconds +} + +/// Note: It is the callers responsibility to ensure the inputs are valid. +/// +/// See https://www.tondering.dk/claus/cal/julperiod.php#formula +/// +fn julian_day_from_ymd(year year: Int, month month: Int, day day: Int) -> Int { + let adjustment = { 14 - month } / 12 + let adjusted_year = year + 4800 - adjustment + let adjusted_month = month + 12 * adjustment - 3 + + day + + { { 153 * adjusted_month } + 2 } + / 5 + + 365 + * adjusted_year + + { adjusted_year / 4 } + - { adjusted_year / 100 } + + { adjusted_year / 400 } + - 32_045 +} + +/// Create a timestamp from a number of seconds since 00:00:00 UTC on 1 January +/// 1970. +/// +pub fn from_unix_seconds(seconds: Int) -> Timestamp { + Timestamp(seconds, 0) +} + +/// Create a timestamp from a number of seconds and nanoseconds since 00:00:00 +/// UTC on 1 January 1970. +/// +/// # JavaScript int limitations +/// +/// Remember that JavaScript can only perfectly represent ints between positive +/// and negative 9,007,199,254,740,991! If you only use the nanosecond field +/// then you will almost certainly not get the date value you want due to this +/// loss of precision. Always use seconds primarily and then use nanoseconds +/// for the final sub-second adjustment. +/// +pub fn from_unix_seconds_and_nanoseconds( + seconds seconds: Int, + nanoseconds nanoseconds: Int, +) -> Timestamp { + Timestamp(seconds, nanoseconds) + |> normalise +} + +/// Convert the timestamp to a number of seconds since 00:00:00 UTC on 1 +/// January 1970. +/// +/// There may be some small loss of precision due to `Timestamp` being +/// nanosecond accurate and `Float` not being able to represent this. +/// +pub fn to_unix_seconds(timestamp: Timestamp) -> Float { + let seconds = int.to_float(timestamp.seconds) + let nanoseconds = int.to_float(timestamp.nanoseconds) + seconds +. { nanoseconds /. 1_000_000_000.0 } +} + +/// Convert the timestamp to a number of seconds and nanoseconds since 00:00:00 +/// UTC on 1 January 1970. There is no loss of precision with this conversion +/// on any target. +pub fn to_unix_seconds_and_nanoseconds(timestamp: Timestamp) -> #(Int, Int) { + #(timestamp.seconds, timestamp.nanoseconds) +} diff --git a/build/packages/gleam_time/src/gleam@time@calendar.erl b/build/packages/gleam_time/src/gleam@time@calendar.erl new file mode 100644 index 0000000..e295d09 --- /dev/null +++ b/build/packages/gleam_time/src/gleam@time@calendar.erl @@ -0,0 +1,468 @@ +-module(gleam@time@calendar). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/time/calendar.gleam"). +-export([local_offset/0, month_to_string/1, month_to_int/1, month_from_int/1, is_leap_year/1, is_valid_date/1, is_valid_time_of_day/1, naive_date_compare/2]). +-export_type([date/0, time_of_day/0, month/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " This module is for working with the Gregorian calendar, established by\n" + " Pope Gregory XIII in 1582!\n" + "\n" + " ## When should you use this module?\n" + "\n" + " > **tldr:** You probably want to use [`gleam/time/timestamp`](./timestamp.html)\n" + " > instead!\n" + "\n" + " Calendar time is difficult to work with programmatically, it is the source\n" + " of most time-related bugs in software. Compared to _epoch time_, which the\n" + " `gleam/time/timestamp` module uses, there are many disadvantages to\n" + " calendar time:\n" + "\n" + " - They are ambiguous if you don't know what time-zone is being used.\n" + "\n" + " - A time-zone database is required to understand calendar time even when\n" + " you have the time zone. These are large and your program has to\n" + " continously be updated as new versions of the database are published.\n" + "\n" + " - The type permits invalid states. e.g. `days` could be set to the number\n" + " 32, but this should not be possible!\n" + "\n" + " - There is not a single unique canonical value for each point in time,\n" + " thanks to time zones. Two different `Date` + `TimeOfDay` value pairs\n" + " could represent the same point in time. This means that you can't check\n" + " for time equality with `==` when using calendar types.\n" + "\n" + " - They are computationally complex, using a more memory to represent and\n" + " requiring a lot more CPU time to manipulate.\n" + "\n" + " There are also advantages to calendar time:\n" + "\n" + " - Calendar time is how human's talk about time, so if you want to show a\n" + " time or take a time from a human user then calendar time will make it\n" + " easier for them.\n" + "\n" + " - They can represent more abstract time periods such as \"New Year's Day\".\n" + " This may seem like an exact window of time at first, but really the\n" + " definition of \"New Year's Day\" is more fuzzy than that. When it starts\n" + " and ends will depend where in the world you are, so if you want to refer\n" + " to a day as a global concept instead of a fixed window of time for that\n" + " day in a specific location, then calendar time can represent that.\n" + "\n" + " So when should you use calendar time? These are our recommendations:\n" + "\n" + " - Default to `gleam/time/timestamp`, which is epoch time. It is\n" + " unambiguous, efficient, and significantly less likely to result in logic\n" + " bugs.\n" + "\n" + " - When writing time to a database or other data storage use epoch time,\n" + " using whatever epoch format it supports. For example, PostgreSQL\n" + " `timestamp` and `timestampz` are both epoch time, and `timestamp` is\n" + " preferred as it is more straightforward to use as your application is\n" + " also using epoch time.\n" + "\n" + " - When communicating with other computer systems continue to use epoch\n" + " time. For example, when sending times to another program you could\n" + " encode time as UNIX timestamps (seconds since 00:00:00 UTC on 1 January\n" + " 1970).\n" + "\n" + " - When communicating with humans use epoch time internally, and convert\n" + " to-and-from calendar time at the last moment, when iteracting with the\n" + " human user. It may also help the users to also show the time as a fuzzy\n" + " duration from the present time, such as \"about 4 days ago\".\n" + "\n" + " - When representing \"fuzzy\" human time concepts that don't exact periods\n" + " in time, such as \"one month\" (varies depending on which month, which\n" + " year, and in which time zone) and \"Christmas Day\" (varies depending on\n" + " which year and time zone) then use calendar time.\n" + "\n" + " Any time you do use calendar time you should be extra careful! It is very\n" + " easy to make mistake with. Avoid it where possible.\n" + "\n" + " ## Time zone offsets\n" + "\n" + " This package includes the `utc_offset` value and the `local_offset`\n" + " function, which are the offset for the UTC time zone and get the time\n" + " offset the computer running the program is configured to respectively.\n" + "\n" + " If you need to use other offsets in your program then you will need to get\n" + " them from somewhere else, such as from a package which loads the\n" + " [IANA Time Zone Database](https://www.iana.org/time-zones), or from the\n" + " website visitor's web browser, which your frontend can send for you.\n" + "\n" + " ## Use in APIs\n" + "\n" + " If you are making an API such as a HTTP JSON API you are encouraged to use\n" + " Unix timestamps instead of calendar times.\n" +). + +-type date() :: {date, integer(), month(), integer()}. + +-type time_of_day() :: {time_of_day, integer(), integer(), integer(), integer()}. + +-type month() :: january | + february | + march | + april | + may | + june | + july | + august | + september | + october | + november | + december. + +-file("src/gleam/time/calendar.gleam", 147). +?DOC( + " Get the offset for the computer's currently configured time zone.\n" + "\n" + " Note this may not be the time zone that is correct to use for your user.\n" + " For example, if you are making a web application that runs on a server you\n" + " want _their_ computer's time zone, not yours.\n" + "\n" + " This is the _current local_ offset, not the current local time zone. This\n" + " means that while it will result in the expected outcome for the current\n" + " time, it may result in unexpected output if used with other timestamps. For\n" + " example: a timestamp that would locally be during daylight savings time if\n" + " is it not currently daylight savings time when this function is called.\n" +). +-spec local_offset() -> gleam@time@duration:duration(). +local_offset() -> + gleam@time@duration:seconds(gleam_time_ffi:local_time_offset_seconds()). + +-file("src/gleam/time/calendar.gleam", 163). +?DOC( + " Returns the English name for a month.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " month_to_string(April)\n" + " // -> \"April\"\n" + " ```\n" +). +-spec month_to_string(month()) -> binary(). +month_to_string(Month) -> + case Month of + january -> + <<"January"/utf8>>; + + february -> + <<"February"/utf8>>; + + march -> + <<"March"/utf8>>; + + april -> + <<"April"/utf8>>; + + may -> + <<"May"/utf8>>; + + june -> + <<"June"/utf8>>; + + july -> + <<"July"/utf8>>; + + august -> + <<"August"/utf8>>; + + september -> + <<"September"/utf8>>; + + october -> + <<"October"/utf8>>; + + november -> + <<"November"/utf8>>; + + december -> + <<"December"/utf8>> + end. + +-file("src/gleam/time/calendar.gleam", 188). +?DOC( + " Returns the number for the month, where January is 1 and December is 12.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " month_to_int(January)\n" + " // -> 1\n" + " ```\n" +). +-spec month_to_int(month()) -> integer(). +month_to_int(Month) -> + case Month of + january -> + 1; + + february -> + 2; + + march -> + 3; + + april -> + 4; + + may -> + 5; + + june -> + 6; + + july -> + 7; + + august -> + 8; + + september -> + 9; + + october -> + 10; + + november -> + 11; + + december -> + 12 + end. + +-file("src/gleam/time/calendar.gleam", 213). +?DOC( + " Returns the month for a given number, where January is 1 and December is 12.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " month_from_int(1)\n" + " // -> Ok(January)\n" + " ```\n" +). +-spec month_from_int(integer()) -> {ok, month()} | {error, nil}. +month_from_int(Month) -> + case Month of + 1 -> + {ok, january}; + + 2 -> + {ok, february}; + + 3 -> + {ok, march}; + + 4 -> + {ok, april}; + + 5 -> + {ok, may}; + + 6 -> + {ok, june}; + + 7 -> + {ok, july}; + + 8 -> + {ok, august}; + + 9 -> + {ok, september}; + + 10 -> + {ok, october}; + + 11 -> + {ok, november}; + + 12 -> + {ok, december}; + + _ -> + {error, nil} + end. + +-file("src/gleam/time/calendar.gleam", 290). +?DOC( + " Determines if a given year is a leap year.\n" + "\n" + " A leap year occurs every 4 years, except for years divisible by 100,\n" + " unless they are also divisible by 400.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " is_leap_year(2024)\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_leap_year(2023)\n" + " // -> False\n" + " ```\n" +). +-spec is_leap_year(integer()) -> boolean(). +is_leap_year(Year) -> + case (Year rem 400) =:= 0 of + true -> + true; + + false -> + case (Year rem 100) =:= 0 of + true -> + false; + + false -> + (Year rem 4) =:= 0 + end + end. + +-file("src/gleam/time/calendar.gleam", 254). +?DOC( + " Checks if a given date is valid.\n" + "\n" + " This function properly accounts for leap years when validating February days.\n" + " A leap year occurs every 4 years, except for years divisible by 100,\n" + " unless they are also divisible by 400.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " is_valid_date(Date(2023, April, 15))\n" + " // -> True\n" + " ```\n" + "\n" + " ```gleam\n" + " is_valid_date(Date(2023, April, 31))\n" + " // -> False\n" + " ```\n" + "\n" + " ```gleam\n" + " is_valid_date(Date(2024, February, 29))\n" + " // -> True (2024 is a leap year)\n" + " ```\n" +). +-spec is_valid_date(date()) -> boolean(). +is_valid_date(Date) -> + {date, Year, Month, Day} = Date, + case Day < 1 of + true -> + false; + + false -> + case Month of + january -> + Day =< 31; + + march -> + Day =< 31; + + may -> + Day =< 31; + + july -> + Day =< 31; + + august -> + Day =< 31; + + october -> + Day =< 31; + + december -> + Day =< 31; + + april -> + Day =< 30; + + june -> + Day =< 30; + + september -> + Day =< 30; + + november -> + Day =< 30; + + february -> + Max_february_days = case is_leap_year(Year) of + true -> + 29; + + false -> + 28 + end, + Day =< Max_february_days + end + end. + +-file("src/gleam/time/calendar.gleam", 313). +?DOC( + " Checks if a time of day is valid.\n" + "\n" + " Validates that hours are 0-23, minutes are 0-59, seconds are 0-59,\n" + " and nanoseconds are 0-999,999,999.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " is_valid_time_of_day(TimeOfDay(12, 30, 45, 123456789))\n" + " // -> True\n" + " ```\n" +). +-spec is_valid_time_of_day(time_of_day()) -> boolean(). +is_valid_time_of_day(Time) -> + {time_of_day, Hours, Minutes, Seconds, Nanoseconds} = Time, + (((((((Hours >= 0) andalso (Hours =< 23)) andalso (Minutes >= 0)) andalso (Minutes + =< 59)) + andalso (Seconds >= 0)) + andalso (Seconds =< 59)) + andalso (Nanoseconds >= 0)) + andalso (Nanoseconds =< 999999999). + +-file("src/gleam/time/calendar.gleam", 340). +?DOC( + " Naively compares two dates without any time zone information, returning an\n" + " order.\n" + "\n" + " ## Correctness\n" + "\n" + " This function compares dates without any time zone information, only using\n" + " the rules for the gregorian calendar. This is typically sufficient, but be\n" + " aware that in reality some time zones will change their calendar date\n" + " occasionally. This can result in days being skipped, out of order, or\n" + " happening multiple times.\n" + "\n" + " If you need real-world correct time ordering then use the\n" + " `gleam/time/timestamp` module instead.\n" +). +-spec naive_date_compare(date(), date()) -> gleam@order:order(). +naive_date_compare(One, Other) -> + _pipe = gleam@int:compare(erlang:element(2, One), erlang:element(2, Other)), + _pipe@1 = gleam@order:lazy_break_tie( + _pipe, + fun() -> + gleam@int:compare( + month_to_int(erlang:element(3, One)), + month_to_int(erlang:element(3, Other)) + ) + end + ), + gleam@order:lazy_break_tie( + _pipe@1, + fun() -> + gleam@int:compare(erlang:element(4, One), erlang:element(4, Other)) + end + ). diff --git a/build/packages/gleam_time/src/gleam@time@duration.erl b/build/packages/gleam_time/src/gleam@time@duration.erl new file mode 100644 index 0000000..7ba7ad2 --- /dev/null +++ b/build/packages/gleam_time/src/gleam@time@duration.erl @@ -0,0 +1,381 @@ +-module(gleam@time@duration). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/time/duration.gleam"). +-export([approximate/1, compare/2, difference/2, add/2, seconds/1, minutes/1, hours/1, milliseconds/1, nanoseconds/1, to_seconds/1, to_seconds_and_nanoseconds/1, to_iso8601_string/1]). +-export_type([duration/0, unit/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-opaque duration() :: {duration, integer(), integer()}. + +-type unit() :: nanosecond | + microsecond | + millisecond | + second | + minute | + hour | + day | + week | + month | + year. + +-file("src/gleam/time/duration.gleam", 110). +?DOC( + " Ensure the duration is represented with `nanoseconds` being positive and\n" + " less than 1 second.\n" + "\n" + " This function does not change the amount of time that the duratoin refers\n" + " to, it only adjusts the values used to represent the time.\n" +). +-spec normalise(duration()) -> duration(). +normalise(Duration) -> + Multiplier = 1000000000, + Nanoseconds = case Multiplier of + 0 -> 0; + Gleam@denominator -> erlang:element(3, Duration) rem Gleam@denominator + end, + Overflow = erlang:element(3, Duration) - Nanoseconds, + Seconds = erlang:element(2, Duration) + (case Multiplier of + 0 -> 0; + Gleam@denominator@1 -> Overflow div Gleam@denominator@1 + end), + case Nanoseconds >= 0 of + true -> + {duration, Seconds, Nanoseconds}; + + false -> + {duration, Seconds - 1, Multiplier + Nanoseconds} + end. + +-file("src/gleam/time/duration.gleam", 76). +?DOC( + " Convert a duration to a number of the largest number of a unit, serving as\n" + " a rough description of the duration that a human can understand.\n" + "\n" + " The size used for each unit are described in the documentation for the\n" + " `Unit` type.\n" + "\n" + " ```gleam\n" + " seconds(125)\n" + " |> approximate\n" + " // -> #(2, Minute)\n" + " ```\n" + "\n" + " This function rounds _towards zero_. This means that if a duration is just\n" + " short of 2 days then it will approximate to 1 day.\n" + "\n" + " ```gleam\n" + " hours(47)\n" + " |> approximate\n" + " // -> #(1, Day)\n" + " ```\n" +). +-spec approximate(duration()) -> {integer(), unit()}. +approximate(Duration) -> + {duration, S, Ns} = Duration, + Minute = 60, + Hour = Minute * 60, + Day = Hour * 24, + Week = Day * 7, + Year = (Day * 365) + (Hour * 6), + Month = Year div 12, + Microsecond = 1000, + Millisecond = Microsecond * 1000, + case nil of + _ when S < 0 -> + {Amount, Unit} = begin + _pipe = {duration, - S, - Ns}, + _pipe@1 = normalise(_pipe), + approximate(_pipe@1) + end, + {- Amount, Unit}; + + _ when S >= Year -> + {case Year of + 0 -> 0; + Gleam@denominator -> S div Gleam@denominator + end, year}; + + _ when S >= Month -> + {case Month of + 0 -> 0; + Gleam@denominator@1 -> S div Gleam@denominator@1 + end, month}; + + _ when S >= Week -> + {case Week of + 0 -> 0; + Gleam@denominator@2 -> S div Gleam@denominator@2 + end, week}; + + _ when S >= Day -> + {case Day of + 0 -> 0; + Gleam@denominator@3 -> S div Gleam@denominator@3 + end, day}; + + _ when S >= Hour -> + {case Hour of + 0 -> 0; + Gleam@denominator@4 -> S div Gleam@denominator@4 + end, hour}; + + _ when S >= Minute -> + {case Minute of + 0 -> 0; + Gleam@denominator@5 -> S div Gleam@denominator@5 + end, minute}; + + _ when S > 0 -> + {S, second}; + + _ when Ns >= Millisecond -> + {case Millisecond of + 0 -> 0; + Gleam@denominator@6 -> Ns div Gleam@denominator@6 + end, millisecond}; + + _ when Ns >= Microsecond -> + {case Microsecond of + 0 -> 0; + Gleam@denominator@7 -> Ns div Gleam@denominator@7 + end, microsecond}; + + _ -> + {Ns, nanosecond} + end. + +-file("src/gleam/time/duration.gleam", 140). +?DOC( + " Compare one duration to another, indicating whether the first spans a\n" + " larger amount of time (and so is greater) or smaller amount of time (and so\n" + " is lesser) than the second.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " compare(seconds(1), seconds(2))\n" + " // -> order.Lt\n" + " ```\n" + "\n" + " Whether a duration is negative or positive doesn't matter for comparing\n" + " them, only the amount of time spanned matters.\n" + "\n" + " ```gleam\n" + " compare(seconds(-2), seconds(1))\n" + " // -> order.Gt\n" + " ```\n" +). +-spec compare(duration(), duration()) -> gleam@order:order(). +compare(Left, Right) -> + Parts = fun(X) -> case erlang:element(2, X) >= 0 of + true -> + {erlang:element(2, X), erlang:element(3, X)}; + + false -> + {(erlang:element(2, X) * -1) - 1, + 1000000000 - erlang:element(3, X)} + end end, + {Ls, Lns} = Parts(Left), + {Rs, Rns} = Parts(Right), + _pipe = gleam@int:compare(Ls, Rs), + gleam@order:break_tie(_pipe, gleam@int:compare(Lns, Rns)). + +-file("src/gleam/time/duration.gleam", 164). +?DOC( + " Calculate the difference between two durations.\n" + "\n" + " This is effectively substracting the first duration from the second.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " difference(seconds(1), seconds(5))\n" + " // -> seconds(4)\n" + " ```\n" +). +-spec difference(duration(), duration()) -> duration(). +difference(Left, Right) -> + _pipe = {duration, + erlang:element(2, Right) - erlang:element(2, Left), + erlang:element(3, Right) - erlang:element(3, Left)}, + normalise(_pipe). + +-file("src/gleam/time/duration.gleam", 178). +?DOC( + " Add two durations together.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " add(seconds(1), seconds(5))\n" + " // -> seconds(6)\n" + " ```\n" +). +-spec add(duration(), duration()) -> duration(). +add(Left, Right) -> + _pipe = {duration, + erlang:element(2, Left) + erlang:element(2, Right), + erlang:element(3, Left) + erlang:element(3, Right)}, + normalise(_pipe). + +-file("src/gleam/time/duration.gleam", 225). +-spec nanosecond_digits(integer(), integer(), binary()) -> binary(). +nanosecond_digits(N, Position, Acc) -> + case Position of + 9 -> + Acc; + + _ when (Acc =:= <<""/utf8>>) andalso ((N rem 10) =:= 0) -> + nanosecond_digits(N div 10, Position + 1, Acc); + + _ -> + Acc@1 = <<(erlang:integer_to_binary(N rem 10))/binary, Acc/binary>>, + nanosecond_digits(N div 10, Position + 1, Acc@1) + end. + +-file("src/gleam/time/duration.gleam", 239). +?DOC(" Create a duration of a number of seconds.\n"). +-spec seconds(integer()) -> duration(). +seconds(Amount) -> + {duration, Amount, 0}. + +-file("src/gleam/time/duration.gleam", 244). +?DOC(" Create a duration of a number of minutes.\n"). +-spec minutes(integer()) -> duration(). +minutes(Amount) -> + seconds(Amount * 60). + +-file("src/gleam/time/duration.gleam", 249). +?DOC(" Create a duration of a number of hours.\n"). +-spec hours(integer()) -> duration(). +hours(Amount) -> + seconds((Amount * 60) * 60). + +-file("src/gleam/time/duration.gleam", 254). +?DOC(" Create a duration of a number of milliseconds.\n"). +-spec milliseconds(integer()) -> duration(). +milliseconds(Amount) -> + Remainder = Amount rem 1000, + Overflow = Amount - Remainder, + Nanoseconds = Remainder * 1000000, + Seconds = Overflow div 1000, + _pipe = {duration, Seconds, Nanoseconds}, + normalise(_pipe). + +-file("src/gleam/time/duration.gleam", 273). +?DOC( + " Create a duration of a number of nanoseconds.\n" + "\n" + " # JavaScript int limitations\n" + "\n" + " Remember that JavaScript can only perfectly represent ints between positive\n" + " and negative 9,007,199,254,740,991! If you use a single call to this\n" + " function to create durations larger than that number of nanoseconds then\n" + " you will likely not get exactly the value you expect. Use `seconds` and\n" + " `milliseconds` as much as possible for large durations.\n" +). +-spec nanoseconds(integer()) -> duration(). +nanoseconds(Amount) -> + _pipe = {duration, 0, Amount}, + normalise(_pipe). + +-file("src/gleam/time/duration.gleam", 283). +?DOC( + " Convert the duration to a number of seconds.\n" + "\n" + " There may be some small loss of precision due to `Duration` being\n" + " nanosecond accurate and `Float` not being able to represent this.\n" +). +-spec to_seconds(duration()) -> float(). +to_seconds(Duration) -> + Seconds = erlang:float(erlang:element(2, Duration)), + Nanoseconds = erlang:float(erlang:element(3, Duration)), + Seconds + (Nanoseconds / 1000000000.0). + +-file("src/gleam/time/duration.gleam", 292). +?DOC( + " Convert the duration to a number of seconds and nanoseconds. There is no\n" + " loss of precision with this conversion on any target.\n" +). +-spec to_seconds_and_nanoseconds(duration()) -> {integer(), integer()}. +to_seconds_and_nanoseconds(Duration) -> + {erlang:element(2, Duration), erlang:element(3, Duration)}. + +-file("src/gleam/time/duration.gleam", 192). +?DOC( + " Convert the duration to an [ISO8601][1] formatted duration string.\n" + "\n" + " The ISO8601 duration format is ambiguous without context due to months and\n" + " years having different lengths, and because of leap seconds. This function\n" + " encodes the duration as days, hours, and seconds without any leap seconds.\n" + " Be sure to take this into account when using the duration strings.\n" + "\n" + " [1]: https://en.wikipedia.org/wiki/ISO_8601#Durations\n" +). +-spec to_iso8601_string(duration()) -> binary(). +to_iso8601_string(Duration) -> + gleam@bool:guard( + Duration =:= {duration, 0, 0}, + <<"PT0S"/utf8>>, + fun() -> + Split = fun(Total, Limit) -> + Amount = case Limit of + 0 -> 0; + Gleam@denominator -> Total rem Gleam@denominator + end, + Remainder = case Limit of + 0 -> 0; + Gleam@denominator@1 -> (Total - Amount) div Gleam@denominator@1 + end, + {Amount, Remainder} + end, + {Seconds, Rest} = Split(erlang:element(2, Duration), 60), + {Minutes, Rest@1} = Split(Rest, 60), + {Hours, Rest@2} = Split(Rest@1, 24), + Days = Rest@2, + Add = fun(Out, Value, Unit) -> case Value of + 0 -> + Out; + + _ -> + <<<>/binary, + Unit/binary>> + end end, + Output = begin + _pipe = <<"P"/utf8>>, + _pipe@1 = Add(_pipe, Days, <<"D"/utf8>>), + _pipe@2 = gleam@string:append(_pipe@1, <<"T"/utf8>>), + _pipe@3 = Add(_pipe@2, Hours, <<"H"/utf8>>), + Add(_pipe@3, Minutes, <<"M"/utf8>>) + end, + case {Seconds, erlang:element(3, Duration)} of + {0, 0} -> + Output; + + {_, 0} -> + <<<>/binary, + "S"/utf8>>; + + {_, _} -> + F = nanosecond_digits( + erlang:element(3, Duration), + 0, + <<""/utf8>> + ), + <<<<<<<>/binary, + "."/utf8>>/binary, + F/binary>>/binary, + "S"/utf8>> + end + end + ). diff --git a/build/packages/gleam_time/src/gleam@time@timestamp.erl b/build/packages/gleam_time/src/gleam@time@timestamp.erl new file mode 100644 index 0000000..0d7413a --- /dev/null +++ b/build/packages/gleam_time/src/gleam@time@timestamp.erl @@ -0,0 +1,1188 @@ +-module(gleam@time@timestamp). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleam/time/timestamp.gleam"). +-export([compare/2, system_time/0, difference/2, add/2, to_calendar/2, to_rfc3339/2, from_unix_seconds/1, from_unix_seconds_and_nanoseconds/2, to_unix_seconds/1, to_unix_seconds_and_nanoseconds/1, from_calendar/3, parse_rfc3339/1]). +-export_type([timestamp/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC( + " Welcome to the timestamp module! This module and its `Timestamp` type are\n" + " what you will be using most commonly when working with time in Gleam.\n" + "\n" + " A timestamp represents a moment in time, represented as an amount of time\n" + " since the calendar time 00:00:00 UTC on 1 January 1970, also known as the\n" + " _Unix epoch_.\n" + "\n" + " # Wall clock time and monotonicity\n" + "\n" + " Time is very complicated, especially on computers! While they generally do\n" + " a good job of keeping track of what the time is, computers can get\n" + " out-of-sync and start to report a time that is too late or too early. Most\n" + " computers use \"network time protocol\" to tell each other what they think\n" + " the time is, and computers that realise they are running too fast or too\n" + " slow will adjust their clock to correct it. When this happens it can seem\n" + " to your program that the current time has changed, and it may have even\n" + " jumped backwards in time!\n" + "\n" + " This measure of time is called _wall clock time_, and it is what people\n" + " commonly think of when they think of time. It is important to be aware that\n" + " it can go backwards, and your program must not rely on it only ever going\n" + " forwards at a steady rate. For example, for tracking what order events happen\n" + " in. \n" + "\n" + " This module uses wall clock time. If your program needs time values to always\n" + " increase you will need a _monotonic_ time instead. It's uncommon that you\n" + " would need monotonic time, one example might be if you're making a\n" + " benchmarking framework.\n" + "\n" + " The exact way that time works will depend on what runtime you use. The\n" + " Erlang documentation on time has a lot of detail about time generally as well\n" + " as how it works on the BEAM, it is worth reading.\n" + " .\n" + "\n" + " # Converting to local calendar time\n" + "\n" + " Timestamps don't take into account time zones, so a moment in time will\n" + " have the same timestamp value regardless of where you are in the world. To\n" + " convert them to local time you will need to know the offset for the time\n" + " zone you wish to use, likely from a time zone database. See the\n" + " `gleam/time/calendar` module for more information.\n" + "\n" +). + +-opaque timestamp() :: {timestamp, integer(), integer()}. + +-file("src/gleam/time/timestamp.gleam", 119). +?DOC( + " Ensure the time is represented with `nanoseconds` being positive and less\n" + " than 1 second.\n" + "\n" + " This function does not change the time that the timestamp refers to, it\n" + " only adjusts the values used to represent the time.\n" +). +-spec normalise(timestamp()) -> timestamp(). +normalise(Timestamp) -> + Multiplier = 1000000000, + Nanoseconds = case Multiplier of + 0 -> 0; + Gleam@denominator -> erlang:element(3, Timestamp) rem Gleam@denominator + end, + Overflow = erlang:element(3, Timestamp) - Nanoseconds, + Seconds = erlang:element(2, Timestamp) + (case Multiplier of + 0 -> 0; + Gleam@denominator@1 -> Overflow div Gleam@denominator@1 + end), + case Nanoseconds >= 0 of + true -> + {timestamp, Seconds, Nanoseconds}; + + false -> + {timestamp, Seconds - 1, Multiplier + Nanoseconds} + end. + +-file("src/gleam/time/timestamp.gleam", 141). +?DOC( + " Compare one timestamp to another, indicating whether the first is further\n" + " into the future (greater) or further into the past (lesser) than the\n" + " second.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " compare(from_unix_seconds(1), from_unix_seconds(2))\n" + " // -> order.Lt\n" + " ```\n" +). +-spec compare(timestamp(), timestamp()) -> gleam@order:order(). +compare(Left, Right) -> + gleam@order:break_tie( + gleam@int:compare(erlang:element(2, Left), erlang:element(2, Right)), + gleam@int:compare(erlang:element(3, Left), erlang:element(3, Right)) + ). + +-file("src/gleam/time/timestamp.gleam", 160). +?DOC( + " Get the current system time.\n" + "\n" + " Note this time is not unique or monotonic, it could change at any time or\n" + " even go backwards! The exact behaviour will depend on the runtime used. See\n" + " the module documentation for more information.\n" + "\n" + " On Erlang this uses [`erlang:system_time/1`][1]. On JavaScript this uses\n" + " [`Date.now`][2].\n" + "\n" + " [1]: https://www.erlang.org/doc/apps/erts/erlang#system_time/1\n" + " [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now\n" +). +-spec system_time() -> timestamp(). +system_time() -> + {Seconds, Nanoseconds} = gleam_time_ffi:system_time(), + normalise({timestamp, Seconds, Nanoseconds}). + +-file("src/gleam/time/timestamp.gleam", 180). +?DOC( + " Calculate the difference between two timestamps.\n" + "\n" + " This is effectively substracting the first timestamp from the second.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " difference(from_unix_seconds(1), from_unix_seconds(5))\n" + " // -> duration.seconds(4)\n" + " ```\n" +). +-spec difference(timestamp(), timestamp()) -> gleam@time@duration:duration(). +difference(Left, Right) -> + Seconds = gleam@time@duration:seconds( + erlang:element(2, Right) - erlang:element(2, Left) + ), + Nanoseconds = gleam@time@duration:nanoseconds( + erlang:element(3, Right) - erlang:element(3, Left) + ), + gleam@time@duration:add(Seconds, Nanoseconds). + +-file("src/gleam/time/timestamp.gleam", 195). +?DOC( + " Add a duration to a timestamp.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " add(from_unix_seconds(1000), duration.seconds(5))\n" + " // -> from_unix_seconds(1005)\n" + " ```\n" +). +-spec add(timestamp(), gleam@time@duration:duration()) -> timestamp(). +add(Timestamp, Duration) -> + {Seconds, Nanoseconds} = gleam@time@duration:to_seconds_and_nanoseconds( + Duration + ), + _pipe = {timestamp, + erlang:element(2, Timestamp) + Seconds, + erlang:element(3, Timestamp) + Nanoseconds}, + normalise(_pipe). + +-file("src/gleam/time/timestamp.gleam", 262). +-spec pad_digit(integer(), integer()) -> binary(). +pad_digit(Digit, Desired_length) -> + _pipe = erlang:integer_to_binary(Digit), + gleam@string:pad_start(_pipe, Desired_length, <<"0"/utf8>>). + +-file("src/gleam/time/timestamp.gleam", 308). +-spec duration_to_minutes(gleam@time@duration:duration()) -> integer(). +duration_to_minutes(Duration) -> + erlang:round(gleam@time@duration:to_seconds(Duration) / 60.0). + +-file("src/gleam/time/timestamp.gleam", 370). +-spec modulo(integer(), integer()) -> integer(). +modulo(N, M) -> + case gleam@int:modulo(N, M) of + {ok, N@1} -> + N@1; + + {error, _} -> + 0 + end. + +-file("src/gleam/time/timestamp.gleam", 377). +-spec floored_div(integer(), float()) -> integer(). +floored_div(Numerator, Denominator) -> + N = case Denominator of + +0.0 -> +0.0; + -0.0 -> -0.0; + Gleam@denominator -> erlang:float(Numerator) / Gleam@denominator + end, + erlang:round(math:floor(N)). + +-file("src/gleam/time/timestamp.gleam", 383). +-spec to_civil(integer()) -> {integer(), integer(), integer()}. +to_civil(Minutes) -> + Raw_day = floored_div(Minutes, (60.0 * 24.0)) + 719468, + Era = case Raw_day >= 0 of + true -> + Raw_day div 146097; + + false -> + (Raw_day - 146096) div 146097 + end, + Day_of_era = Raw_day - (Era * 146097), + Year_of_era = (((Day_of_era - (Day_of_era div 1460)) + (Day_of_era div 36524)) + - (Day_of_era div 146096)) + div 365, + Year = Year_of_era + (Era * 400), + Day_of_year = Day_of_era - (((365 * Year_of_era) + (Year_of_era div 4)) - (Year_of_era + div 100)), + Mp = ((5 * Day_of_year) + 2) div 153, + Month = case Mp < 10 of + true -> + Mp + 3; + + false -> + Mp - 9 + end, + Day = (Day_of_year - (((153 * Mp) + 2) div 5)) + 1, + Year@1 = case Month =< 2 of + true -> + Year + 1; + + false -> + Year + end, + {Year@1, Month, Day}. + +-file("src/gleam/time/timestamp.gleam", 312). +-spec to_calendar_from_offset(timestamp(), integer()) -> {integer(), + integer(), + integer(), + integer(), + integer(), + integer()}. +to_calendar_from_offset(Timestamp, Offset) -> + Total = erlang:element(2, Timestamp) + (Offset * 60), + Seconds = modulo(Total, 60), + Total_minutes = floored_div(Total, 60.0), + Minutes = modulo(Total, 60 * 60) div 60, + Hours = case (60 * 60) of + 0 -> 0; + Gleam@denominator -> modulo(Total, (24 * 60) * 60) div Gleam@denominator + end, + {Year, Month, Day} = to_civil(Total_minutes), + {Year, Month, Day, Hours, Minutes, Seconds}. + +-file("src/gleam/time/timestamp.gleam", 281). +?DOC( + " Convert a `Timestamp` to calendar time, suitable for presenting to a human\n" + " to read.\n" + "\n" + " If you want a machine to use the time value then you should not use this\n" + " function and should instead keep it as a timestamp. See the documentation\n" + " for the `gleam/time/calendar` module for more information.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " timestamp.from_unix_seconds(0)\n" + " |> timestamp.to_calendar(calendar.utc_offset)\n" + " // -> #(Date(1970, January, 1), TimeOfDay(0, 0, 0, 0))\n" + " ```\n" +). +-spec to_calendar(timestamp(), gleam@time@duration:duration()) -> {gleam@time@calendar:date(), + gleam@time@calendar:time_of_day()}. +to_calendar(Timestamp, Offset) -> + Offset@1 = duration_to_minutes(Offset), + {Year, Month, Day, Hours, Minutes, Seconds} = to_calendar_from_offset( + Timestamp, + Offset@1 + ), + Month@1 = case Month of + 1 -> + january; + + 2 -> + february; + + 3 -> + march; + + 4 -> + april; + + 5 -> + may; + + 6 -> + june; + + 7 -> + july; + + 8 -> + august; + + 9 -> + september; + + 10 -> + october; + + 11 -> + november; + + _ -> + december + end, + Nanoseconds = erlang:element(3, Timestamp), + Date = {date, Year, Month@1, Day}, + Time = {time_of_day, Hours, Minutes, Seconds, Nanoseconds}, + {Date, Time}. + +-file("src/gleam/time/timestamp.gleam", 446). +-spec do_remove_trailing_zeros(list(integer())) -> list(integer()). +do_remove_trailing_zeros(Reversed_digits) -> + case Reversed_digits of + [] -> + []; + + [Digit | Digits] when Digit =:= 0 -> + do_remove_trailing_zeros(Digits); + + Reversed_digits@1 -> + lists:reverse(Reversed_digits@1) + end. + +-file("src/gleam/time/timestamp.gleam", 440). +?DOC(" Given a list of digits, return new list with any trailing zeros removed.\n"). +-spec remove_trailing_zeros(list(integer())) -> list(integer()). +remove_trailing_zeros(Digits) -> + Reversed_digits = lists:reverse(Digits), + do_remove_trailing_zeros(Reversed_digits). + +-file("src/gleam/time/timestamp.gleam", 461). +-spec do_get_zero_padded_digits(integer(), list(integer()), integer()) -> list(integer()). +do_get_zero_padded_digits(Number, Digits, Count) -> + case Number of + Number@1 when (Number@1 =< 0) andalso (Count >= 9) -> + Digits; + + Number@2 when Number@2 =< 0 -> + do_get_zero_padded_digits(Number@2, [0 | Digits], Count + 1); + + Number@3 -> + Digit = Number@3 rem 10, + Number@4 = floored_div(Number@3, 10.0), + do_get_zero_padded_digits(Number@4, [Digit | Digits], Count + 1) + end. + +-file("src/gleam/time/timestamp.gleam", 457). +?DOC( + " Returns the list of digits of `number`. If the number of digits is less \n" + " than 9, the result is zero-padded at the front.\n" +). +-spec get_zero_padded_digits(integer()) -> list(integer()). +get_zero_padded_digits(Number) -> + do_get_zero_padded_digits(Number, [], 0). + +-file("src/gleam/time/timestamp.gleam", 420). +?DOC( + " Converts nanoseconds into a `String` representation of fractional seconds.\n" + " \n" + " Assumes that `nanoseconds < 1_000_000_000`, which will be true for any \n" + " normalised timestamp.\n" +). +-spec show_second_fraction(integer()) -> binary(). +show_second_fraction(Nanoseconds) -> + case gleam@int:compare(Nanoseconds, 0) of + lt -> + <<""/utf8>>; + + eq -> + <<""/utf8>>; + + gt -> + Second_fraction_part = begin + _pipe = Nanoseconds, + _pipe@1 = get_zero_padded_digits(_pipe), + _pipe@2 = remove_trailing_zeros(_pipe@1), + _pipe@3 = gleam@list:map( + _pipe@2, + fun erlang:integer_to_binary/1 + ), + gleam@string:join(_pipe@3, <<""/utf8>>) + end, + <<"."/utf8, Second_fraction_part/binary>> + end. + +-file("src/gleam/time/timestamp.gleam", 240). +?DOC( + " Convert a timestamp to a RFC 3339 formatted time string, with an offset\n" + " supplied as an additional argument.\n" + "\n" + " The output of this function is also ISO 8601 compatible so long as the\n" + " offset not negative. Offsets have at-most minute precision, so an offset\n" + " with higher precision will be rounded to the nearest minute.\n" + "\n" + " If you are making an API such as a HTTP JSON API you are encouraged to use\n" + " Unix timestamps instead of this format or ISO 8601. Unix timestamps are a\n" + " better choice as they don't contain offset information. Consider:\n" + "\n" + " - UTC offsets are not time zones. This does not and cannot tell us the time\n" + " zone in which the date was recorded. So what are we supposed to do with\n" + " this information?\n" + " - Users typically want dates formatted according to their local time zone.\n" + " What if the provided UTC offset is different from the current user's time\n" + " zone? What are we supposed to do with it then?\n" + " - Despite it being useless (or worse, a source of bugs), the UTC offset\n" + " creates a larger payload to transfer.\n" + "\n" + " They also uses more memory than a unix timestamp. The way they are better\n" + " than Unix timestamp is that it is easier for a human to read them, but\n" + " this is a hinderance that tooling can remedy, and APIs are not primarily\n" + " for humans.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " timestamp.from_unix_seconds_and_nanoseconds(1000, 123_000_000)\n" + " |> to_rfc3339(calendar.utc_offset)\n" + " // -> \"1970-01-01T00:16:40.123Z\"\n" + " ```\n" + "\n" + " ```gleam\n" + " timestamp.from_unix_seconds(1000)\n" + " |> to_rfc3339(duration.seconds(3600))\n" + " // -> \"1970-01-01T01:16:40+01:00\"\n" + " ```\n" +). +-spec to_rfc3339(timestamp(), gleam@time@duration:duration()) -> binary(). +to_rfc3339(Timestamp, Offset) -> + Offset@1 = duration_to_minutes(Offset), + {Years, Months, Days, Hours, Minutes, Seconds} = to_calendar_from_offset( + Timestamp, + Offset@1 + ), + Offset_minutes = modulo(Offset@1, 60), + Offset_hours = gleam@int:absolute_value(floored_div(Offset@1, 60.0)), + N2 = fun(_capture) -> pad_digit(_capture, 2) end, + N4 = fun(_capture@1) -> pad_digit(_capture@1, 4) end, + Out = <<""/utf8>>, + Out@1 = <<<<<<<<<>/binary, "-"/utf8>>/binary, + (N2(Months))/binary>>/binary, + "-"/utf8>>/binary, + (N2(Days))/binary>>, + Out@2 = <>, + Out@3 = <<<<<<<<<>/binary, ":"/utf8>>/binary, + (N2(Minutes))/binary>>/binary, + ":"/utf8>>/binary, + (N2(Seconds))/binary>>, + Out@4 = <>, + case gleam@int:compare(Offset@1, 0) of + eq -> + <>; + + gt -> + <<<<<<<>/binary, (N2(Offset_hours))/binary>>/binary, + ":"/utf8>>/binary, + (N2(Offset_minutes))/binary>>; + + lt -> + <<<<<<<>/binary, (N2(Offset_hours))/binary>>/binary, + ":"/utf8>>/binary, + (N2(Offset_minutes))/binary>> + end. + +-file("src/gleam/time/timestamp.gleam", 611). +-spec is_leap_year(integer()) -> boolean(). +is_leap_year(Year) -> + ((Year rem 4) =:= 0) andalso (((Year rem 100) /= 0) orelse ((Year rem 400) + =:= 0)). + +-file("src/gleam/time/timestamp.gleam", 715). +-spec parse_sign(bitstring()) -> {ok, {binary(), bitstring()}} | {error, nil}. +parse_sign(Bytes) -> + case Bytes of + <<"+"/utf8, Remaining_bytes/binary>> -> + {ok, {<<"+"/utf8>>, Remaining_bytes}}; + + <<"-"/utf8, Remaining_bytes@1/binary>> -> + {ok, {<<"-"/utf8>>, Remaining_bytes@1}}; + + _ -> + {error, nil} + end. + +-file("src/gleam/time/timestamp.gleam", 762). +?DOC(" Accept the given value from `bytes` and move past it if found.\n"). +-spec accept_byte(bitstring(), integer()) -> {ok, bitstring()} | {error, nil}. +accept_byte(Bytes, Value) -> + case Bytes of + <> when Byte =:= Value -> + {ok, Remaining_bytes}; + + _ -> + {error, nil} + end. + +-file("src/gleam/time/timestamp.gleam", 780). +-spec accept_empty(bitstring()) -> {ok, nil} | {error, nil}. +accept_empty(Bytes) -> + case Bytes of + <<>> -> + {ok, nil}; + + _ -> + {error, nil} + end. + +-file("src/gleam/time/timestamp.gleam", 840). +?DOC( + " Note: It is the callers responsibility to ensure the inputs are valid.\n" + " \n" + " See https://www.tondering.dk/claus/cal/julperiod.php#formula\n" +). +-spec julian_day_from_ymd(integer(), integer(), integer()) -> integer(). +julian_day_from_ymd(Year, Month, Day) -> + Adjustment = (14 - Month) div 12, + Adjusted_year = (Year + 4800) - Adjustment, + Adjusted_month = (Month + (12 * Adjustment)) - 3, + (((((Day + (((153 * Adjusted_month) + 2) div 5)) + (365 * Adjusted_year)) + (Adjusted_year + div 4)) + - (Adjusted_year div 100)) + + (Adjusted_year div 400)) + - 32045. + +-file("src/gleam/time/timestamp.gleam", 859). +?DOC( + " Create a timestamp from a number of seconds since 00:00:00 UTC on 1 January\n" + " 1970.\n" +). +-spec from_unix_seconds(integer()) -> timestamp(). +from_unix_seconds(Seconds) -> + {timestamp, Seconds, 0}. + +-file("src/gleam/time/timestamp.gleam", 874). +?DOC( + " Create a timestamp from a number of seconds and nanoseconds since 00:00:00\n" + " UTC on 1 January 1970.\n" + "\n" + " # JavaScript int limitations\n" + "\n" + " Remember that JavaScript can only perfectly represent ints between positive\n" + " and negative 9,007,199,254,740,991! If you only use the nanosecond field\n" + " then you will almost certainly not get the date value you want due to this\n" + " loss of precision. Always use seconds primarily and then use nanoseconds\n" + " for the final sub-second adjustment.\n" +). +-spec from_unix_seconds_and_nanoseconds(integer(), integer()) -> timestamp(). +from_unix_seconds_and_nanoseconds(Seconds, Nanoseconds) -> + _pipe = {timestamp, Seconds, Nanoseconds}, + normalise(_pipe). + +-file("src/gleam/time/timestamp.gleam", 888). +?DOC( + " Convert the timestamp to a number of seconds since 00:00:00 UTC on 1\n" + " January 1970.\n" + "\n" + " There may be some small loss of precision due to `Timestamp` being\n" + " nanosecond accurate and `Float` not being able to represent this.\n" +). +-spec to_unix_seconds(timestamp()) -> float(). +to_unix_seconds(Timestamp) -> + Seconds = erlang:float(erlang:element(2, Timestamp)), + Nanoseconds = erlang:float(erlang:element(3, Timestamp)), + Seconds + (Nanoseconds / 1000000000.0). + +-file("src/gleam/time/timestamp.gleam", 897). +?DOC( + " Convert the timestamp to a number of seconds and nanoseconds since 00:00:00\n" + " UTC on 1 January 1970. There is no loss of precision with this conversion\n" + " on any target.\n" +). +-spec to_unix_seconds_and_nanoseconds(timestamp()) -> {integer(), integer()}. +to_unix_seconds_and_nanoseconds(Timestamp) -> + {erlang:element(2, Timestamp), erlang:element(3, Timestamp)}. + +-file("src/gleam/time/timestamp.gleam", 723). +-spec offset_to_seconds(binary(), integer(), integer()) -> integer(). +offset_to_seconds(Sign, Hours, Minutes) -> + Abs_seconds = (Hours * 3600) + (Minutes * 60), + case Sign of + <<"-"/utf8>> -> + - Abs_seconds; + + _ -> + Abs_seconds + end. + +-file("src/gleam/time/timestamp.gleam", 819). +?DOC( + " `julian_seconds_from_parts(year, month, day, hours, minutes, seconds)` \n" + " returns the number of Julian \n" + " seconds represented by the given arguments.\n" + " \n" + " Note: It is the callers responsibility to ensure the inputs are valid.\n" + " \n" + " See https://www.tondering.dk/claus/cal/julperiod.php#formula\n" +). +-spec julian_seconds_from_parts( + integer(), + integer(), + integer(), + integer(), + integer(), + integer() +) -> integer(). +julian_seconds_from_parts(Year, Month, Day, Hours, Minutes, Seconds) -> + Julian_day_seconds = julian_day_from_ymd(Year, Month, Day) * 86400, + ((Julian_day_seconds + (Hours * 3600)) + (Minutes * 60)) + Seconds. + +-file("src/gleam/time/timestamp.gleam", 662). +-spec do_parse_second_fraction_as_nanoseconds(bitstring(), integer(), integer()) -> {ok, + {integer(), bitstring()}} | + {error, any()}. +do_parse_second_fraction_as_nanoseconds(Bytes, Acc, Power) -> + Power@1 = Power div 10, + case Bytes of + <> when ((16#30 =< Byte) andalso (Byte =< 16#39)) andalso (Power@1 < 1) -> + do_parse_second_fraction_as_nanoseconds( + Remaining_bytes, + Acc, + Power@1 + ); + + <> when (16#30 =< Byte@1) andalso (Byte@1 =< 16#39) -> + Digit = Byte@1 - 16#30, + do_parse_second_fraction_as_nanoseconds( + Remaining_bytes@1, + Acc + (Digit * Power@1), + Power@1 + ); + + _ -> + {ok, {Acc, Bytes}} + end. + +-file("src/gleam/time/timestamp.gleam", 642). +-spec parse_second_fraction_as_nanoseconds(bitstring()) -> {ok, + {integer(), bitstring()}} | + {error, nil}. +parse_second_fraction_as_nanoseconds(Bytes) -> + case Bytes of + <<"."/utf8, Byte, Remaining_bytes/binary>> when (16#30 =< Byte) andalso (Byte =< 16#39) -> + do_parse_second_fraction_as_nanoseconds( + <>, + 0, + 1000000000 + ); + + <<"."/utf8, _/binary>> -> + {error, nil}; + + _ -> + {ok, {0, Bytes}} + end. + +-file("src/gleam/time/timestamp.gleam", 741). +-spec do_parse_digits(bitstring(), integer(), integer(), integer()) -> {ok, + {integer(), bitstring()}} | + {error, nil}. +do_parse_digits(Bytes, Count, Acc, K) -> + case Bytes of + _ when K >= Count -> + {ok, {Acc, Bytes}}; + + <> when (16#30 =< Byte) andalso (Byte =< 16#39) -> + do_parse_digits( + Remaining_bytes, + Count, + (Acc * 10) + (Byte - 16#30), + K + 1 + ); + + _ -> + {error, nil} + end. + +-file("src/gleam/time/timestamp.gleam", 734). +?DOC(" Parse and return the given number of digits from the given bytes.\n"). +-spec parse_digits(bitstring(), integer()) -> {ok, {integer(), bitstring()}} | + {error, nil}. +parse_digits(Bytes, Count) -> + do_parse_digits(Bytes, Count, 0, 0). + +-file("src/gleam/time/timestamp.gleam", 573). +-spec parse_year(bitstring()) -> {ok, {integer(), bitstring()}} | {error, nil}. +parse_year(Bytes) -> + parse_digits(Bytes, 4). + +-file("src/gleam/time/timestamp.gleam", 577). +-spec parse_month(bitstring()) -> {ok, {integer(), bitstring()}} | {error, nil}. +parse_month(Bytes) -> + gleam@result:'try'( + parse_digits(Bytes, 2), + fun(_use0) -> + {Month, Bytes@1} = _use0, + case (1 =< Month) andalso (Month =< 12) of + true -> + {ok, {Month, Bytes@1}}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/time/timestamp.gleam", 585). +-spec parse_day(bitstring(), integer(), integer()) -> {ok, + {integer(), bitstring()}} | + {error, nil}. +parse_day(Bytes, Year, Month) -> + gleam@result:'try'( + parse_digits(Bytes, 2), + fun(_use0) -> + {Day, Bytes@1} = _use0, + gleam@result:'try'(case Month of + 1 -> + {ok, 31}; + + 3 -> + {ok, 31}; + + 5 -> + {ok, 31}; + + 7 -> + {ok, 31}; + + 8 -> + {ok, 31}; + + 10 -> + {ok, 31}; + + 12 -> + {ok, 31}; + + 4 -> + {ok, 30}; + + 6 -> + {ok, 30}; + + 9 -> + {ok, 30}; + + 11 -> + {ok, 30}; + + 2 -> + case is_leap_year(Year) of + true -> + {ok, 29}; + + false -> + {ok, 28} + end; + + _ -> + {error, nil} + end, fun(Max_day) -> case (1 =< Day) andalso (Day =< Max_day) of + true -> + {ok, {Day, Bytes@1}}; + + false -> + {error, nil} + end end) + end + ). + +-file("src/gleam/time/timestamp.gleam", 615). +-spec parse_hours(bitstring()) -> {ok, {integer(), bitstring()}} | {error, nil}. +parse_hours(Bytes) -> + gleam@result:'try'( + parse_digits(Bytes, 2), + fun(_use0) -> + {Hours, Bytes@1} = _use0, + case (0 =< Hours) andalso (Hours =< 23) of + true -> + {ok, {Hours, Bytes@1}}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/time/timestamp.gleam", 623). +-spec parse_minutes(bitstring()) -> {ok, {integer(), bitstring()}} | + {error, nil}. +parse_minutes(Bytes) -> + gleam@result:'try'( + parse_digits(Bytes, 2), + fun(_use0) -> + {Minutes, Bytes@1} = _use0, + case (0 =< Minutes) andalso (Minutes =< 59) of + true -> + {ok, {Minutes, Bytes@1}}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/time/timestamp.gleam", 631). +-spec parse_seconds(bitstring()) -> {ok, {integer(), bitstring()}} | + {error, nil}. +parse_seconds(Bytes) -> + gleam@result:'try'( + parse_digits(Bytes, 2), + fun(_use0) -> + {Seconds, Bytes@1} = _use0, + case (0 =< Seconds) andalso (Seconds =< 60) of + true -> + {ok, {Seconds, Bytes@1}}; + + false -> + {error, nil} + end + end + ). + +-file("src/gleam/time/timestamp.gleam", 704). +-spec parse_numeric_offset(bitstring()) -> {ok, {integer(), bitstring()}} | + {error, nil}. +parse_numeric_offset(Bytes) -> + gleam@result:'try'( + parse_sign(Bytes), + fun(_use0) -> + {Sign, Bytes@1} = _use0, + gleam@result:'try'( + parse_hours(Bytes@1), + fun(_use0@1) -> + {Hours, Bytes@2} = _use0@1, + gleam@result:'try'( + accept_byte(Bytes@2, 16#3A), + fun(Bytes@3) -> + gleam@result:'try'( + parse_minutes(Bytes@3), + fun(_use0@2) -> + {Minutes, Bytes@4} = _use0@2, + Offset_seconds = offset_to_seconds( + Sign, + Hours, + Minutes + ), + {ok, {Offset_seconds, Bytes@4}} + end + ) + end + ) + end + ) + end + ). + +-file("src/gleam/time/timestamp.gleam", 696). +-spec parse_offset(bitstring()) -> {ok, {integer(), bitstring()}} | {error, nil}. +parse_offset(Bytes) -> + case Bytes of + <<"Z"/utf8, Remaining_bytes/binary>> -> + {ok, {0, Remaining_bytes}}; + + <<"z"/utf8, Remaining_bytes/binary>> -> + {ok, {0, Remaining_bytes}}; + + _ -> + parse_numeric_offset(Bytes) + end. + +-file("src/gleam/time/timestamp.gleam", 769). +-spec accept_date_time_separator(bitstring()) -> {ok, bitstring()} | + {error, nil}. +accept_date_time_separator(Bytes) -> + case Bytes of + <> when ((Byte =:= 16#54) orelse (Byte =:= 16#74)) orelse (Byte =:= 16#20) -> + {ok, Remaining_bytes}; + + _ -> + {error, nil} + end. + +-file("src/gleam/time/timestamp.gleam", 789). +?DOC(" Note: The caller of this function must ensure that all inputs are valid.\n"). +-spec from_date_time( + integer(), + integer(), + integer(), + integer(), + integer(), + integer(), + integer(), + integer() +) -> timestamp(). +from_date_time( + Year, + Month, + Day, + Hours, + Minutes, + Seconds, + Second_fraction_as_nanoseconds, + Offset_seconds +) -> + Julian_seconds = julian_seconds_from_parts( + Year, + Month, + Day, + Hours, + Minutes, + Seconds + ), + Julian_seconds_since_epoch = Julian_seconds - 210866803200, + _pipe = {timestamp, + Julian_seconds_since_epoch - Offset_seconds, + Second_fraction_as_nanoseconds}, + normalise(_pipe). + +-file("src/gleam/time/timestamp.gleam", 339). +?DOC( + " Create a `Timestamp` from a human-readable calendar time.\n" + "\n" + " # Examples\n" + "\n" + " ```gleam\n" + " timestamp.from_calendar(\n" + " date: calendar.Date(2024, calendar.December, 25),\n" + " time: calendar.TimeOfDay(12, 30, 50, 0),\n" + " offset: calendar.utc_offset,\n" + " )\n" + " |> timestamp.to_rfc3339(calendar.utc_offset)\n" + " // -> \"2024-12-25T12:30:50Z\"\n" + " ```\n" +). +-spec from_calendar( + gleam@time@calendar:date(), + gleam@time@calendar:time_of_day(), + gleam@time@duration:duration() +) -> timestamp(). +from_calendar(Date, Time, Offset) -> + Month = case erlang:element(3, Date) of + january -> + 1; + + february -> + 2; + + march -> + 3; + + april -> + 4; + + may -> + 5; + + june -> + 6; + + july -> + 7; + + august -> + 8; + + september -> + 9; + + october -> + 10; + + november -> + 11; + + december -> + 12 + end, + from_date_time( + erlang:element(2, Date), + Month, + erlang:element(4, Date), + erlang:element(2, Time), + erlang:element(3, Time), + erlang:element(4, Time), + erlang:element(5, Time), + erlang:round(gleam@time@duration:to_seconds(Offset)) + ). + +-file("src/gleam/time/timestamp.gleam", 533). +?DOC( + " Parses an [RFC 3339 formatted time string][spec] into a `Timestamp`.\n" + "\n" + " [spec]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6\n" + " \n" + " # Examples\n" + "\n" + " ```gleam\n" + " let assert Ok(ts) = timestamp.parse_rfc3339(\"1970-01-01T00:00:01Z\")\n" + " timestamp.to_unix_seconds_and_nanoseconds(ts)\n" + " // -> #(1, 0)\n" + " ```\n" + " \n" + " Parsing an invalid timestamp returns an error.\n" + " \n" + " ```gleam\n" + " let assert Error(Nil) = timestamp.parse_rfc3339(\"1995-10-31\")\n" + " ```\n" + "\n" + " ## Time zones\n" + "\n" + " It may at first seem that the RFC 3339 format includes timezone\n" + " information, as it can specify an offset such as `Z` or `+3`, so why does\n" + " this function not return calendar time with a time zone? There are multiple\n" + " reasons:\n" + "\n" + " - RFC 3339's timestamp format is based on calendar time, but it is\n" + " unambigous, so it can be converted into epoch time when being parsed. It\n" + " is always better to internally use epoch time to represent unambiguous\n" + " points in time, so we perform that conversion as a convenience and to\n" + " ensure that programmers with less time experience don't accidentally use\n" + " a less suitable time representation.\n" + "\n" + " - RFC 3339's contains _calendar time offset_ information, not time zone\n" + " information. This is enough to convert it to an unambiguous timestamp,\n" + " but it is not enough information to reliably work with calendar time.\n" + " Without the time zone and the time zone database it's not possible to\n" + " know what time period that offset is valid for, so it cannot be used\n" + " without risk of bugs.\n" + "\n" + " ## Behaviour details\n" + " \n" + " - Follows the grammar specified in section 5.6 Internet Date/Time Format of \n" + " RFC 3339 .\n" + " - The `T` and `Z` characters may alternatively be lower case `t` or `z`, \n" + " respectively.\n" + " - Full dates and full times must be separated by `T` or `t`. A space is also \n" + " permitted.\n" + " - Leap seconds rules are not considered. That is, any timestamp may \n" + " specify digts `00` - `60` for the seconds.\n" + " - Any part of a fractional second that cannot be represented in the \n" + " nanosecond precision is tructated. That is, for the time string, \n" + " `\"1970-01-01T00:00:00.1234567899Z\"`, the fractional second `.1234567899` \n" + " will be represented as `123_456_789` in the `Timestamp`.\n" +). +-spec parse_rfc3339(binary()) -> {ok, timestamp()} | {error, nil}. +parse_rfc3339(Input) -> + Bytes = gleam_stdlib:identity(Input), + gleam@result:'try'( + parse_year(Bytes), + fun(_use0) -> + {Year, Bytes@1} = _use0, + gleam@result:'try'( + accept_byte(Bytes@1, 16#2D), + fun(Bytes@2) -> + gleam@result:'try'( + parse_month(Bytes@2), + fun(_use0@1) -> + {Month, Bytes@3} = _use0@1, + gleam@result:'try'( + accept_byte(Bytes@3, 16#2D), + fun(Bytes@4) -> + gleam@result:'try'( + parse_day(Bytes@4, Year, Month), + fun(_use0@2) -> + {Day, Bytes@5} = _use0@2, + gleam@result:'try'( + accept_date_time_separator( + Bytes@5 + ), + fun(Bytes@6) -> + gleam@result:'try'( + parse_hours(Bytes@6), + fun(_use0@3) -> + {Hours, Bytes@7} = _use0@3, + gleam@result:'try'( + accept_byte( + Bytes@7, + 16#3A + ), + fun(Bytes@8) -> + gleam@result:'try'( + parse_minutes( + Bytes@8 + ), + fun( + _use0@4 + ) -> + {Minutes, + Bytes@9} = _use0@4, + gleam@result:'try'( + accept_byte( + Bytes@9, + 16#3A + ), + fun( + Bytes@10 + ) -> + gleam@result:'try'( + parse_seconds( + Bytes@10 + ), + fun( + _use0@5 + ) -> + {Seconds, + Bytes@11} = _use0@5, + gleam@result:'try'( + parse_second_fraction_as_nanoseconds( + Bytes@11 + ), + fun( + _use0@6 + ) -> + {Second_fraction_as_nanoseconds, + Bytes@12} = _use0@6, + gleam@result:'try'( + parse_offset( + Bytes@12 + ), + fun( + _use0@7 + ) -> + {Offset_seconds, + Bytes@13} = _use0@7, + gleam@result:'try'( + accept_empty( + Bytes@13 + ), + fun( + _use0@8 + ) -> + nil = _use0@8, + {ok, + from_date_time( + Year, + Month, + Day, + Hours, + Minutes, + Seconds, + Second_fraction_as_nanoseconds, + Offset_seconds + )} + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ) + end + ). diff --git a/build/packages/gleam_time/src/gleam_time.app.src b/build/packages/gleam_time/src/gleam_time.app.src new file mode 100644 index 0000000..1e7cce4 --- /dev/null +++ b/build/packages/gleam_time/src/gleam_time.app.src @@ -0,0 +1,12 @@ +{application, gleam_time, [ + {vsn, "1.6.0"}, + {applications, [gleam_stdlib]}, + {description, "Work with time in Gleam!"}, + {modules, [gleam@time@calendar, + gleam@time@duration, + gleam@time@timestamp, + gleam_time@@main, + gleam_time_ffi, + gleam_time_test_ffi]}, + {registered, []} +]}. diff --git a/build/packages/gleam_time/src/gleam_time_ffi.erl b/build/packages/gleam_time/src/gleam_time_ffi.erl new file mode 100644 index 0000000..34d8c88 --- /dev/null +++ b/build/packages/gleam_time/src/gleam_time_ffi.erl @@ -0,0 +1,12 @@ +-module(gleam_time_ffi). +-export([system_time/0, local_time_offset_seconds/0]). + +system_time() -> + {0, erlang:system_time(nanosecond)}. + +local_time_offset_seconds() -> + Utc = calendar:universal_time(), + Local = calendar:local_time(), + UtcSeconds = calendar:datetime_to_gregorian_seconds(Utc), + LocalSeconds = calendar:datetime_to_gregorian_seconds(Local), + LocalSeconds - UtcSeconds. diff --git a/build/packages/gleam_time/src/gleam_time_ffi.mjs b/build/packages/gleam_time/src/gleam_time_ffi.mjs new file mode 100644 index 0000000..27d09aa --- /dev/null +++ b/build/packages/gleam_time/src/gleam_time_ffi.mjs @@ -0,0 +1,11 @@ +export function system_time() { + const now = Date.now(); + const milliseconds = now % 1_000; + const nanoseconds = milliseconds * 1000_000; + const seconds = (now - milliseconds) / 1_000; + return [seconds, nanoseconds]; +} + +export function local_time_offset_seconds() { + return new Date().getTimezoneOffset() * -60; +} diff --git a/build/packages/gleeunit/LICENCE b/build/packages/gleeunit/LICENCE new file mode 100644 index 0000000..c7967c3 --- /dev/null +++ b/build/packages/gleeunit/LICENCE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2021, Louis Pilfold . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/build/packages/gleeunit/README.md b/build/packages/gleeunit/README.md new file mode 100644 index 0000000..4d81364 --- /dev/null +++ b/build/packages/gleeunit/README.md @@ -0,0 +1,44 @@ +# gleeunit + +A simple test runner for Gleam, using EUnit on Erlang and a custom runner on JS. + +[![Package Version](https://img.shields.io/hexpm/v/gleeunit)](https://hex.pm/packages/gleeunit) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gleeunit/) + + +```sh +gleam add gleeunit@1 --dev +``` +```gleam +// In test/yourapp_test.gleam +import gleeunit + +pub fn main() { + gleeunit.main() +} +``` + +Now any public function with a name ending in `_test` in the `test` directory +will be found and run as a test. + +```gleam +pub fn some_function_test() { + assert some_function() == "Hello!" +} +``` + +Run the tests by entering `gleam test` in the command line. + +### Deno + +If using the Deno JavaScript runtime, you will need to add the following to your +`gleam.toml`. + +```toml +[javascript.deno] +allow_read = [ + "gleam.toml", + "test", + "build", +] +``` diff --git a/build/packages/gleeunit/gleam.toml b/build/packages/gleeunit/gleam.toml new file mode 100644 index 0000000..c7df0c5 --- /dev/null +++ b/build/packages/gleeunit/gleam.toml @@ -0,0 +1,16 @@ +name = "gleeunit" +version = "1.9.0" +licences = ["Apache-2.0"] +description = "A simple test runner for Gleam, using EUnit on Erlang" +repository = { type = "github", user = "lpil", repo = "gleeunit" } +links = [{ title = "Sponsor", href = "https://github.com/sponsors/lpil" }] +gleam = ">= 1.13.0" + +[javascript.deno] +allow_read = ["gleam.toml", "test", "build"] + +[dependencies] +gleam_stdlib = ">= 0.60.0 and < 1.0.0" + +[dev-dependencies] +testhelper = { "path" = "./testhelper" } diff --git a/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_Assert.hrl b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_Assert.hrl new file mode 100644 index 0000000..9360941 --- /dev/null +++ b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_Assert.hrl @@ -0,0 +1,6 @@ +-record(assert, { + start :: integer(), + 'end' :: integer(), + expression_start :: integer(), + kind :: gleeunit@internal@gleam_panic:assert_kind() +}). diff --git a/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_AssertedExpression.hrl b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_AssertedExpression.hrl new file mode 100644 index 0000000..812663c --- /dev/null +++ b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_AssertedExpression.hrl @@ -0,0 +1,5 @@ +-record(asserted_expression, { + start :: integer(), + 'end' :: integer(), + kind :: gleeunit@internal@gleam_panic:expression_kind() +}). diff --git a/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_BinaryOperator.hrl b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_BinaryOperator.hrl new file mode 100644 index 0000000..eee44c9 --- /dev/null +++ b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_BinaryOperator.hrl @@ -0,0 +1,5 @@ +-record(binary_operator, { + operator :: binary(), + left :: gleeunit@internal@gleam_panic:asserted_expression(), + right :: gleeunit@internal@gleam_panic:asserted_expression() +}). diff --git a/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_Expression.hrl b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_Expression.hrl new file mode 100644 index 0000000..e7ffaa0 --- /dev/null +++ b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_Expression.hrl @@ -0,0 +1 @@ +-record(expression, {value :: gleam@dynamic:dynamic_()}). diff --git a/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_FunctionCall.hrl b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_FunctionCall.hrl new file mode 100644 index 0000000..9d55488 --- /dev/null +++ b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_FunctionCall.hrl @@ -0,0 +1,3 @@ +-record(function_call, { + arguments :: list(gleeunit@internal@gleam_panic:asserted_expression()) +}). diff --git a/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_GleamPanic.hrl b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_GleamPanic.hrl new file mode 100644 index 0000000..cf36764 --- /dev/null +++ b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_GleamPanic.hrl @@ -0,0 +1,8 @@ +-record(gleam_panic, { + message :: binary(), + file :: binary(), + module :: binary(), + function :: binary(), + line :: integer(), + kind :: gleeunit@internal@gleam_panic:panic_kind() +}). diff --git a/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_LetAssert.hrl b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_LetAssert.hrl new file mode 100644 index 0000000..11f865e --- /dev/null +++ b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_LetAssert.hrl @@ -0,0 +1,7 @@ +-record(let_assert, { + start :: integer(), + 'end' :: integer(), + pattern_start :: integer(), + pattern_end :: integer(), + value :: gleam@dynamic:dynamic_() +}). diff --git a/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_Literal.hrl b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_Literal.hrl new file mode 100644 index 0000000..2396489 --- /dev/null +++ b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_Literal.hrl @@ -0,0 +1 @@ +-record(literal, {value :: gleam@dynamic:dynamic_()}). diff --git a/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_OtherExpression.hrl b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_OtherExpression.hrl new file mode 100644 index 0000000..0424990 --- /dev/null +++ b/build/packages/gleeunit/include/gleeunit@internal@gleam_panic_OtherExpression.hrl @@ -0,0 +1,3 @@ +-record(other_expression, { + expression :: gleeunit@internal@gleam_panic:asserted_expression() +}). diff --git a/build/packages/gleeunit/include/gleeunit@internal@reporting_State.hrl b/build/packages/gleeunit/include/gleeunit@internal@reporting_State.hrl new file mode 100644 index 0000000..575ccce --- /dev/null +++ b/build/packages/gleeunit/include/gleeunit@internal@reporting_State.hrl @@ -0,0 +1 @@ +-record(state, {passed :: integer(), failed :: integer(), skipped :: integer()}). diff --git a/build/packages/gleeunit/src/gleeunit.app.src b/build/packages/gleeunit/src/gleeunit.app.src new file mode 100644 index 0000000..e72987c --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit.app.src @@ -0,0 +1,16 @@ +{application, gleeunit, [ + {vsn, "1.9.0"}, + {applications, [gleam_stdlib]}, + {description, "A simple test runner for Gleam, using EUnit on Erlang"}, + {modules, [erlang_test_module, + gleeunit, + gleeunit@@main, + gleeunit@internal@gleam_panic, + gleeunit@internal@reporting, + gleeunit@should, + gleeunit_ffi, + gleeunit_gleam_panic_ffi, + gleeunit_progress, + gleeunit_test_ffi]}, + {registered, []} +]}. diff --git a/build/packages/gleeunit/src/gleeunit.erl b/build/packages/gleeunit/src/gleeunit.erl new file mode 100644 index 0000000..bcb58b6 --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit.erl @@ -0,0 +1,89 @@ +-module(gleeunit). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleeunit.gleam"). +-export([main/0]). +-export_type([atom_/0, encoding/0, report_module_name/0, gleeunit_progress_option/0, eunit_option/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +-type atom_() :: any(). + +-type encoding() :: utf8. + +-type report_module_name() :: gleeunit_progress. + +-type gleeunit_progress_option() :: {colored, boolean()}. + +-type eunit_option() :: verbose | + no_tty | + {report, {report_module_name(), list(gleeunit_progress_option())}} | + {scale_timeouts, integer()}. + +-file("src/gleeunit.gleam", 42). +-spec gleam_to_erlang_module_name(binary()) -> binary(). +gleam_to_erlang_module_name(Path) -> + case gleam_stdlib:string_ends_with(Path, <<".gleam"/utf8>>) of + true -> + _pipe = Path, + _pipe@1 = gleam@string:replace( + _pipe, + <<".gleam"/utf8>>, + <<""/utf8>> + ), + gleam@string:replace(_pipe@1, <<"/"/utf8>>, <<"@"/utf8>>); + + false -> + _pipe@2 = Path, + _pipe@3 = gleam@string:split(_pipe@2, <<"/"/utf8>>), + _pipe@4 = gleam@list:last(_pipe@3), + _pipe@5 = gleam@result:unwrap(_pipe@4, Path), + gleam@string:replace(_pipe@5, <<".erl"/utf8>>, <<""/utf8>>) + end. + +-file("src/gleeunit.gleam", 18). +-spec do_main() -> nil. +do_main() -> + Options = [verbose, + no_tty, + {report, {gleeunit_progress, [{colored, true}]}}, + {scale_timeouts, 10}], + Result = begin + _pipe = gleeunit_ffi:find_files( + <<"**/*.{erl,gleam}"/utf8>>, + <<"test"/utf8>> + ), + _pipe@1 = gleam@list:map(_pipe, fun gleam_to_erlang_module_name/1), + _pipe@2 = gleam@list:map( + _pipe@1, + fun(_capture) -> erlang:binary_to_atom(_capture, utf8) end + ), + gleeunit_ffi:run_eunit(_pipe@2, Options) + end, + Code = case Result of + {ok, _} -> + 0; + + {error, _} -> + 1 + end, + erlang:halt(Code). + +-file("src/gleeunit.gleam", 13). +?DOC( + " Find and run all test functions for the current project using Erlang's EUnit\n" + " test framework, or a custom JavaScript test runner.\n" + "\n" + " Any Erlang or Gleam function in the `test` directory with a name ending in\n" + " `_test` is considered a test function and will be run.\n" + "\n" + " A test that panics is considered a failure.\n" +). +-spec main() -> nil. +main() -> + do_main(). diff --git a/build/packages/gleeunit/src/gleeunit.gleam b/build/packages/gleeunit/src/gleeunit.gleam new file mode 100644 index 0000000..7d3155e --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit.gleam @@ -0,0 +1,86 @@ +import gleam/list +import gleam/result +import gleam/string + +/// Find and run all test functions for the current project using Erlang's EUnit +/// test framework, or a custom JavaScript test runner. +/// +/// Any Erlang or Gleam function in the `test` directory with a name ending in +/// `_test` is considered a test function and will be run. +/// +/// A test that panics is considered a failure. +/// +pub fn main() -> Nil { + do_main() +} + +@external(javascript, "./gleeunit_ffi.mjs", "main") +fn do_main() -> Nil { + let options = [ + Verbose, + NoTty, + Report(#(GleeunitProgress, [Colored(True)])), + ScaleTimeouts(10), + ] + + let result = + find_files(matching: "**/*.{erl,gleam}", in: "test") + |> list.map(gleam_to_erlang_module_name) + |> list.map(dangerously_convert_string_to_atom(_, Utf8)) + |> run_eunit(options) + + let code = case result { + Ok(_) -> 0 + Error(_) -> 1 + } + halt(code) +} + +@external(erlang, "erlang", "halt") +fn halt(a: Int) -> Nil + +fn gleam_to_erlang_module_name(path: String) -> String { + case string.ends_with(path, ".gleam") { + True -> + path + |> string.replace(".gleam", "") + |> string.replace("/", "@") + + False -> + path + |> string.split("/") + |> list.last + |> result.unwrap(path) + |> string.replace(".erl", "") + } +} + +@external(erlang, "gleeunit_ffi", "find_files") +fn find_files(matching matching: String, in in: String) -> List(String) + +type Atom + +type Encoding { + Utf8 +} + +@external(erlang, "erlang", "binary_to_atom") +fn dangerously_convert_string_to_atom(a: String, b: Encoding) -> Atom + +type ReportModuleName { + GleeunitProgress +} + +type GleeunitProgressOption { + Colored(Bool) +} + +type EunitOption { + Verbose + NoTty + Report(#(ReportModuleName, List(GleeunitProgressOption))) + ScaleTimeouts(Int) +} + +@external(erlang, "gleeunit_ffi", "run_eunit") +fn run_eunit(a: List(Atom), b: List(EunitOption)) -> Result(Nil, a) diff --git a/build/packages/gleeunit/src/gleeunit/internal/gleam_panic.gleam b/build/packages/gleeunit/src/gleeunit/internal/gleam_panic.gleam new file mode 100644 index 0000000..6a5d309 --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit/internal/gleam_panic.gleam @@ -0,0 +1,49 @@ +import gleam/dynamic + +pub type GleamPanic { + GleamPanic( + message: String, + file: String, + module: String, + function: String, + line: Int, + kind: PanicKind, + ) +} + +pub type PanicKind { + Todo + Panic + LetAssert( + start: Int, + end: Int, + pattern_start: Int, + pattern_end: Int, + value: dynamic.Dynamic, + ) + Assert(start: Int, end: Int, expression_start: Int, kind: AssertKind) +} + +pub type AssertKind { + BinaryOperator( + operator: String, + left: AssertedExpression, + right: AssertedExpression, + ) + FunctionCall(arguments: List(AssertedExpression)) + OtherExpression(expression: AssertedExpression) +} + +pub type AssertedExpression { + AssertedExpression(start: Int, end: Int, kind: ExpressionKind) +} + +pub type ExpressionKind { + Literal(value: dynamic.Dynamic) + Expression(value: dynamic.Dynamic) + Unevaluated +} + +@external(erlang, "gleeunit_gleam_panic_ffi", "from_dynamic") +@external(javascript, "./gleeunit_gleam_panic_ffi.mjs", "from_dynamic") +pub fn from_dynamic(data: dynamic.Dynamic) -> Result(GleamPanic, Nil) diff --git a/build/packages/gleeunit/src/gleeunit/internal/gleeunit_gleam_panic_ffi.erl b/build/packages/gleeunit/src/gleeunit/internal/gleeunit_gleam_panic_ffi.erl new file mode 100644 index 0000000..d78f5e5 --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit/internal/gleeunit_gleam_panic_ffi.erl @@ -0,0 +1,49 @@ +-module(gleeunit_gleam_panic_ffi). +-export([from_dynamic/1]). + +from_dynamic(#{ + gleam_error := assert, + start := Start, + 'end' := End, + expression_start := EStart +} = E) -> + wrap(E, {assert, Start, End, EStart, assert_kind(E)}); +from_dynamic(#{ + gleam_error := let_assert, + start := Start, + 'end' := End, + pattern_start := PStart, + pattern_end := PEnd, + value := Value +} = E) -> + wrap(E, {let_assert, Start, End, PStart, PEnd, Value}); +from_dynamic(#{gleam_error := panic} = E) -> + wrap(E, panic); +from_dynamic(#{gleam_error := todo} = E) -> + wrap(E, todo); +from_dynamic(_) -> + {error, nil}. + +assert_kind(#{kind := binary_operator, left := L, right := R, operator := O}) -> + {binary_operator, atom_to_binary(O), expression(L), expression(R)}; +assert_kind(#{kind := function_call, arguments := Arguments}) -> + {function_call, lists:map(fun expression/1, Arguments)}; +assert_kind(#{kind := expression, expression := Expression}) -> + {other_expression, expression(Expression)}. + +expression(#{start := S, 'end' := E, kind := literal, value := Value}) -> + {asserted_expression, S, E, {literal, Value}}; +expression(#{start := S, 'end' := E, kind := expression, value := Value}) -> + {asserted_expression, S, E, {expression, Value}}; +expression(#{start := S, 'end' := E, kind := unevaluated}) -> + {asserted_expression, S, E, unevaluated}. + +wrap(#{ + gleam_error := _, + file := File, + message := Message, + module := Module, + function := Function, + line := Line +}, Kind) -> + {ok, {gleam_panic, Message, File, Module, Function, Line, Kind}}. diff --git a/build/packages/gleeunit/src/gleeunit/internal/gleeunit_gleam_panic_ffi.mjs b/build/packages/gleeunit/src/gleeunit/internal/gleeunit_gleam_panic_ffi.mjs new file mode 100644 index 0000000..03f6025 --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit/internal/gleeunit_gleam_panic_ffi.mjs @@ -0,0 +1,91 @@ +import { Result$Ok, Result$Error, List$Empty, List$NonEmpty } from "../../gleam.mjs"; +import { + GleamPanic$GleamPanic, + PanicKind$Todo, + PanicKind$Panic, + PanicKind$LetAssert, + PanicKind$Assert, + AssertKind$BinaryOperator, + AssertKind$FunctionCall, + AssertKind$OtherExpression, + AssertedExpression$AssertedExpression, + ExpressionKind$Literal, + ExpressionKind$Expression, + ExpressionKind$Unevaluated, +} from "./gleam_panic.mjs"; + +export function from_dynamic(error) { + if (!(error instanceof globalThis.Error) || !error.gleam_error) { + return Result$Error(undefined); + } + + if (error.gleam_error === "todo") { + return wrap(error, PanicKind$Todo()); + } + + if (error.gleam_error === "panic") { + return wrap(error, PanicKind$Panic()); + } + + if (error.gleam_error === "let_assert") { + let kind = PanicKind$LetAssert( + error.start, + error.end, + error.pattern_start, + error.pattern_end, + error.value, + ); + return wrap(error, kind); + } + + if (error.gleam_error === "assert") { + let kind = PanicKind$Assert( + error.start, + error.end, + error.expression_start, + assert_kind(error), + ); + return wrap(error, kind); + } + + return Result$Error(undefined); +} + +function assert_kind(error) { + if (error.kind == "binary_operator") { + return AssertKind$BinaryOperator( + error.operator, + expression(error.left), + expression(error.right), + ); + } + + if (error.kind == "function_call") { + let list = List$Empty(); + let i = error.arguments.length; + while (i--) { + list = List$NonEmpty(expression(error.arguments[i]), list); + } + return AssertKind$FunctionCall(list); + } + + return AssertKind$OtherExpression(expression(error.expression)); +} + +function expression(data) { + const expression = AssertedExpression$AssertedExpression(data.start, data.end, undefined); + if (data.kind == "literal") { + expression.kind = ExpressionKind$Literal(data.value); + } else if (data.kind == "expression") { + expression.kind = ExpressionKind$Expression(data.value); + } else { + expression.kind = ExpressionKind$Unevaluated(); + } + return expression; +} + +function wrap(e, kind) { + return Result$Ok( + GleamPanic$GleamPanic(e.message, e.file, e.module, e.function, e.line, kind), + ); +} diff --git a/build/packages/gleeunit/src/gleeunit/internal/reporting.gleam b/build/packages/gleeunit/src/gleeunit/internal/reporting.gleam new file mode 100644 index 0000000..72f766f --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit/internal/reporting.gleam @@ -0,0 +1,240 @@ +import gleam/bit_array +import gleam/dynamic +import gleam/int +import gleam/io +import gleam/list +import gleam/option.{type Option} +import gleam/result +import gleam/string +import gleeunit/internal/gleam_panic.{type GleamPanic} + +pub type State { + State(passed: Int, failed: Int, skipped: Int) +} + +pub fn new_state() -> State { + State(passed: 0, failed: 0, skipped: 0) +} + +pub fn finished(state: State) -> Int { + case state { + State(passed: 0, failed: 0, skipped: 0) -> { + io.println("\nNo tests found!") + 1 + } + State(failed: 0, skipped: 0, ..) -> { + let message = + "\n" <> int.to_string(state.passed) <> " passed, no failures" + io.println(green(message)) + 0 + } + State(skipped: 0, ..) -> { + let message = + "\n" + <> int.to_string(state.passed) + <> " passed, " + <> int.to_string(state.failed) + <> " failures" + io.println(red(message)) + 1 + } + State(failed: 0, ..) -> { + let message = + "\n" + <> int.to_string(state.passed) + <> " passed, 0 failures, " + <> int.to_string(state.skipped) + <> " skipped" + io.println(yellow(message)) + 1 + } + State(..) -> { + let message = + "\n" + <> int.to_string(state.passed) + <> " passed, " + <> int.to_string(state.failed) + <> " failures, " + <> int.to_string(state.skipped) + <> " skipped" + io.println(red(message)) + 1 + } + } +} + +pub fn test_passed(state: State) -> State { + io.print(green(".")) + State(..state, passed: state.passed + 1) +} + +pub fn test_failed( + state: State, + module: String, + function: String, + error: dynamic.Dynamic, +) -> State { + let message = case gleam_panic.from_dynamic(error) { + Ok(error) -> { + let src = option.from_result(read_file(error.file)) + format_gleam_error(error, module, function, src) + } + Error(_) -> format_unknown(module, function, error) + } + + io.print("\n" <> message) + State(..state, failed: state.failed + 1) +} + +pub fn eunit_missing() -> Result(never, Nil) { + let message = bold(red("Error")) <> ": EUnit libraries not found. + +Your Erlang installation seems to be incomplete. If you installed Erlang using +a package manager ensure that you have installed the full Erlang +distribution instead of a stripped-down version. +" + io.print_error(message) + Error(Nil) +} + +fn format_unknown( + module: String, + function: String, + error: dynamic.Dynamic, +) -> String { + string.concat([ + grey(module <> "." <> function) <> "\n", + "An unexpected error occurred:\n", + "\n", + " " <> string.inspect(error) <> "\n", + ]) +} + +fn format_gleam_error( + error: GleamPanic, + module: String, + function: String, + src: Option(BitArray), +) -> String { + let location = grey(error.file <> ":" <> int.to_string(error.line)) + + case error.kind { + gleam_panic.Panic -> { + string.concat([ + bold(red("panic")) <> " " <> location <> "\n", + cyan(" test") <> ": " <> module <> "." <> function <> "\n", + cyan(" info") <> ": " <> error.message <> "\n", + ]) + } + + gleam_panic.Todo -> { + string.concat([ + bold(yellow("todo")) <> " " <> location <> "\n", + cyan(" test") <> ": " <> module <> "." <> function <> "\n", + cyan(" info") <> ": " <> error.message <> "\n", + ]) + } + + gleam_panic.Assert(start:, end:, kind:, ..) -> { + string.concat([ + bold(red("assert")) <> " " <> location <> "\n", + cyan(" test") <> ": " <> module <> "." <> function <> "\n", + code_snippet(src, start, end), + assert_info(kind), + cyan(" info") <> ": " <> error.message <> "\n", + ]) + } + + gleam_panic.LetAssert(start:, end:, value:, ..) -> { + string.concat([ + bold(red("let assert")) <> " " <> location <> "\n", + cyan(" test") <> ": " <> module <> "." <> function <> "\n", + code_snippet(src, start, end), + cyan("value") <> ": " <> string.inspect(value) <> "\n", + cyan(" info") <> ": " <> error.message <> "\n", + ]) + } + } +} + +fn assert_info(kind: gleam_panic.AssertKind) -> String { + case kind { + gleam_panic.BinaryOperator(left:, right:, ..) -> { + string.concat([assert_value(" left", left), assert_value("right", right)]) + } + + gleam_panic.FunctionCall(arguments:) -> { + arguments + |> list.index_map(fn(e, i) { + let number = string.pad_start(int.to_string(i), 5, " ") + assert_value(number, e) + }) + |> string.concat + } + + gleam_panic.OtherExpression(..) -> "" + } +} + +fn assert_value(name: String, value: gleam_panic.AssertedExpression) -> String { + cyan(name) <> ": " <> inspect_value(value) <> "\n" +} + +fn inspect_value(value: gleam_panic.AssertedExpression) -> String { + case value.kind { + gleam_panic.Unevaluated -> grey("unevaluated") + gleam_panic.Literal(..) -> grey("literal") + gleam_panic.Expression(value:) -> string.inspect(value) + } +} + +fn code_snippet(src: Option(BitArray), start: Int, end: Int) -> String { + { + use src <- result.try(option.to_result(src, Nil)) + use snippet <- result.try(bit_array.slice(src, start, end - start)) + use snippet <- result.try(bit_array.to_string(snippet)) + let snippet = cyan(" code") <> ": " <> snippet <> "\n" + Ok(snippet) + } + |> result.unwrap("") +} + +pub fn test_skipped(state: State, module: String, function: String) -> State { + io.print("\n" <> module <> "." <> function <> yellow(" skipped")) + State(..state, skipped: state.skipped + 1) +} + +fn bold(text: String) -> String { + "\u{001b}[1m" <> text <> "\u{001b}[22m" +} + +fn cyan(text: String) -> String { + "\u{001b}[36m" <> text <> "\u{001b}[39m" +} + +fn yellow(text: String) -> String { + "\u{001b}[33m" <> text <> "\u{001b}[39m" +} + +fn green(text: String) -> String { + "\u{001b}[32m" <> text <> "\u{001b}[39m" +} + +fn red(text: String) -> String { + "\u{001b}[31m" <> text <> "\u{001b}[39m" +} + +fn grey(text: String) -> String { + "\u{001b}[90m" <> text <> "\u{001b}[39m" +} + +@external(erlang, "file", "read_file") +fn read_file(path: String) -> Result(BitArray, dynamic.Dynamic) { + case read_file_text(path) { + Ok(text) -> Ok(bit_array.from_string(text)) + Error(e) -> Error(e) + } +} + +@external(javascript, "../../gleeunit_ffi.mjs", "read_file") +fn read_file_text(path: String) -> Result(String, dynamic.Dynamic) diff --git a/build/packages/gleeunit/src/gleeunit/should.gleam b/build/packages/gleeunit/src/gleeunit/should.gleam new file mode 100644 index 0000000..99cd16c --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit/should.gleam @@ -0,0 +1,72 @@ +//// Use the `assert` keyword instead of this module. + +import gleam/option.{type Option, None, Some} +import gleam/string + +pub fn equal(a: t, b: t) -> Nil { + case a == b { + True -> Nil + _ -> + panic as string.concat([ + "\n", + string.inspect(a), + "\nshould equal\n", + string.inspect(b), + ]) + } +} + +pub fn not_equal(a: t, b: t) -> Nil { + case a != b { + True -> Nil + _ -> + panic as string.concat([ + "\n", + string.inspect(a), + "\nshould not equal\n", + string.inspect(b), + ]) + } +} + +pub fn be_ok(a: Result(a, e)) -> a { + case a { + Ok(value) -> value + _ -> panic as string.concat(["\n", string.inspect(a), "\nshould be ok"]) + } +} + +pub fn be_error(a: Result(a, e)) -> e { + case a { + Error(error) -> error + _ -> panic as string.concat(["\n", string.inspect(a), "\nshould be error"]) + } +} + +pub fn be_some(a: Option(a)) -> a { + case a { + Some(value) -> value + _ -> panic as string.concat(["\n", string.inspect(a), "\nshould be some"]) + } +} + +pub fn be_none(a: Option(a)) -> Nil { + case a { + None -> Nil + _ -> panic as string.concat(["\n", string.inspect(a), "\nshould be none"]) + } +} + +pub fn be_true(actual: Bool) -> Nil { + actual + |> equal(True) +} + +pub fn be_false(actual: Bool) -> Nil { + actual + |> equal(False) +} + +pub fn fail() -> Nil { + be_true(False) +} diff --git a/build/packages/gleeunit/src/gleeunit@internal@gleam_panic.erl b/build/packages/gleeunit/src/gleeunit@internal@gleam_panic.erl new file mode 100644 index 0000000..398ea7d --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit@internal@gleam_panic.erl @@ -0,0 +1,56 @@ +-module(gleeunit@internal@gleam_panic). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleeunit/internal/gleam_panic.gleam"). +-export([from_dynamic/1]). +-export_type([gleam_panic/0, panic_kind/0, assert_kind/0, asserted_expression/0, expression_kind/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC(false). + +-type gleam_panic() :: {gleam_panic, + binary(), + binary(), + binary(), + binary(), + integer(), + panic_kind()}. + +-type panic_kind() :: todo | + panic | + {let_assert, + integer(), + integer(), + integer(), + integer(), + gleam@dynamic:dynamic_()} | + {assert, integer(), integer(), integer(), assert_kind()}. + +-type assert_kind() :: {binary_operator, + binary(), + asserted_expression(), + asserted_expression()} | + {function_call, list(asserted_expression())} | + {other_expression, asserted_expression()}. + +-type asserted_expression() :: {asserted_expression, + integer(), + integer(), + expression_kind()}. + +-type expression_kind() :: {literal, gleam@dynamic:dynamic_()} | + {expression, gleam@dynamic:dynamic_()} | + unevaluated. + +-file("src/gleeunit/internal/gleam_panic.gleam", 49). +?DOC(false). +-spec from_dynamic(gleam@dynamic:dynamic_()) -> {ok, gleam_panic()} | + {error, nil}. +from_dynamic(Data) -> + gleeunit_gleam_panic_ffi:from_dynamic(Data). diff --git a/build/packages/gleeunit/src/gleeunit@internal@reporting.erl b/build/packages/gleeunit/src/gleeunit@internal@reporting.erl new file mode 100644 index 0000000..8c37c79 --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit@internal@reporting.erl @@ -0,0 +1,343 @@ +-module(gleeunit@internal@reporting). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleeunit/internal/reporting.gleam"). +-export([new_state/0, test_skipped/3, test_passed/1, finished/1, eunit_missing/0, test_failed/4]). +-export_type([state/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC(false). + +-type state() :: {state, integer(), integer(), integer()}. + +-file("src/gleeunit/internal/reporting.gleam", 15). +?DOC(false). +-spec new_state() -> state(). +new_state() -> + {state, 0, 0, 0}. + +-file("src/gleeunit/internal/reporting.gleam", 207). +?DOC(false). +-spec bold(binary()) -> binary(). +bold(Text) -> + <<<<"\x{001b}[1m"/utf8, Text/binary>>/binary, "\x{001b}[22m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 211). +?DOC(false). +-spec cyan(binary()) -> binary(). +cyan(Text) -> + <<<<"\x{001b}[36m"/utf8, Text/binary>>/binary, "\x{001b}[39m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 191). +?DOC(false). +-spec code_snippet(gleam@option:option(bitstring()), integer(), integer()) -> binary(). +code_snippet(Src, Start, End) -> + _pipe = begin + gleam@result:'try'( + gleam@option:to_result(Src, nil), + fun(Src@1) -> + gleam@result:'try'( + gleam_stdlib:bit_array_slice(Src@1, Start, End - Start), + fun(Snippet) -> + gleam@result:'try'( + gleam@bit_array:to_string(Snippet), + fun(Snippet@1) -> + Snippet@2 = <<<<<<(cyan(<<" code"/utf8>>))/binary, + ": "/utf8>>/binary, + Snippet@1/binary>>/binary, + "\n"/utf8>>, + {ok, Snippet@2} + end + ) + end + ) + end + ) + end, + gleam@result:unwrap(_pipe, <<""/utf8>>). + +-file("src/gleeunit/internal/reporting.gleam", 215). +?DOC(false). +-spec yellow(binary()) -> binary(). +yellow(Text) -> + <<<<"\x{001b}[33m"/utf8, Text/binary>>/binary, "\x{001b}[39m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 202). +?DOC(false). +-spec test_skipped(state(), binary(), binary()) -> state(). +test_skipped(State, Module, Function) -> + gleam_stdlib:print( + <<<<<<<<"\n"/utf8, Module/binary>>/binary, "."/utf8>>/binary, + Function/binary>>/binary, + (yellow(<<" skipped"/utf8>>))/binary>> + ), + {state, + erlang:element(2, State), + erlang:element(3, State), + erlang:element(4, State) + 1}. + +-file("src/gleeunit/internal/reporting.gleam", 219). +?DOC(false). +-spec green(binary()) -> binary(). +green(Text) -> + <<<<"\x{001b}[32m"/utf8, Text/binary>>/binary, "\x{001b}[39m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 66). +?DOC(false). +-spec test_passed(state()) -> state(). +test_passed(State) -> + gleam_stdlib:print(green(<<"."/utf8>>)), + {state, + erlang:element(2, State) + 1, + erlang:element(3, State), + erlang:element(4, State)}. + +-file("src/gleeunit/internal/reporting.gleam", 223). +?DOC(false). +-spec red(binary()) -> binary(). +red(Text) -> + <<<<"\x{001b}[31m"/utf8, Text/binary>>/binary, "\x{001b}[39m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 19). +?DOC(false). +-spec finished(state()) -> integer(). +finished(State) -> + case State of + {state, 0, 0, 0} -> + gleam_stdlib:println(<<"\nNo tests found!"/utf8>>), + 1; + + {state, _, 0, 0} -> + Message = <<<<"\n"/utf8, + (erlang:integer_to_binary(erlang:element(2, State)))/binary>>/binary, + " passed, no failures"/utf8>>, + gleam_stdlib:println(green(Message)), + 0; + + {state, _, _, 0} -> + Message@1 = <<<<<<<<"\n"/utf8, + (erlang:integer_to_binary(erlang:element(2, State)))/binary>>/binary, + " passed, "/utf8>>/binary, + (erlang:integer_to_binary(erlang:element(3, State)))/binary>>/binary, + " failures"/utf8>>, + gleam_stdlib:println(red(Message@1)), + 1; + + {state, _, 0, _} -> + Message@2 = <<<<<<<<"\n"/utf8, + (erlang:integer_to_binary(erlang:element(2, State)))/binary>>/binary, + " passed, 0 failures, "/utf8>>/binary, + (erlang:integer_to_binary(erlang:element(4, State)))/binary>>/binary, + " skipped"/utf8>>, + gleam_stdlib:println(yellow(Message@2)), + 1; + + {state, _, _, _} -> + Message@3 = <<<<<<<<<<<<"\n"/utf8, + (erlang:integer_to_binary( + erlang:element(2, State) + ))/binary>>/binary, + " passed, "/utf8>>/binary, + (erlang:integer_to_binary(erlang:element(3, State)))/binary>>/binary, + " failures, "/utf8>>/binary, + (erlang:integer_to_binary(erlang:element(4, State)))/binary>>/binary, + " skipped"/utf8>>, + gleam_stdlib:println(red(Message@3)), + 1 + end. + +-file("src/gleeunit/internal/reporting.gleam", 89). +?DOC(false). +-spec eunit_missing() -> {ok, any()} | {error, nil}. +eunit_missing() -> + Message = <<(bold(red(<<"Error"/utf8>>)))/binary, + ": EUnit libraries not found. + +Your Erlang installation seems to be incomplete. If you installed Erlang using +a package manager ensure that you have installed the full Erlang +distribution instead of a stripped-down version. +"/utf8>>, + gleam_stdlib:print_error(Message), + {error, nil}. + +-file("src/gleeunit/internal/reporting.gleam", 227). +?DOC(false). +-spec grey(binary()) -> binary(). +grey(Text) -> + <<<<"\x{001b}[90m"/utf8, Text/binary>>/binary, "\x{001b}[39m"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 100). +?DOC(false). +-spec format_unknown(binary(), binary(), gleam@dynamic:dynamic_()) -> binary(). +format_unknown(Module, Function, Error) -> + erlang:list_to_binary( + [<<(grey(<<<>/binary, Function/binary>>))/binary, + "\n"/utf8>>, + <<"An unexpected error occurred:\n"/utf8>>, + <<"\n"/utf8>>, + <<<<" "/utf8, (gleam@string:inspect(Error))/binary>>/binary, + "\n"/utf8>>] + ). + +-file("src/gleeunit/internal/reporting.gleam", 183). +?DOC(false). +-spec inspect_value(gleeunit@internal@gleam_panic:asserted_expression()) -> binary(). +inspect_value(Value) -> + case erlang:element(4, Value) of + unevaluated -> + grey(<<"unevaluated"/utf8>>); + + {literal, _} -> + grey(<<"literal"/utf8>>); + + {expression, Value@1} -> + gleam@string:inspect(Value@1) + end. + +-file("src/gleeunit/internal/reporting.gleam", 179). +?DOC(false). +-spec assert_value( + binary(), + gleeunit@internal@gleam_panic:asserted_expression() +) -> binary(). +assert_value(Name, Value) -> + <<<<<<(cyan(Name))/binary, ": "/utf8>>/binary, + (inspect_value(Value))/binary>>/binary, + "\n"/utf8>>. + +-file("src/gleeunit/internal/reporting.gleam", 160). +?DOC(false). +-spec assert_info(gleeunit@internal@gleam_panic:assert_kind()) -> binary(). +assert_info(Kind) -> + case Kind of + {binary_operator, _, Left, Right} -> + erlang:list_to_binary( + [assert_value(<<" left"/utf8>>, Left), + assert_value(<<"right"/utf8>>, Right)] + ); + + {function_call, Arguments} -> + _pipe = Arguments, + _pipe@1 = gleam@list:index_map( + _pipe, + fun(E, I) -> + Number = gleam@string:pad_start( + erlang:integer_to_binary(I), + 5, + <<" "/utf8>> + ), + assert_value(Number, E) + end + ), + erlang:list_to_binary(_pipe@1); + + {other_expression, _} -> + <<""/utf8>> + end. + +-file("src/gleeunit/internal/reporting.gleam", 113). +?DOC(false). +-spec format_gleam_error( + gleeunit@internal@gleam_panic:gleam_panic(), + binary(), + binary(), + gleam@option:option(bitstring()) +) -> binary(). +format_gleam_error(Error, Module, Function, Src) -> + Location = grey( + <<<<(erlang:element(3, Error))/binary, ":"/utf8>>/binary, + (erlang:integer_to_binary(erlang:element(6, Error)))/binary>> + ), + case erlang:element(7, Error) of + panic -> + erlang:list_to_binary( + [<<<<<<(bold(red(<<"panic"/utf8>>)))/binary, " "/utf8>>/binary, + Location/binary>>/binary, + "\n"/utf8>>, + <<<<<<<<<<(cyan(<<" test"/utf8>>))/binary, ": "/utf8>>/binary, + Module/binary>>/binary, + "."/utf8>>/binary, + Function/binary>>/binary, + "\n"/utf8>>, + <<<<<<(cyan(<<" info"/utf8>>))/binary, ": "/utf8>>/binary, + (erlang:element(2, Error))/binary>>/binary, + "\n"/utf8>>] + ); + + todo -> + erlang:list_to_binary( + [<<<<<<(bold(yellow(<<"todo"/utf8>>)))/binary, " "/utf8>>/binary, + Location/binary>>/binary, + "\n"/utf8>>, + <<<<<<<<<<(cyan(<<" test"/utf8>>))/binary, ": "/utf8>>/binary, + Module/binary>>/binary, + "."/utf8>>/binary, + Function/binary>>/binary, + "\n"/utf8>>, + <<<<<<(cyan(<<" info"/utf8>>))/binary, ": "/utf8>>/binary, + (erlang:element(2, Error))/binary>>/binary, + "\n"/utf8>>] + ); + + {assert, Start, End, _, Kind} -> + erlang:list_to_binary( + [<<<<<<(bold(red(<<"assert"/utf8>>)))/binary, " "/utf8>>/binary, + Location/binary>>/binary, + "\n"/utf8>>, + <<<<<<<<<<(cyan(<<" test"/utf8>>))/binary, ": "/utf8>>/binary, + Module/binary>>/binary, + "."/utf8>>/binary, + Function/binary>>/binary, + "\n"/utf8>>, + code_snippet(Src, Start, End), + assert_info(Kind), + <<<<<<(cyan(<<" info"/utf8>>))/binary, ": "/utf8>>/binary, + (erlang:element(2, Error))/binary>>/binary, + "\n"/utf8>>] + ); + + {let_assert, Start@1, End@1, _, _, Value} -> + erlang:list_to_binary( + [<<<<<<(bold(red(<<"let assert"/utf8>>)))/binary, " "/utf8>>/binary, + Location/binary>>/binary, + "\n"/utf8>>, + <<<<<<<<<<(cyan(<<" test"/utf8>>))/binary, ": "/utf8>>/binary, + Module/binary>>/binary, + "."/utf8>>/binary, + Function/binary>>/binary, + "\n"/utf8>>, + code_snippet(Src, Start@1, End@1), + <<<<<<(cyan(<<"value"/utf8>>))/binary, ": "/utf8>>/binary, + (gleam@string:inspect(Value))/binary>>/binary, + "\n"/utf8>>, + <<<<<<(cyan(<<" info"/utf8>>))/binary, ": "/utf8>>/binary, + (erlang:element(2, Error))/binary>>/binary, + "\n"/utf8>>] + ) + end. + +-file("src/gleeunit/internal/reporting.gleam", 71). +?DOC(false). +-spec test_failed(state(), binary(), binary(), gleam@dynamic:dynamic_()) -> state(). +test_failed(State, Module, Function, Error) -> + Message = case gleeunit_gleam_panic_ffi:from_dynamic(Error) of + {ok, Error@1} -> + Src = gleam@option:from_result( + file:read_file(erlang:element(3, Error@1)) + ), + format_gleam_error(Error@1, Module, Function, Src); + + {error, _} -> + format_unknown(Module, Function, Error) + end, + gleam_stdlib:print(<<"\n"/utf8, Message/binary>>), + {state, + erlang:element(2, State), + erlang:element(3, State) + 1, + erlang:element(4, State)}. diff --git a/build/packages/gleeunit/src/gleeunit@should.erl b/build/packages/gleeunit/src/gleeunit@should.erl new file mode 100644 index 0000000..81048de --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit@should.erl @@ -0,0 +1,153 @@ +-module(gleeunit@should). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/gleeunit/should.gleam"). +-export([equal/2, not_equal/2, be_ok/1, be_error/1, be_some/1, be_none/1, be_true/1, be_false/1, fail/0]). + +-if(?OTP_RELEASE >= 27). +-define(MODULEDOC(Str), -moduledoc(Str)). +-define(DOC(Str), -doc(Str)). +-else. +-define(MODULEDOC(Str), -compile([])). +-define(DOC(Str), -compile([])). +-endif. + +?MODULEDOC(" Use the `assert` keyword instead of this module.\n"). + +-file("src/gleeunit/should.gleam", 6). +-spec equal(DOF, DOF) -> nil. +equal(A, B) -> + case A =:= B of + true -> + nil; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould equal\n"/utf8>>, + gleam@string:inspect(B)] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"equal"/utf8>>, + line => 10}) + end. + +-file("src/gleeunit/should.gleam", 19). +-spec not_equal(DOG, DOG) -> nil. +not_equal(A, B) -> + case A /= B of + true -> + nil; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould not equal\n"/utf8>>, + gleam@string:inspect(B)] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"not_equal"/utf8>>, + line => 23}) + end. + +-file("src/gleeunit/should.gleam", 32). +-spec be_ok({ok, DOH} | {error, any()}) -> DOH. +be_ok(A) -> + case A of + {ok, Value} -> + Value; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould be ok"/utf8>>] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"be_ok"/utf8>>, + line => 35}) + end. + +-file("src/gleeunit/should.gleam", 39). +-spec be_error({ok, any()} | {error, DOM}) -> DOM. +be_error(A) -> + case A of + {error, Error} -> + Error; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould be error"/utf8>>] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"be_error"/utf8>>, + line => 42}) + end. + +-file("src/gleeunit/should.gleam", 46). +-spec be_some(gleam@option:option(DOP)) -> DOP. +be_some(A) -> + case A of + {some, Value} -> + Value; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould be some"/utf8>>] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"be_some"/utf8>>, + line => 49}) + end. + +-file("src/gleeunit/should.gleam", 53). +-spec be_none(gleam@option:option(any())) -> nil. +be_none(A) -> + case A of + none -> + nil; + + _ -> + erlang:error(#{gleam_error => panic, + message => erlang:list_to_binary( + [<<"\n"/utf8>>, + gleam@string:inspect(A), + <<"\nshould be none"/utf8>>] + ), + file => <>, + module => <<"gleeunit/should"/utf8>>, + function => <<"be_none"/utf8>>, + line => 56}) + end. + +-file("src/gleeunit/should.gleam", 60). +-spec be_true(boolean()) -> nil. +be_true(Actual) -> + _pipe = Actual, + equal(_pipe, true). + +-file("src/gleeunit/should.gleam", 65). +-spec be_false(boolean()) -> nil. +be_false(Actual) -> + _pipe = Actual, + equal(_pipe, false). + +-file("src/gleeunit/should.gleam", 70). +-spec fail() -> nil. +fail() -> + be_true(false). diff --git a/build/packages/gleeunit/src/gleeunit_ffi.erl b/build/packages/gleeunit/src/gleeunit_ffi.erl new file mode 100644 index 0000000..05c7490 --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit_ffi.erl @@ -0,0 +1,21 @@ +-module(gleeunit_ffi). + +-export([find_files/2, run_eunit/2]). + +find_files(Pattern, In) -> + Results = filelib:wildcard(binary_to_list(Pattern), binary_to_list(In)), + lists:map(fun list_to_binary/1, Results). + +run_eunit(Tests, Options) -> + case code:which(eunit) of + non_existing -> + gleeunit@internal@reporting:eunit_missing(); + + _ -> + case eunit:test(Tests, Options) of + ok -> {ok, nil}; + error -> {error, nil}; + {error, Term} -> {error, Term} + end + end. + diff --git a/build/packages/gleeunit/src/gleeunit_ffi.mjs b/build/packages/gleeunit/src/gleeunit_ffi.mjs new file mode 100644 index 0000000..7bdc071 --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit_ffi.mjs @@ -0,0 +1,100 @@ +import { readFileSync } from "node:fs"; +import { Result$Ok, Result$Error } from "./gleam.mjs"; +import * as reporting from "./gleeunit/internal/reporting.mjs"; + +export function read_file(path) { + try { + return Result$Ok(readFileSync(path)); + } catch { + return Result$Error(undefined); + } +} + +async function* gleamFiles(directory) { + for (let entry of await read_dir(directory)) { + let path = join_path(directory, entry); + if (path.endsWith(".gleam")) { + yield path; + } else { + try { + yield* gleamFiles(path); + } catch (error) { + // Could not read directory, assume it's a file + } + } + } +} + +async function readRootPackageName() { + let toml = await async_read_file("gleam.toml", "utf-8"); + for (let line of toml.split("\n")) { + let matches = line.match(/\s*name\s*=\s*"([a-z][a-z0-9_]*)"/); // Match regexp in compiler-cli/src/new.rs in validate_name() + if (matches) return matches[1]; + } + throw new Error("Could not determine package name from gleam.toml"); +} + +export async function main() { + let state = reporting.new_state(); + + let packageName = await readRootPackageName(); + let dist = `../${packageName}/`; + + for await (let path of await gleamFiles("test")) { + let js_path = path.slice("test/".length).replace(".gleam", ".mjs"); + let module = await import(join_path(dist, js_path)); + for (let fnName of Object.keys(module)) { + if (!fnName.endsWith("_test")) continue; + try { + await module[fnName](); + state = reporting.test_passed(state); + } catch (error) { + let moduleName = js_path.slice(0, -4); + state = reporting.test_failed(state, moduleName, fnName, error); + } + } + } + + const status = reporting.finished(state); + exit(status); +} + +export function crash(message) { + throw new Error(message); +} + +function exit(code) { + if (globalThis.Deno) { + Deno.exit(code); + } else { + process.exit(code); + } +} + +async function read_dir(path) { + if (globalThis.Deno) { + let items = []; + for await (let item of Deno.readDir(path, { withFileTypes: true })) { + items.push(item.name); + } + return items; + } else { + let { readdir } = await import("node:fs/promises"); + return readdir(path); + } +} + +function join_path(a, b) { + if (a.endsWith("/")) return a + b; + return a + "/" + b; +} + +async function async_read_file(path) { + if (globalThis.Deno) { + return Deno.readTextFile(path); + } else { + let { readFile } = await import("node:fs/promises"); + let contents = await readFile(path); + return contents.toString(); + } +} diff --git a/build/packages/gleeunit/src/gleeunit_progress.erl b/build/packages/gleeunit/src/gleeunit_progress.erl new file mode 100644 index 0000000..e6576a5 --- /dev/null +++ b/build/packages/gleeunit/src/gleeunit_progress.erl @@ -0,0 +1,72 @@ +%% A formatter adapted from Sean Cribb's https://github.com/seancribbs/eunit_formatters + +-module(gleeunit_progress). +-define(NOTEST, true). + +%% eunit_listener callbacks +-export([ + init/1, handle_begin/3, handle_end/3, handle_cancel/3, terminate/2, + start/0, start/1 +]). + +-define(reporting, gleeunit@internal@reporting). + +start() -> + start([]). + +start(Options) -> + eunit_listener:start(?MODULE, Options). + +init(_Options) -> + ?reporting:new_state(). + +handle_begin(_test_or_group, _data, State) -> + State. + +handle_end(group, _data, State) -> + State; +handle_end(test, Data, State) -> + {AtomModule, AtomFunction, _Arity} = proplists:get_value(source, Data), + Module = erlang:atom_to_binary(AtomModule), + Function = erlang:atom_to_binary(AtomFunction), + + % EUnit swallows stdout, so print it to make debugging easier. + case proplists:get_value(output, Data) of + undefined -> ok; + <<>> -> ok; + Out -> gleam@io:print(Out) + end, + + case proplists:get_value(status, Data) of + ok -> + ?reporting:test_passed(State); + {skipped, _Reason} -> + ?reporting:test_skipped(State, Module, Function); + {error, {_, Exception, _Stack}} -> + ?reporting:test_failed(State, Module, Function, Exception) + end. + + +handle_cancel(_test_or_group, Data, State) -> + ?reporting:test_failed(State, <<"gleeunit">>, <<"main">>, Data). + +terminate({ok, _Data}, State) -> + ?reporting:finished(State), + ok; +terminate({error, Reason}, State) -> + ?reporting:finished(State), + io:fwrite(" +Eunit failed: + +~80p + +This is probably a bug in gleeunit. Please report it. +", [Reason]), + sync_end(error). + +sync_end(Result) -> + receive + {stop, Reference, ReplyTo} -> + ReplyTo ! {result, Reference, Result}, + ok + end. diff --git a/build/packages/packages.toml b/build/packages/packages.toml new file mode 100644 index 0000000..6098d78 --- /dev/null +++ b/build/packages/packages.toml @@ -0,0 +1,7 @@ +[packages] +gleam_stdlib = "0.65.0" +gleam_community_colour = "2.0.2" +gleam_json = "3.1.0" +gleeunit = "1.9.0" +paint = "1.0.0" +gleam_time = "1.6.0" diff --git a/build/packages/paint/LICENSE.txt b/build/packages/paint/LICENSE.txt new file mode 100644 index 0000000..40cce2a --- /dev/null +++ b/build/packages/paint/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Eli Adelhult + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/build/packages/paint/README.md b/build/packages/paint/README.md new file mode 100644 index 0000000..d762383 --- /dev/null +++ b/build/packages/paint/README.md @@ -0,0 +1,34 @@ +# Paint +**Make 2D drawings, animations, and games using Gleam and the HTML Canvas!** + +[![Package Version](https://img.shields.io/hexpm/v/paint)](https://hex.pm/packages/paint) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/paint/) + +Paint is a tiny embedded domain specific language (inspired by [Gloss](https://hackage.haskell.org/package/gloss)). +Make pictures out of basic shapes then style, transform, and combine them using the provided functions. + +![Frame 3(2)](https://github.com/user-attachments/assets/a8b83b58-990a-432a-9034-deebc4d210a6) + +```gleam +import paint as p +import paint/canvas + +fn main() { + let my_picture = p.combine([ + p.circle(30.0), + p.circle(20.0) |> p.fill(p.colour_rgb(0, 200, 200)), + p.rectangle(50.0, 30.0) |> p.rotate(p.angle_deg(30.0)), + p.text("Hello world", 10) |> p.translate_y(-35.0), + ]) + + canvas.display(fn(_: canvas.Config) { my_picture }, "#canvas_id") +} +``` + +**Want to learn more? Read the [docs](https://hexdocs.pm/paint) or browse the [visual examples](https://adelhult.github.io/paint/).** + +## Logo +Lucy is borrowed from the [Gleam branding page](https://gleam.run/branding/) and the brush is made by [Delapouite (game icons)](https://game-icons.net/1x1/delapouite/paint-brush.html). + +## Changelog +API additions and breaking changes can be found in the file `CHANGELOG.md`. diff --git a/build/packages/paint/gleam.toml b/build/packages/paint/gleam.toml new file mode 100644 index 0000000..d912cdd --- /dev/null +++ b/build/packages/paint/gleam.toml @@ -0,0 +1,23 @@ +name = "paint" +version = "1.0.0" +target = "javascript" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +description = "Make 2D drawings, animations, and games (HTML Canvas)" +licences = ["MIT"] +repository = { type = "github", user = "adelhult", repo = "paint" } +links = [ + { title = "Visual examples", href = "https://adelhult.github.io/paint/" }, +] +dev-dependencies = { gleeunit = ">= 1.6.1 and < 2.0.0" } +# +# 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.58.0 and < 2.0.0" +gleam_community_colour = ">= 2.0.0 and < 3.0.0" +gleam_json = ">= 3.0.2 and < 4.0.0" diff --git a/build/packages/paint/src/impl_canvas_bindings.mjs b/build/packages/paint/src/impl_canvas_bindings.mjs new file mode 100644 index 0000000..5fed16a --- /dev/null +++ b/build/packages/paint/src/impl_canvas_bindings.mjs @@ -0,0 +1,271 @@ +import { Ok, Error } from "./gleam.mjs"; + +class PaintCanvas extends HTMLElement { + // Open an issue if you are in need of any other attributes :) + static observedAttributes = ["width", "height", "style", "picture"]; + + constructor() { + super(); + // Create a canvas + this.canvas = document.createElement("canvas"); + const style = document.createElement("style"); + style.textContent = ` + :host { + display: inline-block; + } + `; + this.shadow = this.attachShadow({ mode: "open" }); + this.shadow.appendChild(style); + this.shadow.appendChild(this.canvas); + this.ctx = this.canvas.getContext("2d"); + } + + attributeChangedCallback(name, _oldValue, newValue) { + if (name === "picture") { + this.picture = newValue; + return; + } else if (name === "width") { + this.width = newValue; + } else if (name === "height") { + this.height = newValue; + } + } + + drawPicture() { + if (!this.pictureString) { + return; + } + + this.ctx.reset(); + const display = + window.PAINT_STATE[ + "display_on_rendering_context_with_default_drawing_state" + ]; + + display(this.pictureString, this.ctx); + } + + set picture(value) { + this.pictureString = value; + this.drawPicture(); + } + + set width(value) { + this.canvas.width = value; + this.drawPicture(); + } + + set height(value) { + this.canvas.height = value; + this.drawPicture(); + } + + get width() { + return this.canvas.width; + } + + get height() { + return this.canvas.height; + } +} + +export function define_web_component() { + window.customElements.define("paint-canvas", PaintCanvas); +} + +export function get_rendering_context(selector) { + // TODO: Handle the case where the canvas element is not found. + return document.querySelector(selector).getContext("2d"); +} + +export function setup_request_animation_frame(callback) { + window.requestAnimationFrame((time) => { + callback(time); + }); +} + +export function setup_input_handler(event_name, callback) { + window.addEventListener(event_name, callback); +} + +export function get_key_code(event) { + return event.keyCode; +} + +export function set_global(state, id) { + if (typeof window.PAINT_STATE == "undefined") { + window.PAINT_STATE = {}; + } + window.PAINT_STATE[id] = state; +} + +export function get_global(id) { + if (!window.PAINT_STATE) { + return new Error(undefined); + } + if (!(id in window.PAINT_STATE)) { + return new Error(undefined); + } + return new Ok(window.PAINT_STATE[id]); +} + +export function get_width(ctx) { + return ctx.canvas.clientWidth; +} + +export function get_height(ctx) { + return ctx.canvas.clientHeight; +} + +// Based on https://stackoverflow.com/questions/17130395/real-mouse-position-in-canvas +export function mouse_pos(ctx, event) { + // Calculate the scaling of the canvas vs its content + const rect = ctx.canvas.getBoundingClientRect(); + const scaleX = ctx.canvas.width / rect.width; + const scaleY = ctx.canvas.height / rect.height; + + return [ + (event.clientX - rect.left) * scaleX, + (event.clientY - rect.top) * scaleY, + ]; +} + +// if check_pressed is true, the function will return true if the button was pressed +// if check_pressed is false, the function will return true if the button was released +export function check_mouse_button( + event, + previous_event, + button_index, + check_pressed, +) { + let previous_buttons = previous_event?.buttons ?? 0; + let current_buttons = event.buttons; + + // ~001 && + // 011 + // ----- + // 010 found the newly pressed! + // + // 011 && + // ~001 + // ----- + // 010 found the newly released! + if (check_pressed) { + previous_buttons = ~previous_buttons; + } else { + current_buttons = ~current_buttons; + } + + let button = previous_buttons & current_buttons & (1 << button_index); + return !!button; +} + +export function reset(ctx) { + ctx.reset(); +} + +export function arc(ctx, radius, start, end, fill, stroke) { + ctx.beginPath(); + ctx.arc(0, 0, radius, start, end); + if (fill) { + ctx.fill(); + } + if (stroke) { + ctx.stroke(); + } +} + +export function polygon(ctx, points, closed, fill, stroke) { + ctx.beginPath(); + ctx.moveTo(0, 0); + let started = false; + for (const point of points) { + let x = point[0]; + let y = point[1]; + if (started) { + ctx.lineTo(x, y); + } else { + ctx.moveTo(x, y); + started = true; + } + } + + if (closed) { + ctx.closePath(); + } + + if (fill && closed) { + ctx.fill(); + } + + if (stroke) { + ctx.stroke(); + } +} + +export function text(ctx, text, style) { + ctx.font = style; + ctx.fillText(text, 0, 0); +} + +export function save(ctx) { + ctx.save(); +} + +export function restore(ctx) { + ctx.restore(); +} + +export function set_fill_colour(ctx, css_colour) { + ctx.fillStyle = css_colour; +} + +export function set_stroke_color(ctx, css_color) { + ctx.strokeStyle = css_color; +} + +export function set_line_width(ctx, width) { + ctx.lineWidth = width; +} + +export function translate(ctx, x, y) { + ctx.translate(x, y); +} + +export function scale(ctx, x, y) { + ctx.scale(x, y); +} + +export function rotate(ctx, radians) { + ctx.rotate(radians); +} + +export function reset_transform(ctx) { + ctx.resetTransform(); +} + +export function draw_image(ctx, image, width_px, height_px) { + ctx.drawImage(image, 0, 0, width_px, height_px); +} + +export function image_from_query(selector) { + return document.querySelector(selector); +} + +export function image_from_src(src) { + const image = new Image(); + image.src = src; + return image; +} + +export function on_image_load(image, callback) { + if (image.complete) { + callback(); + } else { + image.addEventListener("load", callback); + } +} + +export function set_image_smoothing_enabled(ctx, value) { + ctx.imageSmoothingEnabled = value; +} diff --git a/build/packages/paint/src/numbers_ffi.mjs b/build/packages/paint/src/numbers_ffi.mjs new file mode 100644 index 0000000..0e6e6d3 --- /dev/null +++ b/build/packages/paint/src/numbers_ffi.mjs @@ -0,0 +1,3 @@ +export function pi() { + return Math.PI; +} diff --git a/build/packages/paint/src/paint.gleam b/build/packages/paint/src/paint.gleam new file mode 100644 index 0000000..6729228 --- /dev/null +++ b/build/packages/paint/src/paint.gleam @@ -0,0 +1,201 @@ +//// This module contains the main `Picture` type as well as the +//// function you can use to construct, modify and combine pictures. + +import gleam/result +import gleam_community/colour +import paint/internal/types as internal_implementation + +/// A 2D picture. This is the type which this entire library revolves around. +/// +///> [!NOTE] +///> Unless you intend to author a new backend you should **consider this type opaque and never use any of its constructors**. +///> Instead, make use of the many utility functions defined in this module (`circle`, `combine`, `fill`, etc.) +pub type Picture = + internal_implementation.Picture + +/// A reference to an image (i.e. a texture), not to be confused with the `Picture` type. +/// To create an image, see the image functions in the `canvas` back-end. +pub type Image = + internal_implementation.Image + +/// An angle in clock-wise direction. +/// See: `angle_rad` and `angle_deg`. +pub type Angle = + internal_implementation.Angle + +/// Create an angle expressed in radians +pub fn angle_rad(radians: Float) -> Angle { + internal_implementation.Radians(radians) +} + +/// Create an angle expressed in degrees +pub fn angle_deg(degrees: Float) -> Angle { + internal_implementation.Radians(degrees *. pi() /. 180.0) +} + +/// A rexport of the Colour type from [gleam_community/colour](https://hexdocs.pm/gleam_community_colour/). +/// Paint also includes the functions `colour_hex` and `colour_rgb` to +/// easily construct Colours, but feel free to import the `gleam_community/colour` module +/// and use the many utility that are provided from there. +pub type Colour = + colour.Colour + +/// A utility around [colour.from_rgb_hex_string](https://hexdocs.pm/gleam_community_colour/gleam_community/colour.html#from_rgb_hex_string) +/// (from `gleam_community/colour`) that **panics** on an invalid hex code. +pub fn colour_hex(string: String) -> Colour { + result.lazy_unwrap(colour.from_rgb_hex_string(string), fn() { + panic as "Failed to parse hex code" + }) +} + +/// A utility around [colour.from_rgb255](https://hexdocs.pm/gleam_community_colour/gleam_community/colour.html#from_rgb255) +/// (from `gleam_community/colour`) that **panics** if the values are outside of the allowed range. +pub fn colour_rgb(red: Int, green: Int, blue: Int) -> Colour { + result.lazy_unwrap(colour.from_rgb255(red, green, blue), fn() { + panic as "The value was not inside of the valid range [0-255]" + }) +} + +pub type Vec2 = + #(Float, Float) + +/// A blank picture +pub fn blank() -> Picture { + internal_implementation.Blank +} + +/// A circle with some given radius +pub fn circle(radius: Float) -> Picture { + internal_implementation.Arc( + radius, + start: internal_implementation.Radians(0.0), + end: internal_implementation.Radians(2.0 *. pi()), + ) +} + +/// An arc with some radius going from some +/// starting angle to some other angle in clock-wise direction +pub fn arc(radius: Float, start: Angle, end: Angle) -> Picture { + internal_implementation.Arc(radius, start: start, end: end) +} + +/// A polygon consisting of a list of 2d points +pub fn polygon(points: List(#(Float, Float))) -> Picture { + internal_implementation.Polygon(points, True) +} + +/// Lines (same as a polygon but not a closed shape) +pub fn lines(points: List(#(Float, Float))) -> Picture { + internal_implementation.Polygon(points, False) +} + +/// A rectangle with some given width and height +pub fn rectangle(width: Float, height: Float) -> Picture { + polygon([#(0.0, 0.0), #(width, 0.0), #(width, height), #(0.0, height)]) +} + +/// A square +pub fn square(length: Float) -> Picture { + rectangle(length, length) +} + +/// Draw an image such as a PNG, JPEG or an SVG. See the `canvas` back-end for more details on how to load images. +pub fn image(image: Image, width_px width_px, height_px height_px) -> Picture { + // TODO: add a function that allows us to draw only part of an image, flip, and if we want smooth scaling or not + internal_implementation.ImageRef(image, width_px:, height_px:) +} + +/// Set image scaling to be smooth (this is the default behaviour) +pub fn image_scaling_smooth(picture: Picture) -> Picture { + internal_implementation.ImageScalingBehaviour( + picture, + internal_implementation.ScalingSmooth, + ) +} + +/// Disable smooth image scaling, suitable for pixel art. +pub fn image_scaling_pixelated(picture: Picture) -> Picture { + internal_implementation.ImageScalingBehaviour( + picture, + internal_implementation.ScalingPixelated, + ) +} + +/// Text with some given font size +pub fn text(text: String, px font_size: Int) -> Picture { + internal_implementation.Text( + text, + style: internal_implementation.FontProperties(font_size, "sans-serif"), + ) + // TODO: expose more styling options (font and text alignment) +} + +/// Translate a picture in horizontal and vertical direction +pub fn translate_xy(picture: Picture, x: Float, y: Float) -> Picture { + internal_implementation.Translate(picture, #(x, y)) +} + +/// Translate a picture in the horizontal direction +pub fn translate_x(picture: Picture, x: Float) -> Picture { + translate_xy(picture, x, 0.0) +} + +/// Translate a picture in the vertical direction +pub fn translate_y(picture: Picture, y: Float) -> Picture { + translate_xy(picture, 0.0, y) +} + +/// Scale the picture in the horizontal direction +pub fn scale_x(picture: Picture, factor: Float) -> Picture { + internal_implementation.Scale(picture, #(factor, 1.0)) +} + +/// Scale the picture in the vertical direction +pub fn scale_y(picture: Picture, factor: Float) -> Picture { + internal_implementation.Scale(picture, #(1.0, factor)) +} + +/// Scale the picture uniformly in horizontal and vertical direction +pub fn scale_uniform(picture: Picture, factor: Float) -> Picture { + internal_implementation.Scale(picture, #(factor, factor)) +} + +/// Rotate the picture in a clock-wise direction +pub fn rotate(picture: Picture, angle: Angle) -> Picture { + internal_implementation.Rotate(picture, angle) +} + +/// Fill a picture with some given colour, see `Colour`. +pub fn fill(picture: Picture, colour: Colour) -> Picture { + internal_implementation.Fill(picture, colour) +} + +/// Set a solid stroke with some given colour and width +pub fn stroke(picture: Picture, colour: Colour, width width: Float) -> Picture { + internal_implementation.Stroke( + picture, + internal_implementation.SolidStroke(colour, width), + ) +} + +/// Remove the stroke of the given picture +pub fn stroke_none(picture: Picture) -> Picture { + internal_implementation.Stroke(picture, internal_implementation.NoStroke) +} + +/// Concatenate two pictures +pub fn concat(picture: Picture, another_picture: Picture) -> Picture { + combine([picture, another_picture]) +} + +/// Combine multiple pictures into one +pub fn combine(pictures: List(Picture)) -> Picture { + internal_implementation.Combine(pictures) +} + +// Internal utility function to get Pi π +@external(erlang, "math", "pi") +@external(javascript, "./numbers_ffi.mjs", "pi") +fn pi() -> Float { + 3.1415926 +} diff --git a/build/packages/paint/src/paint/canvas.gleam b/build/packages/paint/src/paint/canvas.gleam new file mode 100644 index 0000000..061c102 --- /dev/null +++ b/build/packages/paint/src/paint/canvas.gleam @@ -0,0 +1,461 @@ +//// A HTML canvas backend that can be used for displaying +//// your `Picture`s. There are three different ways of doing so: +//// - `display` (provide a picture and a CSS selector to some canvas element) +//// - `define_web_component` (an alternative to `display` using custom web components, useful if you are using a web framework like Lustre) +//// - `interact` (allows you to make animations and interactive programs) + +import gleam/int +import gleam/option.{type Option, None, Some} +import gleam_community/colour +import paint.{translate_xy} +import paint/encode +import paint/event.{type Event} +import paint/internal/impl_canvas +import paint/internal/types.{ + type Image, type Picture, Arc, Blank, Combine, Fill, FontProperties, Image, + NoStroke, Polygon, Radians, Rotate, Scale, SolidStroke, Stroke, Text, + Translate, +} + +/// The configuration of the "canvas" +pub type Config { + Config(width: Float, height: Float) +} + +/// Create a reference to an image using a CSS query selector. For example: +/// ``` +/// fn kitten() { +/// canvas.image_from_query("#kitten") +/// } +/// // In the HTML file: +/// // +/// ``` +/// +/// > [!WARNING] +/// > **Important**: Make sure the image has loaded before trying to draw a pictures referencing it. +/// > You can do this using `canvas.wait_until_loaded` function. +pub fn image_from_query(selector: String) -> Image { + let id = "image-selector-" <> selector + case impl_canvas.get_global(id) { + // Re-use the cached image if we can + Ok(_) -> { + Nil + } + Error(Nil) -> { + let image = impl_canvas.image_from_query(selector) + impl_canvas.set_global(image, id) + } + } + Image(id) +} + +/// Create a reference to an image using a source path. +/// ``` +/// fn my_logo_image() { +/// canvas.image_from_src("./priv/static/logo.svg") +/// } +/// ``` +/// +/// > [!WARNING] +/// > **Important**: Make sure the image has loaded before trying to draw a pictures referencing it. +/// > You can do this using `canvas.wait_until_loaded` function. +pub fn image_from_src(src: String) -> Image { + let id = "image-src-" <> src + case impl_canvas.get_global(id) { + // Re-use the cached image if we can + Ok(_) -> { + Nil + } + Error(Nil) -> { + let image = impl_canvas.image_from_src(src) + impl_canvas.set_global(image, id) + } + } + Image(id) +} + +/// Wait until a list of images have all been loaded, for example: +/// ``` +/// fn lucy() { +/// canvas.image_from_query("#lucy") +/// } +/// +/// fn cat() { +/// canvas.image_from_src("./path/to/kitten.png") +/// } +/// +/// pub fn main() { +/// use <- canvas.wait_until_loaded([lucy(), kitten()]) +/// // It is now safe to draw Pictures containing the images lucy and kitten :) +/// } +/// ``` +pub fn wait_until_loaded(images: List(Image), on_loaded: fn() -> Nil) -> Nil { + case images { + [] -> on_loaded() + [image, ..rest] -> { + let Image(id:) = image + let assert Ok(js_image) = impl_canvas.get_global(id) + impl_canvas.on_image_load(js_image, fn() { + wait_until_loaded(rest, on_loaded) + }) + } + } +} + +/// Display a picture on a HTML canvas element +/// (specified by some [CSS Selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors)). +/// ``` +/// canvas.display(fn (_: canvas.Config) { circle(50.0) }, "#mycanvas") +/// ``` +pub fn display(init: fn(Config) -> Picture, selector: String) { + let ctx = impl_canvas.get_rendering_context(selector) + impl_canvas.reset(ctx) + let picture = + init(Config(impl_canvas.get_width(ctx), impl_canvas.get_height(ctx))) + display_on_rendering_context(picture, ctx, default_drawing_state) +} + +/// Additional state used when drawing +/// (note that the fill and stroke color as well as the stroke width +/// is stored inside of the context) +type DrawingState { + DrawingState(fill: Bool, stroke: Bool) +} + +const default_drawing_state = DrawingState(fill: False, stroke: True) + +fn display_on_rendering_context( + picture: Picture, + ctx: impl_canvas.RenderingContext2D, + state: DrawingState, +) { + case picture { + Blank -> Nil + + Text(text, properties) -> { + let FontProperties(size_px, font_family) = properties + impl_canvas.save(ctx) + impl_canvas.text( + ctx, + text, + int.to_string(size_px) <> "px " <> font_family, + ) + impl_canvas.restore(ctx) + } + + Polygon(points, closed) -> { + impl_canvas.polygon(ctx, points, closed, state.fill, state.stroke) + } + + Arc(radius, start, end) -> { + let Radians(start_radians) = start + let Radians(end_radians) = end + impl_canvas.arc( + ctx, + radius, + start_radians, + end_radians, + state.fill, + state.stroke, + ) + } + + Fill(p, colour) -> { + impl_canvas.save(ctx) + impl_canvas.set_fill_colour(ctx, colour.to_css_rgba_string(colour)) + display_on_rendering_context(p, ctx, DrawingState(..state, fill: True)) + impl_canvas.restore(ctx) + } + + Stroke(p, stroke) -> { + case stroke { + NoStroke -> + display_on_rendering_context( + p, + ctx, + DrawingState(..state, stroke: False), + ) + SolidStroke(color, width) -> { + impl_canvas.save(ctx) + impl_canvas.set_stroke_color(ctx, colour.to_css_rgba_string(color)) + impl_canvas.set_line_width(ctx, width) + display_on_rendering_context( + p, + ctx, + DrawingState(..state, stroke: True), + ) + impl_canvas.restore(ctx) + } + } + } + + Translate(p, vec) -> { + let #(x, y) = vec + impl_canvas.save(ctx) + impl_canvas.translate(ctx, x, y) + display_on_rendering_context(p, ctx, state) + impl_canvas.restore(ctx) + } + + Scale(p, vec) -> { + let #(x, y) = vec + impl_canvas.save(ctx) + impl_canvas.scale(ctx, x, y) + display_on_rendering_context(p, ctx, state) + impl_canvas.restore(ctx) + } + + Rotate(p, angle) -> { + let Radians(rad) = angle + impl_canvas.save(ctx) + impl_canvas.rotate(ctx, rad) + display_on_rendering_context(p, ctx, state) + impl_canvas.restore(ctx) + } + + Combine(pictures) -> { + case pictures { + [] -> Nil + [p, ..ps] -> { + display_on_rendering_context(p, ctx, state) + display_on_rendering_context(Combine(ps), ctx, state) + } + } + } + types.ImageRef(Image(id:), width_px:, height_px:) -> { + // TODO: log an error if this fails? + let assert Ok(image) = impl_canvas.get_global(id) + impl_canvas.draw_image(ctx, image, width_px, height_px) + } + types.ImageScalingBehaviour(p, behaviour) -> { + impl_canvas.save(ctx) + impl_canvas.set_image_smoothing_enabled(ctx, case behaviour { + types.ScalingPixelated -> False + types.ScalingSmooth -> True + }) + display_on_rendering_context(p, ctx, state) + impl_canvas.restore(ctx) + } + } +} + +/// Animations, interactive applications and tiny games can be built using the +/// `interact` function. It roughly follows the [Elm architecture](https://guide.elm-lang.org/architecture/). +/// Here is a short example: +/// ``` +/// type State = +/// Int +/// +/// fn init(_: canvas.Config) -> State { +/// 0 +/// } +/// +/// fn update(state: State, event: event.Event) -> State { +/// case event { +/// event.Tick(_) -> state + 1 +/// _ -> state +/// } +/// } +/// +/// fn view(state: State) -> Picture { +/// paint.circle(int.to_float(state)) +/// } +/// +/// fn main() { +/// interact(init, update, view, "#mycanvas") +/// } +/// ``` +pub fn interact( + init: fn(Config) -> state, + update: fn(state, Event) -> state, + view: fn(state) -> Picture, + selector: String, +) { + let ctx = impl_canvas.get_rendering_context(selector) + let initial_state = + init(Config(impl_canvas.get_width(ctx), impl_canvas.get_height(ctx))) + + impl_canvas.set_global(initial_state, selector) + + // Handle keyboard input + let create_key_handler = fn(event_name, constructor) { + impl_canvas.setup_input_handler( + event_name, + fn(event: impl_canvas.KeyboardEvent) { + let key = parse_key_code(impl_canvas.get_key_code(event)) + case key { + Some(key) -> { + let assert Ok(old_state) = impl_canvas.get_global(selector) + let new_state = update(old_state, constructor(key)) + impl_canvas.set_global(new_state, selector) + } + None -> Nil + } + }, + ) + } + create_key_handler("keydown", event.KeyboardPressed) + create_key_handler("keyup", event.KeyboardRelased) + + // Handle mouse movement + impl_canvas.setup_input_handler( + "mousemove", + fn(event: impl_canvas.MouseEvent) { + let #(x, y) = impl_canvas.mouse_pos(ctx, event) + let assert Ok(old_state) = impl_canvas.get_global(selector) + let new_state = update(old_state, event.MouseMoved(x, y)) + impl_canvas.set_global(new_state, selector) + Nil + }, + ) + + // Handle mouse buttons + let create_mouse_button_handler = fn(event_name, constructor, check_pressed) { + impl_canvas.setup_input_handler( + event_name, + fn(event: impl_canvas.MouseEvent) { + // Read the previous state of the mouse + let previous_event_id = "PAINT_PREVIOUS_MOUSE_INPUT_FOR_" <> selector + let previous_event = impl_canvas.get_global(previous_event_id) + // Save this state + impl_canvas.set_global(event, previous_event_id) + + // A utility to check which buttons was just pressed/released + let check_button = fn(i) { + impl_canvas.check_mouse_button( + event, + previous_event, + i, + check_pressed, + ) + } + + let trigger_update = fn(button) { + let assert Ok(old_state) = impl_canvas.get_global(selector) + let new_state = update(old_state, constructor(button)) + impl_canvas.set_global(new_state, selector) + } + + // Note: it is rather rare, but it seems that multiple buttons + // can be pressed in the very same MouseEvent, so we may need to + // trigger multiple events at once. + case check_button(0) { + True -> trigger_update(event.MouseButtonLeft) + False -> Nil + } + case check_button(1) { + True -> trigger_update(event.MouseButtonRight) + False -> Nil + } + case check_button(2) { + True -> trigger_update(event.MouseButtonMiddle) + False -> Nil + } + + Nil + }, + ) + } + create_mouse_button_handler("mousedown", event.MousePressed, True) + create_mouse_button_handler("mouseup", event.MouseReleased, False) + + impl_canvas.setup_request_animation_frame(get_tick_func( + ctx, + view, + update, + selector, + )) +} + +fn parse_key_code(key_code: Int) -> Option(event.Key) { + case key_code { + 32 -> Some(event.KeySpace) + 37 -> Some(event.KeyLeftArrow) + 38 -> Some(event.KeyUpArrow) + 39 -> Some(event.KeyRightArrow) + 40 -> Some(event.KeyDownArrow) + 87 -> Some(event.KeyW) + 65 -> Some(event.KeyA) + 83 -> Some(event.KeyS) + 68 -> Some(event.KeyD) + 90 -> Some(event.KeyZ) + 88 -> Some(event.KeyX) + 67 -> Some(event.KeyC) + 18 -> Some(event.KeyEnter) + 27 -> Some(event.KeyEscape) + 8 -> Some(event.KeyBackspace) + _ -> None + } +} + +// Gleam does not have recursive let bindings, so I need +// to do this workaround... +fn get_tick_func(ctx, view, update, selector) { + fn(time) { + let assert Ok(current_state) = impl_canvas.get_global(selector) + + // Trigger a tick event before drawing + let new_state = update(current_state, event.Tick(time)) + impl_canvas.set_global(new_state, selector) + + // Create the picture + let picture = view(new_state) + + // Render the picture on the canvas + impl_canvas.reset(ctx) + display_on_rendering_context(picture, ctx, default_drawing_state) + impl_canvas.setup_request_animation_frame( + // call myself + get_tick_func(ctx, view, update, selector), + ) + } +} + +/// If you are using [Lustre](https://github.com/lustre-labs/lustre) or some other framework to build +/// your web application you may prefer to use the [web components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) API +/// and the `define_web_component` function. +/// ``` +/// // Call this function once to register a custom HTML element +/// canvas.define_web_component() +/// // You can then display your picture by setting the "picture" +/// // property or attribute on the element. +/// +/// // In Lustre it would look something like this: +/// fn canvas(picture: paint.Picture, attributes: List(attribute.Attribute(a))) { +/// element.element( +/// "paint-canvas", +/// [attribute.attribute("picture", encode.to_string(picture)), ..attributes], +/// [], +/// ) +///} +/// ``` +/// A more detailed example for using this API can be found in the `demos/with_lustre` directory. +pub fn define_web_component() -> Nil { + impl_canvas.define_web_component() + // somewhat of an ugly hack, but the setter for the web component will need to call this + // when the picture property changes. Therefore we + // bind this function to the window object so we can access it from the JS side of things. + // + // However, we should be careful of changing this. It is not part of the public API but it seems like + // [Tiramisu](https://hexdocs.pm/tiramisu/index.html) might makes use of it. + impl_canvas.set_global( + fn(encoded_picture, ctx) { + let assert Ok(picture) = encode.from_string(encoded_picture) + as "Invalid picture provided to web component" + display_on_rendering_context(picture, ctx, default_drawing_state) + }, + "display_on_rendering_context_with_default_drawing_state", + ) +} + +/// Utility to set the origin in the center of the canvas +pub fn center(picture: Picture) -> fn(Config) -> Picture { + fn(config) { + let Config(width, height) = config + picture |> translate_xy(width *. 0.5, height *. 0.5) + } +} diff --git a/build/packages/paint/src/paint/encode.gleam b/build/packages/paint/src/paint/encode.gleam new file mode 100644 index 0000000..96aea01 --- /dev/null +++ b/build/packages/paint/src/paint/encode.gleam @@ -0,0 +1,260 @@ +import gleam/dynamic/decode.{type Decoder} +import gleam/json.{type Json} +import gleam_community/colour +import paint.{type Picture} +import paint/internal/types.{ + type Angle, type FontProperties, type StrokeProperties, FontProperties, + NoStroke, Radians, SolidStroke, +} + +/// Serialize a `Picture` to a string. +/// +/// Note, serializing an `Image` texture will only store an ID referencing the image. This means that if you deserialize a Picture containing +/// references to images, you are responsible for making sure all images are loaded before drawing the picture. +/// More advanced APIs to support use cases such as these are planned for a future release. +/// +/// Also, if you wish to store the serialized data, remember that the library currently makes no stability guarantee that +/// the data can be deserialized by *future* versions of the library. +pub fn to_string(picture: Picture) -> String { + let version = "paint:unstable" + json.object([ + #("version", json.string(version)), + #("picture", picture_to_json(picture)), + ]) + |> json.to_string +} + +/// Attempt to deserialize a `Picture` +pub fn from_string(string: String) { + let decoder = { + use picture <- decode.field("picture", decode_picture()) + decode.success(picture) + } + json.parse(string, decoder) +} + +fn decode_angle() { + use radians <- decode.field("radians", decode.float) + decode.success(Radians(radians)) +} + +fn decode_picture() -> Decoder(Picture) { + use <- decode.recursive + use ty <- decode.field("type", decode.string) + + case ty { + "arc" -> { + use radius <- decode.field("radius", decode.float) + use start <- decode.field("start", decode_angle()) + use end <- decode.field("end", decode_angle()) + decode.success(types.Arc(radius, start:, end:)) + } + "blank" -> decode.success(types.Blank) + "combine" -> { + use pictures <- decode.field( + "pictures", + decode.list(of: decode_picture()), + ) + decode.success(types.Combine(pictures)) + } + "fill" -> { + use picture <- decode.field("picture", decode_picture()) + use colour <- decode.field("colour", colour.decoder()) + decode.success(types.Fill(picture, colour)) + } + "polygon" -> { + use points <- decode.field("points", decode.list(of: decode_vec2())) + use closed <- decode.field("closed", decode.bool) + decode.success(types.Polygon(points, closed)) + } + "rotate" -> { + use angle <- decode.field("angle", decode_angle()) + use picture <- decode.field("picture", decode_picture()) + decode.success(types.Rotate(picture, angle)) + } + "scale" -> { + use x <- decode.field("x", decode.float) + use y <- decode.field("y", decode.float) + use picture <- decode.field("picture", decode_picture()) + decode.success(types.Scale(picture, #(x, y))) + } + "stroke" -> { + use stroke <- decode.field("stroke", decode_stroke()) + use picture <- decode.field("picture", decode_picture()) + decode.success(types.Stroke(picture, stroke)) + } + "text" -> { + use text <- decode.field("text", decode.string) + use style <- decode.field("style", decode_font()) + decode.success(types.Text(text, style)) + } + "translate" -> { + use x <- decode.field("x", decode.float) + use y <- decode.field("y", decode.float) + use picture <- decode.field("picture", decode_picture()) + decode.success(types.Translate(picture, #(x, y))) + } + "image" -> { + use id <- decode.field("id", decode.string) + use width_px <- decode.field("width_px", decode.int) + use height_px <- decode.field("height_px", decode.int) + decode.success(types.ImageRef(types.Image(id:), width_px:, height_px:)) + } + "image_scaling_behaviour" -> { + use behaviour <- decode.field("behaviour", decode.string) + use picture <- decode.field("picture", decode_picture()) + case behaviour { + "smooth" -> + decode.success(types.ImageScalingBehaviour( + picture, + types.ScalingSmooth, + )) + "pixelated" -> + decode.success(types.ImageScalingBehaviour( + picture, + types.ScalingPixelated, + )) + _ -> decode.failure(types.Blank, "Picture") + } + } + _ -> decode.failure(types.Blank, "Picture") + } +} + +fn decode_font() -> Decoder(FontProperties) { + use size_px <- decode.field("sizePx", decode.int) + use font_family <- decode.field("fontFamily", decode.string) + decode.success(FontProperties(size_px:, font_family:)) +} + +fn decode_stroke() -> Decoder(StrokeProperties) { + use stroke_type <- decode.field("type", decode.string) + case stroke_type { + "noStroke" -> decode.success(NoStroke) + "solidStroke" -> { + use colour <- decode.field("colour", colour.decoder()) + use thickness <- decode.field("thickness", decode.float) + decode.success(SolidStroke(colour, thickness)) + } + _ -> decode.failure(NoStroke, "StrokeProperties") + } +} + +fn decode_vec2() -> Decoder(#(Float, Float)) { + use x <- decode.field("x", decode.float) + use y <- decode.field("y", decode.float) + decode.success(#(x, y)) +} + +fn picture_to_json(picture: Picture) -> Json { + case picture { + types.Arc(radius:, start:, end:) -> + json.object([ + #("type", json.string("arc")), + #("radius", json.float(radius)), + #("start", angle_to_json(start)), + #("end", angle_to_json(end)), + ]) + types.Blank -> json.object([#("type", json.string("blank"))]) + types.Combine(from) -> + json.object([ + #("type", json.string("combine")), + #("pictures", json.array(from:, of: picture_to_json)), + ]) + types.Fill(picture, colour) -> + json.object([ + #("type", json.string("fill")), + #("colour", colour.encode(colour)), + #("picture", picture_to_json(picture)), + ]) + types.Polygon(points, closed:) -> + json.object([ + #("type", json.string("polygon")), + #( + "points", + json.array(from: points, of: fn(point) { + let #(x, y) = point + json.object([#("x", json.float(x)), #("y", json.float(y))]) + }), + ), + #("closed", json.bool(closed)), + ]) + types.Rotate(picture, angle) -> + json.object([ + #("type", json.string("rotate")), + #("angle", angle_to_json(angle)), + #("picture", picture_to_json(picture)), + ]) + types.Scale(picture, #(x, y)) -> + json.object([ + #("type", json.string("scale")), + #("x", json.float(x)), + #("y", json.float(y)), + #("picture", picture_to_json(picture)), + ]) + types.Stroke(picture, stroke) -> + json.object([ + #("type", json.string("stroke")), + #("stroke", stroke_to_json(stroke)), + #("picture", picture_to_json(picture)), + ]) + types.Text(text:, style:) -> + json.object([ + #("type", json.string("text")), + #("text", json.string(text)), + #("style", font_to_json(style)), + ]) + types.Translate(picture, #(x, y)) -> + json.object([ + #("type", json.string("translate")), + #("x", json.float(x)), + #("y", json.float(y)), + #("picture", picture_to_json(picture)), + ]) + types.ImageRef(types.Image(id:), width_px:, height_px:) -> { + json.object([ + #("type", json.string("image")), + #("id", json.string(id)), + #("width_px", json.int(width_px)), + #("height_px", json.int(height_px)), + ]) + } + types.ImageScalingBehaviour(picture, behaviour) -> + json.object([ + #("type", json.string("image_scaling_behaviour")), + #( + "behaviour", + json.string(case behaviour { + types.ScalingPixelated -> "pixelated" + types.ScalingSmooth -> "smooth" + }), + ), + #("picture", picture_to_json(picture)), + ]) + } +} + +fn font_to_json(font: FontProperties) -> Json { + let FontProperties(size_px:, font_family:) = font + json.object([ + #("sizePx", json.int(size_px)), + #("fontFamily", json.string(font_family)), + ]) +} + +fn stroke_to_json(stroke: StrokeProperties) -> Json { + case stroke { + NoStroke -> json.object([#("type", json.string("noStroke"))]) + SolidStroke(colour, thickness) -> + json.object([ + #("type", json.string("solidStroke")), + #("colour", colour.encode(colour)), + #("thickness", json.float(thickness)), + ]) + } +} + +fn angle_to_json(angle: Angle) -> Json { + let Radians(rad) = angle + json.object([#("radians", json.float(rad))]) +} diff --git a/build/packages/paint/src/paint/event.gleam b/build/packages/paint/src/paint/event.gleam new file mode 100644 index 0000000..08298de --- /dev/null +++ b/build/packages/paint/src/paint/event.gleam @@ -0,0 +1,50 @@ +//// This module contains events that can be triggered when +//// building interactive applications. +//// +//// See `paint/canvas` and the `canvas.interact` function for a +//// practical example of how this is used. + +pub type Event { + /// Triggered before drawing. Contains the number of milliseconds elapsed. + Tick(Float) + /// Triggered when a key is pressed + KeyboardPressed(Key) + /// Triggered when a key is released + KeyboardRelased(Key) + /// Triggered when the mouse is moved. Contains + /// the `x` and `y` value for the mouse position. + MouseMoved(Float, Float) + /// Triggered when a mouse button is pressed + MousePressed(MouseButton) + /// Triggered when a mouse button is released. + /// + /// Note, on the web you might encounter issues where the + /// release event for the right mouse button is not triggered + /// because of the context menu. + MouseReleased(MouseButton) +} + +pub type Key { + KeyLeftArrow + KeyRightArrow + KeyUpArrow + KeyDownArrow + KeySpace + KeyW + KeyA + KeyS + KeyD + KeyZ + KeyX + KeyC + KeyEnter + KeyEscape + KeyBackspace +} + +pub type MouseButton { + MouseButtonLeft + MouseButtonRight + /// The scroll wheel button + MouseButtonMiddle +} diff --git a/build/packages/paint/src/paint/internal/impl_canvas.gleam b/build/packages/paint/src/paint/internal/impl_canvas.gleam new file mode 100644 index 0000000..2b94e9a --- /dev/null +++ b/build/packages/paint/src/paint/internal/impl_canvas.gleam @@ -0,0 +1,118 @@ +pub type RenderingContext2D + +@external(javascript, "./../../impl_canvas_bindings.mjs", "define_web_component") +pub fn define_web_component() -> Nil + +// TODO: forward the timestamp from the callback +@external(javascript, "./../../impl_canvas_bindings.mjs", "setup_request_animation_frame") +pub fn setup_request_animation_frame(callback: fn(Float) -> Nil) -> Nil + +@external(javascript, "./../../impl_canvas_bindings.mjs", "get_rendering_context") +pub fn get_rendering_context(selector: String) -> RenderingContext2D + +@external(javascript, "../../impl_canvas_bindings.mjs", "setup_input_handler") +pub fn setup_input_handler(event: String, callback: fn(event) -> Nil) -> Nil + +pub type KeyboardEvent + +@external(javascript, "./../../impl_canvas_bindings.mjs", "get_key_code") +pub fn get_key_code(event: KeyboardEvent) -> Int + +pub type MouseEvent + +@external(javascript, "./../../impl_canvas_bindings.mjs", "mouse_pos") +pub fn mouse_pos(ctx: RenderingContext2D, event: MouseEvent) -> #(Float, Float) + +@external(javascript, "./../../impl_canvas_bindings.mjs", "check_mouse_button") +pub fn check_mouse_button( + event: MouseEvent, + previous_event: Result(MouseEvent, Nil), + button_index: Int, + check_pressed check_pressed: Bool, +) -> Bool + +@external(javascript, "../../impl_canvas_bindings.mjs", "get_width") +pub fn get_width(ctx: RenderingContext2D) -> Float + +@external(javascript, "../../impl_canvas_bindings.mjs", "get_height") +pub fn get_height(ctx: RenderingContext2D) -> Float + +@external(javascript, "../../impl_canvas_bindings.mjs", "set_global") +pub fn set_global(state: state, id: String) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "get_global") +pub fn get_global(id: String) -> Result(state, Nil) + +@external(javascript, "../../impl_canvas_bindings.mjs", "reset") +pub fn reset(ctx: RenderingContext2D) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "save") +pub fn save(ctx: RenderingContext2D) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "restore") +pub fn restore(ctx: RenderingContext2D) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "translate") +pub fn translate(ctx: RenderingContext2D, x: Float, y: Float) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "scale") +pub fn scale(ctx: RenderingContext2D, x: Float, y: Float) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "rotate") +pub fn rotate(ctx: RenderingContext2D, radians: Float) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "reset_transform") +pub fn reset_transform(ctx: RenderingContext2D) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "set_fill_colour") +pub fn set_fill_colour(ctx: RenderingContext2D, css_colour: String) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "set_stroke_color") +pub fn set_stroke_color(ctx: RenderingContext2D, css_color: String) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "set_line_width") +pub fn set_line_width(ctx: RenderingContext2D, width: Float) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "set_image_smoothing_enabled") +pub fn set_image_smoothing_enabled(ctx: RenderingContext2D, value: Bool) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "arc") +pub fn arc( + ctx: RenderingContext2D, + radius: Float, + start: Float, + end: Float, + fill: Bool, + stroke: Bool, +) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "polygon") +pub fn polygon( + ctx: RenderingContext2D, + points: List(#(Float, Float)), + closed: Bool, + fill: Bool, + stroke: Bool, +) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "text") +pub fn text(ctx: RenderingContext2D, text: String, style: String) -> Nil + +pub type JsImage + +@external(javascript, "../../impl_canvas_bindings.mjs", "draw_image") +pub fn draw_image( + ctx: RenderingContext2D, + image: JsImage, + width_px: Int, + height_px: Int, +) -> Nil + +@external(javascript, "../../impl_canvas_bindings.mjs", "image_from_query") +pub fn image_from_query(selector: String) -> JsImage + +@external(javascript, "../../impl_canvas_bindings.mjs", "image_from_src") +pub fn image_from_src(src: String) -> JsImage + +@external(javascript, "../../impl_canvas_bindings.mjs", "on_image_load") +pub fn on_image_load(image: JsImage, callback: fn() -> Nil) -> Nil diff --git a/build/packages/paint/src/paint/internal/types.gleam b/build/packages/paint/src/paint/internal/types.gleam new file mode 100644 index 0000000..4cd6b5c --- /dev/null +++ b/build/packages/paint/src/paint/internal/types.gleam @@ -0,0 +1,48 @@ +import gleam_community/colour.{type Colour} + +pub type Picture { + // Shapes + Blank + Polygon(List(Vec2), closed: Bool) + Arc(radius: Float, start: Angle, end: Angle) + Text(text: String, style: FontProperties) + ImageRef(Image, width_px: Int, height_px: Int) + // Styling + // TODO: font + Fill(Picture, Colour) + Stroke(Picture, StrokeProperties) + ImageScalingBehaviour(Picture, ImageScalingBehaviour) + // Transform + Translate(Picture, Vec2) + Scale(Picture, Vec2) + Rotate(Picture, Angle) + // Combine + Combine(List(Picture)) +} + +// The ID for an image +// Invariant: the image object is assumed to already be created and stored somewhere (like the PAINT_STATE for the canvas backend) +pub type Image { + Image(id: String) +} + +pub type ImageScalingBehaviour { + ScalingSmooth + ScalingPixelated +} + +pub type StrokeProperties { + NoStroke + SolidStroke(Colour, Float) +} + +pub type FontProperties { + FontProperties(size_px: Int, font_family: String) +} + +pub type Angle { + Radians(Float) +} + +pub type Vec2 = + #(Float, Float) diff --git a/dev_server.py b/dev_server.py new file mode 100644 index 0000000..e87b2ab --- /dev/null +++ b/dev_server.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +import os +from livereload import Server + +server = Server() + +server.watch('build/dev/javascript/stellar_prune/game.mjs') +server.watch('index.html') + +server.serve(port=3000, host='0.0.0.0', root='.') diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..78d0eda --- /dev/null +++ b/gleam.toml @@ -0,0 +1,22 @@ +name = "stellar_prune" +version = "1.0.0" +target = "javascript" + +# 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.44.0 and < 2.0.0" +paint = ">= 1.0.0 and < 2.0.0" +gleam_time = ">= 1.6.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/index.html b/index.html new file mode 100644 index 0000000..50f716e --- /dev/null +++ b/index.html @@ -0,0 +1,59 @@ + + + + + + Stellar prune + + + + + + + diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..7890632 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,17 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, + { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, + { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, + { name = "paint", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_json", "gleam_stdlib"], otp_app = "paint", source = "hex", outer_checksum = "7DD98FE1C1C27D5B4257BB4A08B6E22FD0DCA38CEEEDB2FEE9CB8676C4A7A8E8" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleam_time = { version = ">= 1.6.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +paint = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/res/diamond.svg b/res/diamond.svg new file mode 100644 index 0000000..dbee7c4 --- /dev/null +++ b/res/diamond.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/grain.png b/res/grain.png new file mode 100644 index 0000000..3a388db Binary files /dev/null and b/res/grain.png differ diff --git a/res/heart.svg b/res/heart.svg new file mode 100644 index 0000000..dfca6d1 --- /dev/null +++ b/res/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/lucy.svg b/res/lucy.svg new file mode 100644 index 0000000..81a3ce2 --- /dev/null +++ b/res/lucy.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/no-heart.svg b/res/no-heart.svg new file mode 100644 index 0000000..d5368f3 --- /dev/null +++ b/res/no-heart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/rocket.svg b/res/rocket.svg new file mode 100644 index 0000000..237f0f9 --- /dev/null +++ b/res/rocket.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..e5c2005 --- /dev/null +++ b/shell.nix @@ -0,0 +1,11 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + gleam + erlang + watchexec + python3 + python3Packages.livereload + ]; +} diff --git a/src/browser_ffi.mjs b/src/browser_ffi.mjs new file mode 100644 index 0000000..ef62a4c --- /dev/null +++ b/src/browser_ffi.mjs @@ -0,0 +1,40 @@ +let mouseX = 0; +let mouseY = 0; +let mousePressed = false; + +if (typeof window !== 'undefined') { + const canvas = document.getElementById('canvas'); + + window.addEventListener('mousemove', (e) => { + if (canvas) { + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + mouseX = (e.clientX - rect.left) * scaleX; + mouseY = (e.clientY - rect.top) * scaleY; + } else { + mouseX = e.clientX; + mouseY = e.clientY; + } + }); + + window.addEventListener('mousedown', () => { + mousePressed = true; + }); + + window.addEventListener('mouseup', () => { + mousePressed = false; + }); +} + +export function request_animation_frame(callback) { + requestAnimationFrame(() => callback()); +} + +export function get_mouse_position() { + return { x: mouseX, y: mouseY }; +} + +export function is_mouse_pressed() { + return mousePressed; +} diff --git a/src/game.gleam b/src/game.gleam new file mode 100644 index 0000000..bece8d6 --- /dev/null +++ b/src/game.gleam @@ -0,0 +1,263 @@ +import gleam/list +import hud.{CrossHair, Health, Hud, Score} +import paint +import physics.{type Vec2, Gravity, Physics, Vec2} +import rendering +import target.{type Target, Diamond, Lucy, Rocket} +import types.{ + type GameState, type PlayingState, GameOver, MainMenu, Playing, PlayingState, +} + +pub type TargetClickEffect { + TargetClickEffect(score_change: Int, health_change: Int) +} + +pub type TargetFilterResult { + TargetFilterResult(remaining_targets: List(Target), health_loss: Int) +} + +pub type SpawnResult { + SpawnResult(targets: List(Target), spawn_timer: Float) +} + +@external(javascript, "./browser_ffi.mjs", "request_animation_frame") +pub fn request_animation_frame(callback: fn() -> Nil) -> Nil + +@external(javascript, "./browser_ffi.mjs", "get_mouse_position") +fn get_mouse_position() -> Vec2 + +@external(javascript, "./browser_ffi.mjs", "is_mouse_pressed") +pub fn is_mouse_pressed() -> Bool + +pub fn main() { + game_loop(MainMenu) +} + +fn init_playing_state() -> PlayingState { + PlayingState( + targets: [], + physics: Physics(gravity: Gravity(acceleration: Vec2(0.0, 0.08))), + hud: Hud( + score: Score( + position: Vec2(x: 1050.0, y: 70.0), + colorable: target.Colorable(color: paint.colour_hex("#ffffff")), + score: 0, + ), + cross_hair: CrossHair( + position: Vec2(0.0, 0.0), + colorable: target.Colorable(color: paint.colour_hex("#ffffff")), + size: 5.0, + ), + health: Health( + position: Vec2(x: 30.0, y: 30.0), + size: 120.0, + amount: 3, + max_health: 3, + ), + ), + mouse: Vec2(0.0, 0.0), + spawn_timer: 0.0, + ) +} + +fn get_target_click_effects(kind: target.TargetKind) -> TargetClickEffect { + case kind { + Lucy -> TargetClickEffect(score_change: 1, health_change: 0) + Diamond(points_value) -> + TargetClickEffect(score_change: points_value, health_change: 0) + Rocket -> TargetClickEffect(score_change: 0, health_change: -1) + } +} + +fn filter_out_of_bounds_targets( + targets: List(Target), +) -> TargetFilterResult { + list.fold( + targets, + TargetFilterResult(remaining_targets: [], health_loss: 0), + fn(acc, target) { + case target.position.y >. 900.0 { + True -> { + case target.kind { + Rocket -> acc + _ -> + TargetFilterResult( + remaining_targets: acc.remaining_targets, + health_loss: acc.health_loss + 1, + ) + } + } + False -> + TargetFilterResult( + remaining_targets: [target, ..acc.remaining_targets], + health_loss: acc.health_loss, + ) + } + }, + ) +} + +fn should_spawn_new_target( + spawn_timer: Float, + active_targets: List(Target), + score: Int, +) -> Bool { + let spawn_interval = 60.0 + let max_targets = 1 + score / 5 + spawn_timer >=. spawn_interval && list.length(active_targets) < max_targets +} + +fn handle_target_click( + playing_state: PlayingState, + clicked_target: Target, +) -> PlayingState { + let new_targets = + list.filter(playing_state.targets, fn(target) { target != clicked_target }) + + let effect = get_target_click_effects(clicked_target.kind) + + PlayingState( + ..playing_state, + targets: new_targets, + hud: Hud( + ..playing_state.hud, + score: Score( + ..playing_state.hud.score, + score: playing_state.hud.score.score + effect.score_change, + ), + health: Health( + ..playing_state.hud.health, + amount: playing_state.hud.health.amount + effect.health_change, + ), + ), + ) +} + +fn handle_playing_click(playing_state: PlayingState) -> PlayingState { + case find_clicked_target(playing_state.mouse, playing_state.targets) { + Ok(clicked_target) -> handle_target_click(playing_state, clicked_target) + Error(_) -> playing_state + } +} + +fn update_playing_state(playing_state: PlayingState) -> PlayingState { + let Physics(gravity) = playing_state.physics + + let updated_targets = + list.map(playing_state.targets, fn(target) { + target.update_target(target, gravity.acceleration) + }) + + let filter_result = filter_out_of_bounds_targets(updated_targets) + + let new_spawn_timer = playing_state.spawn_timer +. 1.0 + + let spawn_result = case + should_spawn_new_target( + new_spawn_timer, + filter_result.remaining_targets, + playing_state.hud.score.score, + ) + { + True -> + SpawnResult( + targets: [target.create_target(), ..filter_result.remaining_targets], + spawn_timer: 0.0, + ) + False -> + SpawnResult( + targets: filter_result.remaining_targets, + spawn_timer: new_spawn_timer, + ) + } + + let new_mouse = get_mouse_position() + + PlayingState( + ..playing_state, + targets: spawn_result.targets, + spawn_timer: spawn_result.spawn_timer, + hud: Hud( + ..playing_state.hud, + cross_hair: CrossHair(..playing_state.hud.cross_hair, position: new_mouse), + health: Health( + ..playing_state.hud.health, + amount: playing_state.hud.health.amount - filter_result.health_loss, + ), + ), + mouse: new_mouse, + ) +} + +fn update_state(state: GameState) -> GameState { + case state { + MainMenu -> MainMenu + Playing(playing_state, prev_mouse) -> + Playing(update_playing_state(playing_state), prev_mouse) + GameOver(score, prev_mouse) -> GameOver(score, prev_mouse) + } +} + +fn find_clicked_target( + mouse: Vec2, + targets: List(Target), +) -> Result(Target, Nil) { + list.find(targets, fn(target) { target.is_point_in_target(mouse, target) }) +} + +fn handle_main_menu_click(mouse_pressed: Bool) -> GameState { + case mouse_pressed { + True -> Playing(init_playing_state(), False) + False -> MainMenu + } +} + +fn handle_playing_state( + playing_state: PlayingState, + mouse_pressed: Bool, +) -> GameState { + let new_state = case mouse_pressed { + True -> handle_playing_click(playing_state) + False -> playing_state + } + + case new_state.hud.health.amount <= 0 { + True -> GameOver(new_state.hud.score.score, mouse_pressed) + False -> Playing(new_state, mouse_pressed) + } +} + +fn handle_game_over_click( + final_score: Int, + mouse_pressed: Bool, + previous_mouse_pressed: Bool, +) -> GameState { + let just_clicked = mouse_pressed && !previous_mouse_pressed + case just_clicked { + True -> Playing(init_playing_state(), False) + False -> GameOver(final_score, mouse_pressed) + } +} + +fn handle_events(state: GameState) -> GameState { + let current_mouse_pressed = is_mouse_pressed() + + case state { + MainMenu -> handle_main_menu_click(current_mouse_pressed) + Playing(playing_state, _) -> + handle_playing_state(playing_state, current_mouse_pressed) + GameOver(final_score, previous_mouse_pressed) -> + handle_game_over_click( + final_score, + current_mouse_pressed, + previous_mouse_pressed, + ) + } +} + +fn game_loop(state: GameState) { + rendering.render(state) + request_animation_frame(fn() { + handle_events(state) |> update_state() |> game_loop() + }) +} diff --git a/src/hud.gleam b/src/hud.gleam new file mode 100644 index 0000000..77bb4c6 --- /dev/null +++ b/src/hud.gleam @@ -0,0 +1,73 @@ +import gleam/float +import gleam/int +import paint +import paint/canvas +import physics.{type Vec2, Vec2} +import target.{type Colorable, Colorable} + +pub type Score { + Score(position: Vec2, colorable: Colorable, score: Int) +} + +pub type CrossHair { + CrossHair(position: Vec2, colorable: Colorable, size: Float) +} + +pub type Health { + Health(position: Vec2, size: Float, amount: Int, max_health: Int) +} + +pub type Hud { + Hud(score: Score, cross_hair: CrossHair, health: Health) +} + +pub fn score_to_picture(score: Score) -> paint.Picture { + let Score(position, Colorable(color), score) = score + paint.text("Score: " <> int.to_string(score), 32) + |> paint.translate_xy(position.x, position.y) + |> paint.fill(color) +} + +pub fn cross_hair_to_picture(cross_hair: CrossHair) -> paint.Picture { + let CrossHair(Vec2(x, y), Colorable(color), size) = cross_hair + paint.circle(size) + |> paint.stroke(color, 5.0) + |> paint.translate_xy(x, y) +} + +fn health_to_picture_helper( + health: Health, + acc: paint.Picture, + count: Int, +) -> paint.Picture { + case count < health.max_health { + True -> { + let heart_image = case count < health.amount { + True -> canvas.image_from_src("../res/heart.svg") + False -> canvas.image_from_src("../res/no-heart.svg") + } + + let size = float.round(health.size) + + health_to_picture_helper( + health, + paint.combine([ + acc, + paint.image(heart_image, size, size) + |> paint.image_scaling_pixelated() + |> paint.translate_xy( + int.to_float(count) *. { health.size +. 10.0 }, + 0.0, + ), + ]), + count + 1, + ) + } + False -> acc + } +} + +pub fn health_to_picture(health: Health) -> paint.Picture { + health_to_picture_helper(health, paint.blank(), 0) + |> paint.translate_xy(health.position.x, health.position.y) +} diff --git a/src/math/math.gleam b/src/math/math.gleam new file mode 100644 index 0000000..37c7a5f --- /dev/null +++ b/src/math/math.gleam @@ -0,0 +1,28 @@ +@external(javascript, "./math_ffi.mjs", "sin") +pub fn sin(x: Float) -> Float + +@external(javascript, "./math_ffi.mjs", "cos") +pub fn cos(x: Float) -> Float + +@external(javascript, "./math_ffi.mjs", "tan") +pub fn tan(x: Float) -> Float + +@external(javascript, "./math_ffi.mjs", "atan2") +pub fn atan2(y: Float, x: Float) -> Float + +@external(javascript, "./math_ffi.mjs", "floor") +pub fn floor(x: Float) -> Float + +@external(javascript, "./math_ffi.mjs", "ceil") +pub fn ceil(x: Float) -> Float + +@external(javascript, "./math_ffi.mjs", "round") +pub fn round(x: Float) -> Float + +@external(javascript, "./math_ffi.mjs", "random") +pub fn random() -> Float + +@external(javascript, "./math_ffi.mjs", "pi") +pub fn pi() -> Float { + 3.1415926 +} diff --git a/src/math/math_ffi.mjs b/src/math/math_ffi.mjs new file mode 100644 index 0000000..8736bc2 --- /dev/null +++ b/src/math/math_ffi.mjs @@ -0,0 +1,35 @@ +export function sin(x) { + return Math.sin(x); +} + +export function cos(x) { + return Math.cos(x); +} + +export function tan(x) { + return Math.tan(x); +} + +export function atan2(y, x) { + return Math.atan2(y, x); +} + +export function floor(x) { + return Math.floor(x); +} + +export function ceil(x) { + return Math.ceil(x); +} + +export function round(x) { + return Math.round(x); +} + +export function random() { + return Math.random(); +} + +export function pi() { + return Math.PI; +} diff --git a/src/physics.gleam b/src/physics.gleam new file mode 100644 index 0000000..d3de569 --- /dev/null +++ b/src/physics.gleam @@ -0,0 +1,24 @@ +pub type Vec2 { + Vec2(x: Float, y: Float) +} + +pub type RigidBody { + RigidBody( + velocity: Vec2, + acceleration: Vec2, + rotation: Float, + rotation_velocity: Float, + ) +} + +pub type Gravity { + Gravity(acceleration: Vec2) +} + +pub type Physics { + Physics(gravity: Gravity) +} + +pub fn add_vec2(v1: Vec2, v2: Vec2) -> Vec2 { + Vec2(v1.x +. v2.x, v1.y +. v2.y) +} diff --git a/src/rendering.gleam b/src/rendering.gleam new file mode 100644 index 0000000..c372fe7 --- /dev/null +++ b/src/rendering.gleam @@ -0,0 +1,107 @@ +import gleam/int +import gleam/list +import hud +import paint +import paint/canvas +import target +import types.{type GameState, type PlayingState, GameOver, MainMenu, Playing} + +fn draw_main_menu() -> paint.Picture { + let lucy_image = canvas.image_from_src("../res/lucy.svg") + let diamond_image = canvas.image_from_src("../res/diamond.svg") + let rocket_image = canvas.image_from_src("../res/rocket.svg") + + paint.combine([ + paint.text("Stellar prune", 72) + |> paint.translate_xy(300.0, 100.0) + |> paint.fill(paint.colour_hex("#ffffff")) + |> paint.image_scaling_pixelated(), + paint.text("Click/swipe the targets to score points!", 28) + |> paint.translate_xy(320.0, 200.0) + |> paint.fill(paint.colour_hex("#aaaaaa")) + |> paint.image_scaling_pixelated(), + paint.text("Don't let them escape or you'll lose health!", 28) + |> paint.translate_xy(320.0, 240.0) + |> paint.fill(paint.colour_hex("#aaaaaa")) + |> paint.image_scaling_pixelated(), + paint.image(lucy_image, 80, 80) + |> paint.image_scaling_pixelated() + |> paint.translate_xy(320.0, 320.0), + paint.text("Lucy - Click for 1 point.", 24) + |> paint.translate_xy(420.0, 370.0) + |> paint.fill(paint.colour_hex("#ffffff")) + |> paint.image_scaling_pixelated(), + paint.image(diamond_image, 80, 80) + |> paint.image_scaling_pixelated() + |> paint.translate_xy(320.0, 420.0), + paint.text("Diamond - Click for 3 points!", 24) + |> paint.translate_xy(420.0, 460.0) + |> paint.fill(paint.colour_hex("#ffffff")) + |> paint.image_scaling_pixelated(), + paint.image(rocket_image, 80, 80) + |> paint.image_scaling_pixelated() + |> paint.translate_xy(320.0, 520.0), + paint.text("Rocket - AVOID! Loses 1 health", 24) + |> paint.translate_xy(420.0, 560.0) + |> paint.fill(paint.colour_hex("#ff6666")) + |> paint.image_scaling_pixelated(), + paint.text("Click to play!", 48) + |> paint.translate_xy(420.0, 680.0) + |> paint.fill(paint.colour_hex("#ffaff3")) + |> paint.image_scaling_pixelated(), + ]) +} + +fn draw_game_over(final_score: Int) -> paint.Picture { + paint.combine([ + paint.text("Well done!", 128) + |> paint.translate_xy(300.0, 175.0) + |> paint.fill(paint.colour_hex("#ffaff3")) + |> paint.image_scaling_pixelated(), + paint.text("Final Score: ", 48) + |> paint.translate_xy(440.0, 400.0) + |> paint.fill(paint.colour_hex("#ffffff")) + |> paint.image_scaling_pixelated(), + paint.text(int.to_string(final_score), 48) + |> paint.translate_xy(710.0, 400.0) + |> paint.fill(paint.colour_hex("#ffaff3")) + |> paint.image_scaling_pixelated(), + paint.text("Click to play again!", 32) + |> paint.translate_xy(455.0, 600.0) + |> paint.fill(paint.colour_hex("#ffaff3")) + |> paint.image_scaling_pixelated(), + ]) +} + +fn draw_playing(playing_state: PlayingState) -> paint.Picture { + let target_pictures = + list.map(playing_state.targets, fn(target) { + let target.Target(position, rigid_body, paintable, _kind) = target + target.paintable_to_picture(paintable) + |> paint.rotate(paint.angle_deg(rigid_body.rotation)) + |> paint.translate_xy(position.x, position.y) + }) + + let hud_without_crosshair = + paint.combine([ + hud.health_to_picture(playing_state.hud.health), + hud.score_to_picture(playing_state.hud.score), + ]) + + let crosshair = hud.cross_hair_to_picture(playing_state.hud.cross_hair) + + paint.combine([ + hud_without_crosshair, + ..list.append(target_pictures, [crosshair]) + ]) +} + +pub fn render(state: GameState) { + let picture = case state { + MainMenu -> draw_main_menu() + Playing(playing_state, _) -> draw_playing(playing_state) + GameOver(final_score, _) -> draw_game_over(final_score) + } + + canvas.display(fn(_: canvas.Config) { picture }, "#canvas") +} diff --git a/src/target.gleam b/src/target.gleam new file mode 100644 index 0000000..6b4def3 --- /dev/null +++ b/src/target.gleam @@ -0,0 +1,126 @@ +import gleam/int +import math/math +import paint +import paint/canvas +import physics.{type RigidBody, type Vec2, RigidBody, Vec2, add_vec2} + +pub type Colorable { + Colorable(color: paint.Colour) +} + +pub type Size { + Size(width: Int, height: Int) +} + +pub type Paintable { + Square(colorable: Colorable, size: Float) + Sprite(image: paint.Image, size: Size) +} + +pub type TargetKind { + Lucy + Diamond(points_value: Int) + Rocket +} + +pub type Target { + Target( + position: Vec2, + rigid_body: RigidBody, + paintable: Paintable, + kind: TargetKind, + ) +} + +pub fn create_target() -> Target { + let vx = math.random() *. 6.0 -. 3.0 + let vy = math.random() *. -4.0 -. 8.0 + let rotation_vel = math.random() *. 2.0 -. 1.0 + let start_x = math.random() *. 600.0 +. 200.0 + + let random_type = math.random() + let #(sprite_path, target_kind) = case random_type { + r if r <. 0.7 -> #("../res/lucy.svg", Lucy) + r if r <. 0.85 -> #("../res/diamond.svg", Diamond(points_value: 3)) + _ -> #("../res/rocket.svg", Rocket) + } + + Target( + position: Vec2(x: start_x, y: 900.0), + rigid_body: RigidBody( + velocity: Vec2(vx, vy), + acceleration: Vec2(0.0, 0.0), + rotation: 0.0, + rotation_velocity: rotation_vel, + ), + paintable: Sprite( + image: canvas.image_from_src(sprite_path), + size: Size(120, 120), + ), + kind: target_kind, + ) +} + +pub fn update_target(target: Target, gravity: Vec2) -> Target { + let RigidBody(velocity, acceleration, rotation, rotation_velocity) = + target.rigid_body + let total_acceleration = add_vec2(acceleration, gravity) + let new_velocity = add_vec2(velocity, total_acceleration) + let new_position = add_vec2(target.position, new_velocity) + + let new_rigid_body = + RigidBody( + new_velocity, + acceleration, + rotation +. rotation_velocity, + rotation_velocity, + ) + + Target(..target, rigid_body: new_rigid_body, position: new_position) +} + +pub fn is_point_in_target(point: Vec2, target: Target) -> Bool { + let dx = point.x -. target.position.x + let dy = point.y -. target.position.y + + let angle_rad = target.rigid_body.rotation *. math.pi() /. 180.0 + let cos_angle = math.cos(angle_rad) + let sin_angle = math.sin(angle_rad) + + let local_x = dx *. cos_angle +. dy *. sin_angle + let local_y = dy *. cos_angle -. dx *. sin_angle + + case target.paintable { + Square(_, size) -> { + let half_size = size /. 2.0 + local_x >=. 0.0 -. half_size + && local_x <=. half_size + && local_y >=. 0.0 -. half_size + && local_y <=. half_size + } + Sprite(_, size) -> { + let half_width = int.to_float(size.width) /. 2.0 + let half_height = int.to_float(size.height) /. 2.0 + local_x >=. 0.0 -. half_width + && local_x <=. half_width + && local_y >=. 0.0 -. half_height + && local_y <=. half_height + } + } +} + +pub fn paintable_to_picture(paintable: Paintable) -> paint.Picture { + case paintable { + Square(colorable, size) -> { + paint.square(size) |> paint.fill(colorable.color) + } + Sprite(image, size) -> { + paint.image(image, size.width, size.height) + |> paint.image_scaling_pixelated() + |> paint.translate_xy( + 0.0 -. int.to_float(size.width) /. 2.0, + 0.0 -. int.to_float(size.height) /. 2.0, + ) + } + } +} diff --git a/src/types.gleam b/src/types.gleam new file mode 100644 index 0000000..d5e4292 --- /dev/null +++ b/src/types.gleam @@ -0,0 +1,19 @@ +import hud.{type Hud} +import physics.{type Physics, type Vec2} +import target.{type Target} + +pub type PlayingState { + PlayingState( + targets: List(Target), + physics: Physics, + hud: Hud, + mouse: Vec2, + spawn_timer: Float, + ) +} + +pub type GameState { + MainMenu + Playing(state: PlayingState, previous_mouse_pressed: Bool) + GameOver(final_score: Int, previous_mouse_pressed: Bool) +}