zold 0.20.1 → 0.20.2
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/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
|