reality 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|