numbat 1.23.0

A statically typed programming language for scientific computations with first class support for physical dimensions and units.
Documentation
use core::scalar
use core::functions
use core::error

@description("The length of a string")
@example("str_length(\"Numbat\")")
fn str_length(s: String) -> Scalar

@description("Subslice of a string")
@example("str_slice(3, 6, \"Numbat\")")
fn str_slice(start: Scalar, end: Scalar, s: String) -> String

@description("Get a single-character string from a Unicode code point.")
@example("0x2764 -> chr")
fn chr(n: Scalar) -> String

@description("Get the Unicode code point of the first character in a string.")
@example("\"❤\" -> ord")
fn ord(s: String) -> Scalar

@description("Convert a string to lowercase")
@example("lowercase(\"Numbat\")")
fn lowercase(s: String) -> String

@description("Convert a string to uppercase")
@example("uppercase(\"Numbat\")")
fn uppercase(s: String) -> String

@description("Concatenate two strings")
@example("\"!\" |> str_append(\"Numbat\")")
@example("str_append(\"Numbat\", \"!\")")
fn str_append(a: String, b: String) -> String = "{a}{b}"

@description("Concatenate two strings")
@example("\"Numbat\" |> str_prepend(\"!\")")
@example("str_prepend(\"!\", \"Numbat\")")
fn str_prepend(a: String, b: String) -> String = "{b}{a}"

fn _str_find(needle: String, index: Scalar, haystack: String) -> Scalar =
  if len_haystack == 0
    then -1
    else if str_slice(0, str_length(needle), haystack) == needle
      then index
      else _str_find(needle, index + 1, tail_haystack)
  where len_haystack = str_length(haystack)
    and tail_haystack = str_slice(1, len_haystack, haystack)

@description("Find the first occurrence of a substring in a string")
@example("str_find(\"typed\", \"Numbat is a statically typed programming language.\")")
fn str_find(needle: String, haystack: String) -> Scalar = 
  _str_find(needle, 0, haystack)

@description("Check if a string contains a substring")
@example("str_contains(\"typed\", \"Numbat is a statically typed programming language.\")")
fn str_contains(needle: String, haystack: String) -> Bool =
  str_find(needle, haystack) != -1

@description("Replace all occurrences of a substring in a string")
@example("str_replace(\"statically typed programming language\", \"scientific calculator\", \"Numbat is a statically typed programming language.\")")
fn str_replace(pattern: String, replacement: String, s: String) -> String =
  if pattern == ""
    then s
    else if str_contains(pattern, s)
      then if str_slice(0, pattern_length, s) == pattern
          then (s |> str_slice(pattern_length, s_length) |> str_replace(pattern, replacement) |> str_append(replacement))
          else (s |> str_slice(             1, s_length) |> str_replace(pattern, replacement) |> str_append(str_slice(0, 1, s)))
      else s
  where s_length = str_length(s)
    and pattern_length = str_length(pattern)

@description("Repeat the input string `n` times")
@example("str_repeat(4, \"abc\")")
fn str_repeat(n: Scalar, a: String) -> String =
  if n > 0
    then str_append(a, str_repeat(n - 1, a))
    else ""

fn _bin_digit(x: Scalar) -> String =
  chr(48 + mod(x, 2))

fn _oct_digit(x: Scalar) -> String =
  chr(48 + mod(x, 8))

fn _hex_digit(x: Scalar) -> String =
  if x_16 < 10 then chr(48 + x_16) else chr(97 + x_16 - 10)
  where
    x_16 = mod(x, 16)

fn _digit_in_base(base: Scalar, x: Scalar) -> String =
  if base < 2 || base > 16
    then error("base must be between 2 and 16")
    else if x_16 < 10 then chr(48 + x_16) else chr(97 + x_16 - 10)
  where
    x_16 = mod(x, 16)

# TODO: once we have anonymous functions / closures, we can implement base in a way
# that it returns a partially-applied version of '_number_in_base'. This would allow
# arbitrary 'x -> base(b)' conversions.
@description("Convert a number to the given base.")
@example("42 |> base(16)")
fn base(b: Scalar, x: Scalar) -> String =
  if x < 0
    then "-{base(b, -x)}"
    else if x < b
      then _digit_in_base(b, x)
      else str_append(base(b, floor(x / b)), _digit_in_base(b, mod(x, b)))

@description("Get a binary representation of a number.")
@example("42 -> bin")
fn bin(x: Scalar) -> String = if x < 0 then "-{bin(-x)}" else "0b{base(2, x)}"

@description("Get an octal representation of a number.")
@example("42 -> oct")
fn oct(x: Scalar) -> String = if x < 0 then "-{oct(-x)}" else "0o{base(8, x)}"

@description("Get a decimal representation of a number.")
@example("0b111 -> dec")
fn dec(x: Scalar) -> String = base(10, x)

@description("Get a hexadecimal representation of a number.")
@example("2^31-1 -> hex")
fn hex(x: Scalar) -> String = if x < 0 then "-{hex(-x)}" else "0x{base(16, x)}"