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.
- checksums.yaml +4 -4
- data/.dokaz +1 -0
- data/LICENSE.txt +22 -0
- data/README.md +538 -66
- data/bin/reality +9 -0
- data/config/demo.yml +3 -0
- data/data/wikidata-predicates.json +1 -0
- data/data/wikidata-predicates.yaml +2089 -0
- data/lib/reality.rb +26 -7
- data/lib/reality/config.rb +46 -0
- data/lib/reality/definitions/dictionaries.rb +67 -0
- data/lib/reality/definitions/helpers.rb +34 -0
- data/lib/reality/definitions/wikidata.rb +105 -0
- data/lib/reality/definitions/wikipedia_character.rb +17 -0
- data/lib/reality/definitions/wikipedia_city.rb +19 -0
- data/lib/reality/definitions/wikipedia_continent.rb +21 -0
- data/lib/reality/definitions/wikipedia_country.rb +23 -0
- data/lib/reality/definitions/wikipedia_musical_artist.rb +15 -0
- data/lib/reality/definitions/wikipedia_person.rb +17 -0
- data/lib/reality/entity.rb +152 -0
- data/lib/reality/entity/coercion.rb +76 -0
- data/lib/reality/entity/wikidata_predicates.rb +31 -0
- data/lib/reality/entity/wikipedia_type.rb +73 -0
- data/lib/reality/extras/geonames.rb +29 -0
- data/lib/reality/extras/open_weather_map.rb +63 -0
- data/lib/reality/geo.rb +122 -0
- data/lib/reality/infoboxer_templates.rb +8 -0
- data/lib/reality/list.rb +95 -0
- data/lib/reality/measure.rb +18 -12
- data/lib/reality/measure/unit.rb +5 -1
- data/lib/reality/methods.rb +16 -0
- data/lib/reality/pretty_inspect.rb +11 -0
- data/lib/reality/refinements.rb +26 -0
- data/lib/reality/shortcuts.rb +11 -0
- data/lib/reality/tz_offset.rb +64 -0
- data/lib/reality/util/formatters.rb +35 -0
- data/lib/reality/util/parsers.rb +53 -0
- data/lib/reality/version.rb +6 -0
- data/lib/reality/wikidata.rb +310 -0
- data/reality.gemspec +12 -3
- data/script/extract_wikidata_properties.rb +23 -0
- data/script/lib/nokogiri_more.rb +175 -0
- metadata +137 -7
- data/examples/all_countries.rb +0 -16
- 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
|
data/lib/reality/geo.rb
ADDED
@@ -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
|
data/lib/reality/list.rb
ADDED
@@ -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
|