chronos 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +27 -0
- data/HISTORY.rdoc +4 -0
- data/LICENSE.txt +52 -0
- data/MANIFEST.txt +51 -0
- data/NOTES.rdoc +85 -0
- data/README.rdoc +125 -0
- data/Rakefile +34 -0
- data/TODO.rdoc +63 -0
- data/bench/completebench.rb +24 -0
- data/ext/cchronos/extconf.rb +5 -0
- data/ext/chronos_core/extconf.rb +5 -0
- data/lib/chronos.rb +208 -0
- data/lib/chronos/calendar.rb +16 -0
- data/lib/chronos/calendar/gregorian.rb +94 -0
- data/lib/chronos/data/zones.tab +424 -0
- data/lib/chronos/datetime.rb +299 -0
- data/lib/chronos/datetime/gregorian.rb +698 -0
- data/lib/chronos/duration.rb +141 -0
- data/lib/chronos/duration/gregorian.rb +261 -0
- data/lib/chronos/durationtotext.rb +42 -0
- data/lib/chronos/exceptions.rb +16 -0
- data/lib/chronos/gregorian.rb +27 -0
- data/lib/chronos/interval.rb +132 -0
- data/lib/chronos/interval/gregorian.rb +80 -0
- data/lib/chronos/locale/parsers/de_CH.rb +50 -0
- data/lib/chronos/locale/parsers/en_US.rb +1 -0
- data/lib/chronos/locale/parsers/generic.rb +21 -0
- data/lib/chronos/locale/strings/de_DE.yaml +76 -0
- data/lib/chronos/locale/strings/en_US.yaml +76 -0
- data/lib/chronos/minimalistic.rb +37 -0
- data/lib/chronos/numeric/gregorian.rb +100 -0
- data/lib/chronos/ruby.rb +6 -0
- data/lib/chronos/version.rb +21 -0
- data/lib/chronos/zone.rb +212 -0
- data/rake/initialize.rb +116 -0
- data/rake/lib/assesscode.rb +59 -0
- data/rake/lib/bonesplitter.rb +245 -0
- data/rake/lib/projectclass.rb +69 -0
- data/rake/tasks/copyright.rake +24 -0
- data/rake/tasks/gem.rake +119 -0
- data/rake/tasks/git.rake +40 -0
- data/rake/tasks/loc.rake +33 -0
- data/rake/tasks/manifest.rake +63 -0
- data/rake/tasks/meta.rake +16 -0
- data/rake/tasks/notes.rake +36 -0
- data/rake/tasks/post_load.rake +18 -0
- data/rake/tasks/rdoc.rake +73 -0
- data/rake/tasks/rubyforge.rake +67 -0
- data/rake/tasks/spec.rake +55 -0
- data/spec/bacon_helper.rb +43 -0
- data/spec/lib/chronos/datetime/gregorian_spec.rb +314 -0
- data/spec/lib/chronos/datetime_spec.rb +219 -0
- data/spec/lib/chronos_spec.rb +91 -0
- metadata +111 -0
data/rake/tasks/git.rake
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007-2008 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
namespace :git do
|
10
|
+
if Project.meta.use_git then
|
11
|
+
# A prerequisites task that all other tasks depend upon
|
12
|
+
task :prereqs
|
13
|
+
|
14
|
+
desc 'Show tags from the Git repository'
|
15
|
+
task :tags => 'git:prereqs' do |t|
|
16
|
+
system 'git', 'tag'
|
17
|
+
end
|
18
|
+
|
19
|
+
desc 'Create a new tag in the Git repository'
|
20
|
+
task :create_tag => 'git:prereqs' do |t|
|
21
|
+
v = ENV['VERSION'] or abort 'Must supply VERSION=x.y.z'
|
22
|
+
|
23
|
+
tag = "%s-%s" % [Project.meta.name, Project.meta.version]
|
24
|
+
msg = "Creating tag for #{Project.meta.name} version #{Project.meta.version}"
|
25
|
+
|
26
|
+
puts "Creating Git tag '#{tag}'"
|
27
|
+
unless system "git tag -a -m '#{msg}' #{tag}"
|
28
|
+
abort "Tag creation failed"
|
29
|
+
end
|
30
|
+
|
31
|
+
if %x/git remote/ =~ %r/^origin\s*$/
|
32
|
+
unless system "git push origin #{tag}"
|
33
|
+
abort "Could not push tag to remote Git repository"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end # namespace :git
|
39
|
+
|
40
|
+
task 'gem:release' => 'git:create_tag' if Project.meta.use_git
|
data/rake/tasks/loc.rake
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007-2008 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
namespace :loc do
|
10
|
+
desc 'Assess the number of code and comment lines'
|
11
|
+
task :assess do
|
12
|
+
next unless lib?('assesscode', 'Requires AssessCode lib to count lines of code and comment.')
|
13
|
+
a = AssessCode.new(
|
14
|
+
'.',
|
15
|
+
'lib/**/*.rb',
|
16
|
+
'bin/**/*',
|
17
|
+
'data/**/*.rb'
|
18
|
+
)
|
19
|
+
puts "Code"
|
20
|
+
a.put_assessment
|
21
|
+
|
22
|
+
a = AssessCode.new(
|
23
|
+
'.',
|
24
|
+
'spec/**/*.rb',
|
25
|
+
'test/**/*.rb'
|
26
|
+
)
|
27
|
+
puts "\nTests"
|
28
|
+
a.put_assessment
|
29
|
+
end
|
30
|
+
end # namespace :loc
|
31
|
+
|
32
|
+
desc 'Alias to loc:assess'
|
33
|
+
task :loc => 'loc:assess'
|
@@ -0,0 +1,63 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007-2008 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
namespace :manifest do
|
10
|
+
# prerequisite task finalizes Project.manifest
|
11
|
+
task :prerequisite => 'meta:prerequisite' do
|
12
|
+
abort("No instructions for manifest tasks in Project") unless Project.manifest
|
13
|
+
|
14
|
+
Project.manifest.file ||= Project.meta.manifest
|
15
|
+
Project.manifest.__finalize__
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Verify the manifest'
|
19
|
+
task :check => :prerequisite do
|
20
|
+
files = manifest()
|
21
|
+
cands = manifest_candidates()
|
22
|
+
missing = files-cands
|
23
|
+
added = cands-files
|
24
|
+
|
25
|
+
puts "#{Project.manifest.file.inspect}:"
|
26
|
+
puts added.sort.map { |f|
|
27
|
+
"\e[32m+#{f}\e[0m"
|
28
|
+
}
|
29
|
+
puts missing.sort.map { |f|
|
30
|
+
"\e[31m-#{f}\e[0m"
|
31
|
+
}
|
32
|
+
puts "Manifest is up to date." if missing.empty? and added.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
desc 'Create a new manifest'
|
36
|
+
task :create => :prerequisite do
|
37
|
+
abort("No manifest file path given in Project.manifest.file") unless Project.manifest.file
|
38
|
+
|
39
|
+
unless File.exist?(Project.manifest.file) then
|
40
|
+
files = manifest_candidates()+[Project.manifest.file]
|
41
|
+
files.sort!
|
42
|
+
File.open(Project.manifest.file, 'wb') {|fp| fp.puts files}
|
43
|
+
puts "#{Project.manifest.file.inspect} Created."
|
44
|
+
else
|
45
|
+
abort("#{Project.manifest.file.inspect} exists already.")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
task :assert => :prerequisite do
|
50
|
+
files = manifest()
|
51
|
+
cands = manifest_candidates()
|
52
|
+
missing = files-cands
|
53
|
+
added = cands-files
|
54
|
+
|
55
|
+
unless (missing.empty? and added.empty?)
|
56
|
+
raise "ERROR: #{Project.manifest.file.inspect} is out of date"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end # namespace :manifest
|
61
|
+
|
62
|
+
desc 'Alias to manifest:check'
|
63
|
+
task :manifest => 'manifest:check'
|
@@ -0,0 +1,16 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007-2008 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
namespace :meta do
|
10
|
+
task :prerequisite do
|
11
|
+
Project.manifest.__finalize__
|
12
|
+
end
|
13
|
+
end # namespace :meta
|
14
|
+
|
15
|
+
#desc 'Alias to manifest:check'
|
16
|
+
#task :manifest => 'manifest:check'
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007-2008 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
namespace :notes do
|
10
|
+
desc "Show all annotations"
|
11
|
+
task :show, :tags do |t, args|
|
12
|
+
tags = if args.tags then
|
13
|
+
args.tags.split(/,\s*/)
|
14
|
+
else
|
15
|
+
Project.notes.tags
|
16
|
+
end
|
17
|
+
regex = /^.*(?:#{tags.map { |e| Regexp.escape(e) }.join('|')}).*$/
|
18
|
+
puts "Searching for tags #{tags.join(', ')}"
|
19
|
+
Project.notes.include.each { |glob|
|
20
|
+
Dir.glob(glob) { |file|
|
21
|
+
data = File.read(file)
|
22
|
+
header = false
|
23
|
+
data.scan(regex) {
|
24
|
+
unless header then
|
25
|
+
puts "#{file}:"
|
26
|
+
header = true
|
27
|
+
end
|
28
|
+
printf "- %4d: %s\n", $`.count("\n")+1, $&.strip
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end # namespace :notes
|
34
|
+
|
35
|
+
desc "Alias for notes:show. You have to use notes:show directly to use arguments."
|
36
|
+
task :notes => 'notes:show'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007-2008 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
# This rakefile doesn't define any tasks, it is run after Rakefile has run and before
|
10
|
+
# any other imported rakefile, so it can clean up the Project object and resolve some
|
11
|
+
# dependencies.
|
12
|
+
|
13
|
+
|
14
|
+
# defaultize meta data, have to do this here because many tasks depend on Project.meta
|
15
|
+
# for initialization and task creation.
|
16
|
+
Project.meta.summary ||= proc { extract_summary() }
|
17
|
+
Project.meta.description ||= proc { extract_description() || extract_summary() }
|
18
|
+
Project.meta.__finalize__
|
@@ -0,0 +1,73 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007-2008 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
require 'rake/rdoctask'
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
namespace :doc do
|
14
|
+
if Project.rdoc.nil? then
|
15
|
+
task :missing_project do
|
16
|
+
abort("No instructions in Project to run the gem tasks")
|
17
|
+
end
|
18
|
+
|
19
|
+
task :rdoc => :missing_project
|
20
|
+
task :clobber_rdoc => :missing_project
|
21
|
+
task :coverage => :missing_project
|
22
|
+
task :ri => :missing_project
|
23
|
+
task :clobber_ri => :missing_project
|
24
|
+
elsif !lib?('rake/rdoctask') then
|
25
|
+
task :missing_rdoctask do
|
26
|
+
abort("Missing rake/rdoctask in Project to run the gem tasks")
|
27
|
+
end
|
28
|
+
|
29
|
+
task :rdoc => :missing_rdoctask
|
30
|
+
task :clobber_rdoc => :missing_rdoctask
|
31
|
+
task :coverage => :missing_rdoctask
|
32
|
+
task :ri => :missing_rdoctask
|
33
|
+
task :clobber_ri => :missing_rdoctask
|
34
|
+
else
|
35
|
+
# defaultize rdoc task
|
36
|
+
Project.rdoc.files ||= []
|
37
|
+
Project.rdoc.files += FileList.new(Project.rdoc.include || %w[lib/**/* *.{txt markdown rdoc}])
|
38
|
+
Project.rdoc.files -= FileList.new(Project.rdoc.exclude) if Project.rdoc.exclude
|
39
|
+
Project.rdoc.files.reject! { |f| File.directory?(f) }
|
40
|
+
Project.rdoc.title ||= "#{Project.meta.name}-#{Project.meta.version} Documentation"
|
41
|
+
Project.rdoc.options ||= []
|
42
|
+
Project.rdoc.options.push('-t', Project.rdoc.title)
|
43
|
+
Project.rdoc.main ||= Project.meta.readme
|
44
|
+
Project.rdoc.__finalize__
|
45
|
+
|
46
|
+
Rake::RDocTask.new do |rd|
|
47
|
+
rd.main = Project.rdoc.main
|
48
|
+
rd.rdoc_files = Project.rdoc.files
|
49
|
+
rd.rdoc_dir = Project.rdoc.output_dir
|
50
|
+
rd.template = Project.rdoc.template if Project.rdoc.template
|
51
|
+
|
52
|
+
rd.options.concat(Project.rdoc.options)
|
53
|
+
end
|
54
|
+
|
55
|
+
desc 'Check documentation coverage with dcov'
|
56
|
+
task :coverage do
|
57
|
+
sh "find lib -name '*.rb' | xargs dcov"
|
58
|
+
end
|
59
|
+
|
60
|
+
desc 'Generate ri locally for testing'
|
61
|
+
task :ri => :clobber_ri do
|
62
|
+
sh "#{RDOC} --ri -o ri ."
|
63
|
+
end
|
64
|
+
|
65
|
+
desc 'Remove ri products'
|
66
|
+
task :clobber_ri do
|
67
|
+
rm_r 'ri' rescue nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
desc 'Alias to doc:rdoc'
|
73
|
+
task :doc => 'doc:rdoc'
|
@@ -0,0 +1,67 @@
|
|
1
|
+
namespace :gem do
|
2
|
+
# Empty tasks for missing prerequisites.
|
3
|
+
if !lib?(%w'rubyforge rake/contrib/sshpublisher') then
|
4
|
+
task :missing_prerequisites do
|
5
|
+
'This task requires rubyforge and/or rake/contrib/sshpublisher to run.'
|
6
|
+
end
|
7
|
+
|
8
|
+
task :release => :missing_prerequisites
|
9
|
+
task :release_notes => :missing_prerequisites
|
10
|
+
task :release_changes => :missing_prerequisites
|
11
|
+
task :preformatted => :missing_prerequisites
|
12
|
+
|
13
|
+
# Real tasks if prerequisites are met.
|
14
|
+
else
|
15
|
+
Project.rubyforge.name ||= Project.meta.name
|
16
|
+
Project.rubyforge.description ||= Project.meta.description
|
17
|
+
Project.rubyforge.changes ||= Project.meta.changes
|
18
|
+
Project.rubyforge.version = Project.meta.version
|
19
|
+
|
20
|
+
desc 'Package and upload to RubyForge'
|
21
|
+
task :release => [:clobber, 'gem:package'] do |t|
|
22
|
+
v = ENV['VERSION'] or abort 'Must supply VERSION=x.y.z'
|
23
|
+
abort "Versions don't match #{v} vs #{PROJ.version}" if v != Project.meta.version.to_s
|
24
|
+
pkg = "pkg/#{Project.gem.spec.full_name}"
|
25
|
+
|
26
|
+
if $DEBUG then
|
27
|
+
puts "release_id = rf.add_release #{Project.rubyforge.name.inspect}, #{Project.rubyforge.name.inspect}, #{Project.rubyforge.version.inspect}, \"#{pkg}.tgz\""
|
28
|
+
puts "rf.add_file #{Project.rubyforge.name.inspect}, #{Project.meta.name.inspect}, release_id, \"#{pkg}.gem\""
|
29
|
+
end
|
30
|
+
|
31
|
+
rf = RubyForge.new
|
32
|
+
puts 'Logging in'
|
33
|
+
rf.login
|
34
|
+
|
35
|
+
c = rf.userconfig
|
36
|
+
c['release_notes'] = Project.rubyforge.description if Project.rubyforge.description
|
37
|
+
c['release_changes'] = Project.rubyforge.changes if Project.rubyforge.changes
|
38
|
+
c['preformatted'] = true
|
39
|
+
|
40
|
+
files = [(Project.gem.need_tar ? "#{pkg}.tgz" : nil),
|
41
|
+
(Project.gem.need_zip ? "#{pkg}.zip" : nil),
|
42
|
+
"#{pkg}.gem"].compact
|
43
|
+
|
44
|
+
puts "Releasing #{Project.rubyforge.name} v. #{Project.rubyforge.version}"
|
45
|
+
rf.add_release Project.rubyforge.name, Project.meta.name, Project.rubyforge.version, *files
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end # namespace :gem
|
49
|
+
|
50
|
+
|
51
|
+
namespace :doc do
|
52
|
+
if !lib?(%w'rubyforge rake/contrib/sshpublisher') then
|
53
|
+
desc "Publish RDoc to RubyForge"
|
54
|
+
task :release => 'rubyforge:missing_prerequisite'
|
55
|
+
else
|
56
|
+
desc "Publish RDoc to RubyForge"
|
57
|
+
task :release => %w(doc:clobber_rdoc doc:rdoc) do
|
58
|
+
config = YAML.load_file(File.expand_path('~/.rubyforge/user-config.yml'))
|
59
|
+
host = "#{config['username']}@rubyforge.org"
|
60
|
+
remote_dir = "/var/www/gforge-projects/#{Project.rubyforge.name}/"
|
61
|
+
remote_dir << Project.rdoc.remote_dir if Project.rdoc.remote_dir
|
62
|
+
local_dir = Project.rdoc.dir
|
63
|
+
|
64
|
+
Rake::SshDirPublisher.new(host, remote_dir, local_dir).upload
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end # namespace :doc
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007-2008 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
namespace :spec do
|
10
|
+
desc 'Run all specs with basic output'
|
11
|
+
task :run do
|
12
|
+
dependency(%w'bacon flexmock', 'Requires %s to run')
|
13
|
+
|
14
|
+
Bacon.extend Bacon.const_get('TestUnitOutput') rescue abort "No such formatter: 'TestUnitOutput'"
|
15
|
+
Bacon.summary_on_exit
|
16
|
+
|
17
|
+
Dir.glob("spec/**/*_spec.rb") { |file|
|
18
|
+
load file
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
desc 'Run all specs with text output'
|
23
|
+
task :specdoc do |t|
|
24
|
+
raise "Not implemented"
|
25
|
+
end
|
26
|
+
|
27
|
+
optional_task(:rcov, 'Spec::Rake::SpecTask') do
|
28
|
+
desc 'Run all specs with RCov'
|
29
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
30
|
+
t.ruby_opts = Project.rcov.ruby_opts
|
31
|
+
t.spec_opts = Project.rcov.opts
|
32
|
+
t.spec_files = Project.rcov.files
|
33
|
+
t.libs = Project.rcov.libs || []
|
34
|
+
t.rcov = true
|
35
|
+
t.rcov_dir = Project.rcov.dir
|
36
|
+
t.rcov_opts = Project.rcov.opts
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
optional_task(:verify, 'Rcov::VerifyTask') do
|
41
|
+
Rcov::VerifyTask.new(:verify) do |t|
|
42
|
+
t.threshold = Project.rcov.threshold
|
43
|
+
t.index_html = File.join(Project.rcov.dir, 'index.html')
|
44
|
+
t.require_exact_threshold = Project.rcov.threshold_exact
|
45
|
+
end
|
46
|
+
|
47
|
+
task :verify => :rcov
|
48
|
+
end
|
49
|
+
|
50
|
+
end # namespace :spec
|
51
|
+
|
52
|
+
desc 'Alias to spec:run'
|
53
|
+
task :spec => 'spec:run'
|
54
|
+
|
55
|
+
task :clobber => 'spec:clobber'
|
@@ -0,0 +1,43 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
gem 'bacon', '>= 1.0.0'
|
4
|
+
rescue LoadError
|
5
|
+
warn "Running without rubygems. Make sure bacon is at least 1.0.0"
|
6
|
+
end
|
7
|
+
begin require 'test/unit'; rescue LoadError; end
|
8
|
+
require 'bacon'
|
9
|
+
require 'flexmock'
|
10
|
+
|
11
|
+
base = File.expand_path(File.dirname(__FILE__)+'/..')
|
12
|
+
$LOAD_PATH.unshift(base+'/lib') unless $LOAD_PATH.include?(base+'/lib')
|
13
|
+
$LOAD_PATH.unshift(base+'/spec') unless $LOAD_PATH.include?(base+'/spec')
|
14
|
+
|
15
|
+
include FlexMock::MockContainer
|
16
|
+
|
17
|
+
Test::Unit.run = false
|
18
|
+
|
19
|
+
def equal_unordered(expected)
|
20
|
+
lambda { |actual|
|
21
|
+
seen = Hash.new(0)
|
22
|
+
expected.each { |e| seen[e] += 1 }
|
23
|
+
actual.each { |e| seen[e] -= 1 }
|
24
|
+
seen.invert.keys == [0]
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def iterate(expected, meth=:each, *args)
|
29
|
+
lambda { |obj|
|
30
|
+
t = []
|
31
|
+
obj.__send__(meth, *args) { |v| t << v }
|
32
|
+
t == expected
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def iterate_unordered(expected, meth=:each, *args)
|
37
|
+
lambda { |obj|
|
38
|
+
seen = Hash.new(0)
|
39
|
+
expected.each { |e| seen[e] += 1 }
|
40
|
+
obj.__send__(meth, *args) { |e| seen[e] -= 1 }
|
41
|
+
seen.invert.keys == [0]
|
42
|
+
}
|
43
|
+
end
|