Skip to content

Commit c11e546

Browse files
committed
feat(string-utils): add number parsing functions with validation
Add five new parsing functions to safely parse and validate numeric strings: - NAVParseInteger: parse unsigned integers (0-65535) - NAVParseSignedInteger: parse signed integers (-32768 to 32767) - NAVParseLong: parse unsigned longs (0-4294967295) - NAVParseSignedLong: parse signed longs (-2147483648 to 2147483647) - NAVParseFloat: parse floating-point numbers Key features: - Range validation with detailed error logging - ATOI/ATOF-like parsing behavior (stops at first whitespace after number) - Manual digit-by-digit parsing for Long types to handle full 32-bit range - Proper overflow/underflow detection for all integer types - Extract numbers from mixed content (e.g., 'Volume=50' → 50)
1 parent dfe0808 commit c11e546

File tree

10 files changed

+1257
-18
lines changed

10 files changed

+1257
-18
lines changed

StringUtils/NAVFoundation.StringUtils.axi

Lines changed: 499 additions & 0 deletions
Large diffs are not rendered by default.

StringUtils/README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Working with strings in NetLinx can be challenging due to limited built-in funct
1111
- **String Manipulation**: Trimming, substring extraction, replacing, etc.
1212
- **String Testing**: Contains, startsWith, endsWith, etc.
1313
- **String Searching**: Find index of substrings, count occurrences
14+
- **String Parsing**: Parse integers, signed integers, longs, signed longs, and floats with validation
1415
- **Case Conversion**: Upper/lower case, camelCase, PascalCase, etc.
1516
- **Character Operations**: Check character types, convert case
1617
- **String Splitting and Joining**: Split strings into arrays, join arrays into strings
@@ -203,6 +204,59 @@ result = NAVStringTrainCase('hello world') // Returns 'Hello-World'
203204
result = NAVStringScreamKebabCase('hello world') // Returns 'HELLO-WORLD'
204205
```
205206

207+
### String Parsing and Conversion
208+
209+
```netlinx
210+
// Parse unsigned integer (0-65535)
211+
stack_var integer value
212+
stack_var char success
213+
success = NAVParseInteger('12345', value) // Returns true, value = 12345
214+
success = NAVParseInteger('99999', value) // Returns false (out of range)
215+
success = NAVParseInteger('-100', value) // Returns false (negative)
216+
success = NAVParseInteger('Volume=50', value) // Returns true, value = 50 (extracts first number)
217+
success = NAVParseInteger(' 42 99 ', value) // Returns true, value = 42 (stops at space)
218+
219+
// Parse signed integer (-32768 to 32767)
220+
stack_var sinteger svalue
221+
success = NAVParseSignedInteger('-12345', svalue) // Returns true, svalue = -12345
222+
success = NAVParseSignedInteger('50000', svalue) // Returns false (out of range)
223+
success = NAVParseSignedInteger('Offset=-100', svalue) // Returns true, svalue = -100
224+
success = NAVParseSignedInteger(' -10 20 ', svalue) // Returns true, svalue = -10
225+
226+
// Parse unsigned long (0-4294967295)
227+
stack_var long lvalue
228+
success = NAVParseLong('1234567890', lvalue) // Returns true, lvalue = 1234567890
229+
success = NAVParseLong('4294967295', lvalue) // Returns true (max LONG value)
230+
success = NAVParseLong('5000000000', lvalue) // Returns false (overflow)
231+
success = NAVParseLong('-1000', lvalue) // Returns false (negative)
232+
success = NAVParseLong(' 100 200 ', lvalue) // Returns true, lvalue = 100
233+
234+
// Parse signed long (-2147483648 to 2147483647)
235+
stack_var slong slvalue
236+
success = NAVParseSignedLong('-1234567890', slvalue) // Returns true, slvalue = -1234567890
237+
success = NAVParseSignedLong('2147483647', slvalue) // Returns true (max SLONG)
238+
success = NAVParseSignedLong('3000000000', slvalue) // Returns false (overflow)
239+
success = NAVParseSignedLong('xyz-123', slvalue) // Returns true, slvalue = -123 (extracts number)
240+
success = NAVParseSignedLong(' -100 200 ', slvalue) // Returns true, slvalue = -100
241+
242+
// Parse floating-point number
243+
stack_var float fvalue
244+
success = NAVParseFloat('3.14159', fvalue) // Returns true, fvalue = 3.14159
245+
success = NAVParseFloat('-1.25e-3', fvalue) // Returns true, fvalue = -0.00125
246+
success = NAVParseFloat('Temp=22.5', fvalue) // Returns true, fvalue = 22.5 (extracts first number)
247+
success = NAVParseFloat(' 10.5 20.3 ', fvalue) // Returns true, fvalue = 10.5
248+
```
249+
250+
**Parsing Behavior Notes:**
251+
- All parsing functions skip leading whitespace
252+
- They parse the first valid number encountered
253+
- Parsing stops at the first space or non-digit character after the number (matching ATOI/ATOF behavior)
254+
- Example: `' 10 20 '` parses as `10`, not `20` or `1020`
255+
- The functions validate ranges and return `false` if the value is out of bounds
256+
- `NAVParseInteger` and `NAVParseSignedInteger` use ATOI internally
257+
- `NAVParseLong` and `NAVParseSignedLong` use manual digit-by-digit parsing for full range support and overflow detection
258+
- `NAVParseFloat` uses ATOF internally
259+
206260
### Time and Duration Functions
207261

208262
```netlinx
@@ -305,6 +359,52 @@ age = NAVGetStringBetween(data, '[age=', ']') // '30'
305359
city = NAVGetStringBetween(data, '[city=', ']') // 'New York'
306360
```
307361

362+
## Example: Parsing Numbers from User Input
363+
364+
```netlinx
365+
// Parse a volume level from a command string
366+
stack_var char command[50]
367+
stack_var integer volume
368+
stack_var char success
369+
370+
command = 'VOLUME=75'
371+
success = NAVParseInteger(command, volume)
372+
if (success) {
373+
// volume = 75, valid range 0-65535
374+
send_command dvAudioDevice, "'VOLUME-', itoa(volume)"
375+
}
376+
377+
// Parse temperature with negative values
378+
stack_var char tempStr[20]
379+
stack_var sinteger temperature
380+
381+
tempStr = 'TEMP=-15'
382+
success = NAVParseSignedInteger(tempStr, temperature)
383+
if (success) {
384+
// temperature = -15, valid range -32768 to 32767
385+
}
386+
387+
// Parse large numbers like timestamps
388+
stack_var char timestampStr[20]
389+
stack_var long timestamp
390+
391+
timestampStr = '1672531200'
392+
success = NAVParseLong(timestampStr, timestamp)
393+
if (success) {
394+
// timestamp = 1672531200 (Unix timestamp)
395+
}
396+
397+
// Handle mixed content - extracts first number
398+
stack_var char response[50]
399+
stack_var float value
400+
401+
response = 'Temperature: 22.5 degrees'
402+
success = NAVParseFloat(response, value)
403+
if (success) {
404+
// value = 22.5
405+
}
406+
```
407+
308408
## Performance Tips
309409

310410
- For operations on large strings, be aware of NetLinx's memory limitations

__tests__/NAVFoundation-Tests.apw

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@
189189
<Comments></Comments>
190190
</File>
191191
</System>
192-
<System IsActive="true" Platform="Netlinx" Transport="Serial" TransportEx="TCPIP">
192+
<System IsActive="false" Platform="Netlinx" Transport="Serial" TransportEx="TCPIP">
193193
<Identifier>Jsmn</Identifier>
194194
<SysID>0</SysID>
195195
<TransTCPIP>0.0.0.0</TransTCPIP>
@@ -211,7 +211,7 @@
211211
<Comments></Comments>
212212
</File>
213213
</System>
214-
<System IsActive="false" Platform="Netlinx" Transport="Serial" TransportEx="TCPIP">
214+
<System IsActive="true" Platform="Netlinx" Transport="Serial" TransportEx="TCPIP">
215215
<Identifier>StringUtils</Identifier>
216216
<SysID>0</SysID>
217217
<TransTCPIP>0.0.0.0</TransTCPIP>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
PROGRAM_NAME='NAVParseFloat.axi'
2+
3+
#include 'NAVFoundation.Core.axi'
4+
#include 'NAVFoundation.Testing.axi'
5+
6+
DEFINE_CONSTANT
7+
8+
constant char NAV_PARSE_FLOAT_TESTS[][255] = {
9+
// Valid cases - basic
10+
'0.0', // 1: Zero
11+
'1.0', // 2: One
12+
'3.14159', // 3: Pi
13+
'42.5', // 4: Simple decimal
14+
'-1.5', // 5: Negative decimal
15+
'-99.99', // 6: Negative with multiple decimals
16+
// Valid cases - scientific notation
17+
'1.25e-3', // 7: Scientific notation (0.00125)
18+
'-1.25e-3', // 8: Negative scientific notation
19+
'1.5e3', // 9: Positive exponent (1500.0)
20+
'2.5E2', // 10: Capital E (250.0)
21+
// Valid cases - edge formats
22+
'0.1', // 11: Small decimal
23+
'999999.999999', // 12: Large number with decimals
24+
'.5', // 13: Leading decimal point
25+
'5.', // 14: Trailing decimal point
26+
// Valid cases - with whitespace/text
27+
' 22.5', // 15: Leading whitespace
28+
'22.5 ', // 16: Trailing whitespace
29+
' 3.14 ', // 17: Both whitespace
30+
'Temp=22.5', // 18: Text prefix
31+
'15.5 degrees', // 19: Text suffix
32+
// Valid cases - integers (no decimal required now)
33+
'42', // 20: Integer format
34+
'-10', // 21: Negative integer format
35+
36+
// Invalid cases
37+
'', // 22: Empty string
38+
' ', // 23: Whitespace only
39+
'abc', // 24: No digits
40+
'xyz', // 25: No digits
41+
'not a number', // 26: Text without number
42+
{' ', $0D, $0A}, // 27: Whitespace characters (space, CR, LF)
43+
{' ', $0D, $0A, '2', '2', '.', '5', $09}, // 28: Whitespace with number (space, CR, LF, '22.5', tab)
44+
' 10.5 20.3 ' // 29: Multiple numbers with spaces
45+
}
46+
47+
constant char NAV_PARSE_FLOAT_EXPECTED_RESULT[] = {
48+
// Valid (1-21)
49+
true, true, true, true, true, true, true, true, true, true,
50+
true, true, true, true, true, true, true, true, true, true, true,
51+
// Invalid (22-26)
52+
false, false, false, false, false,
53+
// Invalid (27)
54+
false,
55+
// Valid (28-29)
56+
true, true
57+
}
58+
59+
constant float NAV_PARSE_FLOAT_EXPECTED_VALUES[] = {
60+
0.0, // 1: '0.0'
61+
1.0, // 2: '1.0'
62+
3.14159, // 3: '3.14159'
63+
42.5, // 4: '42.5'
64+
-1.5, // 5: '-1.5'
65+
-99.99, // 6: '-99.99'
66+
0.00125, // 7: '1.25e-3'
67+
-0.00125, // 8: '-1.25e-3'
68+
1500.0, // 9: '1.5e3'
69+
250.0, // 10: '2.5E2'
70+
0.1, // 11: '0.1'
71+
999999.999999, // 12: '999999.999999'
72+
0.5, // 13: '.5'
73+
5.0, // 14: '5.'
74+
22.5, // 15: ' 22.5'
75+
22.5, // 16: '22.5 '
76+
3.14, // 17: ' 3.14 '
77+
22.5, // 18: 'Temp=22.5'
78+
15.5, // 19: '15.5 degrees'
79+
42.0, // 20: '42'
80+
-10.0, // 21: '-10'
81+
22.5, // 28: Whitespace with number
82+
10.5 // 29: ' 10.5 20.3 '
83+
}
84+
85+
define_function TestNAVParseFloat() {
86+
stack_var integer x
87+
stack_var integer validCount
88+
89+
NAVLogTestSuiteStart('NAVParseFloat')
90+
91+
validCount = 0
92+
93+
for (x = 1; x <= length_array(NAV_PARSE_FLOAT_TESTS); x++) {
94+
stack_var char result
95+
stack_var float value
96+
stack_var char shouldPass
97+
98+
shouldPass = NAV_PARSE_FLOAT_EXPECTED_RESULT[x]
99+
result = NAVParseFloat(NAV_PARSE_FLOAT_TESTS[x], value)
100+
101+
// Check if result matches expectation
102+
if (!NAVAssertBooleanEqual('Should parse with the expected result', shouldPass, result)) {
103+
NAVLogTestFailed(x, NAVBooleanToString(shouldPass), NAVBooleanToString(result))
104+
continue
105+
}
106+
107+
if (!shouldPass) {
108+
// If should fail, no further checks needed
109+
NAVLogTestPassed(x)
110+
continue
111+
}
112+
113+
// If should pass, validate the parsed value
114+
{
115+
stack_var float expectedValue
116+
117+
validCount++
118+
expectedValue = NAV_PARSE_FLOAT_EXPECTED_VALUES[validCount]
119+
120+
if (!NAVAssertFloatEqual('Should parse to the expected float value', expectedValue, value)) {
121+
NAVLogTestFailed(x, ftoa(expectedValue), ftoa(value))
122+
continue
123+
}
124+
}
125+
126+
NAVLogTestPassed(x)
127+
}
128+
129+
NAVLogTestSuiteEnd('NAVParseFloat')
130+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
PROGRAM_NAME='NAVParseInteger.axi'
2+
3+
#include 'NAVFoundation.Core.axi'
4+
#include 'NAVFoundation.Testing.axi'
5+
6+
DEFINE_CONSTANT
7+
8+
constant char NAV_PARSE_INTEGER_TESTS[][255] = {
9+
// Valid cases
10+
'0', // 1: Zero
11+
'1', // 2: Single digit
12+
'42', // 3: Two digits
13+
'12345', // 4: Multiple digits
14+
'65535', // 5: Maximum value
15+
'100', // 6: Common value
16+
' 42', // 7: Leading whitespace
17+
'42 ', // 8: Trailing whitespace
18+
' 123 ', // 9: Both whitespace
19+
'Volume=50', // 10: Text prefix (ATOI extracts number)
20+
'99 percent', // 11: Text suffix
21+
22+
// Invalid cases
23+
'', // 12: Empty string
24+
' ', // 13: Whitespace only
25+
'-1', // 14: Negative number
26+
'-100', // 15: Negative number
27+
'65536', // 16: Above maximum (65535)
28+
'99999', // 17: Way above maximum
29+
'100000', // 18: Above maximum
30+
'abc', // 19: No digits
31+
'xyz123', // 20: Letters before number (ATOI should extract)
32+
'123.45', // 21: Decimal point (not integer format)
33+
{' ', $0D, $0A}, // 22: Whitespace characters (space, CR, LF)
34+
{' ', $0D, $0A, '1', '5', '0', $09}, // 23: Whitespace with number (space, CR, LF, '150', tab)
35+
' 10 20 ' // 24: Multiple numbers with spaces
36+
}
37+
38+
constant char NAV_PARSE_INTEGER_EXPECTED_RESULT[] = {
39+
// Valid (1-11)
40+
true, true, true, true, true, true, true, true, true, true, true,
41+
// Invalid (12-19)
42+
false, false, false, false, false, false, false, false,
43+
// Valid (20-21, 23) - ATOI extracts numbers
44+
true, true,
45+
// Invalid (22)
46+
false,
47+
// Valid (23-24)
48+
true, true
49+
}
50+
51+
constant integer NAV_PARSE_INTEGER_EXPECTED_VALUES[] = {
52+
0, // 1: '0'
53+
1, // 2: '1'
54+
42, // 3: '42'
55+
12345, // 4: '12345'
56+
65535, // 5: '65535'
57+
100, // 6: '100'
58+
42, // 7: ' 42'
59+
42, // 8: '42 '
60+
123, // 9: ' 123 '
61+
50, // 10: 'Volume=50'
62+
99, // 11: '99 percent'
63+
123, // 20: 'xyz123' - ATOI extracts 123
64+
123, // 21: '123.45' - ATOI extracts 123
65+
150, // 23: Whitespace with number - ATOI extracts 150
66+
10 // 24: ' 10 20 ' - ATOI stops at space, returns 10
67+
}
68+
69+
define_function TestNAVParseInteger() {
70+
stack_var integer x
71+
stack_var integer validCount
72+
73+
NAVLogTestSuiteStart('NAVParseInteger')
74+
75+
validCount = 0
76+
77+
for (x = 1; x <= length_array(NAV_PARSE_INTEGER_TESTS); x++) {
78+
stack_var char result
79+
stack_var integer value
80+
stack_var char shouldPass
81+
82+
shouldPass = NAV_PARSE_INTEGER_EXPECTED_RESULT[x]
83+
result = NAVParseInteger(NAV_PARSE_INTEGER_TESTS[x], value)
84+
85+
// Check if result matches expectation
86+
if (!NAVAssertBooleanEqual('Should parse with the expected result', shouldPass, result)) {
87+
NAVLogTestFailed(x, NAVBooleanToString(shouldPass), NAVBooleanToString(result))
88+
continue
89+
}
90+
91+
if (!shouldPass) {
92+
// If should fail, no further checks needed
93+
NAVLogTestPassed(x)
94+
continue
95+
}
96+
97+
// If should pass, validate the parsed value
98+
{
99+
validCount++
100+
101+
if (!NAVAssertIntegerEqual('Should parse to the expected integer value', NAV_PARSE_INTEGER_EXPECTED_VALUES[validCount], value)) {
102+
NAVLogTestFailed(x, itoa(NAV_PARSE_INTEGER_EXPECTED_VALUES[validCount]), itoa(value))
103+
continue
104+
}
105+
}
106+
107+
NAVLogTestPassed(x)
108+
}
109+
110+
NAVLogTestSuiteEnd('NAVParseInteger')
111+
}

0 commit comments

Comments
 (0)