diff --git a/docs/markdown/Syntax.md b/docs/markdown/Syntax.md index f4a2e1772a2a..21e73cbb21e9 100644 --- a/docs/markdown/Syntax.md +++ b/docs/markdown/Syntax.md @@ -80,6 +80,15 @@ 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 @@ -87,6 +96,15 @@ 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`. diff --git a/mesonbuild/interpreter/primitives/integer.py b/mesonbuild/interpreter/primitives/integer.py index c59ea6e229a3..1ef5c6584fb3 100644 --- a/mesonbuild/interpreter/primitives/integer.py +++ b/mesonbuild/interpreter/primitives/integer.py @@ -7,12 +7,20 @@ FeatureBroken, InvalidArguments, KwargInfo, noKwargs, noPosargs, typed_operator, typed_kwargs ) +from ..type_checking import in_set_validator import typing as T if T.TYPE_CHECKING: + from typing_extensions import Literal, TypedDict + from ...interpreterbase import TYPE_var, TYPE_kwargs + class ToStringKw(TypedDict): + + fill: int + format: Literal["dec", "hex", "oct", "bin"] + class IntegerHolder(ObjectHolder[int]): # Operators that only require type checks TRIVIAL_OPERATORS = { @@ -55,12 +63,20 @@ 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=in_set_validator({"dec", "hex", "oct", "bin"}), + ), ) @noPosargs @InterpreterObject.method('to_string') - def to_string_method(self, args: T.List[TYPE_var], kwargs: T.Dict[str, T.Any]) -> str: - return str(self.held_object).zfill(kwargs['fill']) + def to_string_method(self, args: T.List[TYPE_var], kwargs: 'ToStringKw') -> str: + format_codes = {"hex": "x", "oct": "o", "bin": "b", "dec": "d"} + return format(self.held_object, f'#0{kwargs["fill"]}{format_codes[kwargs["format"]]}') @typed_operator(MesonOperator.DIV, int) @InterpreterObject.operator(MesonOperator.DIV) diff --git a/mesonbuild/interpreter/primitives/string.py b/mesonbuild/interpreter/primitives/string.py index 190e82a39559..d780ec851679 100644 --- a/mesonbuild/interpreter/primitives/string.py +++ b/mesonbuild/interpreter/primitives/string.py @@ -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') diff --git a/test cases/common/286 number base conversions/meson.build b/test cases/common/286 number base conversions/meson.build new file mode 100644 index 000000000000..42811f6759fa --- /dev/null +++ b/test cases/common/286 number base conversions/meson.build @@ -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') diff --git a/test cases/common/35 string operations/meson.build b/test cases/common/35 string operations/meson.build index 8cc5b0c08f61..6d3993bfce51 100644 --- a/test cases/common/35 string operations/meson.build +++ b/test cases/common/35 string operations/meson.build @@ -54,6 +54,9 @@ assert('#include '.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')