Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/markdown/Syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,31 @@ string_var = '42'
num = string_var.to_int()
```

Hexadecimal, octal, and binary strings can be converted to numbers since
1.10.0:

```meson
hex_var = '0xFF'.to_int() # 255
oct_var = '0o755'.to_int() # 493
bin_var = '0b1010'.to_int() # 10
```

Numbers can be converted to a string:

```meson
int_var = 42
string_var = int_var.to_string()
```

Numbers can be formatted as hexadecimal, octal, or binary strings since 1.10.0:

```meson
int_var = 255
hex_str = int_var.to_string(format: 'hex') # '0xff'
oct_str = int_var.to_string(format: 'oct') # '0o377'
bin_str = int_var.to_string(format: 'bin') # '0b11111111'
```

## Booleans

A boolean is either `true` or `false`.
Expand Down
44 changes: 42 additions & 2 deletions mesonbuild/interpreter/primitives/integer.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,52 @@ def is_odd_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> bool:

@typed_kwargs(
'to_string',
KwargInfo('fill', int, default=0, since='1.3.0')
KwargInfo('fill', int, default=0, since='1.3.0'),
KwargInfo(
"format",
str,
default="dec",
since="1.10.0",
validator=lambda x: (
'format must be "dec", "hex", "oct", or "bin"'
if x not in ("dec", "hex", "oct", "bin")
else None
),
Comment on lines +64 to +68
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the in_set_validator({...}) for this.

),
)
@noPosargs
@InterpreterObject.method('to_string')
def to_string_method(self, args: T.List[TYPE_var], kwargs: T.Dict[str, T.Any]) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're here, can you add a TypedDict to use as the type for kwargs here? there's plenty of examples, preferably with a Literal[] for the format.

return str(self.held_object).zfill(kwargs['fill'])
format_type = kwargs["format"]
fill = kwargs["fill"]

format_codes = {"hex": "#x", "oct": "#o", "bin": "#b", "dec": "d"}

if format_type == "dec":
result = str(self.held_object)
if fill > 0:
result = result.zfill(fill)
else:
result = format(self.held_object, format_codes[format_type])

if fill > 0:
if result.startswith("-"):
sign = "-"
prefixed = result[1:]
else:
sign = ""
prefixed = result

prefix = prefixed[:2]
digits = prefixed[2:]

total_prefix_len = len(sign) + len(prefix)
if fill > total_prefix_len:
digits = digits.zfill(fill - total_prefix_len)

result = sign + prefix + digits
Comment on lines +79 to +101
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this all be implemented as:

template = '0=#{}{}'.format(fill, format_codes[format_code))
return format(self.held_object, template)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even easier:

return '{0:#0{fill}{format}}'.format(self.held_object, fill=fill, format=format_codes[format_code])


return result

@typed_operator(MesonOperator.DIV, int)
@InterpreterObject.operator(MesonOperator.DIV)
Expand Down
14 changes: 13 additions & 1 deletion mesonbuild/interpreter/primitives/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,19 @@ def substring_method(self, args: T.Tuple[T.Optional[int], T.Optional[int]], kwar
@InterpreterObject.method('to_int')
def to_int_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> int:
try:
return int(self.held_object)
s = self.held_object.strip()
s_unsigned = s.lstrip('+-').lower()

if s_unsigned.startswith('0x'):
base = 16
elif s_unsigned.startswith('0o'):
base = 8
elif s_unsigned.startswith('0b'):
base = 2
else:
base = 10

return int(s, base)
except ValueError:
raise InvalidArguments(f'String {self.held_object!r} cannot be converted to int')

Expand Down
131 changes: 131 additions & 0 deletions test cases/common/286 number base conversions/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
project('test number base conversions')

### .to_int()

# hexadecimal strings
assert('0x10'.to_int() == 16, 'Hex string conversion failed')
assert('0X10'.to_int() == 16, 'Hex string conversion (uppercase X) failed')
assert('0xff'.to_int() == 255, 'Hex string conversion (lowercase) failed')
assert('0xFF'.to_int() == 255, 'Hex string conversion (uppercase) failed')
assert('0xDEADBEEF'.to_int() == 3735928559, 'Large hex conversion failed')
assert('0x1'.to_int() == 1, 'Small hex conversion failed')
assert('0x0'.to_int() == 0, 'Zero hex conversion failed')

# signed hexadecimal strings
assert('-0xf'.to_int() == -15, 'Negative hex string conversion failed')
assert('+0x10'.to_int() == 16, 'Positive hex string conversion failed')
assert('-0xFF'.to_int() == -255, 'Negative hex string (uppercase) conversion failed')

# octal strings
assert('0o10'.to_int() == 8, 'Octal string conversion failed')
assert('0O10'.to_int() == 8, 'Octal string conversion (uppercase O) failed')
assert('0o77'.to_int() == 63, 'Octal string conversion failed')
assert('0o755'.to_int() == 493, 'Octal permission-like conversion failed')
assert('0o0'.to_int() == 0, 'Zero octal conversion failed')

# signed octal strings
assert('-0o10'.to_int() == -8, 'Negative octal string conversion failed')
assert('+0o77'.to_int() == 63, 'Positive octal string conversion failed')

# binary strings
assert('0b10'.to_int() == 2, 'Binary string conversion failed')
assert('0B10'.to_int() == 2, 'Binary string conversion (uppercase B) failed')
assert('0b1111'.to_int() == 15, 'Binary string conversion failed')
assert('0b11111111'.to_int() == 255, 'Binary byte conversion failed')
assert('0b0'.to_int() == 0, 'Zero binary conversion failed')

# signed binary strings
assert('-0b101'.to_int() == -5, 'Negative binary string conversion failed')
assert('+0b1111'.to_int() == 15, 'Positive binary string conversion failed')

# decimal strings (backwards compat)
assert('10'.to_int() == 10, 'Decimal string conversion failed')
assert('255'.to_int() == 255, 'Decimal string conversion failed')
assert('0'.to_int() == 0, 'Zero decimal conversion failed')
assert('12345'.to_int() == 12345, 'Large decimal conversion failed')

# leading zeroes are decimal (backwards compat)
assert('010'.to_int() == 10, 'Decimal with leading zero broke backward compatibility')
assert('0123'.to_int() == 123, 'Decimal with leading zeros broke backward compatibility')
assert('007'.to_int() == 7, 'Decimal with leading zeros broke backward compatibility')

### .to_string()

# hex format
assert(16.to_string(format: 'hex') == '0x10', 'Int to hex string failed')
assert(255.to_string(format: 'hex') == '0xff', 'Int to hex string failed')
assert(0.to_string(format: 'hex') == '0x0', 'Zero to hex string failed')
assert(1.to_string(format: 'hex') == '0x1', 'One to hex string failed')
assert(3735928559.to_string(format: 'hex') == '0xdeadbeef', 'Large hex string failed')

# octal format
assert(8.to_string(format: 'oct') == '0o10', 'Int to octal string failed')
assert(63.to_string(format: 'oct') == '0o77', 'Int to octal string failed')
assert(493.to_string(format: 'oct') == '0o755', 'Permission to octal string failed')
assert(0.to_string(format: 'oct') == '0o0', 'Zero to octal string failed')

# binary format
assert(2.to_string(format: 'bin') == '0b10', 'Int to binary string failed')
assert(15.to_string(format: 'bin') == '0b1111', 'Int to binary string failed')
assert(255.to_string(format: 'bin') == '0b11111111', 'Byte to binary string failed')
assert(0.to_string(format: 'bin') == '0b0', 'Zero to binary string failed')

# decimal format (explicit)
assert(10.to_string(format: 'dec') == '10', 'Int to decimal string failed')
assert(255.to_string(format: 'dec') == '255', 'Int to decimal string failed')

# default
assert(42.to_string() == '42', 'Default int to string failed')

# fill and hex format
assert(255.to_string(format: 'hex', fill: 8) == '0x0000ff', 'Hex with fill failed')
assert(1.to_string(format: 'hex', fill: 6) == '0x0001', 'Hex with fill failed')
assert(255.to_string(format: 'hex', fill: 4) == '0xff', 'Hex with fill (no padding needed) failed')

# fill and other formats
assert(8.to_string(format: 'oct', fill: 6) == '0o0010', 'Octal with fill failed')
assert(2.to_string(format: 'bin', fill: 10) == '0b00000010', 'Binary with fill failed')

# negative numbers
assert((-15).to_string(format: 'hex') == '-0xf', 'Negative hex conversion failed')
assert((-8).to_string(format: 'oct') == '-0o10', 'Negative octal conversion failed')
assert((-5).to_string(format: 'bin') == '-0b101', 'Negative binary conversion failed')

# negative numbers and fill
assert((-15).to_string(format: 'hex', fill: 6) == '-0x00f', 'Negative hex with fill failed')
assert((-8).to_string(format: 'oct', fill: 7) == '-0o0010', 'Negative octal with fill failed')
assert((-5).to_string(format: 'bin', fill: 8) == '-0b00101', 'Negative binary with fill failed')
assert((-4).to_string(fill: 3) == '-04', 'Negative decimal with fill failed')

# fill and decimal
assert(4.to_string(fill: 3) == '004', 'Decimal with fill failed')

### Round trip conversions

# positive

hex_val = 0x200
hex_str = hex_val.to_string(format: 'hex')
assert(hex_str.to_int() == hex_val, 'Hex round-trip failed')

oct_val = 0o755
oct_str = oct_val.to_string(format: 'oct')
assert(oct_str.to_int() == oct_val, 'Octal round-trip failed')

bin_val = 0b11010
bin_str = bin_val.to_string(format: 'bin')
assert(bin_str.to_int() == bin_val, 'Binary round-trip failed')

# negative

neg_hex = -255
neg_hex_str = neg_hex.to_string(format: 'hex')
assert(neg_hex_str.to_int() == neg_hex, 'Negative hex round-trip failed')

neg_oct = -63
neg_oct_str = neg_oct.to_string(format: 'oct')
assert(neg_oct_str.to_int() == neg_oct, 'Negative octal round-trip failed')

neg_bin = -15
neg_bin_str = neg_bin.to_string(format: 'bin')
assert(neg_bin_str.to_int() == neg_bin, 'Negative binary round-trip failed')
3 changes: 3 additions & 0 deletions test cases/common/35 string operations/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ assert('#include <foo/bar.h>'.underscorify() == '_include__foo_bar_h_', 'Broken
assert('Do SomeThing 09'.underscorify() == 'Do_SomeThing_09', 'Broken underscorify')

assert('3'.to_int() == 3, 'String int conversion does not work.')
assert('0x10'.to_int() == 16, 'Hex string conversion does not work.')
assert('0o10'.to_int() == 8, 'Octal string conversion does not work.')
assert('0b10'.to_int() == 2, 'Binary string conversion does not work.')

assert(true.to_string() == 'true', 'bool string conversion failed')
assert(false.to_string() == 'false', 'bool string conversion failed')
Expand Down
Loading