Class: Vehicles::Dataset

Inherits:
Object
  • Object
show all
Defined in:
lib/vehicles/dataset.rb

Overview

Loads the bundled snapshot once, builds in-memory indexes, and answers every query. No HTTP, no SQLite, no ActiveRecord on the read path — the first call builds the index, every call after is a hash lookup.

Instances are memoized per data path (see .load), so the JSON is parsed once per process.

Constant Summary collapse

BUILTIN_ALIASES =

Built-in make aliases (normalized key => make slug). Common abbreviations and nicknames so whatever a user types tends to land. Diacritics and case are already handled by Vehicles.normalize, so "škoda"/"citroën" need no entry.

{
  "vw" => "volkswagen", "vdub" => "volkswagen",
  "merc" => "mercedes-benz", "mercedes" => "mercedes-benz", "benz" => "mercedes-benz",
  "mb" => "mercedes-benz",
  "chevy" => "chevrolet",
  "beemer" => "bmw", "bimmer" => "bmw",
  "alfa" => "alfa-romeo",
  "landrover" => "land-rover", "range rover" => "land-rover", "rangerover" => "land-rover"
  # NOTE: no "vauxhall" alias — since 2026.07 the dataset ships Vauxhall
  # and Opel as separate makes (deliberately: separate GB/EU model names),
  # and an alias here would shadow the real Vauxhall records.
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(raw) ⇒ Dataset

Returns a new instance of Dataset.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/vehicles/dataset.rb', line 43

def initialize(raw)
  @version        = raw["version"]
  @schema_version = raw["schema_version"]
  @region         = raw["region"]

  # Name-alphabetical, diacritic-insensitive — the dataset file arrives
  # slug-sorted, which differs subtly ("Austin-Healey" vs "Austin Morris").
  @makes    = (raw["makes"] || []).map { |attrs| Make.new(attrs) }
                                  .sort_by { |m| Vehicles.normalize(m.name) }
  @by_slug  = {}   # raw slug => Make
  @index    = {}   # normalized (ASCII-folded) name/slug/alias => Make
  @exact    = {}   # downcased exact string => Make — for non-Latin aliases
  # (比亚迪, ヤマハ, 현대) that fold to "" under normalize
  @makes.each do |make|
    @by_slug[make.slug] = make
    index(make.name, make)
    index(make.slug, make)
    make.aliases.each do |a|
      index(a, make)
      @exact[a.downcase] ||= make
    end
  end
end

Instance Attribute Details

#regionObject (readonly)

Returns the value of attribute region.



41
42
43
# File 'lib/vehicles/dataset.rb', line 41

def region
  @region
end

#schema_versionObject (readonly)

Returns the value of attribute schema_version.



41
42
43
# File 'lib/vehicles/dataset.rb', line 41

def schema_version
  @schema_version
end

#versionObject (readonly)

Returns the value of attribute version.



41
42
43
# File 'lib/vehicles/dataset.rb', line 41

def version
  @version
end

Class Method Details

.load(path = Vehicles.data_path) ⇒ Object

Memoized per path so the bundled JSON is parsed only once per process.



31
32
33
# File 'lib/vehicles/dataset.rb', line 31

def load(path = Vehicles.data_path)
  (@instances ||= {})[path] ||= new(JSON.parse(File.read(path)))
end

.reset!Object

Drop the cache (used by the test suite between runs).



36
37
38
# File 'lib/vehicles/dataset.rb', line 36

def reset!
  @instances = {}
end

Instance Method Details

#all_modelsObject

Flat list of every Model (memoized + frozen — shared, so don't let callers mutate it). Backs search.



142
143
144
# File 'lib/vehicles/dataset.rb', line 142

def all_models
  @all_models ||= @makes.flat_map(&:models).freeze
end

#all_models_filtered(kind: nil, region: nil, rarity: nil, max_decile: nil) ⇒ Object

Every model matching optional kind/region/rarity filters, ranked by popularity. The "give me a sensible slice" entry point.



167
168
169
170
171
172
173
174
# File 'lib/vehicles/dataset.rb', line 167

def all_models_filtered(kind: nil, region: nil, rarity: nil, max_decile: nil)
  list = all_models
  list = list.select { |m| m.kind == kind.to_sym }          if kind
  list = list.select { |m| m.available_in_region?(region) } if region
  list = list.select { |m| m.rarity == rarity.to_sym }      if rarity
  list = list.select { |m| m.global_decile && m.global_decile <= max_decile } if max_decile
  list
end

#find_make(query) ⇒ Object

Resolve a make from a String/Symbol/Make via aliases, slug, or name.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/vehicles/dataset.rb', line 81

def find_make(query)
  return query if query.is_a?(Make)

  q = Vehicles.normalize(query)
  # Non-Latin queries (比亚迪, 현대) fold to "" — try the exact index first.
  return @exact[query.to_s.strip.downcase] if q.empty?

  # 1. user-supplied aliases win
  if (canonical = Vehicles.configuration.aliases[q])
    return @by_slug[Vehicles.slugify(canonical)] || @index[Vehicles.normalize(canonical)]
  end
  # 2. built-in aliases
  if (slug = BUILTIN_ALIASES[q])
    return @by_slug[slug]
  end

  # 3. direct slug / normalized name / make alias
  @by_slug[q] || @index[q]
end

#find_model(query) ⇒ Object

Resolve a free-text "make + model" string into one Model. Tries the longest leading make prefix first ("land rover defender"), then the remainder as the model. Returns nil if nothing matches.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/vehicles/dataset.rb', line 104

def find_model(query)
  q = Vehicles.normalize(query)
  tokens = q.split
  return nil if tokens.empty?

  (tokens.length - 1).downto(1) do |i|
    make = find_make(tokens[0, i].join(" "))
    next unless make

    model = make.model(tokens[i..].join(" "))
    return model if model
  end
  nil
end

#kindsObject

Every kind present in the snapshot. => [:bus, :car, :moped, ...]



147
148
149
# File 'lib/vehicles/dataset.rb', line 147

def kinds
  @kinds ||= @makes.flat_map(&:kinds).uniq.sort.freeze
end

#makes(kind: nil, region: nil) ⇒ Object

All makes, optionally filtered by kind and/or continent. region: is a CONTINENT (:eu/:na/:as/:sa/:oc/:af) — a make matches if it's evidenced there. (Legacy note: pre-1.0 region: meant "does the snapshot cover this?"; on today's global snapshot both readings return European makes for region: :eu, so existing callers are unaffected.) An unmapped continent honestly returns [].



73
74
75
76
77
78
# File 'lib/vehicles/dataset.rb', line 73

def makes(kind: nil, region: nil)
  list = @makes
  list = list.select { |m| m.kinds.include?(kind.to_sym) } if kind
  list = list.select { |m| m.in_region?(region) }          if region
  list
end

#region?(region) ⇒ Boolean

Does this snapshot cover the given region? A "global" snapshot covers every region, so callers pinned to region: :eu keep working as the dataset outgrows Europe.

Returns:

  • (Boolean)


179
180
181
# File 'lib/vehicles/dataset.rb', line 179

def region?(region)
  region_match?(region)
end

#search(query) ⇒ Object

Every model whose name (or full name) matches the query, ranked: exact name, then prefix, then substring, then full-name substring. Shorter names first.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/vehicles/dataset.rb', line 121

def search(query)
  q = Vehicles.normalize(query)
  return [] if q.empty?

  scored = []
  all_models.each do |m|
    name_n = Vehicles.normalize(m.name)
    score =
      if    name_n == q              then 0
      elsif name_n.start_with?(q)    then 1
      elsif name_n.include?(q)       then 2
      elsif Vehicles.normalize(m.full_name).include?(q) then 3
      else next
      end
    scored << [score, m.name.length, m]
  end
  scored.sort_by { |score, len, _m| [score, len] }.map { |_s, _l, m| m }
end

#top_models(kind: nil, country: nil, region: nil, limit: 20) ⇒ Object

Ranked models by popularity: global decile first, then breadth of availability as the tiebreaker, then name. Unranked models (nil decile — catalog-only evidence) never appear: "unknown" must not outrank "known". top_models(kind: :car, country: :nl, limit: 10) top_models(kind: :motorcycle, region: :as, limit: 10) # by continent



156
157
158
159
160
161
162
163
# File 'lib/vehicles/dataset.rb', line 156

def top_models(kind: nil, country: nil, region: nil, limit: 20)
  c = country&.to_s&.downcase
  list = all_models.select(&:global_decile)
  list = list.select { |m| m.kind == kind.to_sym } if kind
  list = list.select { |m| m.availability.include?(c) } if c
  list = list.select { |m| m.available_in_region?(region) } if region
  list.sort_by { |m| [m.global_decile, -m.availability.size, m.name] }.first(limit)
end