Ruby / Foundations: values, methods, text, and valid objects / Lesson 4
Handle text, encodings, and numeric conversion deliberately
Distinguish mutation, no-change bang results, bytes, characters, whole-string matching, and strict versus permissive parsing.
On this page
Lesson goals
- Avoid chaining a mutating String method that can return nil when nothing changes.
- Validate a complete input grammar before treating converted data as trusted.
A string contains bytes with an encoding
Ruby String can represent text or binary data and carries an encoding. bytesize measures bytes, while length depends on the string’s character interpretation and is not a universal count of user-perceived graphemes. Encoding labels, valid byte sequences, and normalization are separate concerns. Do not truncate arbitrary bytes and assume the result remains valid text. This lesson uses ordinary fixed ASCII examples to avoid claiming a cross-language text policy. For real external text, declare accepted encodings, malformed-input behavior, and size limits before searching, parsing, or displaying it.
Read both mutation and return semantics
String methods with similar names can differ in whether they mutate and what they return when no change occurs. strip returns a new stripped value; strip! modifies the receiver and returns nil if no stripping was needed. Chaining strip!.upcase can therefore fail on already clean input. The sample separates a derived value from the mutable original and explicitly demonstrates the no-change result. String.new avoids relying on frozen-string-literal configuration. No Ruby runtime was present, so the shown results are analytical expectations, not verified output from a selected interpreter.
title = String.new(' archive ')
normalized = title.strip.upcase
puts normalized
puts title.start_with?(' ')
unchanged = String.new('ready')
puts unchanged.strip!.nil?
ARCHIVE
true
true
Expected output is ARCHIVE, true, true. The original title keeps its leading space, while strip! on unchanged text returns nil. The program was not executed.
Match the whole value when validating a code
A regexp that finds a substring does not establish that the entire input follows a grammar. In Ruby, \A and \z anchor the beginning and end of the whole string; line-oriented anchors can behave differently with newline-containing input. match? asks a Boolean matching question without requiring callers to use an implicit global match result. Keep captures local when extraction matters. Escape literal user-supplied fragments with the appropriate API instead of interpolating them as active pattern syntax. Avoid complicated unbounded patterns when a simpler bounded grammar expresses the requirement.
Conversion grammar is not the same as a domain rule
to_i is permissive and can turn a numeric prefix into an integer while ignoring trailing text. Kernel Integer with an explicit base and exception:false rejects input that is not a valid integer under its conversion grammar. That grammar still need not equal the product’s exact accepted text format, and a parsed negative value may violate a positive-count rule. Keep lexical validation, conversion, and domain range checks distinct. The example contrasts a trailing-text case with valid positive and negative integers, using nil as a compact parse-failure result only for this teaching boundary.
puts '12x'.to_i
['42', '42x', '-1'].each do |text|
value = Integer(text, 10, exception: false)
puts(value.nil? ? 'invalid' : value)
end
puts(/\A[A-Z]-[0-9]{2}\z/.match?('A-07'))
12
42
invalid
-1
true
Expected lines are 12, 42, invalid, -1, true. The final match validates only the stated synthetic code shape, not existence or authorization. No interpreter execution occurred.
Normalize only under a declared policy
Whitespace removal and case conversion can be reasonable for a display label but wrong for a signed token, case-sensitive identifier, or preserved archival text. Keep the original value where provenance requires it and avoid logging sensitive input to explain a parse failure. A default zero can conceal malformed text as a legitimate quantity. Prefer an explicit invalid result or a structured reason when callers need feedback. Test empty, whitespace-only, malformed, boundary, and encoding-error cases in a compatible isolated project. This course does not claim that any Ruby parsing or encoding test was executed.
Common errors and fixes
A clean string causes a NoMethodError after strip!.
- Why it happens
- The bang method returned nil because it made no change.
- How to fix it
- Use a non-mutating transformation or handle the documented no-change return.
- Prevent it next time
- Check already-normalized input as well as dirty input.
Trailing junk is accepted as a count.
- Why it happens
- Permissive to_i was used as a validator.
- How to fix it
- Use the intended complete grammar and a strict conversion boundary.
- Prevent it next time
- Include a valid numeric prefix followed by invalid text in checks.
A substring match accepts a larger invalid identifier.
- Why it happens
- The regexp did not require the whole string to match.
- How to fix it
- Use whole-string anchors and the declared code grammar.
- Prevent it next time
- Test leading, trailing, and embedded-newline input explicitly.
Check your understanding
1. Why can to_i hide the difference between zero and malformed text?
Show worked solution
A string without a usable numeric prefix can become zero, which may also be valid data. That loses the rejection reason before the domain check runs. Use an explicit parse-failure outcome and then check the permitted numeric range. If the product accepts only a particular decimal grammar, validate that grammar rather than assuming every accepted Integer form is appropriate.
2. Should an archive import always strip and uppercase every identifier?
Show worked solution
Only when the identifier contract explicitly permits those transformations. They can alter identity, invalidate signatures, or lose original text. Separate display normalization from canonical storage and authentication rules. Preserve provenance where required, and verify the exact policy with representative non-ASCII and boundary inputs in the actual supported environment.
Key takeaways
- String mutation, encoding, full matching, and parsing are distinct contracts.
- Strict conversion still needs the domain’s permitted grammar and range.