{"record":{"id":"0f0f0de0751b8889","repo":"puppetlabs/puppet","slug":"klass-cannot-be-subtracted-from-a-timespan","errorCode":null,"errorMessage":"%{klass} cannot be subtracted from a Timespan","messagePattern":"%(.+?) cannot be subtracted from a Timespan","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/puppet/pops/time/timespan.rb","lineNumber":150,"sourceCode":"      when Timespan\n        Timespan.new(@nsecs + o.nsecs)\n      when Integer, Float\n        # Add seconds\n        Timespan.new(@nsecs + (o * NSECS_PER_SEC).to_i)\n      else\n        raise ArgumentError, _(\"%{klass} cannot be added to a Timespan\") % { klass: a_an_uc(o) } unless o.is_a?(Timespan)\n      end\n    end\n\n    def -(o)\n      case o\n      when Timespan\n        Timespan.new(@nsecs - o.nsecs)\n      when Integer, Float\n        # Subtract seconds\n        Timespan.new(@nsecs - (o * NSECS_PER_SEC).to_i)\n      else\n        raise ArgumentError, _(\"%{klass} cannot be subtracted from a Timespan\") % { klass: a_an_uc(o) }\n      end\n    end\n\n    def -@\n      Timespan.new(-@nsecs)\n    end\n\n    def *(o)\n      case o\n      when Integer, Float\n        Timespan.new((@nsecs * o).to_i)\n      else\n        raise ArgumentError, _(\"A Timestamp cannot be multiplied by %{klass}\") % { klass: a_an(o) }\n      end\n    end\n\n    def divmod(o)\n      case o","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/pops/time/timespan.rb#L132-L168","documentation":"Timespan#- accepts only a Timespan or an Integer/Float (seconds) as the subtrahend. A Timestamp operand is deliberately absent because subtracting a point-in-time from a duration is meaningless; any other class hits the else branch and raises ArgumentError naming the offending class.","triggerScenarios":"ts - '10', ts - nil, ts - some_timestamp (Timespan minus Timestamp), or ts - a Rational/BigDecimal. Passing a Puppet Undef value or a Sensitive wrapper also lands here.","commonSituations":"Computing remaining TTL as Timespan - value from catalog data where the value is a String; accidentally swapping operands (should be Timestamp - Timespan, which is legal, rather than Timespan - Timestamp); nil from an optional variable that was never defaulted.","solutions":["If you meant 'duration before a point in time', swap the operands: timestamp - timespan is valid and yields a Timestamp","Convert string seconds with .to_i / Float(o) before subtracting","Default optional inputs explicitly: (opts['margin'] || 0)","Add an is_a?(Timespan) || is_a?(Integer) || is_a?(Float) check and fail with your own message naming the variable"],"exampleFix":"// before\nremaining = timeout_span - node_value   # node_value is a String\n\n// after\nremaining = timeout_span - (node_value.is_a?(Numeric) ? node_value : node_value.to_i)\n# if you actually wanted 'when does this expire':\n# expiry = last_run_timestamp - timeout_span","handlingStrategy":"type-guard","validationCode":"raise ArgumentError, \"bad subtrahend #{o.inspect}\" unless o.is_a?(Puppet::Pops::Time::Timespan) || o.is_a?(Integer) || o.is_a?(Float)\nresult = ts - o","typeGuard":"def timespan_subtractable?(o)\n  o.is_a?(Puppet::Pops::Time::Timespan) || o.is_a?(Integer) || o.is_a?(Float)\nend","tryCatchPattern":"begin\n  ts - o\nrescue ArgumentError => e\n  raise DataError, \"cannot subtract #{o.class} from Timespan (value: #{o.inspect})\"\nend","preventionTips":["Remember Timespan - Timestamp is never valid; Timestamp - Timespan is - fix operand order first","Wrap external data in a small coerce(v) helper that returns Numeric or raises with the variable name","Unit-test arithmetic paths with String and nil inputs to catch coercion gaps early"],"tags":["puppet","timespan","arithmetic","argument-error","type-mismatch"],"backgroundTag":"invalid-operand-type","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}