feedjira 2.2.0 → 3.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/feed-parsing.md +15 -0
- data/.rubocop.yml +32 -8
- data/.rubocop_todo.yml +11 -0
- data/.travis.yml +3 -7
- data/CHANGELOG.md +18 -9
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -5
- data/README.md +46 -99
- data/Rakefile +8 -6
- data/feedjira.gemspec +31 -20
- data/lib/feedjira.rb +75 -41
- data/lib/feedjira/atom_entry_utilities.rb +51 -0
- data/lib/feedjira/configuration.rb +8 -10
- data/lib/feedjira/core_ext.rb +5 -3
- data/lib/feedjira/core_ext/date.rb +2 -1
- data/lib/feedjira/core_ext/string.rb +2 -1
- data/lib/feedjira/core_ext/time.rb +12 -12
- data/lib/feedjira/date_time_utilities.rb +8 -10
- data/lib/feedjira/date_time_utilities/date_time_epoch_parser.rb +3 -2
- data/lib/feedjira/date_time_utilities/date_time_language_parser.rb +4 -4
- data/lib/feedjira/date_time_utilities/date_time_pattern_parser.rb +11 -15
- data/lib/feedjira/feed.rb +12 -82
- data/lib/feedjira/feed_entry_utilities.rb +14 -7
- data/lib/feedjira/feed_utilities.rb +5 -4
- data/lib/feedjira/parser.rb +6 -1
- data/lib/feedjira/parser/atom.rb +6 -5
- data/lib/feedjira/parser/atom_entry.rb +4 -21
- data/lib/feedjira/parser/atom_feed_burner.rb +7 -6
- data/lib/feedjira/parser/atom_feed_burner_entry.rb +7 -18
- data/lib/feedjira/parser/atom_google_alerts.rb +26 -0
- data/lib/feedjira/parser/atom_google_alerts_entry.rb +21 -0
- data/lib/feedjira/parser/atom_youtube.rb +4 -3
- data/lib/feedjira/parser/atom_youtube_entry.rb +9 -8
- data/lib/feedjira/parser/globally_unique_identifier.rb +21 -0
- data/lib/feedjira/parser/google_docs_atom.rb +6 -6
- data/lib/feedjira/parser/google_docs_atom_entry.rb +3 -19
- data/lib/feedjira/parser/itunes_rss.rb +4 -3
- data/lib/feedjira/parser/itunes_rss_category.rb +6 -5
- data/lib/feedjira/parser/itunes_rss_item.rb +5 -8
- data/lib/feedjira/parser/itunes_rss_owner.rb +2 -1
- data/lib/feedjira/parser/json_feed.rb +41 -0
- data/lib/feedjira/parser/json_feed_item.rb +57 -0
- data/lib/feedjira/parser/podlove_chapter.rb +4 -3
- data/lib/feedjira/parser/rss.rb +5 -3
- data/lib/feedjira/parser/rss_entry.rb +3 -24
- data/lib/feedjira/parser/rss_feed_burner.rb +4 -3
- data/lib/feedjira/parser/rss_feed_burner_entry.rb +6 -26
- data/lib/feedjira/parser/rss_image.rb +2 -0
- data/lib/feedjira/preprocessor.rb +4 -4
- data/lib/feedjira/rss_entry_utilities.rb +53 -0
- data/lib/feedjira/version.rb +3 -1
- data/spec/feedjira/configuration_spec.rb +11 -16
- data/spec/feedjira/date_time_utilities_spec.rb +22 -20
- data/spec/feedjira/feed_entry_utilities_spec.rb +20 -18
- data/spec/feedjira/feed_spec.rb +17 -229
- data/spec/feedjira/feed_utilities_spec.rb +75 -73
- data/spec/feedjira/parser/atom_entry_spec.rb +41 -38
- data/spec/feedjira/parser/atom_feed_burner_entry_spec.rb +22 -20
- data/spec/feedjira/parser/atom_feed_burner_spec.rb +122 -118
- data/spec/feedjira/parser/atom_google_alerts_entry_spec.rb +34 -0
- data/spec/feedjira/parser/atom_google_alerts_spec.rb +62 -0
- data/spec/feedjira/parser/atom_spec.rb +83 -77
- data/spec/feedjira/parser/atom_youtube_entry_spec.rb +41 -39
- data/spec/feedjira/parser/atom_youtube_spec.rb +21 -19
- data/spec/feedjira/parser/google_docs_atom_entry_spec.rb +10 -8
- data/spec/feedjira/parser/google_docs_atom_spec.rb +25 -21
- data/spec/feedjira/parser/itunes_rss_item_spec.rb +39 -37
- data/spec/feedjira/parser/itunes_rss_owner_spec.rb +7 -5
- data/spec/feedjira/parser/itunes_rss_spec.rb +120 -116
- data/spec/feedjira/parser/json_feed_item_spec.rb +81 -0
- data/spec/feedjira/parser/json_feed_spec.rb +55 -0
- data/spec/feedjira/parser/podlove_chapter_spec.rb +14 -12
- data/spec/feedjira/parser/rss_entry_spec.rb +56 -34
- data/spec/feedjira/parser/rss_feed_burner_entry_spec.rb +36 -34
- data/spec/feedjira/parser/rss_feed_burner_spec.rb +49 -45
- data/spec/feedjira/parser/rss_spec.rb +38 -36
- data/spec/feedjira/preprocessor_spec.rb +9 -7
- data/spec/feedjira_spec.rb +166 -0
- data/spec/sample_feeds.rb +32 -29
- data/spec/sample_feeds/HuffPostCanada.xml +279 -0
- data/spec/sample_feeds/Permalinks.xml +22 -0
- data/spec/sample_feeds/a10.xml +72 -0
- data/spec/sample_feeds/google_alerts_atom.xml +1 -0
- data/spec/sample_feeds/json_feed.json +156 -0
- data/spec/spec_helper.rb +7 -5
- metadata +59 -70
- data/Dangerfile +0 -1
- data/fixtures/vcr_cassettes/fetch_failure.yml +0 -62
- data/fixtures/vcr_cassettes/parse_error.yml +0 -222
- data/fixtures/vcr_cassettes/success.yml +0 -281
data/Rakefile
CHANGED
@@ -1,17 +1,19 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
require "rubocop/rake_task"
|
5
|
+
require "yard"
|
4
6
|
|
5
7
|
RSpec::Core::RakeTask.new(:spec) do |t|
|
6
8
|
t.verbose = false
|
7
9
|
end
|
8
10
|
|
9
11
|
RuboCop::RakeTask.new(:rubocop) do |t|
|
10
|
-
t.options = [
|
12
|
+
t.options = ["--display-cop-names"]
|
11
13
|
end
|
12
14
|
|
13
15
|
YARD::Rake::YardocTask.new do |t|
|
14
|
-
t.files = [
|
16
|
+
t.files = ["lib/**/*.rb", "-", "LICENSE"]
|
15
17
|
end
|
16
18
|
|
17
|
-
task default: [
|
19
|
+
task default: %i[spec rubocop]
|
data/feedjira.gemspec
CHANGED
@@ -1,31 +1,42 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path("lib/feedjira/version", __dir__)
|
3
4
|
|
4
5
|
Gem::Specification.new do |s|
|
5
|
-
s.authors
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
s.authors = [
|
7
|
+
"Adam Hess",
|
8
|
+
"Akinori Musha",
|
9
|
+
"Ezekiel Templin",
|
10
|
+
"Jon Allured",
|
11
|
+
"Julien Kirch",
|
12
|
+
"Michael Stock",
|
13
|
+
"Paul Dix"
|
14
|
+
]
|
15
|
+
s.homepage = "https://github.com/feedjira/feedjira"
|
16
|
+
s.license = "MIT"
|
17
|
+
s.name = "feedjira"
|
10
18
|
s.platform = Gem::Platform::RUBY
|
11
|
-
s.summary =
|
19
|
+
s.summary = "A feed parsing library"
|
12
20
|
s.version = Feedjira::VERSION
|
13
21
|
|
22
|
+
s.metadata = {
|
23
|
+
"homepage_uri" => "http://feedjira.com",
|
24
|
+
"source_code_uri" => "https://github.com/feedjira/feedjira",
|
25
|
+
"changelog_uri" => "https://github.com/feedjira/feedjira/blob/master/CHANGELOG.md"
|
26
|
+
}
|
27
|
+
|
14
28
|
s.files = `git ls-files`.split("\n")
|
15
|
-
s.require_paths = [
|
29
|
+
s.require_paths = ["lib"]
|
16
30
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
31
|
|
18
|
-
s.required_ruby_version =
|
32
|
+
s.required_ruby_version = ">=2.2"
|
19
33
|
|
20
|
-
s.add_dependency
|
21
|
-
s.add_dependency
|
22
|
-
s.add_dependency 'loofah', '>= 2.0'
|
23
|
-
s.add_dependency 'sax-machine', '>= 1.0'
|
34
|
+
s.add_dependency "loofah", ">= 2.3.1"
|
35
|
+
s.add_dependency "sax-machine", ">= 1.0"
|
24
36
|
|
25
|
-
s.add_development_dependency
|
26
|
-
s.add_development_dependency
|
27
|
-
s.add_development_dependency
|
28
|
-
s.add_development_dependency
|
29
|
-
s.add_development_dependency
|
30
|
-
s.add_development_dependency 'yard', '~> 0.9'
|
37
|
+
s.add_development_dependency "faraday"
|
38
|
+
s.add_development_dependency "rspec"
|
39
|
+
s.add_development_dependency "rubocop"
|
40
|
+
s.add_development_dependency "vcr"
|
41
|
+
s.add_development_dependency "yard"
|
31
42
|
end
|
data/lib/feedjira.rb
CHANGED
@@ -1,47 +1,81 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
|
8
|
-
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
13
|
-
require
|
14
|
-
require
|
15
|
-
require
|
16
|
-
require
|
17
|
-
require
|
18
|
-
require
|
19
|
-
require
|
20
|
-
require
|
21
|
-
require
|
22
|
-
require
|
23
|
-
require
|
24
|
-
require
|
25
|
-
require
|
26
|
-
|
27
|
-
require
|
28
|
-
require
|
29
|
-
|
30
|
-
require
|
31
|
-
require
|
32
|
-
require
|
33
|
-
require
|
34
|
-
require
|
35
|
-
require
|
36
|
-
require
|
37
|
-
require
|
38
|
-
require
|
39
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zlib"
|
4
|
+
require "sax-machine"
|
5
|
+
require "loofah"
|
6
|
+
require "logger"
|
7
|
+
require "json"
|
8
|
+
|
9
|
+
require "feedjira/core_ext"
|
10
|
+
require "feedjira/configuration"
|
11
|
+
require "feedjira/date_time_utilities/date_time_epoch_parser"
|
12
|
+
require "feedjira/date_time_utilities/date_time_language_parser"
|
13
|
+
require "feedjira/date_time_utilities/date_time_pattern_parser"
|
14
|
+
require "feedjira/date_time_utilities"
|
15
|
+
require "feedjira/feed_entry_utilities"
|
16
|
+
require "feedjira/feed_utilities"
|
17
|
+
require "feedjira/feed"
|
18
|
+
require "feedjira/rss_entry_utilities"
|
19
|
+
require "feedjira/atom_entry_utilities"
|
20
|
+
require "feedjira/parser"
|
21
|
+
require "feedjira/parser/globally_unique_identifier"
|
22
|
+
require "feedjira/parser/rss_entry"
|
23
|
+
require "feedjira/parser/rss_image"
|
24
|
+
require "feedjira/parser/rss"
|
25
|
+
require "feedjira/parser/atom_entry"
|
26
|
+
require "feedjira/parser/atom"
|
27
|
+
require "feedjira/preprocessor"
|
28
|
+
require "feedjira/version"
|
29
|
+
|
30
|
+
require "feedjira/parser/rss_feed_burner_entry"
|
31
|
+
require "feedjira/parser/rss_feed_burner"
|
32
|
+
require "feedjira/parser/podlove_chapter"
|
33
|
+
require "feedjira/parser/itunes_rss_owner"
|
34
|
+
require "feedjira/parser/itunes_rss_category"
|
35
|
+
require "feedjira/parser/itunes_rss_item"
|
36
|
+
require "feedjira/parser/itunes_rss"
|
37
|
+
require "feedjira/parser/atom_feed_burner_entry"
|
38
|
+
require "feedjira/parser/atom_feed_burner"
|
39
|
+
require "feedjira/parser/atom_google_alerts_entry"
|
40
|
+
require "feedjira/parser/atom_google_alerts"
|
41
|
+
require "feedjira/parser/google_docs_atom_entry"
|
42
|
+
require "feedjira/parser/google_docs_atom"
|
43
|
+
require "feedjira/parser/atom_youtube_entry"
|
44
|
+
require "feedjira/parser/atom_youtube"
|
45
|
+
require "feedjira/parser/json_feed"
|
46
|
+
require "feedjira/parser/json_feed_item"
|
40
47
|
|
41
48
|
# Feedjira
|
42
49
|
module Feedjira
|
43
|
-
|
44
|
-
class FetchFailure < StandardError; end
|
50
|
+
NoParserAvailable = Class.new(StandardError)
|
45
51
|
|
46
52
|
extend Configuration
|
53
|
+
|
54
|
+
# Parse XML with first compatible parser
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# xml = HTTParty.get("http://example.com").body
|
58
|
+
# Feedjira.parse(xml)
|
59
|
+
def parse(xml, parser: nil, &block)
|
60
|
+
parser ||= parser_for_xml(xml)
|
61
|
+
|
62
|
+
if parser.nil?
|
63
|
+
raise NoParserAvailable, "No valid parser for XML."
|
64
|
+
end
|
65
|
+
|
66
|
+
parser.parse(xml, &block)
|
67
|
+
end
|
68
|
+
module_function :parse # rubocop:disable Style/AccessModifierDeclarations
|
69
|
+
|
70
|
+
# Find compatible parser for given XML
|
71
|
+
#
|
72
|
+
# @example
|
73
|
+
# xml = HTTParty.get("http://example.com").body
|
74
|
+
# parser = Feedjira.parser_for_xml(xml)
|
75
|
+
# parser.parse(xml)
|
76
|
+
def parser_for_xml(xml)
|
77
|
+
start_of_doc = xml.slice(0, 2000)
|
78
|
+
Feedjira.parsers.detect { |klass| klass.able_to_parse?(start_of_doc) }
|
79
|
+
end
|
80
|
+
module_function :parser_for_xml # rubocop:disable Style/AccessModifierDeclarations
|
47
81
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Feedjira
|
4
|
+
module AtomEntryUtilities
|
5
|
+
def self.included(mod)
|
6
|
+
mod.class_exec do
|
7
|
+
element :title, as: :raw_title, with: { type: "html" }
|
8
|
+
element :title, as: :raw_title, with: { type: "xhtml" }
|
9
|
+
element :title, as: :raw_title, with: { type: "xml" }
|
10
|
+
element :title, as: :title, with: { type: "text" }
|
11
|
+
element :title, as: :title, with: { type: nil }
|
12
|
+
element :title, as: :title_type, value: :type
|
13
|
+
|
14
|
+
element :name, as: :author
|
15
|
+
element :content
|
16
|
+
element :summary
|
17
|
+
element :enclosure, as: :image, value: :href
|
18
|
+
|
19
|
+
element :published
|
20
|
+
element :id, as: :entry_id
|
21
|
+
element :created, as: :published
|
22
|
+
element :issued, as: :published
|
23
|
+
element :updated
|
24
|
+
element :modified, as: :updated
|
25
|
+
|
26
|
+
elements :category, as: :categories, value: :term
|
27
|
+
|
28
|
+
element :link, as: :url, value: :href, with: {
|
29
|
+
type: "text/html",
|
30
|
+
rel: "alternate"
|
31
|
+
}
|
32
|
+
|
33
|
+
elements :link, as: :links, value: :href
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def title
|
38
|
+
@title ||=
|
39
|
+
case @raw_title
|
40
|
+
when String
|
41
|
+
Loofah.fragment(@raw_title).xpath("normalize-space(.)")
|
42
|
+
else
|
43
|
+
@title
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def url
|
48
|
+
@url ||= links.first
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Feedjira::Configuration
|
2
4
|
module Feedjira
|
3
5
|
# Provides global configuration options for Feedjira
|
@@ -8,12 +10,9 @@ module Feedjira
|
|
8
10
|
# end
|
9
11
|
module Configuration
|
10
12
|
attr_accessor(
|
11
|
-
:follow_redirect_limit,
|
12
13
|
:logger,
|
13
14
|
:parsers,
|
14
|
-
:
|
15
|
-
:strip_whitespace,
|
16
|
-
:user_agent
|
15
|
+
:strip_whitespace
|
17
16
|
)
|
18
17
|
|
19
18
|
# Modify Feedjira's current configuration
|
@@ -42,20 +41,17 @@ module Feedjira
|
|
42
41
|
|
43
42
|
# @private
|
44
43
|
def set_default_configuration
|
45
|
-
self.follow_redirect_limit = 3
|
46
44
|
self.logger = default_logger
|
47
45
|
self.parsers = default_parsers
|
48
|
-
self.request_timeout = 30
|
49
46
|
self.strip_whitespace = false
|
50
|
-
self.user_agent = "Feedjira #{Feedjira::VERSION}"
|
51
47
|
end
|
52
48
|
|
53
49
|
private
|
54
50
|
|
55
51
|
# @private
|
56
52
|
def default_logger
|
57
|
-
Logger.new(
|
58
|
-
logger.progname =
|
53
|
+
Logger.new($stdout).tap do |logger|
|
54
|
+
logger.progname = "Feedjira"
|
59
55
|
logger.level = Logger::WARN
|
60
56
|
end
|
61
57
|
end
|
@@ -67,9 +63,11 @@ module Feedjira
|
|
67
63
|
Feedjira::Parser::GoogleDocsAtom,
|
68
64
|
Feedjira::Parser::AtomYoutube,
|
69
65
|
Feedjira::Parser::AtomFeedBurner,
|
66
|
+
Feedjira::Parser::AtomGoogleAlerts,
|
70
67
|
Feedjira::Parser::Atom,
|
71
68
|
Feedjira::Parser::ITunesRSS,
|
72
|
-
Feedjira::Parser::RSS
|
69
|
+
Feedjira::Parser::RSS,
|
70
|
+
Feedjira::Parser::JSONFeed
|
73
71
|
]
|
74
72
|
end
|
75
73
|
end
|
data/lib/feedjira/core_ext.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Date code pulled and adapted from:
|
2
4
|
# Ruby Cookbook by Lucas Carlson and Leonard Richardson
|
3
5
|
# Published by O'Reilly
|
4
6
|
# ISBN: 0-596-52369-6
|
5
|
-
# rubocop:disable Style/DocumentationMethod
|
6
7
|
class Date
|
7
8
|
def feed_utils_to_gm_time
|
8
9
|
feed_utils_to_time(new_offset, :gm)
|
@@ -1,7 +1,8 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
require "date"
|
3
5
|
|
4
|
-
# rubocop:disable Style/DocumentationMethod
|
5
6
|
class Time
|
6
7
|
# Parse a time string and convert it to UTC without raising errors.
|
7
8
|
# Parses a flattened 14-digit time (YYYYmmddHHMMMSS) as UTC.
|
@@ -11,17 +12,16 @@ class Time
|
|
11
12
|
#
|
12
13
|
# === Returns
|
13
14
|
# A Time instance in UTC or nil if there were errors while parsing.
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
parse_string_safely dt.to_s
|
15
|
+
def self.parse_safely(datetime)
|
16
|
+
if datetime.is_a?(Time)
|
17
|
+
datetime.utc
|
18
|
+
elsif datetime.respond_to?(:to_datetime)
|
19
|
+
datetime.to_datetime.utc
|
20
|
+
elsif datetime.respond_to? :to_s
|
21
|
+
parse_string_safely datetime.to_s
|
22
22
|
end
|
23
23
|
rescue StandardError => e
|
24
|
-
Feedjira.logger.debug { "Failed to parse time #{
|
24
|
+
Feedjira.logger.debug { "Failed to parse time #{datetime}" }
|
25
25
|
Feedjira.logger.debug(e)
|
26
26
|
nil
|
27
27
|
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Feedjira
|
3
4
|
module DateTimeUtilities
|
4
5
|
# This is our date parsing heuristic.
|
@@ -12,16 +13,13 @@ module Feedjira
|
|
12
13
|
|
13
14
|
# Parse the given string starting with the most common parser (default ruby)
|
14
15
|
# and going over all other available parsers
|
15
|
-
# rubocop:disable Metrics/MethodLength
|
16
16
|
def parse_datetime(string)
|
17
|
-
res = DATE_PARSERS.
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
nil
|
24
|
-
end
|
17
|
+
res = DATE_PARSERS.detect do |parser|
|
18
|
+
return parser.parse(string).feed_utils_to_gm_time
|
19
|
+
rescue StandardError => e
|
20
|
+
Feedjira.logger.debug { "Failed to parse date #{string}" }
|
21
|
+
Feedjira.logger.debug(e)
|
22
|
+
nil
|
25
23
|
end
|
26
24
|
|
27
25
|
Feedjira.logger.warn { "Failed to parse date #{string}" } if res.nil?
|
@@ -1,11 +1,12 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
3
|
module Feedjira
|
4
4
|
module DateTimeUtilities
|
5
5
|
class DateTimeEpochParser
|
6
6
|
def self.parse(string)
|
7
7
|
epoch_time = string.to_i
|
8
8
|
return Time.at(epoch_time).to_datetime if epoch_time.to_s == string
|
9
|
+
|
9
10
|
raise "#{string} is not a valid epoch time"
|
10
11
|
end
|
11
12
|
end
|
@@ -1,12 +1,12 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
3
|
module Feedjira
|
4
4
|
module DateTimeUtilities
|
5
5
|
class DateTimeLanguageParser
|
6
6
|
MONTHS_ENGLISH =
|
7
|
-
%w
|
7
|
+
%w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec].freeze
|
8
8
|
MONTHS_SPANISH =
|
9
|
-
%w
|
9
|
+
%w[Ene Feb Mar Abr May Jun Jul Ago Sep Oct Nov Dic].freeze
|
10
10
|
|
11
11
|
def self.parse(string)
|
12
12
|
DateTime.parse(translate(string))
|