puppetlabs/puppet · error · ArgumentError
Format specifiers %L and %N denotes fractions and must be us
Error message
Format specifiers %L and %N denotes fractions and must be used together with a specifier of higher magnitude
What it means
When parsing (not formatting), the %L (milliseconds) and %N (nanoseconds) directives are only meaningful as the fractional part of a larger component. The format parser marks the highest-magnitude segment with use_total; if that highest segment is a FragmentSegment (%L or %N alone, or %L with only smaller-or-equal segments), FragmentSegment#nanoseconds raises ArgumentError because interpreting a bare number as 'total milliseconds' or 'total nanoseconds' would be ambiguous with sign handling and digit width.
Source
Thrown at lib/puppet/pops/time/timespan.rb:481
super(padchar, width, 2)
end
def multiplier
NSECS_PER_SEC
end
def append_to(bld, ts)
append_value(bld, use_total? ? ts.total_seconds : ts.seconds)
end
end
# Class that assumes that leading zeroes are significant and that trailing zeroes are not and left justifies when formatting.
# Applicable after a decimal point, and hence to the %L and %N formats.
class FragmentSegment < ValueSegment
def nanoseconds(group)
# Using %L or %N to parse a string only makes sense when they are considered to be fractions. Using them
# as a total quantity would introduce ambiguities.
raise ArgumentError, _('Format specifiers %L and %N denotes fractions and must be used together with a specifier of higher magnitude') if use_total?
n = group.to_i
p = 9 - group.length
p <= 0 ? n : n * 10**p
end
def create_format
if @padchar.nil?
'%d'
else
"%-#{@width || @default_width}d"
end
end
def append_value(bld, n)
# Strip trailing zeroes when default format is used
n = n.to_s.sub(/\A([0-9]+?)0*\z/, '\1').to_i unless use_total? || @padchar == '0'
super(bld, n)View on GitHub (pinned to e227c27540)
Solutions
- Anchor the fraction to a higher-magnitude specifier: use '%S.%N' or '%S.%L' and pass whole seconds plus fraction
- If the input is a plain count of milliseconds or nanoseconds, skip format parsing entirely and construct directly: Timespan(0, nsec_count) or Timespan.from_hash('milliseconds' => m)
- Remember the rule: at least one of %D %H %M %S must appear to the left of %L/%N when parsing
- Test format strings with both a format round-trip and a parse before shipping
Example fix
// before
ms = '42150'
span = Timespan.parse(ms, '%L') # raises: %L is highest magnitude
// after (input is a total millisecond count)
span = Timespan(0, ms.to_i * 1_000_000) # nsecs
# or parse as seconds + fraction if input actually is '42.15' seconds:
# span = Timespan.parse('42.15', '%S.%N') Defensive patterns
Strategy: validation
Validate before calling
# a parseable format must contain a higher-magnitude directive than %L/%N higher = /%[-_0-9]*[DHMS]/ raise ArgumentError, "format needs %D/%H/%M/%S alongside %L/%N for parsing" unless fmt.match?(higher) span = Timespan.parse(input, fmt)
Type guard
def parseable_fraction_format?(fmt) # true when at least one of D/H/M/S appears (so %L/%N are not the highest magnitude) fmt.match?(/%[-_0-9]*[DHMS]/) end
Try / catch
begin
Timespan.parse(input, fmt)
rescue ArgumentError => e
raise DataError, "format #{fmt} cannot parse fractions alone: #{e.message}"
end Prevention
- Parse raw counters by constructing Timespan(0, nanoseconds) instead of abusing %L/%N
- Round-trip test every format: parse(format(x)) == x
- Keep %L/%N strictly to the right of a seconds-or-larger directive
When it happens
Trigger: Timespan.parse('500', '%N') or Timespan.parse('250', '%L') - a format whose highest-magnitude specifier is %L or %N. Also '%-N', '%3L', '%L.%N' (N is still the highest magnitude). Formatting with such formats is fine; only parsing raises.
Common situations: Converting a raw millisecond counter from monitoring data into a Timespan using an intuitive-looking format like '%L'; copying a format string from a formatting call site and reusing it for parsing; upgrading code from strftime-style assumptions to Puppet's Timespan formats.
Related errors
- Unable to parse '%{timespan}' using format '%{format}'
- Bad format specifier '%{expression}' in '%{format}', at posi
- %{klass} cannot be added to a Timespan
- %{klass} cannot be subtracted from a Timespan
- A Timestamp cannot be multiplied by %{klass}
AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21).
Data as JSON: /api/errors/7fea630143ff9603.
Report an issue: GitHub.