///|
/// Object text helpers (ASCII checks, strip, formatting).
///|
fn is_ascii_alpha(c : Char) -> Bool {
let code = c.to_int()
(code >= 'a'.to_int() && code <= 'z'.to_int()) ||
(code >= 'A'.to_int() && code <= 'Z'.to_int())
}
///|
fn is_ascii_lower(c : Char) -> Bool {
let code = c.to_int()
code >= 'a'.to_int() && code <= 'z'.to_int()
}
///|
fn is_ascii_upper(c : Char) -> Bool {
let code = c.to_int()
code >= 'A'.to_int() && code <= 'Z'.to_int()
}
///|
fn is_ascii_digit(c : Char) -> Bool {
let code = c.to_int()
code >= '0'.to_int() && code <= '9'.to_int()
}
///|
fn is_ascii_alnum(c : Char) -> Bool {
is_ascii_alpha(c) || is_ascii_digit(c)
}
///|
fn is_ascii_whitespace(c : Char) -> Bool {
let code = c.to_int()
code == ' '.to_int() ||
code == '\t'.to_int() ||
code == '\n'.to_int() ||
code == '\r'.to_int() ||
code == 0x0b ||
code == 0x0c
}
///|
fn is_python_whitespace(c : Char) -> Bool {
if is_ascii_whitespace(c) {
return true
}
let code = c.to_int()
code == 0x0085 ||
code == 0x00a0 ||
code == 0x1680 ||
(code >= 0x2000 && code <= 0x200a) ||
code == 0x2028 ||
code == 0x2029 ||
code == 0x202f ||
code == 0x205f ||
code == 0x3000
}
///|
priv enum StripMode {
Both
Left
Right
} derive(Eq)
///|
fn parse_strip_chars(
name : String,
value_opt : Value?,
) -> Result[Array[Char]?, RuntimeError] {
match value_opt {
None => Ok(None)
Some(Value::None) => Ok(None)
Some(Value::Str(text)) => Ok(Some(text.to_array()))
Some(_) =>
Err(
make_runtime_error(
RuntimeErrorKind::Type,
name + "() expects a str".to_string(),
),
)
}
}
///|
fn strip_has_char(c : Char, chars_opt : Array[Char]?) -> Bool {
match chars_opt {
None => is_python_whitespace(c)
Some(chars) => {
for item in chars {
if item == c {
return true
}
}
false
}
}
}
///|
fn strip_text(
text : String,
chars_opt : Array[Char]?,
mode : StripMode,
) -> String {
let chars = text.to_array()
let mut start = 0
let mut end = chars.length()
if mode == StripMode::Both || mode == StripMode::Left {
while start < end && strip_has_char(chars[start], chars_opt) {
start += 1
}
}
if mode == StripMode::Both || mode == StripMode::Right {
while end > start && strip_has_char(chars[end - 1], chars_opt) {
end -= 1
}
}
substring(text, start, end)
}
///|
fn lookup_keyword_value(
keywords : Array[(String, Value)],
name : String,
) -> Value? {
for pair in keywords {
if pair.0 == name {
return Some(pair.1)
}
}
None
}
///|
fn format_value_with_spec(
value : Value,
spec : String,
globals : Array[(String, Value)],
builtins : Array[(String, Value)],
io : MockIO,
) -> Result[String, RuntimeError] {
fn stringify_for_default_format(
value : Value,
) -> Result[String, RuntimeError] {
match value {
Value::Str(text) => Ok(text)
Value::Instance(_) =>
match get_attr_from_value(value, "__str__", globals, builtins, io) {
Ok(callable) =>
match callable {
Value::BoundMethod(bound) =>
if bound.function.body.length() > 0 {
match
(eval_function_with_kwargs_ref.val)(
bound.function,
[bound.self],
[],
globals,
builtins,
io,
) {
Ok(Value::Str(text)) => Ok(text)
Ok(_) =>
Err(
make_runtime_error(
RuntimeErrorKind::Type,
"__str__ returned non-string".to_string(),
),
)
Err(err) => Err(err)
}
} else {
Ok(value_to_string(value))
}
Value::Function(func) =>
if func.body.length() > 0 {
match
(eval_function_with_kwargs_ref.val)(
func,
[value],
[],
globals,
builtins,
io,
) {
Ok(Value::Str(text)) => Ok(text)
Ok(_) =>
Err(
make_runtime_error(
RuntimeErrorKind::Type,
"__str__ returned non-string".to_string(),
),
)
Err(err) => Err(err)
}
} else {
Ok(value_to_string(value))
}
_ => Ok(value_to_string(value))
}
Err(err) =>
match err.kind {
RuntimeErrorKind::Attribute => Ok(value_to_string(value))
_ => Err(err)
}
}
_ => Ok(value_to_string(value))
}
}
if spec.length() == 0 {
return stringify_for_default_format(value)
}
let chars = spec.to_array()
let mut type_char : Char? = None
let mut end = chars.length()
if chars.length() > 0 && chars[chars.length() - 1].is_ascii_alphabetic() {
type_char = Some(chars[chars.length() - 1])
end = chars.length() - 1
}
let mut zero_pad = false
let mut width = 0
let mut idx = 0
if end > 0 && chars[0] == '0' {
zero_pad = true
idx = 1
}
while idx < end && chars[idx].is_ascii_digit() {
width = width * 10 + (chars[idx].to_int() - '0'.to_int())
idx += 1
}
if idx != end {
return Err(
make_runtime_error(
RuntimeErrorKind::Type,
"unsupported format string".to_string(),
),
)
}
match type_char {
None | Some('s') => {
let out = match stringify_for_default_format(value) {
Ok(v) => v
Err(err) => return Err(err)
}
Ok(pad_left(out, width, if zero_pad { '0' } else { ' ' }))
}
Some('d') | Some('i') => {
let raw = match format_decimal_value(value) {
Ok(v) => v
Err(err) => return Err(err)
}
Ok(pad_numeric(raw, width, zero_pad))
}
Some('x') => {
let raw = match format_hex_value(value, false) {
Ok(v) => v
Err(err) => return Err(err)
}
Ok(pad_numeric(raw, width, zero_pad))
}
Some('X') => {
let raw = match format_hex_value(value, true) {
Ok(v) => v
Err(err) => return Err(err)
}
Ok(pad_numeric(raw, width, zero_pad))
}
_ =>
Err(
make_runtime_error(
RuntimeErrorKind::Type,
"unsupported format string".to_string(),
),
)
}
}
///|
fn format_string_with_args(
text : String,
positional : Array[Value],
keywords : Array[(String, Value)],
globals : Array[(String, Value)],
builtins : Array[(String, Value)],
io : MockIO,
) -> Result[String, RuntimeError] {
let chars = text.to_array()
let buf = StringBuilder()
let mut auto_index = 0
let mut i = 0
while i < chars.length() {
let c = chars[i]
if c == '{' {
if i + 1 < chars.length() && chars[i + 1] == '{' {
buf.write_char('{')
i += 2
continue
}
let mut close = i + 1
while close < chars.length() && chars[close] != '}' {
close += 1
}
if close >= chars.length() {
return Err(
make_runtime_error(
RuntimeErrorKind::Type,
"unmatched '{' in format string".to_string(),
),
)
}
let field = substring(text, i + 1, close)
let field_chars = field.to_array()
let mut colon : Int32? = None
for j = 0; j < field_chars.length(); j = j + 1 {
if field_chars[j] == ':' {
colon = Some(j)
break
}
}
// Basic conversion support: `{name!r}` / `{0!s}` / `{name!a}`.
let head = match colon {
Some(pos) => substring(field, 0, pos)
None => field
}
let head_trimmed = head.trim(chars=" \t").to_owned()
let head_chars = head_trimmed.to_array()
let mut bang : Int32? = None
for j = 0; j < head_chars.length(); j = j + 1 {
if head_chars[j] == '!' {
bang = Some(j)
break
}
}
let (field_name, conv) = match bang {
Some(pos) => {
let name = substring(head_trimmed, 0, pos)
.trim(chars=" \t")
.to_owned()
let conv_char = if pos + 1 < head_chars.length() {
Some(head_chars[pos + 1])
} else {
None
}
(name, conv_char)
}
None => (head_trimmed, None)
}
let spec = match colon {
Some(pos) => substring(field, pos + 1, field.length())
None => "".to_string()
}
let value = if field_name.length() == 0 {
if auto_index >= positional.length() {
return Err(
make_runtime_error(
RuntimeErrorKind::Type,
"not enough arguments for format string".to_string(),
),
)
}
let v = positional[auto_index]
auto_index += 1
v
} else {
let mut is_number = true
for ch in field_name.to_array() {
if !ch.is_ascii_digit() {
is_number = false
break
}
}
if is_number {
let index = @string.parse_int(field_name) catch {
_ =>
return Err(
make_runtime_error(
RuntimeErrorKind::Type,
"invalid format index".to_string(),
),
)
}
if index < 0 || index >= positional.length() {
return Err(
make_runtime_error(
RuntimeErrorKind::Type,
"not enough arguments for format string".to_string(),
),
)
}
positional[index]
} else {
match lookup_keyword_value(keywords, field_name) {
Some(v) => v
None =>
return Err(
make_runtime_error(
RuntimeErrorKind::Type,
"missing format keyword".to_string(),
),
)
}
}
}
fn repr_string(value : Value) -> String {
match value {
Value::Str(text) => {
let buf = StringBuilder()
for c in text.to_array() {
if c == '\\' {
buf.write_string("\\\\")
} else if c == '\'' {
buf.write_string("\\'")
} else if c == '\n' {
buf.write_string("\\n")
} else if c == '\r' {
buf.write_string("\\r")
} else if c == '\t' {
buf.write_string("\\t")
} else {
buf.write_char(c)
}
}
"'" + buf.to_string() + "'"
}
_ => value_to_string(value)
}
}
let value_for_format = match conv {
Some('r') | Some('a') => Value::Str(repr_string(value))
Some('s') =>
match
format_value_with_spec(value, "".to_string(), globals, builtins, io) {
Ok(v) => Value::Str(v)
Err(err) => return Err(err)
}
_ => value
}
let formatted = match
format_value_with_spec(value_for_format, spec, globals, builtins, io) {
Ok(v) => v
Err(err) => return Err(err)
}
buf.write_string(formatted)
i = close + 1
continue
}
if c == '}' {
if i + 1 < chars.length() && chars[i + 1] == '}' {
buf.write_char('}')
i += 2
continue
}
return Err(
make_runtime_error(
RuntimeErrorKind::Type,
"single '}' encountered in format string".to_string(),
),
)
}
buf.write_char(c)
i += 1
}
Ok(buf.to_string())
}