puppetlabs/puppet · error · ArgumentError

Using a Timezone designator in format specification is mutua

Error message

Using a Timezone designator in format specification is mutually exclusive to providing an explicit timezone argument

What it means

After a successful parse, Timestamp.parse checks whether an explicit timezone argument was given AND the parsed result contains a :zone key (i.e. the format contained a timezone designator like %Z or %z and the string satisfied it). Because both would determine the zone, the combination is rejected with ArgumentError - the two are mutually exclusive by design.

Source

Thrown at lib/puppet/pops/time/timestamp.rb:98

        if parsed.include?(:leftover) || (has_timezone && parsed.include?(:zone))
          parsed = nil
          next
        end
        break
      end
      if parsed.nil?
        raise ArgumentError, _(
          "Unable to parse '%{str}' using any of the formats %{formats}"
        ) % { str: str, formats: format.join(', ') }
      end
    else
      parsed = DateTime._strptime(str, format)
      if parsed.nil? || parsed.include?(:leftover)
        raise ArgumentError, _("Unable to parse '%{str}' using format '%{format}'") % { str: str, format: format }
      end

      if has_timezone && parsed.include?(:zone)
        raise ArgumentError, _(
          'Using a Timezone designator in format specification is mutually exclusive to providing an explicit timezone argument'
        )
      end
    end
    unless has_timezone
      timezone = parsed[:zone]
      has_timezone = !timezone.nil?
    end
    fraction = parsed[:sec_fraction]

    # Convert msec rational found in _strptime hash to usec
    fraction *= 1_000_000 unless fraction.nil?

    # Create the Time instance and adjust for timezone
    parsed_time = ::Time.utc(parsed[:year], parsed[:mon], parsed[:mday], parsed[:hour], parsed[:min], parsed[:sec], fraction)
    parsed_time -= utc_offset(timezone) if has_timezone

    # Convert to Timestamp

View on GitHub (pinned to e227c27540)

Solutions

  1. Pick one mechanism: either give the timezone argument and use zone-less formats ('%F %T'), or embed %Z/%z in the format and pass no timezone
  2. Strip the zone from the string when a timezone argument must win: raw.sub(/\s*(UTC|[+-]\d{2}:?\d{2}|Z)\z/, '')
  3. When the string's own zone is authoritative, drop the timezone argument entirely
  4. Audit helper functions that inject a default timezone arg on every call

Example fix

// before
ts = Timestamp.parse('2023-01-31 12:00:00 UTC', '%F %T %Z', '+01:00')   # raise

// after (string zone wins)
ts = Timestamp.parse('2023-01-31 12:00:00 UTC', '%F %T %Z')
// after (argument wins)
ts = Timestamp.parse('2023-01-31 12:00:00', '%F %T', '+01:00')
Defensive patterns

Strategy: validation

Validate before calling

has_zone_directive = fmt.match?(/%[-_0-9^#]*[zZ]/)
raise ConfigError, 'format already encodes a zone - drop the timezone argument' if has_zone_directive && timezone
ts = Timestamp.parse(str, fmt)

Try / catch

begin
  ts = Timestamp.parse(str, fmt, timezone)
rescue ArgumentError => e
  timezone = nil   # let the embedded %Z/%z win and retry once
  retry if e.message.include?('mutually exclusive')
  raise
end

Prevention

When it happens

Trigger: Timestamp.parse('2023-01-31 12:00:00 UTC', '%F %T %Z', '+00:00'), or Puppet DSL Timestamp('... %Z format...', timezone => '+02:00'). Also with the default format set: passing a timezone argument selects DEFAULT_FORMATS_WO_TZ precisely to avoid this, so it fires mainly with user-supplied formats.

Common situations: Layered defaults: a helper always passes a timezone argument while the caller's format string already encodes the zone; migrating configs that grew both mechanisms; ISO strings ending in 'Z' or '+02:00' paired with an explicit zone parameter.

Related errors


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