reality 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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