date_named_file 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +115 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/date_named_file.gemspec +34 -0
  12. data/lib/date_named_file.rb +13 -0
  13. data/lib/date_named_file/.yardoc/checksums +0 -0
  14. data/lib/date_named_file/.yardoc/complete +0 -0
  15. data/lib/date_named_file/.yardoc/object_types +0 -0
  16. data/lib/date_named_file/.yardoc/objects/root.dat +0 -0
  17. data/lib/date_named_file/.yardoc/proxy_types +0 -0
  18. data/lib/date_named_file/dated_file.rb +133 -0
  19. data/lib/date_named_file/dateish.rb +190 -0
  20. data/lib/date_named_file/directory.rb +73 -0
  21. data/lib/date_named_file/doc/_index.html +85 -0
  22. data/lib/date_named_file/doc/class_list.html +51 -0
  23. data/lib/date_named_file/doc/css/common.css +1 -0
  24. data/lib/date_named_file/doc/css/full_list.css +58 -0
  25. data/lib/date_named_file/doc/css/style.css +496 -0
  26. data/lib/date_named_file/doc/file_list.html +51 -0
  27. data/lib/date_named_file/doc/frames.html +17 -0
  28. data/lib/date_named_file/doc/index.html +85 -0
  29. data/lib/date_named_file/doc/js/app.js +303 -0
  30. data/lib/date_named_file/doc/js/full_list.js +216 -0
  31. data/lib/date_named_file/doc/js/jquery.js +4 -0
  32. data/lib/date_named_file/doc/method_list.html +51 -0
  33. data/lib/date_named_file/doc/top-level-namespace.html +100 -0
  34. data/lib/date_named_file/error.rb +0 -0
  35. data/lib/date_named_file/template.rb +139 -0
  36. data/lib/date_named_file/version.rb +3 -0
  37. metadata +137 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8f891f2477e3244bcdd8dbe173a82c77d2b69f95dd851fc4334c49d8ca21f5d8
4
+ data.tar.gz: 82314bb7a4f069c15a82d8f87bd4fe114cbdcf504fec8495a2484ae0ac8af85e
5
+ SHA512:
6
+ metadata.gz: 2d92757d19200579ecee7c88e85e35e8ed7a3329835fbea57f5f361c3ba20227fe82006618183ec870f4d8e99449e54524c270081e552eda46bf0c84f7aabc26
7
+ data.tar.gz: f84bc8f8eb057a47706d44fa7d65abbc74a38fdb56d539d0b4ab289f99a669e360a5488e79d89cbfe406cb200f9cc3317e5f487f456aff539c9b424c0c654880
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.2
7
+ before_install: gem install bundler -v 2.0.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in date_named_file.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Bill Dueber
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # DateNamedFile -- for files with embedded dates
2
+
3
+ ## Usage
4
+
5
+ ```ruby
6
+
7
+ require 'date_named_file'
8
+
9
+ update_file_template = DateNamedFile.new('hathi_upd_%Y%m%d.txt.gz')
10
+ upd = update_file_template.in_dir('/tmp/')
11
+ #=> #<DateNamedFile::Directory:0x00007fa4105de1a0...>
12
+
13
+ # Today is 2019-11-22
14
+ # All of these will produce either a DatedFile or a MissingFile,
15
+ # depending on whether or not they exist in teh directory
16
+
17
+ # These are all the same
18
+ f = upd.yesterday
19
+ f = upd.at(:yesterday)
20
+ f = upd.at(20191121)
21
+ f = upd.at('2019-11-21')
22
+ f = upd.at('2019_11_21')
23
+ f = upd.at('11/21/2019')
24
+ f = upd.at(-1)
25
+
26
+ #=> <DateNamedFile::DatedFile:/private/tmp/hathi_upd_20191121.txt.gz>
27
+
28
+ # Yesterday's file is there
29
+ f.exist? #=> true
30
+
31
+ # ...but today's is not
32
+ upd.today #=> <DateNamedFile::MissingFile:/private/tmp/hathi_upd_20191122.txt.gz>
33
+ upd.today.exist? #=> false
34
+
35
+ # So, which ones are there?
36
+ upd.matching_files
37
+ # => [<DateNamedFile::DatedFile:/private/tmp/hathi_upd_20191118.txt.gz>,
38
+ # <DateNamedFile::DatedFile:/private/tmp/hathi_upd_20191119.txt.gz>,
39
+ # <DateNamedFile::DatedFile:/private/tmp/hathi_upd_20191120.txt.gz>,
40
+ # <DateNamedFile::DatedFile:/private/tmp/hathi_upd_20191121.txt.gz>]
41
+
42
+ # A DateNamedFile::Directory is enumerable
43
+
44
+ upd.each { ... }
45
+ f = upd.first #=> <DateNamedFile::DatedFile:/private/tmp/hathi_upd_20191118.txt.gz>
46
+
47
+ # Let's get everything since the 20th
48
+
49
+ upd.since '2019-11-20' #or
50
+ upd.since -2
51
+ # => [<DateNamedFile::DatedFile:/private/tmp/hathi_upd_20191120.txt.gz>,
52
+ # <DateNamedFile::DatedFile:/private/tmp/hathi_upd_20191121.txt.gz>]
53
+
54
+
55
+
56
+ # A DatedFiles compares based on the embedded DateTime
57
+ upd.at('2019-11-11') > upd.at('2019-11-10') #=> true
58
+
59
+ # ..even across different templates and embedded date formats
60
+ full_files = DateNamedFile.new('hathi_full_%Y-%m-%d.txt').in_dir('/tmp')
61
+
62
+ last_update = upd.last #=> <DateNamedFile::DatedFile:/private/tmp/hathi_upd_20191121.txt.gz>
63
+ last_full = full.last #=> <DateNamedFile::DatedFile:/private/tmp/hathi_full_2019-11-20.txt.gz>
64
+
65
+ last_update > last_full #=> true
66
+
67
+ # A DatedFile delegates most things to a Pathname object
68
+ last_update.ctime # => 2019-11-22 14:22:49 -0500
69
+ last_update.basename('.gz') #=> #<Pathname:hathi_upd_20191121.txt>
70
+
71
+ #...but we override #open to automatically deal with .gz files
72
+ # if need be
73
+
74
+ last_update #=> <DateNamedFile::DatedFile:/private/tmp/hathi_upd_20191121.txt.gz>
75
+ last_update.open.first #=> "mdp.39015018415946\tdeny\t..."
76
+
77
+
78
+ ```
79
+
80
+
81
+
82
+
83
+ ## Installation
84
+
85
+ Add this line to your application's Gemfile:
86
+
87
+ ```ruby
88
+ gem 'date_named_file'
89
+ ```
90
+
91
+ And then execute:
92
+
93
+ $ bundle
94
+
95
+ Or install it yourself as:
96
+
97
+ $ gem install date_named_file
98
+
99
+ ## Usage
100
+
101
+ TODO: Write usage instructions here
102
+
103
+ ## Development
104
+
105
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
106
+
107
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
108
+
109
+ ## Contributing
110
+
111
+ Bug reports and pull requests are welcome on GitHub at https://github.com/billdueber/date_named_file.
112
+
113
+ ## License
114
+
115
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "date_named_file"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,34 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "date_named_file/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "date_named_file"
8
+ spec.version = DateNamedFile::VERSION
9
+ spec.authors = ["Bill Dueber"]
10
+ spec.email = ["bill@dueber.com"]
11
+
12
+ spec.summary = %q{Utility to deal with files with embedded dates}
13
+ spec.homepage = "https://github.com/billdueber/date_named_file"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = spec.homepage + "CHANGELOG.md"
19
+
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 2.0"
31
+ spec.add_development_dependency "rake", ">= 12.3.3"
32
+ spec.add_development_dependency "rspec", "~> 3.0"
33
+ spec.add_development_dependency "pry"
34
+ end
@@ -0,0 +1,13 @@
1
+ require "date_named_file/version"
2
+ require 'date_named_file/template'
3
+ require 'date_named_file/directory'
4
+ require 'date_named_file/dated_file'
5
+
6
+ module DateNamedFile
7
+ class Error < StandardError; end
8
+
9
+ def self.new(*args)
10
+ DateNamedFile::Template.new(*args)
11
+ end
12
+
13
+ end
File without changes
File without changes
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'date_named_file/template'
5
+
6
+ module DateNamedFile
7
+
8
+ class DatedFile < SimpleDelegator
9
+ include Comparable
10
+
11
+ attr_reader :datetime
12
+ # @param [DateNamedFile::Template] template
13
+ # @param [<anything date_ish>] date_ish (see #forgiving_dateify)
14
+ def initialize(template, date_ish=DateTime.now)
15
+ @template = template
16
+ self.datetime = date_ish
17
+ end
18
+
19
+ def self.from_filename(template, filename)
20
+ raise Error.new("String #{filename} does not match template '#{template.template_string}'") unless template.match? filename
21
+ newobject = self.new(template)
22
+ newobject.datetime = newobject.extract_datetime_from_filename(filename)
23
+ newobject
24
+ end
25
+
26
+ # Defining to_datetime allows Dateish.forgiving_datetime to
27
+ # deal with it directly
28
+ def to_datetime
29
+ self.datetime
30
+ end
31
+
32
+ def datetime=(date_ish)
33
+ @datetime = Dateish.forgiving_dateify(date_ish)
34
+ @path = Pathname.new(@template.filename_for(@datetime).to_s)
35
+ __setobj__ @path
36
+ end
37
+
38
+ def match?(other)
39
+ @template.match? other.to_s
40
+ end
41
+
42
+ def <=>(other)
43
+ if self.match? other.to_s
44
+ self.datetime <=> extract_datetime_from_filename(other)
45
+ else
46
+ d2 = Dateish.forgiving_dateify(other)
47
+ z = self.datetime <=> d2
48
+ end
49
+ end
50
+
51
+ def extract_datetime_from_filename(str)
52
+ if m = @template.matcher.match(str)
53
+ Dateish.forgiving_dateify(m[1..-1].join(''))
54
+ else
55
+ DateTime.new(0)
56
+ end
57
+ end
58
+
59
+ def open
60
+ raise "File #{@path.to_s} doesn't exist" unless @path.exist?
61
+ begin
62
+ Zlib::GzipReader.open(@path)
63
+ rescue Zlib::GzipFile::Error
64
+ File.open(@path)
65
+ end
66
+ end
67
+
68
+ # Override pretty-print so it shows up correctly in pry
69
+ def pretty_print(q)
70
+ q.text "<#{self.class}:#{@path}>"
71
+ end
72
+
73
+
74
+ end
75
+
76
+ class MissingFile < DatedFile
77
+
78
+ def exist?
79
+ false
80
+ end
81
+
82
+ alias_method :exists?, :exist?
83
+ end
84
+
85
+ class OldDatedFile < SimpleDelegator
86
+
87
+ attr_reader :embedded_date, :dft
88
+
89
+ alias_method :template, :dft
90
+ def initialize(dft, filename)
91
+ @path = Pathname.new(filename)
92
+ self.__setobj__ @path
93
+ @dft = dft
94
+ @embedded_date = dft.datetime_from_filename(@path.basename.to_s)
95
+ end
96
+
97
+ def self.from_filename(dft, filename)
98
+ raise Error.new("String #{filename} does not match template '#{dft.template}'") unless dft.match? filename
99
+ self.new(dft, filename)
100
+ end
101
+
102
+ def self.from_date(dft, date_ish)
103
+ self.new(dft, dft.filename_for(date_ish))
104
+ end
105
+
106
+ def open
107
+ raise "File #{@path.to_s} doesn't exist" unless @path.exist?
108
+ begin
109
+ Zlib::GzipReader.open(@path)
110
+ rescue Zlib::GzipFile::Error
111
+ File.open(@path)
112
+ end
113
+ end
114
+
115
+ def ==(other)
116
+ self.basename.to_s == other.basename.to_s
117
+ end
118
+
119
+ def to_s
120
+ @path.to_s
121
+ end
122
+
123
+ def inspect
124
+ "#<#{self.class.to_s}:#{@path} template=#{@dft.template}:#{object_id}>"
125
+ end
126
+
127
+ def pretty_inspect
128
+ "#<#{self.class.to_s}: #{@path}\n @template=#{@dft.template}\n @embedded_date=#{@embedded_date.to_s}\n #{object_id}>"
129
+ end
130
+
131
+ end
132
+
133
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ module DateNamedFile
5
+
6
+ class InvalidDateFormat < StandardError
7
+ end
8
+
9
+ class NonDigitsInDelimitedDate < InvalidDateFormat
10
+ end
11
+
12
+ class NonTwoDigitDateParts < InvalidDateFormat
13
+ end
14
+
15
+ # Provide some simple and very naïve methods to turn something that might be a date
16
+ # into a date.
17
+ module DateishHelpers
18
+
19
+ ALL_DIGITS = /\A\d+\Z/
20
+ VALID_DELIMITERS = /[-_ :]/
21
+
22
+ # Attempt to turn big integer (turned into a string of digits),
23
+ # an actual string of digits, or a delimited
24
+ # string of digits (delimited by chars in VALID_DELIMITERS) into
25
+ # a DateTime.
26
+ #
27
+ # Should handle:
28
+ # * Something that response to #to_datetime, which just calls that.
29
+ # * The symbols :today, :yesterday, and :tomorrow
30
+ # * unix timestamp (see #extract_unix_timestamp)
31
+ # * string of digits YYYYMMDD (see #extract_undelimited_datetime)
32
+ # * delimited string of digits (see #extract_delimited_datetime)
33
+ # @param [#to_datetime, Integer, String] date_ish The thing to try to convert
34
+ # @return [DateTime] Our best shot at a datetime
35
+ # @raise [InvalidDateFormat] if we can't pull a datetime out of it
36
+ def forgiving_dateify(date_ish)
37
+ return DateTime.now if date_ish == :today
38
+ return (DateTime.now - 1) if date_ish == :yesterday
39
+ return (DateTime.now + 1) if date_ish == :tomorrow
40
+ if date_ish.respond_to?(:to_i) and date_ish.to_i < 0
41
+ return DateTime.now + date_ish.to_i
42
+ end
43
+ if date_ish.respond_to? :to_datetime
44
+ date_ish.to_datetime
45
+ else
46
+ str = date_ish.to_s
47
+ if digit_string?(str)
48
+ extract_from_digitstring(str) or
49
+ raise InvalidDateFormat.new("All-digit string '#{str}' doesn't parse as date string or unix timestamp")
50
+ else
51
+ extract_delimited_datetime(str)
52
+ end
53
+ end
54
+ rescue InvalidDateFormat => e
55
+ raise InvalidDateFormat.new("Can't turn '#{date_ish}' into a date-time: #{e.message}")
56
+ end
57
+
58
+
59
+ def extract_from_digitstring(str)
60
+ extract_unix_timestamp(str) or
61
+ extract_undelimited_datetime(str)
62
+ end
63
+
64
+ # An undelimited datetime is a string of digits at least
65
+ # eight digits long (to get YYYYMMDD). Anything after that
66
+ # is pulled out into two-digit chunks and sent along to
67
+ # DateTime.new as integers. This means:
68
+ # * YYYYMMDD is always necessary; everything after that is optional
69
+ # * The rest must be in order: Hour, Minute, Second
70
+ # * Everything is assumed to be two digits and zero-padded
71
+ #
72
+ # Limitations:
73
+ # * No support for milliseconds with normal dates. Milliseconds are
74
+ # parsed but silently thrown out. Because c'mon, really?
75
+ # @param [String<0-9>] digit_string A string of digits
76
+ # @return [DateTime, FalseClass] The valid DateTime, or false (for chaining)
77
+ def extract_undelimited_datetime(digit_string)
78
+ return false unless digit_string?(digit_string)
79
+ m = /\A(\d{4})(\d{2})(\d{2})(\d{2})?(\d{2})?(\d{2})?\d*\Z/.match(digit_string)
80
+ if m
81
+ datetime_from_parts(m[1..-1].compact)
82
+ else
83
+ false
84
+ end
85
+ end
86
+
87
+ # Any string that is (a) exactly 10 digits, and (b) starts with '1'
88
+ # will be considered a unix timestamp and treated as such
89
+ # LIMITATION: Only good back to Sept 2001
90
+ # @param [String<0-9>] digit_string A string of digits, should be a unix timestamp
91
+ # @return [DateTime, FalseClass] The valid DateTime, or false (for chaining)
92
+ def extract_unix_timestamp(digit_string)
93
+ if looks_like_unix_timestamp?(digit_string)
94
+ DateTime.strptime(digit_string, '%s')
95
+ else
96
+ false
97
+ end
98
+ end
99
+
100
+ # Is this plausible a modern unix timestamp? Valid back to 2001
101
+ # @param [String] digit_string
102
+ # @return [Boolean]
103
+ def looks_like_unix_timestamp?(digit_string)
104
+ digit_string?(digit_string) and
105
+ digit_string.size == 10 and
106
+ /\A1[0-9]/.match(digit_string[0..1])
107
+ end
108
+
109
+
110
+ def extract_delimited_datetime(str)
111
+ str = perform_simple_transforms(str)
112
+ validate_delimited_datetime!(str)
113
+ year = extract_year(str)
114
+ non_year_parts = extract_non_year_parts(str)
115
+ all_parts = non_year_parts.unshift(year)
116
+ datetime_from_parts(all_parts)
117
+ rescue ArgumentError => e
118
+ # presumably a DateTime parse error
119
+ false
120
+ rescue NonTwoDigitDateParts
121
+ raise InvalidDateFormat.new("Trying to parse as delimited date. '#{str}' looks to have non-two-digit parts (no zero padding?).")
122
+ rescue NonDigitsInDelimitedDate
123
+ raise InvalidDateFormat.new("Trying to parse as delimited date. '#{str}' looks to have non-digits between delimiters.")
124
+ end
125
+
126
+ # Deal with d/m/yyyy
127
+ def perform_simple_transforms(str)
128
+ slash_matcher = %r[(\d{1,2})/(\d{1,2})/(\d{4})]
129
+ if m = slash_matcher.match(str)
130
+ '%4d-%02d-%02d' % [m[3], m[1], m[2]]
131
+ else
132
+ str
133
+ end
134
+ end
135
+
136
+ def datetime_from_parts(parts)
137
+ year = parts[0]
138
+ non_year_parts = parts[1..-1].map { |dstring| dstring.scan(/\d\d/) }.flatten
139
+ all_parts = non_year_parts.unshift(year).map(&:to_i)
140
+ DateTime.new(*all_parts)
141
+ rescue ArgumentError
142
+ raise InvalidDateFormat.new("DateTime.new rejected extracted parts ([#{parts.join(',')}]).")
143
+ end
144
+
145
+ def extract_non_year_parts(str)
146
+ parts = extract_rest(str).split(VALID_DELIMITERS)
147
+ validate_parts!(parts)
148
+ parts
149
+ end
150
+
151
+ def validate_parts!(parts)
152
+ unless parts.all? { |p| digit_string?(p) }
153
+ raise NonDigitsInDelimitedDate.new
154
+ end
155
+
156
+ unless parts.all? { |p| p.size == 2 }
157
+ raise NonTwoDigitDateParts.new
158
+ end
159
+ end
160
+
161
+
162
+
163
+ def digit_string?(str)
164
+ ALL_DIGITS.match(str)
165
+ end
166
+
167
+ def validate_delimited_datetime!(str)
168
+ /\A\d{4}/.match(str) or raise InvalidDateFormat.new("'#{str}' doesn't obviously start with a year")
169
+ end
170
+
171
+ def extract_year(str)
172
+ str[0..3]
173
+ end
174
+
175
+ def extract_rest(str)
176
+ everything_after_the_year = str[4..-1]
177
+ ditch_leading_delimiters(everything_after_the_year)
178
+ end
179
+
180
+ def ditch_leading_delimiters(str)
181
+ str.sub(/\A#{VALID_DELIMITERS}/, '')
182
+ end
183
+
184
+
185
+ end
186
+
187
+ module Dateish
188
+ extend DateNamedFile::DateishHelpers
189
+ end
190
+ end