{"record":{"id":"7fea630143ff9603","repo":"puppetlabs/puppet","slug":"format-specifiers-l-and-n-denotes-fractions-and","errorCode":null,"errorMessage":"Format specifiers %L and %N denotes fractions and must be used together with a specifier of higher magnitude","messagePattern":"Format specifiers %L and %N denotes fractions and must be used together with a specifier of higher magnitude","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/puppet/pops/time/timespan.rb","lineNumber":481,"sourceCode":"          super(padchar, width, 2)\n        end\n\n        def multiplier\n          NSECS_PER_SEC\n        end\n\n        def append_to(bld, ts)\n          append_value(bld, use_total? ? ts.total_seconds : ts.seconds)\n        end\n      end\n\n      # Class that assumes that leading zeroes are significant and that trailing zeroes are not and left justifies when formatting.\n      # Applicable after a decimal point, and hence to the %L and %N formats.\n      class FragmentSegment < ValueSegment\n        def nanoseconds(group)\n          # Using %L or %N to parse a string only makes sense when they are considered to be fractions. Using them\n          # as a total quantity would introduce ambiguities.\n          raise ArgumentError, _('Format specifiers %L and %N denotes fractions and must be used together with a specifier of higher magnitude') if use_total?\n\n          n = group.to_i\n          p = 9 - group.length\n          p <= 0 ? n : n * 10**p\n        end\n\n        def create_format\n          if @padchar.nil?\n            '%d'\n          else\n            \"%-#{@width || @default_width}d\"\n          end\n        end\n\n        def append_value(bld, n)\n          # Strip trailing zeroes when default format is used\n          n = n.to_s.sub(/\\A([0-9]+?)0*\\z/, '\\1').to_i unless use_total? || @padchar == '0'\n          super(bld, n)","sourceCodeStart":463,"sourceCodeEnd":499,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/pops/time/timespan.rb#L463-L499","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nms = '42150'\nspan = Timespan.parse(ms, '%L')   # raises: %L is highest magnitude\n\n// after (input is a total millisecond count)\nspan = Timespan(0, ms.to_i * 1_000_000)   # nsecs\n# or parse as seconds + fraction if input actually is '42.15' seconds:\n# span = Timespan.parse('42.15', '%S.%N')","handlingStrategy":"validation","validationCode":"# a parseable format must contain a higher-magnitude directive than %L/%N\nhigher = /%[-_0-9]*[DHMS]/\nraise ArgumentError, \"format needs %D/%H/%M/%S alongside %L/%N for parsing\" unless fmt.match?(higher)\nspan = Timespan.parse(input, fmt)","typeGuard":"def parseable_fraction_format?(fmt)\n  # true when at least one of D/H/M/S appears (so %L/%N are not the highest magnitude)\n  fmt.match?(/%[-_0-9]*[DHMS]/)\nend","tryCatchPattern":"begin\n  Timespan.parse(input, fmt)\nrescue ArgumentError => e\n  raise DataError, \"format #{fmt} cannot parse fractions alone: #{e.message}\"\nend","preventionTips":["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"],"tags":["puppet","timespan","format-specifier","parse-error","argument-error"],"backgroundTag":"invalid-format-string","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}