Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First class functions, function types, conversion functions #200

Merged
merged 16 commits into from
Feb 11, 2024
Merged
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- [Operations and precedence](./operations.md)
- [Constants](./constant-definitions.md)
- [Unit conversions](./unit-conversions.md)
- [Other conversions](./conversion-functions.md)
- [Function definitions](./function-definitions.md)
- [Conditionals](./conditionals.md)
- [Date and time](./date-and-time.md)
Expand Down
33 changes: 33 additions & 0 deletions book/src/conversion-functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Other conversions

The conversion operator `->` (or `to`) can not just be used for [unit conversions](./unit-conversions.md), but also for other types of conversions.
The way this is set up in Numbat is that you can call `x -> f` for any function `f` that takes a single argument of the same type as `x`.

The following functions are available for this purpose:

```nbt
# Convert a date and time to a Unix timestamp
now() -> unixtime

# Convert a duration to days, hours, minutes, seconds
10 million seconds -> human

# Convert a number to its binary representation
42 -> bin

# Convert a number to its octal representation
42 -> oct

# Convert a number to its hexadecimal representation
2^31-1 -> hex

# Convert a number to a custom base
42 -> base(16)

# Convert an ASCII code point number to a character
78 -> chr

# Convert a string to upper/lower case
"numbat is awesome" -> uppercase
"vier bis elf weiße Querbänder" -> lowercase
```
6 changes: 3 additions & 3 deletions book/src/date-and-time.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ now() -> "Asia/Kathmandu" # use tab completion to find time zone names
parse_datetime("2024-11-01 12:30:00 Australia/Sydney") -> "local"

# What is the current UNIX timestamp?
now() // to_unixtime
now() -> unixtime

# What is the date corresponding to the UNIX timestamp 1707568901?
from_unixtime(1707568901)

# How long are one million seconds in days, hours, minutes, seconds
1 million seconds // human
1 million seconds -> human
```

## Date and time arithmetic
Expand Down Expand Up @@ -60,7 +60,7 @@ The following functions are available for date and time handling:
- `now() -> DateTime`: Returns the current date and time.
- `parse_datetime(input: String) -> DateTime`: Parses a string into a `DateTime` object.
- `format_datetime(format: String, dt: DateTime) -> String`: Formats a `DateTime` object as a string. See [this page](https://docs.rs/chrono/latest/chrono/format/strftime/index.html#specifiers) for possible format specifiers.
- `to_unixtime(dt: DateTime) -> Scalar`: Converts a `DateTime` to a UNIX timestamp.
- `unixtime(dt: DateTime) -> Scalar`: Converts a `DateTime` to a UNIX timestamp.
- `from_unixtime(ut: Scalar) -> DateTime`: Converts a UNIX timestamp to a `DateTime` object.
- `human(duration: Time) -> String`: Converts a `Time` to a human-readable string in days, hours, minutes and seconds

Expand Down
6 changes: 3 additions & 3 deletions book/src/list-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ fn now() -> DateTime
fn parse_datetime(input: String) -> DateTime
fn format_datetime(format: String, dt: DateTime) -> String
fn from_unixtime(t: Scalar) -> DateTime
fn to_unixtime(dt: DateTime) -> Scalar
fn unixtime(dt: DateTime) -> Scalar
fn human(t: Time) -> String
```

Expand All @@ -116,9 +116,9 @@ fn human(t: Time) -> String

```nbt
fn from_celsius(t_celsius: Scalar) -> Temperature
fn to_celsius(t_kelvin: Temperature) -> Scalar
fn celsius(t_kelvin: Temperature) -> Scalar
fn from_fahrenheit(t_fahrenheit: Scalar) -> Temperature
fn to_fahrenheit(t_kelvin: Temperature) -> Scalar
fn fahrenheit(t_kelvin: Temperature) -> Scalar
```

## Strings
Expand Down
17 changes: 11 additions & 6 deletions book/src/number-notation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ Numbers in Numbat can be written in the following forms:

## Convert numbers to other bases

You can use the `bin`, `oct`, and `hex` functions to convert numbers to binary, octal, and hexadecimal bases, respectively.
As with any other function, you can call those using `hex(2^16 - 1)`, but it's often more convenient to use the `… // hex`
convention to convert a number.
You can use the `bin`, `oct`, `dec` and `hex` functions to convert numbers to binary, octal, decimal and hexadecimal bases,
respectively. You can call those using `hex(2^16 - 1)`, or `2^16 - 1 // hex`, but they are also available as targets of the
conversion operator `->`/`to`, so you can write expressions like:

Examples:
```nbt
0xffee // bin
42 // oct
2^16 - 1 // hex
0xffee to bin
42 ot oct
2^16 - 1 to hex
```

You can also use `base(b)` to convert a number to base `b`:
```nbt
0xffee to base(2)
```
76 changes: 38 additions & 38 deletions examples/datetime_human_tests.nbt
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
assert((0 second // human) == "0 seconds")
assert((1 second // human) == "1 second")
assert((5 second // human) == "5 seconds")
assert((1.5 second // human) == "1.500 seconds")

assert((60 seconds // human) == "1 minute")
assert((73 seconds // human) == "1 minute + 13 seconds")
assert((120 seconds // human) == "2 minutes")
assert((60.1 seconds // human) == "1 minute + 0.100 seconds")
assert((1 minute // human) == "1 minute")
assert((1.25 minute // human) == "1 minute + 15 seconds")
assert((2.5 minute // human) == "2 minutes + 30 seconds")

assert((1 hour // human) == "1 hour")
assert((1.5 hour // human) == "1 hour + 30 minutes")
assert((2 hour // human) == "2 hours")
assert((1 hour + 1 sec // human) == "1 hour + 1 second")

assert((1 day // human) == "1 day")
assert((1.37 day // human) == "1 day + 8 hours + 52 minutes + 48 seconds")

assert((1 week // human) == "7 days")
assert((1.5 weeks // human) == "10 days + 12 hours")
assert((2 weeks // human) == "14 days")

assert((1 sidereal_day // human) == "23 hours + 56 minutes + 4.090500 seconds")

assert((10000 days // human) == "10000 days")
assert((50 million days // human) == "50_000_000 days")

assert((1e12 days // human) == "1_000_000_000_000 days")
assert((1e15 days // human) == "1.0e+15 days")

assert((1 ms // human) == "0.001 seconds")
assert((1 µs // human) == "0.000001 seconds")
assert((1 ns // human) == "0.000000001 seconds")
assert((1234 ns // human) == "0.000001234 seconds")
assert((1s + 1234 ns // human) == "1.000001234 seconds")
assert((0 second -> human) == "0 seconds")
assert((1 second -> human) == "1 second")
assert((5 second -> human) == "5 seconds")
assert((1.5 second -> human) == "1.500 seconds")

assert((60 seconds -> human) == "1 minute")
assert((73 seconds -> human) == "1 minute + 13 seconds")
assert((120 seconds -> human) == "2 minutes")
assert((60.1 seconds -> human) == "1 minute + 0.100 seconds")
assert((1 minute -> human) == "1 minute")
assert((1.25 minute -> human) == "1 minute + 15 seconds")
assert((2.5 minute -> human) == "2 minutes + 30 seconds")

assert((1 hour -> human) == "1 hour")
assert((1.5 hour -> human) == "1 hour + 30 minutes")
assert((2 hour -> human) == "2 hours")
assert((1 hour + 1 sec -> human) == "1 hour + 1 second")

assert((1 day -> human) == "1 day")
assert((1.37 day -> human) == "1 day + 8 hours + 52 minutes + 48 seconds")

assert((1 week -> human) == "7 days")
assert((1.5 weeks -> human) == "10 days + 12 hours")
assert((2 weeks -> human) == "14 days")

assert((1 sidereal_day -> human) == "23 hours + 56 minutes + 4.090500 seconds")

assert((10000 days -> human) == "10000 days")
assert((50 million days -> human) == "50_000_000 days")

assert((1e12 days -> human) == "1_000_000_000_000 days")
assert((1e15 days -> human) == "1.0e+15 days")

assert((1 ms -> human) == "0.001 seconds")
assert((1 µs -> human) == "0.000001 seconds")
assert((1 ns -> human) == "0.000000001 seconds")
assert((1234 ns -> human) == "0.000001234 seconds")
assert((1s + 1234 ns -> human) == "1.000001234 seconds")
8 changes: 4 additions & 4 deletions examples/datetime_tests.nbt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
let epoch = parse_datetime("1970-01-01T00:00:00Z")
assert_eq(to_unixtime(epoch), 0)
assert_eq(epoch -> unixtime, 0)

assert_eq(to_unixtime(epoch + 1000 milliseconds + 2 seconds), 3)
assert_eq(epoch + 1000 milliseconds + 2 seconds -> unixtime, 3)

let x = parse_datetime("Wed, 20 Jul 2022 21:52:05 +0200")
assert_eq(to_unixtime(x), 1658346725)
assert_eq(x -> unixtime, 1658346725)

assert_eq(to_unixtime(from_unixtime(1658346725)), 1658346725)
assert_eq(from_unixtime(1658346725) -> unixtime, 1658346725)

# 2020 was a leap year
let y = parse_datetime("2020-02-28T20:00:00Z")
Expand Down
16 changes: 16 additions & 0 deletions examples/numerical_diff.nbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
let eps = 1e-10

fn diff(f: Fn[(Scalar) -> Scalar], x: Scalar) -> Scalar =
(f(x + eps) - f(x)) / eps

assert_eq(diff(log, 2.0), 1 / 2, 1e-5)
assert_eq(diff(sin, 0.0), 1.0, 1e-5)

fn f(x: Scalar) -> Scalar = x * x + 4 * x + 1

assert_eq(diff(f, 2.0), 8.0, 1e-5)

# TODO: It would be so cool if we could write it in a generic way:
#
# fn diff<X, Y>(f: Fn[(X) -> Y], x: X) -> Y / X =
# (f(x + eps · unit_of(x)) - f(x)) / (eps · unit_of(x))
44 changes: 22 additions & 22 deletions examples/prelude_tests.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,25 @@ assert(str_replace("xxx", "x", "yY") == "yYyYyY")

assert(str_repeat("xy", 3) == "xyxyxy")

assert(bin(0b0) == "0b0")
assert(bin(0b1) == "0b1")
assert(bin(0b10) == "0b10")
assert(bin(0b11) == "0b11")
assert(bin(0b10101010101010101010101010101010) == "0b10101010101010101010101010101010")
assert(bin(-0b11110000) == "-0b11110000")

assert(oct(0o0) == "0o0")
assert(oct(0o1) == "0o1")
assert(oct(0o7) == "0o7")
assert(oct(0o10) == "0o10")
assert(oct(0o77) == "0o77")
assert(oct(0o12345670) == "0o12345670")
assert(oct(-0o12345670) == "-0o12345670")

assert(hex(0x0) == "0x0")
assert(hex(0x1) == "0x1")
assert(hex(0x9) == "0x9")
assert(hex(0xa) == "0xa")
assert(hex(0xf) == "0xf")
assert(hex(0xabc1234567890) == "0xabc1234567890")
assert(hex(-0xc0ffee) == "-0xc0ffee")
assert((0b0 -> bin) == "0b0")
assert((0b1 -> bin) == "0b1")
assert((0b10 -> bin) == "0b10")
assert((0b11 -> bin) == "0b11")
assert((0b10101010101010101010101010101010 -> bin) == "0b10101010101010101010101010101010")
assert((-0b11110000 -> bin) == "-0b11110000")

assert((0o0 -> oct) == "0o0")
assert((0o1 -> oct) == "0o1")
assert((0o7 -> oct) == "0o7")
assert((0o10 -> oct) == "0o10")
assert((0o77 -> oct) == "0o77")
assert((0o12345670 -> oct) == "0o12345670")
assert((-0o12345670 -> oct) == "-0o12345670")

assert((0x0 -> hex) == "0x0")
assert((0x1 -> hex) == "0x1")
assert((0x9 -> hex) == "0x9")
assert((0xa -> hex) == "0xa")
assert((0xf -> hex) == "0xf")
assert((0xabc1234567890 -> hex) == "0xabc1234567890")
assert((-0xc0ffee -> hex) == "-0xc0ffee")
48 changes: 31 additions & 17 deletions numbat/modules/core/strings.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ fn str_slice(s: String, start: Scalar, end: Scalar) -> String

fn chr(n: Scalar) -> String

fn lowercase(s: String) -> String
fn uppercase(s: String) -> String

fn str_append(a: String, b: String) -> String = "{a}{b}"

fn str_contains(haystack: String, needle: String) -> Bool =
Expand Down Expand Up @@ -39,23 +42,34 @@ fn _oct_digit(x: Scalar) -> String =
fn _hex_digit(x: Scalar) -> String =
if mod(x, 16) < 10 then chr(48 + mod(x, 16)) else chr(97 + mod(x, 16) - 10)

fn bin(x: Scalar) -> String =
if x < 0
then "-{bin(-x)}"
else if x < 2
then "0b{chr(48 + x)}"
else str_append(bin(floor(x / 2)), _bin_digit(mod(x, 2)))
fn _digit_in_base(x: Scalar, base: Scalar) -> String =
if base < 2 || base > 16
then "?" # TODO: better error handling once we can specify the return type of 'error(msg)' as '!' (see error.nbt).
else if mod(x, 16) < 10 then chr(48 + mod(x, 16)) else chr(97 + mod(x, 16) - 10)

fn oct(x: Scalar) -> String =
fn _number_in_base(x: Scalar, b: Scalar) -> String =
if x < 0
then "-{oct(-x)}"
else if x < 8
then "0o{chr(48 + x)}"
else str_append(oct(floor(x / 8)), _oct_digit(x))
then "-{_number_in_base(-x, b)}"
else if x < b
then _digit_in_base(x, b)
else str_append(_number_in_base(floor(x / b), b), _digit_in_base(mod(x, b), b))

fn hex(x: Scalar) -> String =
if x < 0
then "-{hex(-x)}"
else if floor(x / 16) == 0
then str_append("0x", _hex_digit(x))
else str_append(hex(floor(x / 16)), _hex_digit(x))
fn bin(x: Scalar) -> String = if x < 0 then "-{bin(-x)}" else "0b{_number_in_base(x, 2)}"
fn oct(x: Scalar) -> String = if x < 0 then "-{oct(-x)}" else "0o{_number_in_base(x, 8)}"
fn dec(x: Scalar) -> String = _number_in_base(x, 10)
fn hex(x: Scalar) -> String = if x < 0 then "-{hex(-x)}" else "0x{_number_in_base(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.
fn _not_implemented(unused: Scalar) -> String = "<bases other than 2, 8, 10, and 16 are currently not implemented>"
fn base(b: Scalar) -> Fn[(Scalar) -> String] =
if b == 2
then bin
else if b == 8
then oct
else if b == 10
then dec
else if b == 16
then hex
else _not_implemented
2 changes: 1 addition & 1 deletion numbat/modules/datetime/functions.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ use units::si
fn now() -> DateTime
fn parse_datetime(input: String) -> DateTime
fn format_datetime(format: String, input: DateTime) -> String
fn to_unixtime(input: DateTime) -> Scalar
fn unixtime(input: DateTime) -> Scalar
fn from_unixtime(input: Scalar) -> DateTime
4 changes: 2 additions & 2 deletions numbat/modules/physics/temperature_conversion.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ use units::si
let _offset_celsius = 273.15

fn from_celsius(t_celsius: Scalar) -> Temperature = (t_celsius + _offset_celsius) kelvin
fn to_celsius(t_kelvin: Temperature) -> Scalar = t_kelvin / kelvin - _offset_celsius
fn celsius(t_kelvin: Temperature) -> Scalar = t_kelvin / kelvin - _offset_celsius

let _offset_fahrenheit = 459.67
let _scale_fahrenheit = 5 / 9

fn from_fahrenheit(t_fahrenheit: Scalar) -> Temperature = ((t_fahrenheit + _offset_fahrenheit) × _scale_fahrenheit) kelvin
fn to_fahrenheit(t_kelvin: Temperature) -> Scalar = (t_kelvin / kelvin) / _scale_fahrenheit - _offset_fahrenheit
fn fahrenheit(t_kelvin: Temperature) -> Scalar = (t_kelvin / kelvin) / _scale_fahrenheit - _offset_fahrenheit
Loading
Loading