Parsing of Strings into Floats and Integers

This adds support for parsing a String into a Float and an Integer.
There are two ways of doing so:

1. By sending `to_integer` or `to_float` to a `String`.
2. Using `Integer.from_string` or `Float.from_string`.

Using `to_integer` and `to_float` will perform a lossy conversion:
returning 0 or 0.0 for invalid input. Using the `from_string` methods
will result in a strict conversion, with an error being thrown for
invalid input.

Fixes #134
Fixes #156
parent a38d0f56
Pipeline #40344374 passed with stages
in 12 minutes and 48 seconds
......@@ -163,6 +163,8 @@ module Inkoc
PointerAddress
ForeignTypeSize
ForeignTypeAlignment
StringToInteger
StringToFloat
]
.each_with_index
.each_with_object({}) { |(value, index), hash| hash[value] = index }
......
......@@ -1723,6 +1723,14 @@ module Inkoc
typedb.integer_type.new_instance
end
def on_raw_string_to_integer(*)
typedb.integer_type.new_instance
end
def on_raw_string_to_float(*)
typedb.float_type.new_instance
end
def define_block_signature(node, scope, expected_block = nil)
define_type_parameters(node, scope)
define_argument_types(node, scope, expected_block)
......
......@@ -1488,6 +1488,14 @@ module Inkoc
raw_unary_instruction(:ForeignTypeAlignment, node, body)
end
def on_raw_string_to_integer(node, body)
raw_binary_instruction(:StringToInteger, node, body)
end
def on_raw_string_to_float(node, body)
raw_unary_instruction(:StringToFloat, node, body)
end
def on_return(node, body)
location = node.location
register =
......
......@@ -4,10 +4,14 @@
import std::compare::Compare
import std::conversion::(ToInteger, ToString, ToFloat)
import std::error::StandardError
import std::hash::(Hasher, Hash)
import std::numeric::Numeric
import std::operators::*
let INFINITY_LABEL = 'Infinity'
let NEGATIVE_INFINITY_LABEL = '-Infinity'
impl Add!(Self) for Float {
def +(other: Self) -> Self {
_INKOC.float_add(self, other)
......@@ -109,6 +113,45 @@ impl Numeric for Float {
}
impl Float {
## Parses a `Float` from a `String`.
##
## # Error handling
##
## This method will throw if the input `String` is not in the right format.
##
## # Format
##
## The input `String` is expected to be in base 10. Leading and/or trailing
## whitespace is considered to be invalid, and will result in an error being
## thrown.
##
## # Examples
##
## Parsing a `Float` from a `String`:
##
## try! Float.from_string('10.5') # => 10.5
##
## Parsing a `Float` with an exponent:
##
## try! Float.from_string('1.2e1') # => 12.0
def from_string(string: String) !! StandardError -> Float {
string == INFINITY_LABEL
.if_true {
return INFINITY
}
string == NEGATIVE_INFINITY_LABEL
.if_true {
return NEGATIVE_INFINITY
}
try {
_INKOC.string_to_float(string)
} else (error) {
throw StandardError.new(error as String)
}
}
## Returns `True` if `self` is not a number (NAN).
##
## # Examples
......@@ -282,12 +325,11 @@ impl ToString for Float {
##
## INFINITY.to_string # => 'INFINITY'
def to_string -> String {
infinite?.if_true {
positive?.if true: {
return 'Infinity'
return INFINITY_LABEL
}, false: {
return '-Infinity'
return NEGATIVE_INFINITY_LABEL
}
}
......
......@@ -2,11 +2,59 @@
import std::compare::Compare
import std::conversion::(ToInteger, ToString, ToFloat)
import std::error::StandardError
import std::hash::(Hasher, Hash)
import std::numeric::Numeric
import std::operators::*
import std::successor::Successor
impl Integer {
## Parses an `Integer` from a `String` in a given base.
##
## # Error handling
##
## This method will panic if `radix` is not between `2` and `36`. If the input
## `String` is invalid, an error will be thrown instead.
##
## # Format
##
## The `String` is expected to start with an optional `+` or `-` sign,
## followed by one or more digits. Digits are a subset of the following
## ranges, depending on the value of the `radix` argument:
##
## * 0-9
## * a-z
## * A-Z
##
## Leading and/or trailing whitespace is considered to be invalid, and will
## result in an error being thrown.
##
## # Examples
##
## Parsing a `String` into a base 2 `Integer`:
##
## try! Integer.from_string('11', 2) # => 3
##
## Parsing a `String` into a base 8 `Integer`:
##
## try! Integer.from_string('0o52', 8) # => 42
##
## Parsing a `String` into a base 10 `Integer`:
##
## try! Integer.from_string('10') # => 10
##
## Parsing a `String` into a base 16 `Integer`:
##
## try! Integer.from_string('F', 16) # => 15
def from_string(string: String, radix = 10) !! StandardError -> Integer {
try {
_INKOC.string_to_integer(string, radix)
} else (error) {
throw StandardError.new(error as String)
}
}
}
impl ToInteger for Integer {
def to_integer -> Integer {
self
......
......@@ -3,7 +3,7 @@
#! Strings are UTF-8 encoded and immutable. A String must _always_ contain valid
#! UTF-8.
import std::conversion::ToString
import std::conversion::(ToFloat, ToInteger, ToString)
import std::hash::(Hasher, Hash)
import std::length::Length
import std::operators::(Equal, Add)
......@@ -134,6 +134,40 @@ impl ToString for String {
}
}
impl ToInteger for String {
## Converts `self` to an `Integer`.
##
## The string is parsed in base 10. If the `String` can not be converted to
## an `Integer`, `0` is returned instead.
##
## # Examples
##
## Converting a `String` to an `Integer`:
##
## '10'.to_integer # => 10
## 'a'.to_integer # => 0
def to_integer -> Integer {
try Integer.from_string(self, 10) else (_) 0
}
}
impl ToFloat for String {
## Converts `self` to a `Float`.
##
## If the `String` can not be converted to a `Float`, `0.0` is returned
## instead.
##
## # Examples
##
## Converting a `String` to a `Float`:
##
## '10.5'.to_float # => 10.5
## 'a'.to_float # => 0.0
def to_float -> Float {
try Float.from_string(self) else (_) 0.0
}
}
impl Length for String {
## Returns the number of characters (_not_ bytes) in this `String`.
##
......
......@@ -822,3 +822,52 @@ test.group('std::float::Float.hash') do (g) {
assert.equal(hasher1.finish, hasher2.finish)
}
}
test.group('std::float::Float.from_string') do (g) {
g.test('Parsing a Float') {
assert.equal(try! Float.from_string('1.2'), 1.2)
}
g.test('Parsing a Float with leading whitespace') {
assert.panic {
try! Float.from_string(' 1.2')
}
}
g.test('Parsing a Float with trailing whitespace') {
assert.panic {
try! Float.from_string('1.2 ')
}
}
g.test('Parsing a Float with an exponentn') {
assert.equal(try! Float.from_string('1.2e1'), 12.0)
assert.equal(try! Float.from_string('1.2E1'), 12.0)
}
g.test('Parsing a Float with just a decimal point') {
assert.equal(try! Float.from_string('.5'), 0.5)
}
g.test('Parsing a Float without a decimal digit') {
assert.equal(try! Float.from_string('5.'), 5.0)
}
g.test('Parsing a negative Float') {
assert.equal(try! Float.from_string('-1.2'), -1.2)
}
g.test('Parsing infinity') {
assert.equal(try! Float.from_string('inf'), INFINITY)
assert.equal(try! Float.from_string('Infinity'), INFINITY)
}
g.test('Parsing negative infinity') {
assert.equal(try! Float.from_string('-inf'), NEGATIVE_INFINITY)
assert.equal(try! Float.from_string('-Infinity'), NEGATIVE_INFINITY)
}
g.test('Parsing a NaN') {
assert.true(try! { Float.from_string('NaN') }.not_a_number?)
}
}
......@@ -217,3 +217,62 @@ test.group('std::integer::Integer.successor') do (g) {
assert.equal(-2.successor, -1)
}
}
test.group('std::integer::Integer.from_string') do (g) {
g.test('Parsing a base 2 Integer') {
assert.equal(try! Integer.from_string(string: '11', radix: 2), 3)
}
g.test('Parsing an Integer with leading whitespace') {
assert.panic {
try! Integer.from_string(' 1')
}
}
g.test('Parsing an Integer with trailing whitespace') {
assert.panic {
try! Integer.from_string('1 ')
}
}
g.test('Parsing a base 8 Integer') {
assert.equal(try! Integer.from_string(string: '52', radix: 8), 42)
}
g.test('Parsing a base 10 Integer') {
assert.equal(try! Integer.from_string(string: '12', radix: 10), 12)
}
g.test('Parsing a base 12 Integer') {
assert.equal(try! Integer.from_string(string: '10', radix: 12), 12)
}
g.test('Parsing a base 16 Integer') {
assert.equal(try! Integer.from_string(string: 'F', radix: 16), 15)
}
g.test('Parsing a base 20 Integer') {
assert.equal(try! Integer.from_string(string: 'H', radix: 20), 17)
}
g.test('Parsing a negative base 10 Integer') {
assert.equal(try! Integer.from_string(string: '-5', radix: 10), -5)
}
g.test('Parsing an invalid Integer') {
assert.panic {
try! Integer.from_string(string: 'Z', radix: 16)
}
}
g.test('Parsing a large base 10 Integer') {
let integer = try! Integer.from_string(
'123456789123456789123456789123456789123456789123456789123456789123456789123456789'
)
assert.equal(
integer,
123456789123456789123456789123456789123456789123456789123456789123456789123456789
)
}
}
......@@ -93,3 +93,25 @@ test.group('std::string::String.to_path') do (g) {
assert.equal('foo.inko'.to_path, Path.new('foo.inko'))
}
}
test.group('std::string::String.to_integer') do (g) {
g.test('Converting a String to an Integer') {
assert.equal('10'.to_integer, 10)
assert.equal('-5'.to_integer, -5)
}
g.test('Converting an invalid String to an Integer') {
assert.equal('foo'.to_integer, 0)
}
}
test.group('std::string::String.to_float') do (g) {
g.test('Converting a String to a Float') {
assert.equal('10.5'.to_float, 10.5)
assert.equal('-5.0'.to_float, -5.0)
}
g.test('Converting an invalid String to an Integer') {
assert.equal('foo'.to_float, 0.0)
}
}
......@@ -161,6 +161,8 @@ pub enum InstructionType {
PointerAddress,
ForeignTypeSize,
ForeignTypeAlignment,
StringToInteger,
StringToFloat,
}
/// Struct for storing information about a single instruction.
......
......@@ -1699,6 +1699,29 @@ impl Machine {
context.set_register(reg, res);
}
InstructionType::StringToInteger => {
let reg = instruction.arg(0);
let val = context.get_register(instruction.arg(1));
let rdx = context.get_register(instruction.arg(2));
match string::to_integer(&self.state, process, val, rdx)? {
Ok(value) => context.set_register(reg, value),
Err(err) => throw_error_message!(
self, process, err, context, index
),
};
}
InstructionType::StringToFloat => {
let reg = instruction.arg(0);
let val = context.get_register(instruction.arg(1));
match string::to_float(&self.state, process, val)? {
Ok(value) => context.set_register(reg, value),
Err(err) => throw_error_message!(
self, process, err, context, index
),
};
}
};
}
......
//! VM functions for working with Inko strings.
use num_bigint::BigInt;
use object_pointer::ObjectPointer;
use object_value;
use process::RcProcess;
use slicing;
use vm::state::RcState;
/// The result of a string conversion. The OK value is the value converted to,
/// the Err value is an error message to thrown in the VM.
type ConversionResult = Result<ObjectPointer, String>;
pub fn to_lower(
state: &RcState,
process: &RcProcess,
......@@ -151,3 +156,49 @@ pub fn format_debug(
Ok(process.allocate(object_value::string(new_str), state.string_prototype))
}
/// Converts a string to an integer.
pub fn to_integer(
state: &RcState,
process: &RcProcess,
str_ptr: ObjectPointer,
radix_ptr: ObjectPointer,
) -> Result<ConversionResult, String> {
let string = str_ptr.string_value()?;
let radix = radix_ptr.integer_value()?;
if radix < 2 || radix > 36 {
return Err("radix must be between 2 and 32, not {}".to_string());
}
let int_ptr = if let Ok(value) = i64::from_str_radix(string, radix as u32) {
process.allocate_i64(value, state.integer_prototype)
} else if let Ok(val) = string.parse::<BigInt>() {
process.allocate(object_value::bigint(val), state.integer_prototype)
} else {
return Ok(Err(format!(
"{:?} can not be converted to an Integer",
string
)));
};
Ok(Ok(int_ptr))
}
/// Converts a string to a float.
pub fn to_float(
state: &RcState,
process: &RcProcess,
str_ptr: ObjectPointer,
) -> Result<ConversionResult, String> {
let string = str_ptr.string_value()?;
if let Ok(value) = string.parse::<f64>() {
let pointer =
process.allocate(object_value::float(value), state.float_prototype);
Ok(Ok(pointer))
} else {
Ok(Err(format!("{:?} can not be converted to a Float", string)))
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment