feedjira 2.2.0 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/feed-parsing.md +15 -0
  3. data/.rubocop.yml +32 -8
  4. data/.rubocop_todo.yml +11 -0
  5. data/.travis.yml +3 -7
  6. data/CHANGELOG.md +18 -9
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +8 -5
  9. data/README.md +46 -99
  10. data/Rakefile +8 -6
  11. data/feedjira.gemspec +31 -20
  12. data/lib/feedjira.rb +75 -41
  13. data/lib/feedjira/atom_entry_utilities.rb +51 -0
  14. data/lib/feedjira/configuration.rb +8 -10
  15. data/lib/feedjira/core_ext.rb +5 -3
  16. data/lib/feedjira/core_ext/date.rb +2 -1
  17. data/lib/feedjira/core_ext/string.rb +2 -1
  18. data/lib/feedjira/core_ext/time.rb +12 -12
  19. data/lib/feedjira/date_time_utilities.rb +8 -10
  20. data/lib/feedjira/date_time_utilities/date_time_epoch_parser.rb +3 -2
  21. data/lib/feedjira/date_time_utilities/date_time_language_parser.rb +4 -4
  22. data/lib/feedjira/date_time_utilities/date_time_pattern_parser.rb +11 -15
  23. data/lib/feedjira/feed.rb +12 -82
  24. data/lib/feedjira/feed_entry_utilities.rb +14 -7
  25. data/lib/feedjira/feed_utilities.rb +5 -4
  26. data/lib/feedjira/parser.rb +6 -1
  27. data/lib/feedjira/parser/atom.rb +6 -5
  28. data/lib/feedjira/parser/atom_entry.rb +4 -21
  29. data/lib/feedjira/parser/atom_feed_burner.rb +7 -6
  30. data/lib/feedjira/parser/atom_feed_burner_entry.rb +7 -18
  31. data/lib/feedjira/parser/atom_google_alerts.rb +26 -0
  32. data/lib/feedjira/parser/atom_google_alerts_entry.rb +21 -0
  33. data/lib/feedjira/parser/atom_youtube.rb +4 -3
  34. data/lib/feedjira/parser/atom_youtube_entry.rb +9 -8
  35. data/lib/feedjira/parser/globally_unique_identifier.rb +21 -0
  36. data/lib/feedjira/parser/google_docs_atom.rb +6 -6
  37. data/lib/feedjira/parser/google_docs_atom_entry.rb +3 -19
  38. data/lib/feedjira/parser/itunes_rss.rb +4 -3
  39. data/lib/feedjira/parser/itunes_rss_category.rb +6 -5
  40. data/lib/feedjira/parser/itunes_rss_item.rb +5 -8
  41. data/lib/feedjira/parser/itunes_rss_owner.rb +2 -1
  42. data/lib/feedjira/parser/json_feed.rb +41 -0
  43. data/lib/feedjira/parser/json_feed_item.rb +57 -0
  44. data/lib/feedjira/parser/podlove_chapter.rb +4 -3
  45. data/lib/feedjira/parser/rss.rb +5 -3
  46. data/lib/feedjira/parser/rss_entry.rb +3 -24
  47. data/lib/feedjira/parser/rss_feed_burner.rb +4 -3
  48. data/lib/feedjira/parser/rss_feed_burner_entry.rb +6 -26
  49. data/lib/feedjira/parser/rss_image.rb +2 -0
  50. data/lib/feedjira/preprocessor.rb +4 -4
  51. data/lib/feedjira/rss_entry_utilities.rb +53 -0
  52. data/lib/feedjira/version.rb +3 -1
  53. data/spec/feedjira/configuration_spec.rb +11 -16
  54. data/spec/feedjira/date_time_utilities_spec.rb +22 -20
  55. data/spec/feedjira/feed_entry_utilities_spec.rb +20 -18
  56. data/spec/feedjira/feed_spec.rb +17 -229
  57. data/spec/feedjira/feed_utilities_spec.rb +75 -73
  58. data/spec/feedjira/parser/atom_entry_spec.rb +41 -38
  59. data/spec/feedjira/parser/atom_feed_burner_entry_spec.rb +22 -20
  60. data/spec/feedjira/parser/atom_feed_burner_spec.rb +122 -118
  61. data/spec/feedjira/parser/atom_google_alerts_entry_spec.rb +34 -0
  62. data/spec/feedjira/parser/atom_google_alerts_spec.rb +62 -0
  63. data/spec/feedjira/parser/atom_spec.rb +83 -77
  64. data/spec/feedjira/parser/atom_youtube_entry_spec.rb +41 -39
  65. data/spec/feedjira/parser/atom_youtube_spec.rb +21 -19
  66. data/spec/feedjira/parser/google_docs_atom_entry_spec.rb +10 -8
  67. data/spec/feedjira/parser/google_docs_atom_spec.rb +25 -21
  68. data/spec/feedjira/parser/itunes_rss_item_spec.rb +39 -37
  69. data/spec/feedjira/parser/itunes_rss_owner_spec.rb +7 -5
  70. data/spec/feedjira/parser/itunes_rss_spec.rb +120 -116
  71. data/spec/feedjira/parser/json_feed_item_spec.rb +81 -0
  72. data/spec/feedjira/parser/json_feed_spec.rb +55 -0
  73. data/spec/feedjira/parser/podlove_chapter_spec.rb +14 -12
  74. data/spec/feedjira/parser/rss_entry_spec.rb +56 -34
  75. data/spec/feedjira/parser/rss_feed_burner_entry_spec.rb +36 -34
  76. data/spec/feedjira/parser/rss_feed_burner_spec.rb +49 -45
  77. data/spec/feedjira/parser/rss_spec.rb +38 -36
  78. data/spec/feedjira/preprocessor_spec.rb +9 -7
  79. data/spec/feedjira_spec.rb +166 -0
  80. data/spec/sample_feeds.rb +32 -29
  81. data/spec/sample_feeds/HuffPostCanada.xml +279 -0
  82. data/spec/sample_feeds/Permalinks.xml +22 -0
  83. data/spec/sample_feeds/a10.xml +72 -0
  84. data/spec/sample_feeds/google_alerts_atom.xml +1 -0
  85. data/spec/sample_feeds/json_feed.json +156 -0
  86. data/spec/spec_helper.rb +7 -5
  87. metadata +59 -70
  88. data/Dangerfile +0 -1
  89. data/fixtures/vcr_cassettes/fetch_failure.yml +0 -62
  90. data/fixtures/vcr_cassettes/parse_error.yml +0 -222
  91. data/fixtures/vcr_cassettes/success.yml +0 -281
data/Rakefile CHANGED
@@ -1,17 +1,19 @@
1
- require 'rspec/core/rake_task'
2
- require 'rubocop/rake_task'
3
- require 'yard'
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 = ['--display-cop-names']
12
+ t.options = ["--display-cop-names"]
11
13
  end
12
14
 
13
15
  YARD::Rake::YardocTask.new do |t|
14
- t.files = ['lib/**/*.rb', '-', 'LICENSE']
16
+ t.files = ["lib/**/*.rb", "-", "LICENSE"]
15
17
  end
16
18
 
17
- task default: [:spec, :rubocop]
19
+ task default: %i[spec rubocop]
@@ -1,31 +1,42 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/feedjira/version', __FILE__)
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 = ['Paul Dix', 'Julien Kirch', 'Ezekiel Templin', 'Jon Allured']
6
- s.email = 'feedjira@gmail.com'
7
- s.homepage = 'http://feedjira.com'
8
- s.license = 'MIT'
9
- s.name = 'feedjira'
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 = 'A feed fetching and parsing library'
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 = ['lib']
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 = '>=1.9.3'
32
+ s.required_ruby_version = ">=2.2"
19
33
 
20
- s.add_dependency 'faraday', '>= 0.9'
21
- s.add_dependency 'faraday_middleware', '>= 0.9'
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 'danger'
26
- s.add_development_dependency 'danger-commit_lint'
27
- s.add_development_dependency 'rspec'
28
- s.add_development_dependency 'rubocop', '0.46'
29
- s.add_development_dependency 'vcr'
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
@@ -1,47 +1,81 @@
1
- require 'zlib'
2
- require 'faraday'
3
- require 'faraday_middleware'
4
- require 'sax-machine'
5
- require 'loofah'
6
- require 'logger'
7
-
8
- require 'feedjira/core_ext'
9
- require 'feedjira/configuration'
10
- require 'feedjira/date_time_utilities/date_time_epoch_parser'
11
- require 'feedjira/date_time_utilities/date_time_language_parser'
12
- require 'feedjira/date_time_utilities/date_time_pattern_parser'
13
- require 'feedjira/date_time_utilities'
14
- require 'feedjira/date_time_utilities'
15
- require 'feedjira/feed_entry_utilities'
16
- require 'feedjira/feed_utilities'
17
- require 'feedjira/feed'
18
- require 'feedjira/parser'
19
- require 'feedjira/parser/rss_entry'
20
- require 'feedjira/parser/rss_image'
21
- require 'feedjira/parser/rss'
22
- require 'feedjira/parser/atom_entry'
23
- require 'feedjira/parser/atom'
24
- require 'feedjira/preprocessor'
25
- require 'feedjira/version'
26
-
27
- require 'feedjira/parser/rss_feed_burner_entry'
28
- require 'feedjira/parser/rss_feed_burner'
29
- require 'feedjira/parser/podlove_chapter'
30
- require 'feedjira/parser/itunes_rss_owner'
31
- require 'feedjira/parser/itunes_rss_category'
32
- require 'feedjira/parser/itunes_rss_item'
33
- require 'feedjira/parser/itunes_rss'
34
- require 'feedjira/parser/atom_feed_burner_entry'
35
- require 'feedjira/parser/atom_feed_burner'
36
- require 'feedjira/parser/google_docs_atom_entry'
37
- require 'feedjira/parser/google_docs_atom'
38
- require 'feedjira/parser/atom_youtube_entry'
39
- require 'feedjira/parser/atom_youtube'
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
- class NoParserAvailable < StandardError; end
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
- :request_timeout,
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(STDOUT).tap do |logger|
58
- logger.progname = 'Feedjira'
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
@@ -1,3 +1,5 @@
1
- require 'feedjira/core_ext/time'
2
- require 'feedjira/core_ext/date'
3
- require 'feedjira/core_ext/string'
1
+ # frozen_string_literal: true
2
+
3
+ require "feedjira/core_ext/time"
4
+ require "feedjira/core_ext/date"
5
+ require "feedjira/core_ext/string"
@@ -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,4 +1,5 @@
1
- # rubocop:disable Style/DocumentationMethod Style/Documentation
1
+ # frozen_string_literal: true
2
+
2
3
  class String
3
4
  def sanitize!
4
5
  replace(sanitize)
@@ -1,7 +1,8 @@
1
- require 'time'
2
- require 'date'
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
- # rubocop:disable Metrics/MethodLength
15
- def self.parse_safely(dt)
16
- if dt.is_a?(Time)
17
- dt.utc
18
- elsif dt.respond_to?(:to_datetime)
19
- dt.to_datetime.utc
20
- elsif dt.respond_to? :to_s
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 #{dt}" }
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
- # rubocop:disable Style/Documentation
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.find do |parser|
18
- begin
19
- return parser.parse(string).feed_utils_to_gm_time
20
- rescue StandardError => e
21
- Feedjira.logger.debug { "Failed to parse date #{string}" }
22
- Feedjira.logger.debug(e)
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
- # rubocop:disable Style/Documentation
2
- # rubocop:disable Style/DocumentationMethod
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
- # rubocop:disable Style/Documentation
2
- # rubocop:disable Style/DocumentationMethod
1
+ # frozen_string_literal: true
2
+
3
3
  module Feedjira
4
4
  module DateTimeUtilities
5
5
  class DateTimeLanguageParser
6
6
  MONTHS_ENGLISH =
7
- %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec).freeze
7
+ %w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec].freeze
8
8
  MONTHS_SPANISH =
9
- %w(Ene Feb Mar Abr May Jun Jul Ago Sep Oct Nov Dic).freeze
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))