zold 0.20.1 → 0.20.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/zold +23 -7
- data/fixtures/merge/into-no-wallet/copies/0123456789abcdef/scores.zc +1 -1
- data/fixtures/merge/random-expenses/copies/0123456789abcdef/scores.zc +5 -5
- data/fixtures/merge/simple-case/copies/0123456789abcdef/scores.zc +1 -1
- data/lib/zold/age.rb +2 -1
- data/lib/zold/amount.rb +3 -0
- data/lib/zold/cached_wallets.rb +2 -2
- data/lib/zold/commands/calculate.rb +1 -1
- data/lib/zold/commands/clean.rb +6 -1
- data/lib/zold/commands/fetch.rb +5 -4
- data/lib/zold/commands/merge.rb +2 -1
- data/lib/zold/commands/node.rb +46 -10
- data/lib/zold/commands/push.rb +4 -2
- data/lib/zold/commands/routines/audit.rb +53 -0
- data/lib/zold/commands/routines/gc.rb +1 -1
- data/lib/zold/copies.rb +15 -8
- data/lib/zold/head.rb +15 -14
- data/lib/zold/hungry_wallets.rb +2 -2
- data/lib/zold/id.rb +4 -8
- data/lib/zold/metronome.rb +4 -4
- data/lib/zold/node/async_entrance.rb +7 -3
- data/lib/zold/node/farm.rb +1 -1
- data/lib/zold/node/front.rb +8 -5
- data/lib/zold/node/sync_entrance.rb +4 -0
- data/lib/zold/remotes.rb +57 -55
- data/lib/zold/tax.rb +2 -2
- data/lib/zold/thread_pool.rb +18 -14
- data/lib/zold/tree_wallets.rb +1 -1
- data/lib/zold/txn.rb +32 -3
- data/lib/zold/txns.rb +14 -14
- data/lib/zold/verbose_thread.rb +7 -0
- data/lib/zold/version.rb +1 -1
- data/lib/zold/wallet.rb +2 -2
- data/test/commands/routines/test_audit.rb +41 -0
- data/test/commands/test_clean.rb +3 -3
- data/test/node/fake_node.rb +2 -1
- data/test/node/test_async_entrance.rb +3 -1
- data/test/node/test_front.rb +1 -0
- data/test/node/test_sync_entrance.rb +2 -2
- data/test/test_copies.rb +14 -5
- data/test/test_tree_wallets.rb +17 -7
- data/test/test_txn.rb +8 -0
- data/test/test_wallet.rb +17 -0
- data/zold.gemspec +1 -1
- metadata +8 -5
data/lib/zold/head.rb
CHANGED
@@ -40,26 +40,27 @@ module Zold
|
|
40
40
|
def fetch
|
41
41
|
raise "Wallet file '#{@file}' is absent" unless File.exist?(@file)
|
42
42
|
lines = IO.read(@file).split(/\n/)
|
43
|
+
# lines = ['', '', '0123456701234567', '']
|
43
44
|
raise "Not enough lines in #{@file}, just #{lines.count}" if lines.count < 4
|
44
45
|
lines.take(4)
|
45
46
|
end
|
47
|
+
end
|
46
48
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
49
|
+
# Cached head.
|
50
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
51
|
+
# Copyright:: Copyright (c) 2018 Yegor Bugayenko
|
52
|
+
# License:: MIT
|
53
|
+
class CachedHead
|
54
|
+
def initialize(head)
|
55
|
+
@head = head
|
56
|
+
end
|
55
57
|
|
56
|
-
|
57
|
-
|
58
|
-
|
58
|
+
def flush
|
59
|
+
@fetch = nil
|
60
|
+
end
|
59
61
|
|
60
|
-
|
61
|
-
|
62
|
-
end
|
62
|
+
def fetch
|
63
|
+
@fetch ||= @head.fetch
|
63
64
|
end
|
64
65
|
end
|
65
66
|
end
|
data/lib/zold/hungry_wallets.rb
CHANGED
@@ -58,7 +58,7 @@ module Zold
|
|
58
58
|
@mutex.synchronize do
|
59
59
|
unless @queue.include?(id)
|
60
60
|
@queue << id
|
61
|
-
@log.debug("Hungry queue got #{id}, at the pos no.#{@queue.size}")
|
61
|
+
@log.debug("Hungry queue got #{id}, at the pos no.#{@queue.size - 1}")
|
62
62
|
end
|
63
63
|
end
|
64
64
|
end
|
@@ -76,7 +76,7 @@ module Zold
|
|
76
76
|
end
|
77
77
|
begin
|
78
78
|
Pull.new(wallets: @wallets, remotes: @remotes, copies: @copies, log: @log).run(
|
79
|
-
['pull', id.to_s, "--network=#{@network}"]
|
79
|
+
['pull', id.to_s, "--network=#{@network}", '--tolerate-edges', '--tolerate-quorum=1']
|
80
80
|
)
|
81
81
|
rescue Fetch::EdgesOnly => e
|
82
82
|
@log.error("Can't hungry-pull #{id}: #{e.message}")
|
data/lib/zold/id.rb
CHANGED
@@ -31,14 +31,10 @@ module Zold
|
|
31
31
|
PTN = Regexp.new('^[0-9a-fA-F]{16}$')
|
32
32
|
private_constant :PTN
|
33
33
|
|
34
|
-
def initialize(id =
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
raise "Invalid wallet ID type: #{id.class.name}" unless id.is_a?(String)
|
39
|
-
raise "Invalid wallet ID: #{id}" unless id =~ PTN
|
40
|
-
@id = Integer("0x#{id}", 16)
|
41
|
-
end
|
34
|
+
def initialize(id = format('%016x', rand(2**32..2**64 - 1)))
|
35
|
+
raise "Invalid wallet ID type: #{id.class.name}" unless id.is_a?(String)
|
36
|
+
raise "Invalid wallet ID: #{id}" unless PTN.match?(id)
|
37
|
+
@id = Integer("0x#{id}", 16)
|
42
38
|
end
|
43
39
|
|
44
40
|
# The ID of the root wallet.
|
data/lib/zold/metronome.rb
CHANGED
@@ -23,6 +23,7 @@
|
|
23
23
|
require 'backtrace'
|
24
24
|
require_relative 'log'
|
25
25
|
require_relative 'age'
|
26
|
+
require_relative 'endless'
|
26
27
|
require_relative 'verbose_thread'
|
27
28
|
require_relative 'thread_pool'
|
28
29
|
|
@@ -58,10 +59,10 @@ module Zold
|
|
58
59
|
def start
|
59
60
|
@routines.each_with_index do |r, idx|
|
60
61
|
@threads.add do
|
61
|
-
Thread.current.name = "#{r.class.name}-#{idx}"
|
62
62
|
step = 0
|
63
|
-
|
63
|
+
Endless.new("#{r.class.name}-#{idx}", log: @log).run do
|
64
64
|
Thread.current.thread_variable_set(:start, Time.now)
|
65
|
+
step += 1
|
65
66
|
begin
|
66
67
|
r.exec(step)
|
67
68
|
@log.debug("Routine #{r.class.name} ##{step} done \
|
@@ -70,9 +71,8 @@ in #{Age.new(Thread.current.thread_variable_get(:start))}")
|
|
70
71
|
@failures[r.class.name] = Time.now.utc.iso8601 + "\n" + Backtrace.new(e).to_s
|
71
72
|
@log.error("Routine #{r.class.name} ##{step} failed \
|
72
73
|
in #{Age.new(Thread.current.thread_variable_get(:start))}")
|
73
|
-
|
74
|
+
raise e
|
74
75
|
end
|
75
|
-
step += 1
|
76
76
|
sleep(1)
|
77
77
|
end
|
78
78
|
end
|
@@ -60,10 +60,14 @@ module Zold
|
|
60
60
|
def start
|
61
61
|
raise 'Block must be given to start()' unless block_given?
|
62
62
|
FileUtils.mkdir_p(@dir)
|
63
|
-
DirItems.new(@dir).fetch.
|
63
|
+
DirItems.new(@dir).fetch.each do |f|
|
64
64
|
file = File.join(@dir, f)
|
65
|
-
|
66
|
-
|
65
|
+
if /^[0-9a-f]{16}-/.match?(f)
|
66
|
+
id = f.split('-')[0]
|
67
|
+
@queue << { id: Id.new(id), file: file }
|
68
|
+
else
|
69
|
+
File.delete(file)
|
70
|
+
end
|
67
71
|
end
|
68
72
|
@log.info("#{@queue.size} wallets pre-loaded into async_entrace from #{@dir}") unless @queue.size.zero?
|
69
73
|
@entrance.start do
|
data/lib/zold/node/farm.rb
CHANGED
@@ -53,7 +53,7 @@ module Zold
|
|
53
53
|
#
|
54
54
|
# <tt>cache</tt> is the file where the farm will keep all the scores it
|
55
55
|
# manages to find. If the file is absent, it will be created, together with
|
56
|
-
# the
|
56
|
+
# the necessary parent directories.
|
57
57
|
#
|
58
58
|
# <tt>lifetime</tt> is the amount of seconds for a score to live in the farm, by default
|
59
59
|
# it's the entire day, since the Score expires in 24 hours; can be decreased for the
|
data/lib/zold/node/front.rb
CHANGED
@@ -48,7 +48,7 @@ require_relative 'soft_error'
|
|
48
48
|
module Zold
|
49
49
|
# Web front
|
50
50
|
class Front < Sinatra::Base
|
51
|
-
# The minimum score required in order to
|
51
|
+
# The minimum score required in order to recognize a requester
|
52
52
|
# as a valuable node and add it to the list of remotes.
|
53
53
|
MIN_SCORE = 4
|
54
54
|
|
@@ -208,17 +208,19 @@ from #{request.ip} in #{Age.new(@start, limit: 1)}")
|
|
208
208
|
cpus: settings.zache.get(:cpus) do
|
209
209
|
Concurrent.processor_count
|
210
210
|
end,
|
211
|
-
memory: settings.zache.get(:memory, lifetime:
|
211
|
+
memory: settings.zache.get(:memory, lifetime: settings.opts['no-cache'] ? 0 : 60) do
|
212
212
|
mem = GetProcessMem.new.bytes.to_i
|
213
213
|
if mem > settings.opts['oom-limit'] * 1024 * 1024 &&
|
214
214
|
!settings.opts['skip-oom'] && !settings.opts['never-reboot']
|
215
|
-
settings.log.error("We are too big in memory (#{Size.new(mem)}), quitting;
|
215
|
+
settings.log.error("We are too big in memory (#{Size.new(mem)}), quitting; \
|
216
|
+
use --skip-oom to never quit or --memory-dump to print the entire memory usage summary on exit; \
|
217
|
+
this is not a normal behavior, you may want to report a bug to our GitHub repository")
|
216
218
|
Front.stop!
|
217
219
|
end
|
218
220
|
mem
|
219
221
|
end,
|
220
222
|
platform: RUBY_PLATFORM,
|
221
|
-
load: settings.zache.get(:load, lifetime:
|
223
|
+
load: settings.zache.get(:load, lifetime: settings.opts['no-cache'] ? 0 : 60) do
|
222
224
|
require 'usagewatch_ext'
|
223
225
|
Object.const_defined?('Usagewatch') ? Usagewatch.uw_load.to_f : 0.0
|
224
226
|
end,
|
@@ -248,7 +250,7 @@ from #{request.ip} in #{Age.new(@start, limit: 1)}")
|
|
248
250
|
digest: wallet.digest,
|
249
251
|
copies: Copies.new(File.join(settings.copies, wallet.id)).all.count,
|
250
252
|
balance: wallet.balance.to_i,
|
251
|
-
body:
|
253
|
+
body: IO.read(wallet.path)
|
252
254
|
)
|
253
255
|
end
|
254
256
|
end
|
@@ -460,6 +462,7 @@ time to stop; use --skip-oom to never quit")
|
|
460
462
|
end
|
461
463
|
|
462
464
|
def total_wallets
|
465
|
+
return 256 unless settings.opts['no-cache']
|
463
466
|
settings.zache.get(:wallets, lifetime: settings.opts['no-cache'] ? 0 : 60) do
|
464
467
|
settings.wallets.count
|
465
468
|
end
|
data/lib/zold/remotes.rb
CHANGED
@@ -39,6 +39,61 @@ require_relative 'node/farm'
|
|
39
39
|
# Copyright:: Copyright (c) 2018 Yegor Bugayenko
|
40
40
|
# License:: MIT
|
41
41
|
module Zold
|
42
|
+
# One remote.
|
43
|
+
class RemoteNode
|
44
|
+
def initialize(host:, port:, score:, idx:, master:, network: 'test', log: Log::NULL)
|
45
|
+
@host = host
|
46
|
+
@port = port
|
47
|
+
@score = score
|
48
|
+
@idx = idx
|
49
|
+
@master = master
|
50
|
+
@network = network
|
51
|
+
@log = log
|
52
|
+
end
|
53
|
+
|
54
|
+
def http(path = '/')
|
55
|
+
Http.new(uri: "http://#{@host}:#{@port}#{path}", score: @score, network: @network)
|
56
|
+
end
|
57
|
+
|
58
|
+
def master?
|
59
|
+
@master
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_s
|
63
|
+
"#{@host}:#{@port}/#{@idx}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def assert_code(code, response)
|
67
|
+
msg = response.status_line.strip
|
68
|
+
return if response.status.to_i == code
|
69
|
+
if response.headers && response.headers['X-Zold-Error']
|
70
|
+
raise "Error ##{response.status} \"#{response.headers['X-Zold-Error']}\"
|
71
|
+
at #{response.headers['X-Zold-Path']}"
|
72
|
+
end
|
73
|
+
raise "Unexpected HTTP code #{response.status}, instead of #{code}" if msg.empty?
|
74
|
+
raise "#{msg} (HTTP code #{response.status}, instead of #{code})"
|
75
|
+
end
|
76
|
+
|
77
|
+
def assert_valid_score(score)
|
78
|
+
raise "Invalid score #{score.reduced(4)}" unless score.valid?
|
79
|
+
raise "Expired score (#{Age.new(score.time)}) #{score.reduced(4)}" if score.expired?
|
80
|
+
end
|
81
|
+
|
82
|
+
def assert_score_ownership(score)
|
83
|
+
raise "Masqueraded host #{@host} as #{score.host}: #{score.reduced(4)}" if @host != score.host
|
84
|
+
raise "Masqueraded port #{@port} as #{score.port}: #{score.reduced(4)}" if @port != score.port
|
85
|
+
end
|
86
|
+
|
87
|
+
def assert_score_strength(score)
|
88
|
+
return if score.strength >= Score::STRENGTH
|
89
|
+
raise "Score #{score.strength} is too weak (<#{Score::STRENGTH}): #{score.reduced(4)}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def assert_score_value(score, min)
|
93
|
+
raise "Score #{score.value} is too small (<#{min}): #{score.reduced(4)}" if score.value < min
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
42
97
|
# All remotes
|
43
98
|
class Remotes
|
44
99
|
# The default TCP port all nodes are supposed to use.
|
@@ -73,60 +128,6 @@ module Zold
|
|
73
128
|
end
|
74
129
|
end
|
75
130
|
|
76
|
-
# One remote.
|
77
|
-
class Remote
|
78
|
-
def initialize(host:, port:, score:, idx:, network: 'test', log: Log::NULL)
|
79
|
-
@host = host
|
80
|
-
@port = port
|
81
|
-
@score = score
|
82
|
-
@idx = idx
|
83
|
-
@network = network
|
84
|
-
@log = log
|
85
|
-
end
|
86
|
-
|
87
|
-
def http(path = '/')
|
88
|
-
Http.new(uri: "http://#{@host}:#{@port}#{path}", score: @score, network: @network)
|
89
|
-
end
|
90
|
-
|
91
|
-
def master?
|
92
|
-
!MASTERS.find { |r| r[0] == @host && r[1].to_i == @port }.nil?
|
93
|
-
end
|
94
|
-
|
95
|
-
def to_s
|
96
|
-
"#{@host}:#{@port}/#{@idx}"
|
97
|
-
end
|
98
|
-
|
99
|
-
def assert_code(code, response)
|
100
|
-
msg = response.status_line.strip
|
101
|
-
return if response.status.to_i == code
|
102
|
-
if response.headers && response.headers['X-Zold-Error']
|
103
|
-
raise "Error ##{response.status} \"#{response.headers['X-Zold-Error']}\"
|
104
|
-
at #{response.headers['X-Zold-Path']}"
|
105
|
-
end
|
106
|
-
raise "Unexpected HTTP code #{response.status}, instead of #{code}" if msg.empty?
|
107
|
-
raise "#{msg} (HTTP code #{response.status}, instead of #{code})"
|
108
|
-
end
|
109
|
-
|
110
|
-
def assert_valid_score(score)
|
111
|
-
raise "Invalid score #{score.reduced(4)}" unless score.valid?
|
112
|
-
raise "Expired score (#{Age.new(score.time)}) #{score.reduced(4)}" if score.expired?
|
113
|
-
end
|
114
|
-
|
115
|
-
def assert_score_ownership(score)
|
116
|
-
raise "Masqueraded host #{@host} as #{score.host}: #{score.reduced(4)}" if @host != score.host
|
117
|
-
raise "Masqueraded port #{@port} as #{score.port}: #{score.reduced(4)}" if @port != score.port
|
118
|
-
end
|
119
|
-
|
120
|
-
def assert_score_strength(score)
|
121
|
-
return if score.strength >= Score::STRENGTH
|
122
|
-
raise "Score #{score.strength} is too weak (<#{Score::STRENGTH}): #{score.reduced(4)}"
|
123
|
-
end
|
124
|
-
|
125
|
-
def assert_score_value(score, min)
|
126
|
-
raise "Score #{score.value} is too small (<#{min}): #{score.reduced(4)}" if score.value < min
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
131
|
def initialize(file:, network: 'test', timeout: 60)
|
131
132
|
@file = file
|
132
133
|
@network = network
|
@@ -197,11 +198,12 @@ module Zold
|
|
197
198
|
start = Time.now
|
198
199
|
best = farm.best[0]
|
199
200
|
begin
|
200
|
-
yield
|
201
|
+
yield RemoteNode.new(
|
201
202
|
host: r[:host],
|
202
203
|
port: r[:port],
|
203
204
|
score: best.nil? ? Score::ZERO : best,
|
204
205
|
idx: idx,
|
206
|
+
master: master?(r[:host], r[:port]),
|
205
207
|
log: log,
|
206
208
|
network: @network
|
207
209
|
)
|
data/lib/zold/tax.rb
CHANGED
@@ -60,8 +60,8 @@ module Zold
|
|
60
60
|
# When score strengths were updated. The numbers here indicate the
|
61
61
|
# strengths we accepted before these dates.
|
62
62
|
MILESTONES = {
|
63
|
-
|
64
|
-
|
63
|
+
Txn.parse_time('2018-11-30T00:00:00Z') => 6,
|
64
|
+
Txn.parse_time('2018-12-09T00:00:00Z') => 7
|
65
65
|
}.freeze
|
66
66
|
private_constant :MILESTONES
|
67
67
|
|
data/lib/zold/thread_pool.rb
CHANGED
@@ -41,25 +41,29 @@ module Zold
|
|
41
41
|
# Run this code in many threads
|
42
42
|
def run(threads, set = (0..threads - 1).to_a)
|
43
43
|
raise "Number of threads #{threads} has to be positive" unless threads.positive?
|
44
|
-
idx = Concurrent::AtomicFixnum.new
|
45
|
-
mutex = Mutex.new
|
46
44
|
list = set.dup
|
47
45
|
total = [threads, set.count].min
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
46
|
+
if total == 1
|
47
|
+
list.each_with_index { |r, i| yield(r, i) }
|
48
|
+
elsif total.positive?
|
49
|
+
idx = Concurrent::AtomicFixnum.new
|
50
|
+
mutex = Mutex.new
|
51
|
+
latch = Concurrent::CountDownLatch.new(total)
|
52
|
+
total.times do |i|
|
53
|
+
add do
|
54
|
+
Thread.current.name = "#{@title}-#{i}"
|
55
|
+
loop do
|
56
|
+
r = mutex.synchronize { list.pop }
|
57
|
+
break if r.nil?
|
58
|
+
yield(r, idx.increment - 1)
|
59
|
+
end
|
60
|
+
ensure
|
61
|
+
latch.count_down
|
56
62
|
end
|
57
|
-
ensure
|
58
|
-
latch.count_down
|
59
63
|
end
|
64
|
+
latch.wait
|
65
|
+
kill
|
60
66
|
end
|
61
|
-
latch.wait
|
62
|
-
kill
|
63
67
|
end
|
64
68
|
|
65
69
|
# Add a new thread
|
data/lib/zold/tree_wallets.rb
CHANGED
data/lib/zold/txn.rb
CHANGED
@@ -42,6 +42,14 @@ module Zold
|
|
42
42
|
RE_PREFIX = '[a-zA-Z0-9]+'
|
43
43
|
private_constant :RE_PREFIX
|
44
44
|
|
45
|
+
# To validate the prefix
|
46
|
+
REGEX_PREFIX = Regexp.new("^#{RE_PREFIX}$")
|
47
|
+
private_constant :REGEX_PREFIX
|
48
|
+
|
49
|
+
# To validate details
|
50
|
+
REGEX_DETAILS = Regexp.new("^#{RE_DETAILS}$")
|
51
|
+
private_constant :REGEX_DETAILS
|
52
|
+
|
45
53
|
attr_reader :id, :date, :amount, :prefix, :bnf, :details, :sign
|
46
54
|
attr_writer :sign, :amount, :bnf
|
47
55
|
def initialize(id, date, amount, prefix, bnf, details)
|
@@ -62,12 +70,12 @@ module Zold
|
|
62
70
|
raise 'Prefix can\'t be NIL' if prefix.nil?
|
63
71
|
raise "Prefix is too short: \"#{prefix}\"" if prefix.length < 8
|
64
72
|
raise "Prefix is too long: \"#{prefix}\"" if prefix.length > 32
|
65
|
-
raise "Prefix is wrong: \"#{prefix}\" (#{RE_PREFIX})" unless
|
73
|
+
raise "Prefix is wrong: \"#{prefix}\" (#{RE_PREFIX})" unless REGEX_PREFIX.match?(prefix)
|
66
74
|
@prefix = prefix
|
67
75
|
raise 'Details can\'t be NIL' if details.nil?
|
68
76
|
raise 'Details can\'t be empty' if details.empty?
|
69
77
|
raise "Details are too long: \"#{details}\"" if details.length > 512
|
70
|
-
raise "Wrong details \"#{details}\" (#{RE_DETAILS})" unless
|
78
|
+
raise "Wrong details \"#{details}\" (#{RE_DETAILS})" unless REGEX_DETAILS.match?(details)
|
71
79
|
@details = details
|
72
80
|
end
|
73
81
|
|
@@ -149,7 +157,7 @@ module Zold
|
|
149
157
|
raise "Invalid line ##{idx}: #{line.inspect} #{regex}" unless parts
|
150
158
|
txn = Txn.new(
|
151
159
|
Hexnum.parse(parts[:id]).to_i,
|
152
|
-
|
160
|
+
parse_time(parts[:date]),
|
153
161
|
Amount.new(zents: Hexnum.parse(parts[:amount]).to_i),
|
154
162
|
parts[:prefix],
|
155
163
|
Id.new(parts[:bnf]),
|
@@ -158,5 +166,26 @@ module Zold
|
|
158
166
|
txn.sign = parts[:sign]
|
159
167
|
txn
|
160
168
|
end
|
169
|
+
|
170
|
+
ISO8601 = Regexp.new(
|
171
|
+
'^' + [
|
172
|
+
'(?<year>\d{4})',
|
173
|
+
'-(?<month>\d{2})',
|
174
|
+
'-(?<day>\d{2})',
|
175
|
+
'T(?<hours>\d{2})',
|
176
|
+
':(?<minutes>\d{2})',
|
177
|
+
':(?<seconds>\d{2})Z'
|
178
|
+
].join
|
179
|
+
)
|
180
|
+
private_constant :ISO8601
|
181
|
+
|
182
|
+
def self.parse_time(iso)
|
183
|
+
parts = ISO8601.match(iso)
|
184
|
+
raise "Invalid ISO 8601 date \"#{iso}\"" if parts.nil?
|
185
|
+
Time.gm(
|
186
|
+
parts[:year].to_i, parts[:month].to_i, parts[:day].to_i,
|
187
|
+
parts[:hours].to_i, parts[:minutes].to_i, parts[:seconds].to_i
|
188
|
+
)
|
189
|
+
end
|
161
190
|
end
|
162
191
|
end
|