Skip to content

Commit 0706712

Browse files
miharpclaude
andcommitted
Document typecasting
The language collection had no page covering conversion between data types. Add lang_typecasting.md, covering calling a data type as a constructor, conversion to numbers, strings, booleans, arrays and hashes, extracting numbers with scanf, and automatic coercion in arithmetic. Every example is verified against OpenVox 8.28.1 with puppet apply. Two behaviors documented here differ from what the upstream new() docstring and the Puppet Core typecasting page describe: * The docstring gives Float.new and Numeric.new an $abs default of true; at runtime the default is false, so Numeric('-42.3') returns -42.3 rather than 42.3. * Automatic string-to-number coercion in arithmetic is an error by default, because the strict setting defaults to error. The page shows explicit conversion instead of relaxing the setting. Conversion is described as something you ask for rather than something Puppet never does, since arithmetic coerces strings to numbers when strict is not error, and interpolation converts any value to a string even under strict=error. Both exceptions are signposted from the top of the page. Part of #407 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Michael Harp <mike@mikeharp.com>
1 parent 2ec6cd1 commit 0706712

3 files changed

Lines changed: 341 additions & 0 deletions

File tree

_data/nav/openvox_8x.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@
215215
link: "lang_data_type.html"
216216
- text: Abstract data types
217217
link: "lang_data_abstract.html"
218+
- text: Typecasting
219+
link: "lang_typecasting.html"
218220
- text: Using templates
219221
link: "lang_template.html"
220222
- text: Embedded Puppet (EPP) template syntax

docs/_openvox_8x/lang_data.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,8 @@ For special abstract data types, which you can use to do more sophisticated or p
4949

5050
* [Abstract Data Types](./lang_data_abstract.html)
5151

52+
Converting a value from one data type to another is something you ask for explicitly. To turn a value of one
53+
type into a value of another, such as a string holding a number into an actual number, see:
54+
55+
* [Typecasting](./lang_typecasting.html)
56+
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
---
2+
layout: default
3+
title: "Language: Typecasting"
4+
---
5+
6+
[data type]: lang_data_type.html
7+
[data types]: lang_data_type.html
8+
[new function]: function.html#new
9+
[scanf function]: function.html#scanf
10+
[strings]: lang_data_string.html
11+
[numbers]: lang_data_number.html
12+
[booleans]: lang_data_boolean.html
13+
[arrays]: lang_data_array.html
14+
[hashes]: lang_data_hash.html
15+
[abstract types]: lang_data_abstract.html
16+
[regexp]: lang_data_regexp.html
17+
[binary]: lang_data_binary.html
18+
[time]: lang_data_time.html
19+
20+
Every value in the Puppet language has a [data type][], and converting between types is something you ask
21+
for rather than something Puppet does for you. When you have a string that holds a number, or an array of
22+
pairs that ought to be a hash, you convert it by asking for a new value of the type you want.
23+
24+
Converting never changes the original value. It produces a new value, leaving the one you started with
25+
alone.
26+
27+
Two things do convert without being asked: arithmetic on a string that looks like a number, and string
28+
interpolation. Both are covered in [automatic coercion](#automatic-coercion-in-arithmetic) below.
29+
30+
## Calling a data type as a constructor
31+
32+
To convert a value, call the target data type as though it were a function, passing the value you want to
33+
convert:
34+
35+
```puppet
36+
notice(Integer('42')) # 42
37+
notice(Boolean('yes')) # true
38+
notice(String(42)) # 42
39+
```
40+
41+
Calling a data type this way is exactly the same as calling the [`new` function][new function] on it. These
42+
two lines do the same thing:
43+
44+
```puppet
45+
$a = Integer.new('42')
46+
$b = Integer('42')
47+
```
48+
49+
Most people find the shorter form easier to read, so the rest of this page uses it.
50+
51+
Some conversions take extra arguments. You can pass them by position, or by name using a hash:
52+
53+
```puppet
54+
notice(Integer('42', 8)) # 34
55+
notice(Integer({'from' => '42', 'radix' => 8})) # 34
56+
```
57+
58+
Both of those read the string `'42'` as an octal number, giving the decimal value 34.
59+
60+
If the data type has constraints, the converted value is checked against them and an error is raised if it
61+
doesn't fit. For example, `Integer[0]` only accepts values of zero or more:
62+
63+
```puppet
64+
Integer[0]('-100')
65+
# Error: Converted value from Integer[0].new() has wrong
66+
# type, expects an Integer[0] value, got Integer[-100, -100]
67+
```
68+
69+
You cannot construct most [abstract types][], such as `Variant`. `Optional[T]` and `NotUndef[T]` are
70+
exceptions: they convert exactly as their type argument `T` does, and `Optional[T]` also accepts `undef`:
71+
72+
```puppet
73+
notice(Optional[Integer]('42')) # 42
74+
notice(Optional[Integer](undef) =~ Undef) # true
75+
```
76+
77+
## Converting to numbers
78+
79+
For what the resulting values can do once you have them, see [numbers][].
80+
81+
### Integer
82+
83+
`Integer` accepts strings, floats, and booleans. When converting a string, you can give a radix (base) as a
84+
second argument:
85+
86+
```puppet
87+
notice(Integer('0xFF', 16)) # 255
88+
notice(Integer('010', 10)) # 10
89+
```
90+
91+
If you don't specify a radix, Puppet detects it from the start of the string: a leading `0b` or `0B` means
92+
binary, `0x` or `0X` means hexadecimal, and a leading `0` means octal. Everything else is decimal. Passing
93+
`default` as the radix asks for this same detection explicitly.
94+
95+
```puppet
96+
notice(Integer('010')) # 8
97+
notice(Integer('0b1011')) # 11
98+
notice(Integer('-42')) # -42
99+
```
100+
101+
Converting a float truncates the fraction rather than rounding it, and a boolean becomes `1` or `0`:
102+
103+
```puppet
104+
notice(Integer(3.99)) # 3
105+
notice(Integer(-3.99)) # -3
106+
notice(Integer(true)) # 1
107+
notice(Integer(false)) # 0
108+
```
109+
110+
A third argument of `true` makes the result absolute:
111+
112+
```puppet
113+
notice(Integer(-38, 10, true)) # 38
114+
```
115+
116+
### Float and Numeric
117+
118+
`Float` always produces a float, adding a `.0` fraction to whole numbers:
119+
120+
```puppet
121+
notice(Float('3.14')) # 3.14
122+
notice(Float(3)) # 3.0
123+
notice(Float(true)) # 1.0
124+
notice(Float('0xFF')) # 255.0
125+
```
126+
127+
`Numeric` picks the type to suit the value. A value with a decimal point or an exponent becomes a float,
128+
and anything else becomes an integer:
129+
130+
```puppet
131+
notice(Numeric('3.14')) # 3.14
132+
notice(Numeric('1e3')) # 1000.0
133+
notice(Numeric('0xFF')) # 255
134+
notice(Numeric(true)) # 1
135+
```
136+
137+
Both accept an extra `true` argument to return an absolute value:
138+
139+
```puppet
140+
notice(Numeric('-42.3')) # -42.3
141+
notice(Numeric(-42.3, true)) # 42.3
142+
```
143+
144+
> **Note:** conversion to a number is strict about the text it accepts. A string with surrounding whitespace,
145+
> such as `' 42 '`, cannot be converted, and neither can `undef`. Both raise an error rather than
146+
> returning a default. If you need to tolerate stray text, see
147+
> [extracting numbers from strings](#extracting-numbers-from-strings) below.
148+
149+
## Converting to strings
150+
151+
`String` converts any value, including arrays and hashes, into a [string][strings]:
152+
153+
```puppet
154+
notice(String(42)) # 42
155+
notice(String(true)) # true
156+
notice(String([1,2,3])) # [1, 2, 3]
157+
```
158+
159+
A second argument gives a format directive controlling how the value is rendered:
160+
161+
```puppet
162+
notice(String(255, '%x')) # ff
163+
notice(String(255, '%#x')) # 0xff
164+
notice(String(3.14159, '%.2f')) # 3.14
165+
```
166+
167+
Two defaults are worth knowing. A float with no format is rendered with six decimal places, and `undef`
168+
becomes an empty string:
169+
170+
```puppet
171+
notice(String(1.0)) # 1.000000
172+
notice(String(undef) == '') # true
173+
```
174+
175+
`String` accepts a large set of format directives, and can take a hash mapping data types to formats so that
176+
values inside an array or hash are each rendered differently. For the full set, see the
177+
[`new` function][new function].
178+
179+
## Converting to booleans
180+
181+
Unlike some languages, Puppet has no notion of "truthy" values to fall back on, so conversion to a
182+
[boolean][booleans] accepts a small, fixed vocabulary. The strings `'true'`, `'yes'`, and `'y'` become `true`, and
183+
`'false'`, `'no'`, and `'n'` become `false`. The comparison ignores case:
184+
185+
```puppet
186+
notice(Boolean('true')) # true
187+
notice(Boolean('YES')) # true
188+
notice(Boolean('No')) # false
189+
```
190+
191+
For numbers, zero is `false` and everything else, including negative numbers, is `true`:
192+
193+
```puppet
194+
notice(Boolean(0)) # false
195+
notice(Boolean(0.0)) # false
196+
notice(Boolean(1)) # true
197+
notice(Boolean(-1)) # true
198+
```
199+
200+
Any other string raises an error, so `Boolean('maybe')` fails rather than guessing.
201+
202+
## Converting between arrays and hashes
203+
204+
[Arrays][arrays] and [hashes][hashes] convert into one another, which is a common need when data arrives in
205+
one shape and your code expects the other.
206+
207+
### To an array
208+
209+
A hash becomes an array of key/value pairs:
210+
211+
```puppet
212+
notice(Array({'a' => 1, 'b' => 2}))
213+
# [[a, 1], [b, 2]]
214+
```
215+
216+
An array is returned unchanged, an empty hash becomes an empty array, and a [`Binary`][binary] becomes an
217+
array of byte values:
218+
219+
```puppet
220+
notice(Array(Binary('aGk='))) # [104, 105]
221+
```
222+
223+
Anything iterable is turned into an array of the things it iterates over, which is easy to trip over. A
224+
string iterates over its characters, and an integer iterates over the numbers below it:
225+
226+
```puppet
227+
notice(Array('hello')) # [h, e, l, l, o]
228+
notice(Array(1)) # [0]
229+
```
230+
231+
When what you actually want is "wrap this in an array unless it already is one", pass a second argument of
232+
`true`:
233+
234+
```puppet
235+
notice(Array('hello', true)) # [hello]
236+
notice(Array([1, 2], true)) # [1, 2]
237+
```
238+
239+
Note that `undef` wraps to an empty array, not to an array containing `undef`:
240+
241+
```puppet
242+
notice(Array(undef, true)) # []
243+
```
244+
245+
### To a hash
246+
247+
An array of pairs becomes a hash, and so does a flat array with an even number of entries:
248+
249+
```puppet
250+
notice(Hash([['a', 1], ['b', 2]])) # {a => 1, b => 2}
251+
notice(Hash(['a', 1, 'b', 2])) # {a => 1, b => 2}
252+
```
253+
254+
An empty array becomes an empty hash, and a hash is returned unchanged. An array with an odd number of
255+
entries raises an `odd number of arguments for Hash` error.
256+
257+
### Tuple and Struct
258+
259+
`Tuple` converts exactly as `Array` does, and `Struct` exactly as `Hash` does. The difference is that the
260+
result is then checked against the type you asked for:
261+
262+
```puppet
263+
notice(Tuple[String, Integer](['a', 1])) # [a, 1]
264+
notice(Struct[{'a' => Integer}]([['a', 1]])) # {a => 1}
265+
```
266+
267+
## Extracting numbers from strings
268+
269+
Conversion requires the whole string to be a number. To pull a number out of a longer piece of text, use the
270+
[`scanf` function][scanf function], which takes a format and always returns an array of what it found:
271+
272+
```puppet
273+
notice(scanf('42 apples', '%d')) # [42]
274+
notice('2.5kg'.scanf('%f')) # [2.5]
275+
```
276+
277+
If nothing matches, you get an empty array rather than an error, which makes `scanf` a good way to handle
278+
input that might not contain a number at all:
279+
280+
```puppet
281+
notice(scanf('no digits', '%d')) # []
282+
```
283+
284+
## Automatic coercion in arithmetic
285+
286+
Arithmetic operators can coerce a string to a number, but by default OpenVox treats doing so as an error:
287+
288+
```puppet
289+
notice('1' + 1)
290+
# Error: The string '1' was automatically coerced to the
291+
# numerical value 1
292+
```
293+
294+
This is controlled by the `strict` setting, which defaults to `error`. Setting it to `warning` lets the
295+
arithmetic proceed and logs a warning instead; setting it to `off` silences the message entirely. Rather
296+
than relaxing the setting, convert explicitly, which works under any value of `strict`:
297+
298+
```puppet
299+
notice(Integer('1') + 1) # 2
300+
```
301+
302+
String interpolation is a separate matter and is always allowed. Putting a value in a string converts it
303+
without complaint:
304+
305+
```puppet
306+
$n = 42
307+
notice("interpolated:${n}") # interpolated:42
308+
```
309+
310+
## Other conversions
311+
312+
A string becomes a [`Regexp`][regexp], which is useful when the pattern is built at runtime or comes from
313+
data:
314+
315+
```puppet
316+
$r = Regexp('[a-z]+\.com')
317+
notice('foo.com' =~ $r) # true
318+
```
319+
320+
A string naming a data type becomes that data type:
321+
322+
```puppet
323+
notice(Type('Integer') == Integer) # true
324+
```
325+
326+
The [`Timestamp` and `Timespan`][time] types have their own rich set of conversions from strings, numbers,
327+
and hashes, covered on their own page.
328+
329+
## Further reading
330+
331+
* The [`new` function][new function] documents every conversion signature in full, including the complete
332+
list of string format directives.
333+
* [Data type syntax][data types] covers how data types are written and where you can use them.
334+
* [Abstract data types][abstract types] covers `Optional`, `NotUndef`, `Variant`, and the rest.

0 commit comments

Comments
 (0)