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

  1. Anchor the fraction to a higher-magnitude specifier: use '%S.%N' or '%S.%L' and pass whole seconds plus fraction
  2. 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)
  3. Remember the rule: at least one of %D %H %M %S must appear to the left of %L/%N when parsing
  4. 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

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


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/7fea630143ff9603. Report an issue: GitHub.