reality 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.dokaz +1 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +538 -66
  5. data/bin/reality +9 -0
  6. data/config/demo.yml +3 -0
  7. data/data/wikidata-predicates.json +1 -0
  8. data/data/wikidata-predicates.yaml +2089 -0
  9. data/lib/reality.rb +26 -7
  10. data/lib/reality/config.rb +46 -0
  11. data/lib/reality/definitions/dictionaries.rb +67 -0
  12. data/lib/reality/definitions/helpers.rb +34 -0
  13. data/lib/reality/definitions/wikidata.rb +105 -0
  14. data/lib/reality/definitions/wikipedia_character.rb +17 -0
  15. data/lib/reality/definitions/wikipedia_city.rb +19 -0
  16. data/lib/reality/definitions/wikipedia_continent.rb +21 -0
  17. data/lib/reality/definitions/wikipedia_country.rb +23 -0
  18. data/lib/reality/definitions/wikipedia_musical_artist.rb +15 -0
  19. data/lib/reality/definitions/wikipedia_person.rb +17 -0
  20. data/lib/reality/entity.rb +152 -0
  21. data/lib/reality/entity/coercion.rb +76 -0
  22. data/lib/reality/entity/wikidata_predicates.rb +31 -0
  23. data/lib/reality/entity/wikipedia_type.rb +73 -0
  24. data/lib/reality/extras/geonames.rb +29 -0
  25. data/lib/reality/extras/open_weather_map.rb +63 -0
  26. data/lib/reality/geo.rb +122 -0
  27. data/lib/reality/infoboxer_templates.rb +8 -0
  28. data/lib/reality/list.rb +95 -0
  29. data/lib/reality/measure.rb +18 -12
  30. data/lib/reality/measure/unit.rb +5 -1
  31. data/lib/reality/methods.rb +16 -0
  32. data/lib/reality/pretty_inspect.rb +11 -0
  33. data/lib/reality/refinements.rb +26 -0
  34. data/lib/reality/shortcuts.rb +11 -0
  35. data/lib/reality/tz_offset.rb +64 -0
  36. data/lib/reality/util/formatters.rb +35 -0
  37. data/lib/reality/util/parsers.rb +53 -0
  38. data/lib/reality/version.rb +6 -0
  39. data/lib/reality/wikidata.rb +310 -0
  40. data/reality.gemspec +12 -3
  41. data/script/extract_wikidata_properties.rb +23 -0
  42. data/script/lib/nokogiri_more.rb +175 -0
  43. metadata +137 -7
  44. data/examples/all_countries.rb +0 -16
  45. data/lib/reality/country.rb +0 -283
@@ -0,0 +1,76 @@
1
+ module Reality
2
+ class Entity
3
+ module Coercion
4
+ using Refinements
5
+
6
+ COERCERS = {
7
+ entity: ->(val, **opts){
8
+ case val
9
+ when Wikidata::Link
10
+ Entity.new(val.label || val.id, wikidata_id: val.id)
11
+ when Infoboxer::Tree::Wikilink
12
+ Entity.new(val.link)
13
+ else
14
+ fail ArgumentError, "Can't coerce #{val.inspect} to Entity"
15
+ end
16
+ },
17
+ measure: ->(val, **opts){
18
+ u = opts[:unit] || opts[:units] or fail("Units are not defined for measure type")
19
+ Measure.coerce(Util::Parse.scaled_number(val.to_s), u)
20
+ },
21
+ string: ->(val, **opts){
22
+ val.to_s
23
+ },
24
+ tz_offset: ->(val, **opts){
25
+ TZOffset.parse(val.to_s)
26
+ },
27
+ coord: -> (val, **opts){
28
+ val.is_a?(Geo::Coord) ? val : nil
29
+ },
30
+ date: ->(val, **opts){
31
+ case val
32
+ when DateTime
33
+ # FIXME: in future, parse strings?..
34
+ val.to_date
35
+ when Date
36
+ val
37
+ else
38
+ nil
39
+ end
40
+ },
41
+ datetime: -> (val, **opts){
42
+ val.is_a?(DateTime) ? val : nil # FIXME: in future, parse strings?..
43
+ },
44
+ }
45
+
46
+ module_function
47
+
48
+ def coerce(val, type, **opts)
49
+ if val.kind_of?(Array) && !type.kind_of?(Array)
50
+ val = val.first
51
+ end
52
+
53
+ if opts[:parse]
54
+ val = opts[:parse].call(val)
55
+ end
56
+
57
+ return nil if val.nil?
58
+
59
+ # FIXME: better errors: including field name & class name
60
+ case type
61
+ when Array
62
+ type.count == 1 or fail("Only homogenous array types supported, #{type.inspect} received")
63
+ val.kind_of?(Array) or fail("Array type expected, #{val.inspect} received")
64
+ val.map{|row| coerce(row, type.first, **opts.except(:parse))}.
65
+ derp{|arr| arr.all?{|e| e.is_a?(Entity)} ? List.new(*arr) : arr}
66
+ when Symbol
67
+ parser = COERCERS[type] or fail("No coercion to #{type.inspect}")
68
+ parser.call(val, **opts)
69
+ else
70
+ fail("No parser for type #{type.inspect}")
71
+ end
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,31 @@
1
+ module Reality
2
+ class Entity
3
+ module WikidataPredicates
4
+ module_function
5
+
6
+ def define(&block)
7
+ instance_eval(&block)
8
+ end
9
+
10
+ def parse(wikidata)
11
+ wikidata.predicates.map{|key, val|
12
+ [val, definitions[key]]
13
+ }.reject{|_, dfn| !dfn}.
14
+ map{|val, (symbol, type, opts)|
15
+ [symbol, Entity::Coercion.coerce(val, type, **opts)]
16
+ }.to_h
17
+ end
18
+
19
+ private
20
+ module_function
21
+
22
+ def predicate(pred, symbol, type, **opts)
23
+ definitions[pred] = [symbol, type, opts]
24
+ end
25
+
26
+ def definitions
27
+ @definitions ||= {}
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,73 @@
1
+ module Reality
2
+ class Entity
3
+ module WikipediaType
4
+ def infobox_name(*infobox_names)
5
+ infobox_names.each do |n|
6
+ WikipediaType.types_by_infobox[n] = self
7
+ end
8
+ end
9
+
10
+ def infobox(name, symbol, type, **opts)
11
+ infobox_fields[name] = [symbol, type, opts]
12
+ end
13
+
14
+ def parse(symbol, type, **opts, &parser)
15
+ page_parsers << [symbol, type, opts, parser]
16
+ end
17
+
18
+ def extended(entity)
19
+ return unless entity.is_a? Entity
20
+ return if !entity.wikipage || !entity.wikipage.infobox
21
+
22
+ values = infobox_fields.map{|name, (symbol, type, opts)|
23
+ var = entity.wikipage.infobox.fetch(name)
24
+ if var.empty?
25
+ [symbol, nil]
26
+ else
27
+ [symbol, Entity::Coercion.coerce(var, type, **opts)]
28
+ end
29
+ }.reject{|k, v| !v}.to_h
30
+
31
+ parsed = page_parsers.map{|symbol, type, opts, parser|
32
+ [symbol, Entity::Coercion.coerce(parser.call(entity.wikipage), type, **opts)]
33
+ }.reject{|k, v| !v}.to_h
34
+
35
+ entity.values.update(values){|k, o, n| o || n} # Don't rewrite already fetched from WP
36
+ entity.values.update(parsed){|k, o, n| o || n} # Don't rewrite already fetched from WP or infobox
37
+ end
38
+
39
+ def symbol
40
+ return nil unless name
41
+ # FIXME: to core ext
42
+ name.
43
+ gsub(/^.+::/, '').
44
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
45
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
46
+ downcase.
47
+ to_sym
48
+ end
49
+
50
+ private
51
+
52
+ def infobox_fields
53
+ @infobox_fields ||= {}
54
+ end
55
+
56
+ def page_parsers
57
+ @page_parsers ||= []
58
+ end
59
+
60
+ class << self
61
+ def types_by_infobox
62
+ # TODO: should be Hashie::Rash, in fact, for supporting Regexp keys
63
+ @types_by_infobox ||= {}
64
+ end
65
+
66
+ def for(entity)
67
+ entity.wikipage.infobox &&
68
+ types_by_infobox[entity.wikipage.infobox.name]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,29 @@
1
+ require 'timezone'
2
+ require 'tzinfo'
3
+
4
+ module Reality
5
+ module Extras
6
+ module Geonames
7
+ module CoordTimezone
8
+ def timezone
9
+ @timezone ||= guess_timezone
10
+ end
11
+
12
+ private
13
+
14
+ def guess_timezone
15
+ Timezone::Configure.username = Reality.config.fetch('keys', 'geonames')
16
+
17
+ gnzone = Timezone::Zone.new(latlon: [lat.to_f, lng.to_f])
18
+ gnzone && TZInfo::Timezone.new(gnzone.zone)
19
+ end
20
+ end
21
+
22
+ def self.included(reality)
23
+ reality.config.register('keys', 'geonames',
24
+ desc: 'GeoNames username. Can be received from http://www.geonames.org/login')
25
+ reality::Geo::Coord.include CoordTimezone
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,63 @@
1
+ require 'open_weather'
2
+
3
+ module Reality
4
+ module Extras
5
+ module OpenWeatherMap
6
+ class Weather < Hashie::Mash
7
+ def temp
8
+ temperature
9
+ end
10
+
11
+ def to_h
12
+ to_hash(symbolize_keys: true)
13
+ end
14
+
15
+ def inspect
16
+ "#<Reality::Weather(%s)>" %
17
+ [temperature, sky].map(&:to_s).reject(&:empty?).join(', ')
18
+ end
19
+
20
+ class << self
21
+ def from_hash(hash)
22
+ hash = hash.dup.extend Hashie::Extensions::DeepFetch
23
+ new(
24
+ humidity: fetch(hash, 'main', 'humidity', '%'),
25
+ sky: hash.deep_fetch('weather', 0, 'main'),
26
+ temperature: fetch(hash, 'main', 'temp', '°C'),
27
+ pressure: fetch(hash, 'main', 'pressure', 'Pa')
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def fetch(hash, *path, unit)
34
+ Reality::Measure.new(hash.deep_fetch(*path), unit)
35
+ rescue Hashie::Extensions::DeepFetch::UndefinedPathError, KeyError
36
+ nil
37
+ end
38
+ end
39
+ end
40
+
41
+ module CoordWeather
42
+ def weather
43
+ res = OpenWeather::Current.geocode(lat.to_f, lng.to_f,
44
+ units: 'metric', APPID: appid)
45
+
46
+ Weather.from_hash(res)
47
+ end
48
+
49
+ private
50
+
51
+ def appid
52
+ Reality.config.fetch('keys', 'open_weather_map')
53
+ end
54
+ end
55
+
56
+ def self.included(reality)
57
+ reality.config.register('keys', 'open_weather_map',
58
+ desc: 'OpenWeatherMap APPID. Can be obtained here: http://openweathermap.org/appid')
59
+ reality::Geo::Coord.include CoordWeather
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,122 @@
1
+ require 'geokit'
2
+ require 'sun_times'
3
+ Geokit::default_units = :kms # TODO: use global settings
4
+
5
+ module Reality
6
+ module Geo
7
+ # GeoKit, RGeo, GeoRuby -- I know, ok?
8
+ # It's just incredibly simple class to hold two values
9
+ # and show them prettily.
10
+ #
11
+ # GeoKit will be used at its time.
12
+ class Coord
13
+ attr_reader :lat, :lng
14
+
15
+ alias_method :latitude, :lat
16
+ alias_method :longitude, :lng
17
+
18
+ class << self
19
+ def from_dms(lat, lng)
20
+ new(decimal_from_dms(lat), decimal_from_dms(lng))
21
+ end
22
+
23
+ private
24
+
25
+ DIRS = {
26
+ 'N' => +1,
27
+ 'S' => -1,
28
+ 'E' => +1,
29
+ 'W' => -1
30
+ }
31
+
32
+ def parse_direction(dir)
33
+ DIRS[dir] or fail("Undefined coordinates direction: #{dir.inspect}")
34
+ end
35
+
36
+ def decimal_from_dms(dms)
37
+ sign = if dms.last.is_a?(String)
38
+ parse_direction(dms.pop)
39
+ else
40
+ dms.first.to_i <=> 0
41
+ end
42
+
43
+ d, m, s = *dms
44
+ sign * (d.abs.to_i + Rational(m.to_i) / 60 + Rational(s.to_f) / 3600)
45
+ end
46
+ end
47
+
48
+ def initialize(lat, lng)
49
+ @lat, @lng = Rational(lat), Rational(lng)
50
+ end
51
+
52
+ def distance_to(point)
53
+ destination_coords = normalize_point(point).to_s
54
+ res = Geokit::LatLng.distance_between(to_s, destination_coords, formula: :sphere)
55
+ Reality::Measure(res, 'km')
56
+ end
57
+
58
+ def direction_to(point)
59
+ destination_coords = normalize_point(point).to_s
60
+ res = Geokit::LatLng.heading_between(to_s, destination_coords)
61
+ Reality::Measure(res, '°')
62
+ end
63
+
64
+ def endpoint(direction, distance)
65
+ res = Geokit::LatLng.endpoint(to_s, direction.to_f, distance.to_f)
66
+ Coord.new res.lat, res.lng
67
+ end
68
+
69
+ def close_to?(point, radius)
70
+ area = Geokit::Bounds.from_point_and_radius(to_s, radius.to_f)
71
+ area.contains?(normalize_point(point).to_s)
72
+ end
73
+
74
+ def lat_dms(direction = true)
75
+ seconds = (lat.abs % 1.0) * 3600.0
76
+ d, m, s = lat.to_i, (seconds / 60).to_i, (seconds % 60)
77
+ if direction
78
+ [d.abs, m, s, d >= 0 ? 'N' : 'S']
79
+ else
80
+ [d, m, s]
81
+ end
82
+ end
83
+
84
+ def lng_dms(direction = true)
85
+ seconds = (lng.abs % 1.0) * 3600.0
86
+ d, m, s = lng.to_i, (seconds / 60).to_i, (seconds % 60)
87
+ if direction
88
+ [d.abs, m, s, d >= 0 ? 'E' : 'W']
89
+ else
90
+ [d, m, s]
91
+ end
92
+ end
93
+
94
+ def to_s
95
+ "#{lat.to_f},#{lng.to_f}"
96
+ end
97
+
98
+ def ==(other)
99
+ other.is_a?(self.class) && lat == other.lat && lng == self.lng
100
+ end
101
+
102
+ def inspect
103
+ "#<%s(%i°%i′%i″%s,%i°%i′%i″%s)>" % [self.class, *lat_dms, *lng_dms]
104
+ end
105
+
106
+ def sunrise(date = Date.today)
107
+ SunTimes.new.rise(date, lat.to_f, lng.to_f)
108
+ end
109
+
110
+ def sunset(date = Date.today)
111
+ SunTimes.new.set(date, lat.to_f, lng.to_f)
112
+ end
113
+
114
+ private
115
+
116
+ def normalize_point(point)
117
+ return point if point.is_a?(Coord)
118
+ point.coord if point.respond_to?(:coord)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -6,6 +6,14 @@ Infoboxer::MediaWiki::Traits.for('en.wikipedia.org') do
6
6
  end
7
7
  end
8
8
 
9
+ template 'lang-*', match: /^lang-(\w+)$/ do
10
+ def children
11
+ fetch('1')
12
+ end
13
+ end
14
+
9
15
  show 'US$' # TODO: in fact, has second option (year)
16
+
17
+ show 'nts'
10
18
  end
11
19
  end
@@ -0,0 +1,95 @@
1
+ module Reality
2
+ class List < Array
3
+ using Refinements
4
+
5
+ def initialize(*names)
6
+ super names.map(&method(:coerce))
7
+ end
8
+
9
+ def load!
10
+ partition(&:wikidata_id).tap{|wd, wp|
11
+ load_by_wikipedia(wp)
12
+ load_by_wikidata(wd)
13
+ }
14
+ self
15
+ end
16
+
17
+ [:select, :reject, :sort, :sort_by,
18
+ :compact, :-, :map, :first, :last, :sample, :shuffle].each do |sym|
19
+ define_method(sym){|*args, &block|
20
+ ensure_type super(*args, &block)
21
+ }
22
+ end
23
+
24
+ def inspect
25
+ "#<#{self.class.name}[#{map(&:to_s?).join(', ')}]>"
26
+ end
27
+
28
+ def describe
29
+ load! unless all?(&:loaded?)
30
+
31
+ meta = {
32
+ types: map(&:wikipedia_type).compact.map(&:symbol).
33
+ group_count.sort_by(&:first).map{|t,c| "#{t} (#{c})"}.join(', '),
34
+ keys: map(&:values).map(&:keys).flatten.
35
+ group_count.sort_by(&:first).map{|k,c| "#{k} (#{c})"}.join(', '),
36
+ }
37
+ # hard to read, yet informative version:
38
+ #keys = map(&:values).map(&:to_a).flatten(1).
39
+ #group_by(&:first).map{|key, vals|
40
+ #values = vals.map(&:last)
41
+ #[key, "(#{values.compact.count}) example: #{values.compact.first.inspect}"]
42
+ #}.to_h
43
+ puts Util::Format.describe("#<#{self.class.name}(#{count} items)>", meta)
44
+ end
45
+
46
+ private
47
+
48
+ def load_by_wikipedia(entities)
49
+ return if entities.empty?
50
+
51
+ pages = Infoboxer.wp.get_h(*entities.map(&:name))
52
+ datum = Wikidata::Entity.
53
+ fetch_list(*pages.values.compact.map(&:title))
54
+
55
+ entities.each do |entity|
56
+ page = pages[entity.name]
57
+ data = page && datum[page.title]
58
+ entity.setup!(wikipage: page, wikidata: data)
59
+ end
60
+ end
61
+
62
+ def load_by_wikidata(entities)
63
+ return if entities.empty?
64
+
65
+ datum = Wikidata::Entity.
66
+ fetch_list_by_id(*entities.map(&:wikidata_id))
67
+ pages = Infoboxer.wp.
68
+ get_h(*datum.values.compact.map(&:en_wikipage).compact)
69
+ entities.each do |entity|
70
+ data = datum[entity.wikidata_id]
71
+ page = data && pages[data.en_wikipage]
72
+ entity.setup!(wikipage: page, wikidata: data)
73
+ end
74
+ end
75
+
76
+ def ensure_type(arr)
77
+ if arr.kind_of?(Array) && arr.all?{|e| e.is_a?(Entity)}
78
+ List[*arr]
79
+ else
80
+ arr
81
+ end
82
+ end
83
+
84
+ def coerce(val)
85
+ case val
86
+ when String
87
+ Entity.new(val)
88
+ when Entity
89
+ val
90
+ else
91
+ fail ArgumentError, "Can't coerce #{val.inspect} to Entity"
92
+ end
93
+ end
94
+ end
95
+ end