require 'infoboxer' module Reality using Refinements module DataSources class Wikivoyage def get(title) internal.get(title).derp(&method(:parse)) end private def internal @internal ||= Infoboxer.wikivoyage end def parse(page) simple_templates(page) #+ #%w[see do buy eat drink sleep].flat_map do |activity| #page.templates(name: activity).map(&method(:parse_venue)).map { |v| [activity, v] } #end + #page.templates(name: 'listing').map(&method(:parse_venue)).map { |v| ['listing', v] } end def parse_venue(template) template.variables .map { |var| [var.name.to_sym, var.children.text] } .reject { |n, v| v.empty? }.to_h .tap { |hash| if hash.key?(:lat) && hash.key?(:long) hash[:coord] = Geo::Coord.new(hash.delete(:lat).to_f, hash.delete(:long).to_f) end }.merge(section: template.in_sections.first.heading.text_) end QUALITIES = /^(usable|guide|star|outline)/i def simple_templates(page) [ on_template(page, :coord, name: 'geo') { |t| Geo::Coord.new(*t.unnamed_variables.map(&:children).map(&:text).map(&:to_f)) }, on_template(page, :iata, name: 'IATA') { |t| t .unnamed_variables.first.children.text }, on_template(page, :is_part_of, name: 'IsPartOf') { |t| '#' }, on_template(page, :quality, name: QUALITIES) { |t| t.name.scan(QUALITIES).flatten.first }, *on_template(page, :climate, name: 'climate', merge: true, &method(:parse_climate)), on_template(page, :route, name: 'routebox', &method(:parse_routes)), on_template(page, :region, name: 'regionlist', &method(:parse_regions)), on_template(page, :currency, name: 'exchange rates', &method(:parse_currency)) ].compact end def on_template(page, symbol, *selectors, &block) merge = selectors.last.is_a?(Hash) ? selectors.last.delete(:merge) : false tpl = page.templates(*selectors).first or return nil if merge block.call(tpl) else [symbol, block.call(tpl)] end end MONTHES = Date::ABBR_MONTHNAMES.compact.map(&:downcase).freeze def parse_climate(template) MONTHES.map { |m| ["climate.#{m}", { temperature: Measure.new(template.fetch("#{m}low").text.to_f, '°C')..Measure.new(template.fetch("#{m}high").text.to_f, '°C'), precipation: Measure.new(template.fetch("#{m}precip").text.to_f, 'mm'), } ] } end ROSE = { N: 'north', S: 'south', E: 'east', W: 'west' } def parse_routes(template) max = template.variables.map(&:name).grep(/\d+$/).map { |n| n[/\d+$/].to_i }.max (1..max).map { |n| %w[left right].map { |dir| l = dir.chars.first direction = template.fetch("direction#{l}#{n}").text.chars.map { |c| ROSE[c.to_sym] or fail("Can't guess: #{c}") }.join('_') { "#{direction}.major": template.fetch("major#{l}#{n}").text, "#{direction}.minor": template.fetch("minor#{l}#{n}").text, }.reject { |_, v| v.empty? } }.inject(&:merge).merge(name: template.fetch("image#{n}").text.sub(/( icon)?\..+$/, '')) } end def parse_regions(template) # TODO: too naive! see Paris max = template.variables.map(&:name).grep(/region\d+/).map { |n| n[/\d+/].to_i }.max (1..max).map { |n| "#" } end def parse_currency(template) # TODO: fetch date, make observations timed # TODO: more complicated for Australia. It fetches in fact Template:Exchangerate/AUD-EUR and similar { name: template.fetch('currency').text, symbol: template.fetch('currencyCode').text, }.merge(%w[USD EUR GBP].map { |to| {"exchange.#{to.downcase}": template.fetch(to).text.to_f} }.inject(:merge)) end end end end