puppetlabs/puppet · error · ArgumentError

Unable to parse '%{timespan}' using format '%{format}'

Error message

Unable to parse '%{timespan}' using format '%{format}'

What it means

Format#parse first matches the input string against a regexp compiled from the format segments. If Regexp#match returns nil (the overall shape does not fit - wrong separators, missing groups, extra characters), it raises ArgumentError 'Unable to parse ... using format ...'. This is the pre-validation pass; segment-level digit checking (line 566) is a second, separate raise.

Source

Thrown at lib/puppet/pops/time/timespan.rb:558

          end
          append_value(bld, ns)
        end
      end

      def initialize(format, segments)
        @format = format.freeze
        @segments = segments.freeze
      end

      def format(timespan)
        bld = timespan.negative? ? '-'.dup : ''.dup
        @segments.each { |segment| segment.append_to(bld, timespan) }
        bld
      end

      def parse(timespan)
        md = regexp.match(timespan)
        raise ArgumentError, _("Unable to parse '%{timespan}' using format '%{format}'") % { timespan: timespan, format: @format } if md.nil?

        nanoseconds = 0
        md.captures.each_with_index do |group, index|
          segment = @segments[index]
          next if segment.is_a?(LiteralSegment)

          group.lstrip!
          raise ArgumentError, _("Unable to parse '%{timespan}' using format '%{format}'") % { timespan: timespan, format: @format } unless group =~ /\A[0-9]+\z/

          nanoseconds += segment.nanoseconds(group)
        end
        Timespan.new(timespan.start_with?('-') ? -nanoseconds : nanoseconds)
      end

      def to_s
        @format
      end

View on GitHub (pinned to e227c27540)

Solutions

  1. Call Timespan.parse(str) with no format first - it tries the built-in DEFAULTS list which covers the common shapes
  2. Normalize the input before parsing: strip whitespace, replace ',' with '.', strip unit suffixes
  3. Pass an Array of candidate formats if your Ruby code calls Format directly, and rescue to try the next one
  4. For human-style durations ('1h30m'), write a small unit-aware parser and build the Timespan from seconds instead of forcing a fixed format

Example fix

// before
span = Timespan.parse(input, '%H:%M:%S')   # input '1h 30m' -> raise

// after
input = input.to_s.strip.gsub(',', '.')
begin
  span = Timespan.parse(input)
rescue ArgumentError
  span = Timespan(0, parse_human_duration(input))
end
Defensive patterns

Strategy: try-catch

Validate before calling

# optional cheap pre-check: only digits, ':', '-', '.', spaces
return DataError.new("malformed duration #{str}") unless str.to_s.match?(/\A[-0-9: .]+\z/)
span = Timespan.parse(str)   # defaults tolerate common shapes

Try / catch

begin
  span = Timespan.parse(str, fmt)
rescue ArgumentError
  span = Timespan.parse(str)   # fall back to default formats
  raise DataError, "unparseable duration #{str.inspect}" if span.nil?
end

Prevention

When it happens

Trigger: Timespan.parse('10-30', '%H:%M:%S') (dash vs colon), Timespan.parse('12', '%D-%H') (missing hours), Timespan.parse('1h30m', '%H:%M') (non-spec separators), or passing a default-format string to a custom-format call. Any structural mismatch between input and format raises here.

Common situations: User-supplied duration strings in module parameters ('90 mins', '1h', 'PT1H') fed to a fixed %H:%M format; locale differences (comma decimal separator); trailing whitespace or units in the input; empty string input.

Understand the failure class

Related errors


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