Module: ConvertSdk::Comparisons Private

Defined in:
lib/convert_sdk/comparisons.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

The 13 rule comparison operators — the cross-SDK audience-targeting predicate set, ported BYTE-FOR-BYTE from the JS SDK packages/utils/src/comparisons.ts.

JS is the ONLY truth here. The PHP reference is QUARANTINED for this surface: it ships zero +exists+/+not_exists+ handling (a disk-verified gap) and folds case on the isIn values side (a second divergence), so it must not influence the Ruby contract. Every operator below mirrors its JS body at the cited comparisons.ts line; the goldens in the cross-SDK vector suite are the CI proof of parity.

Two-worlds dispatch (operators): the platform sends operator names as camelCase WIRE strings inside the rule JSON (+equalsNumber+, startsWith, …). Those strings are config-world identifiers and stay byte-identical; the Ruby methods underneath are snake_case. Comparisons.dispatch is the map from the wire name to the Ruby method symbol, consumed by RuleManager.

The undefined/nil distinction (the subtle one): JS distinguishes undefined (a data key is ABSENT) from null (the key is present with a null value). Ruby hashes collapse both to nil, so absence is modeled EXPLICITLY with the frozen private UNDEFINED marker — RuleManager passes UNDEFINED for an absent key so the existence operators (and JS-parity need-more-data propagation) behave exactly as JS does with undefined. For the existence operators themselves UNDEFINED, nil, and "" are all "does not exist", matching value !== undefined && value !== null && value !== '' (+comparisons.ts:159+).

Pure and stateless (NFR1): every method is a singleton (+self.+) method with no I/O and no instance state (the same module form as MurmurHash3).

Constant Summary collapse

UNDEFINED =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Sentinel for an ABSENT data key, distinct from nil (a present null value). Frozen so it is a stable, comparable singleton. Mirrors JS undefined on the rule-evaluation path.

Object.new.freeze
NUMERIC_REGEXP =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

JS isNumeric regex (+string-utils.ts:69+): optional leading minus, then either grouped thousands (+1,234+) or plain digits, with an optional fractional part, or a bare fraction (+.5+).

/\A-?(?:(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?|\.\d+)\z/
DISPATCH =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Maps each wire comparison operator name to its implementing method symbol.

{
  "equals" => :equals,
  "equalsNumber" => :equals_number,
  "matches" => :matches,
  "less" => :less,
  "lessEqual" => :less_equal,
  "contains" => :contains,
  "isIn" => :is_in,
  "startsWith" => :starts_with,
  "endsWith" => :ends_with,
  "regexMatches" => :regex_matches,
  "exists" => :exists,
  "not_exists" => :not_exists,
  "doesNotExist" => :does_not_exist
}.freeze

Class Method Summary collapse

Class Method Details

.contains(value, test_against, negation = false) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Case-insensitive substring test (+comparisons.ts:71-87+). PRESERVED JS quirk: an empty or whitespace-only test_against returns true (ts:80-81) — do not "fix" it.

Returns:

  • (Boolean)


93
94
95
96
97
98
99
# File 'lib/convert_sdk/comparisons.rb', line 93

def self.contains(value, test_against, negation = false)
  value = value.to_s.downcase
  test_against = test_against.to_s.downcase
  return negation_check(true, negation) if test_against.gsub(/\A\s*|\s*\z/, "").empty?

  negation_check(value.include?(test_against), negation)
end

.dispatchHash{String=>Symbol}

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The wire-name -> Ruby-method dispatch map (the two-worlds rule for operators). Keys are byte-identical to the rule JSON match_type strings; RuleManager looks an operator up here and invokes the mapped method.

Returns:

  • (Hash{String=>Symbol})

    frozen 13-entry map.



195
196
197
# File 'lib/convert_sdk/comparisons.rb', line 195

def self.dispatch
  DISPATCH
end

.does_not_exist(value, test_against = nil, negation = false) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Alias of not_exists (+comparisons.ts:172+ — doesNotExist = this.not_exists).



169
170
171
# File 'lib/convert_sdk/comparisons.rb', line 169

def self.does_not_exist(value, test_against = nil, negation = false)
  not_exists(value, test_against, negation)
end

.ends_with(value, test_against, negation = false) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Case-insensitive suffix test (+comparisons.ts:130-141+).

Returns:

  • (Boolean)


134
135
136
137
138
# File 'lib/convert_sdk/comparisons.rb', line 134

def self.ends_with(value, test_against, negation = false)
  value = value.to_s.downcase
  test_against = test_against.to_s.downcase
  negation_check(value.end_with?(test_against), negation)
end

.equals(value, test_against, negation = false) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Case-insensitive equality. Mirrors comparisons.ts:15-40: Array value -> membership of test_against; non-empty Hash value -> key membership; otherwise both sides are stringified, lowercased, and compared.

Parameters:

  • value (Object)

    the data value (String, Numeric, Boolean, Array, Hash).

  • test_against (Object)

    the rule's expected value.

  • negation (Boolean) (defaults to: false)

    when true, the result is inverted.

Returns:

  • (Boolean)


54
55
56
57
58
59
# File 'lib/convert_sdk/comparisons.rb', line 54

def self.equals(value, test_against, negation = false)
  return negation_check(value.include?(test_against), negation) if value.is_a?(Array)
  return negation_check(value.key?(test_against.to_s), negation) if value.is_a?(Hash) && !value.empty?

  negation_check(value.to_s.downcase == test_against.to_s.downcase, negation)
end

.equals_number(value, test_against, negation = false) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Alias of equals (+comparisons.ts:42+ — equalsNumber = this.equals).



62
63
64
# File 'lib/convert_sdk/comparisons.rb', line 62

def self.equals_number(value, test_against, negation = false)
  equals(value, test_against, negation)
end

.exists(value, _test_against = nil, negation = false) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Presence test (+comparisons.ts:154-161+): true unless the value is UNDEFINED (JS undefined), nil (JS null), or the empty string.

Returns:

  • (Boolean)


155
156
157
158
# File 'lib/convert_sdk/comparisons.rb', line 155

def self.exists(value, _test_against = nil, negation = false)
  value_exists = !value.equal?(UNDEFINED) && !value.nil? && value != ""
  negation_check(value_exists, negation)
end

.is_in(values, test_against, negation = false, splitter = "|") ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Pipe-split membership (+comparisons.ts:89-115+). BOTH values and a string test_against are split on the splitter. Only the test_against items are lowercased after splitting; the values items are compared AS-IS against the lowercased list (so an uppercased value does not match a lowercased entry — exact JS semantics at ts:106-110).

Parameters:

  • values (Object)

    the data value (pipe-joined string or scalar).

  • test_against (Object)

    an Array, or a pipe-joined string to split.

  • negation (Boolean) (defaults to: false)
  • splitter (String) (defaults to: "|")

    the delimiter (default "|").

Returns:

  • (Boolean)


112
113
114
115
116
117
118
119
120
# File 'lib/convert_sdk/comparisons.rb', line 112

def self.is_in(values, test_against, negation = false, splitter = "|")
  matched_values = values.to_s.split(splitter, -1).map(&:to_s)
  test_against = test_against.split(splitter, -1) if test_against.is_a?(String)
  unless test_against.is_a?(Array)
    test_against = [] #: Array[untyped]
  end
  test_against = test_against.map { |item| item.to_s.downcase }
  negation_check(matched_values.any? { |item| test_against.include?(item) }, negation)
end

.less(value, test_against, negation = false) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Strict less-than over numerically-normalized inputs (+comparisons.ts:45-56+). Numeric-looking strings/numbers normalize to a number; anything else keeps its type. When the normalized types differ the result is false (JS typeof value !== typeof testAgainst, ts:52-54).

Returns:

  • (Boolean)


77
78
79
# File 'lib/convert_sdk/comparisons.rb', line 77

def self.less(value, test_against, negation = false)
  compare_numeric(value, test_against, negation) { |a, b| a < b }
end

.less_equal(value, test_against, negation = false) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Less-than-or-equal counterpart of less (+comparisons.ts:58-69+).

Returns:

  • (Boolean)


84
85
86
# File 'lib/convert_sdk/comparisons.rb', line 84

def self.less_equal(value, test_against, negation = false)
  compare_numeric(value, test_against, negation) { |a, b| a <= b }
end

.matches(value, test_against, negation = false) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Alias of equals (+comparisons.ts:43+ — matches = this.equals).



67
68
69
# File 'lib/convert_sdk/comparisons.rb', line 67

def self.matches(value, test_against, negation = false)
  equals(value, test_against, negation)
end

.not_exists(value, _test_against = nil, negation = false) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Absence test — the logical inverse of exists (+comparisons.ts:163-170+).

Returns:

  • (Boolean)


163
164
165
166
# File 'lib/convert_sdk/comparisons.rb', line 163

def self.not_exists(value, _test_against = nil, negation = false)
  value_not_exists = value.equal?(UNDEFINED) || value.nil? || value == ""
  negation_check(value_not_exists, negation)
end

.numeric?(value) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

JS isNumeric (+string-utils.ts:68-74+): numbers are numeric when finite; strings are numeric only when they match NUMERIC_REGEXP and parse finite.

Returns:

  • (Boolean)


202
203
204
205
206
207
208
209
# File 'lib/convert_sdk/comparisons.rb', line 202

def self.numeric?(value)
  return value.finite? if value.is_a?(Numeric)
  return false unless value.is_a?(String) && NUMERIC_REGEXP.match?(value)

  Float(value.delete(",")).finite?
rescue ArgumentError, TypeError
  false
end

.regex_matches(value, test_against, negation = false) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Case-insensitive regex test (+comparisons.ts:143-152+ — new RegExp(t, 'i')). value is lowercased; the pattern keeps its case but matches case-insensitively via the i flag.

Returns:

  • (Boolean)


145
146
147
148
149
# File 'lib/convert_sdk/comparisons.rb', line 145

def self.regex_matches(value, test_against, negation = false)
  value = value.to_s.downcase
  pattern = Regexp.new(test_against.to_s, Regexp::IGNORECASE)
  negation_check(!pattern.match(value).nil?, negation)
end

.starts_with(value, test_against, negation = false) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Case-insensitive prefix test (+comparisons.ts:117-128+ — indexOf === 0).

Returns:

  • (Boolean)


125
126
127
128
129
# File 'lib/convert_sdk/comparisons.rb', line 125

def self.starts_with(value, test_against, negation = false)
  value = value.to_s.downcase
  test_against = test_against.to_s.downcase
  negation_check(value.start_with?(test_against), negation)
end

.to_number(value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

JS toNumber (+string-utils.ts:81-91+): numbers pass through; strings with a leading "0" thousands segment treat commas as decimal points (all commas replaced with dots via tr, matching JS's global replace(/,/g, '.')), otherwise commas are stripped. The result is parsed with to_f (lenient, never raises) — matching JS parseFloat(): the leading numeric portion is extracted and a trailing-garbage input like "0.123.456" returns 0.123 rather than raising. This is a verified parity fix: Ruby Float() (strict) raises on that input while JS parseFloat and Ruby to_f do not.



220
221
222
223
224
225
226
227
# File 'lib/convert_sdk/comparisons.rb', line 220

def self.to_number(value)
  return value if value.is_a?(Numeric)

  str = value.to_s
  parts = str.split(",")
  normalized = parts[0] == "0" ? str.tr(",", ".") : str.delete(",")
  normalized.to_f
end