From 02ac43c94a9db3f06739c26f693839044b85e855 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 11 Jul 2016 14:35:36 -0700 Subject: [PATCH 1/9] Update minimum ruby version to 2.2.2. See https://github.com/ruby-rdf/rdf/issues/307. --- .travis.yml | 12 +++++----- Gemfile | 1 + README.md | 2 +- examples/bpotw.csv | 1 + examples/bpotw.json | 53 +++++++++++++++++++++++++++++++++++++++++++++ rdf-tabular.gemspec | 2 +- 6 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 examples/bpotw.csv create mode 100644 examples/bpotw.json diff --git a/.travis.yml b/.travis.yml index 469c4c0..ee457e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,15 +4,13 @@ script: "bundle exec rspec spec" env: - CI=true rvm: - - 2.0 - - 2.1 - - 2.2.4 - - 2.3.0 - - jruby-9.0.4.0 - - rbx-2 + - 2.2.5 + - 2.3.1 + - jruby-9.0.5.0 + - rbx cache: bundler sudo: false matrix: allow_failures: - - rvm: rbx-2 + - rvm: rbx diff --git a/Gemfile b/Gemfile index b15024b..404b828 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ gem 'rdf', github: "ruby-rdf/rdf", branch: "develop" gem 'rdf-xsd', github: "ruby-rdf/rdf-xsd", branch: "develop" group :development do + gem 'linkeddata', github: "ruby-rdf/linkeddata", branch: "develop" gem 'ebnf', github: "gkellogg/ebnf", branch: "develop" gem 'json-ld', github: "ruby-rdf/json-ld", branch: "develop" gem 'rdf-aggregate-repo', github: "ruby-rdf/rdf-aggregate-repo", branch: "develop" diff --git a/README.md b/README.md index aa53d12..ebe87b1 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,7 @@ Full documentation available on [RubyDoc](http://rubydoc.info/gems/rdf-tabular/f * {RDF::Tabular::Reader} ## Dependencies -* [Ruby](http://ruby-lang.org/) (>= 2.0) +* [Ruby](http://ruby-lang.org/) (>= 2.2.2) * [RDF.rb](http://rubygems.org/gems/rdf) (>= 2.0) * [JSON](https://rubygems.org/gems/json) (>= 1.5) diff --git a/examples/bpotw.csv b/examples/bpotw.csv new file mode 100644 index 0000000..acadc54 --- /dev/null +++ b/examples/bpotw.csv @@ -0,0 +1 @@ +Identifier,Name,Description,Latitude,Longitude,ZONE,URL \ No newline at end of file diff --git a/examples/bpotw.json b/examples/bpotw.json new file mode 100644 index 0000000..ef5628c --- /dev/null +++ b/examples/bpotw.json @@ -0,0 +1,53 @@ +{ + "@context": ["http://www.w3.org/ns/csvw", {"@language": "en"}], + "url": "bpotw.csv", + "dc:title": "CSV distribution of bus-stops-2015-05-05 dataset", + "dcat:keyword": ["bus", "stop", "mobility"], + "dc:publisher": { + "schema:name": "Transport Agency of MyCity", + "schema:url": {"@id": "http://example.org"} + }, + "dc:license": {"@id": "http://opendefinition.org/licenses/cc-by/"}, + "dc:modified": {"@value": "2015-05-05", "@type": "xsd:date"}, + "tableSchema": { + "columns": [{ + "name": "stop_id", + "titles": ["Identifier"], + "dc:description": "An identifier for the bus stop.", + "datatype": "string", + "required": true + }, { + "name": "stop_name", + "titles": ["Name"], + "dc:description": "The name of the bus stop.", + "datatype": "string" + }, { + "name": "stop_desc", + "titles": ["Description"], + "dc:description": "A description for the bus stop.", + "datatype": "string" + }, { + "name": "stop_lat", + "titles": ["Latitude"], + "dc:description": "The latitude of the bus stop.", + "datatype": "number" + }, { + "name": "stop_long", + "titles": ["Longitude"], + "dc:description": "The longitude of the bus stop.", + "datatype": "number" + }, { + "name": "zone_id", + "titles": ["ZONE"], + "dc:description": "An identifier for the zone where the bus stop is located.", + "datatype": "string" + }, + { + "name": "stop_url", + "titles": ["URL"], + "dc:description": "URL that identifies the bus stop.", + "datatype": "string" + }], + "primaryKey": "stop_id" + } +} diff --git a/rdf-tabular.gemspec b/rdf-tabular.gemspec index 544a043..da1ed4f 100755 --- a/rdf-tabular.gemspec +++ b/rdf-tabular.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |gem| gem.test_files = Dir.glob('spec/**') gem.has_rdoc = false - gem.required_ruby_version = '>= 2.0' + gem.required_ruby_version = '>= 2.2.2' gem.requirements = [] gem.add_runtime_dependency 'bcp47', '~> 0.3', '>= 0.3.3' gem.add_runtime_dependency 'rdf', '~> 2.0' From b7d9a1e3f71fb23df2ab6fdb5808cb22b58ecf5b Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Thu, 14 Jul 2016 13:37:47 -0700 Subject: [PATCH 2/9] Allow jruby fail due to multi_json issues. --- .travis.yml | 3 ++- Gemfile | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ee457e0..8ea9bd0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,12 @@ env: rvm: - 2.2.5 - 2.3.1 - - jruby-9.0.5.0 + - jruby-9.1.2.0 - rbx cache: bundler sudo: false matrix: allow_failures: - rvm: rbx + - rvm: jruby-9.1.2.0 diff --git a/Gemfile b/Gemfile index 404b828..0651c4d 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,8 @@ gem 'rdf-xsd', github: "ruby-rdf/rdf-xsd", branch: "develop" group :development do gem 'linkeddata', github: "ruby-rdf/linkeddata", branch: "develop" gem 'ebnf', github: "gkellogg/ebnf", branch: "develop" - gem 'json-ld', github: "ruby-rdf/json-ld", branch: "develop" + #gem 'json-ld', github: "ruby-rdf/json-ld", branch: "develop" + gem 'json-ld', path: '../json-ld' gem 'rdf-aggregate-repo', github: "ruby-rdf/rdf-aggregate-repo", branch: "develop" gem 'rdf-isomorphic', github: "ruby-rdf/rdf-isomorphic", branch: "develop" gem "rdf-spec", github: "ruby-rdf/rdf-spec", branch: "develop" From e8537297ea0f7324a7576040f4426358db9af290 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Thu, 28 Jul 2016 11:56:11 -0400 Subject: [PATCH 3/9] Update sxp repo reference in Gemfile. --- Gemfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 0651c4d..46ed6b3 100644 --- a/Gemfile +++ b/Gemfile @@ -7,8 +7,7 @@ gem 'rdf-xsd', github: "ruby-rdf/rdf-xsd", branch: "develop" group :development do gem 'linkeddata', github: "ruby-rdf/linkeddata", branch: "develop" gem 'ebnf', github: "gkellogg/ebnf", branch: "develop" - #gem 'json-ld', github: "ruby-rdf/json-ld", branch: "develop" - gem 'json-ld', path: '../json-ld' + gem 'json-ld', github: "ruby-rdf/json-ld", branch: "develop" gem 'rdf-aggregate-repo', github: "ruby-rdf/rdf-aggregate-repo", branch: "develop" gem 'rdf-isomorphic', github: "ruby-rdf/rdf-isomorphic", branch: "develop" gem "rdf-spec", github: "ruby-rdf/rdf-spec", branch: "develop" @@ -16,7 +15,7 @@ group :development do gem 'rdf-vocab', github: "ruby-rdf/rdf-vocab", branch: "develop" gem 'sparql', github: "ruby-rdf/sparql", branch: "develop" gem 'sparql-client', github: "ruby-rdf/sparql-client", branch: "develop" - gem 'sxp', github: "gkellogg/sxp-ruby", branch: "develop" + gem 'sxp', github: "dryruby/sxp.rb", branch: "develop" end group :debug do From 3df159492b3f58cd309672cb9d8218eaebad5c67 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Fri, 12 Aug 2016 18:15:19 -0700 Subject: [PATCH 4/9] Remove wirble from Gemfile, as dependency-ci objects that it has no license and it's not really neccessary. --- Gemfile | 1 - examples/niklas.csv | 2 ++ examples/niklas.json | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 examples/niklas.csv create mode 100644 examples/niklas.json diff --git a/Gemfile b/Gemfile index 46ed6b3..50ec860 100644 --- a/Gemfile +++ b/Gemfile @@ -19,7 +19,6 @@ group :development do end group :debug do - gem "wirble" gem "byebug", platforms: :mri end diff --git a/examples/niklas.csv b/examples/niklas.csv new file mode 100644 index 0000000..abec62e --- /dev/null +++ b/examples/niklas.csv @@ -0,0 +1,2 @@ +role +"aut,li" diff --git a/examples/niklas.json b/examples/niklas.json new file mode 100644 index 0000000..2ad3b5f --- /dev/null +++ b/examples/niklas.json @@ -0,0 +1,11 @@ +{ + "@context": "http://www.w3.org/ns/csvw", + "url": "niklas.csv", + "tableSchema": { + "columns": [{ + "name": "role", + "separator": ",", + "valueUrl": "http://id.loc.gov/vocabulary/relators/{/role*}" + }] + } +} From 344c8ff9786d6f522a86213efc572c8cef01c9d5 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 13 Aug 2016 15:22:13 -0700 Subject: [PATCH 5/9] Change Travis JRuby to default and allow failures. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8ea9bd0..36a642d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,12 @@ env: rvm: - 2.2.5 - 2.3.1 - - jruby-9.1.2.0 + - jruby - rbx cache: bundler sudo: false matrix: allow_failures: - rvm: rbx - - rvm: jruby-9.1.2.0 + - rvm: jruby From ba237fa09fbe30d9bc9b6d2dbd749705d450dadf Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 10 Sep 2016 14:36:30 -0700 Subject: [PATCH 6/9] Add priorities to content types. --- dependencyci.yml | 5 +++++ lib/rdf/tabular/format.rb | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 dependencyci.yml diff --git a/dependencyci.yml b/dependencyci.yml new file mode 100644 index 0000000..0c67c1b --- /dev/null +++ b/dependencyci.yml @@ -0,0 +1,5 @@ +platform: + Rubygems: + rdf-isomorphic: + tests: + unmaintained: skip \ No newline at end of file diff --git a/lib/rdf/tabular/format.rb b/lib/rdf/tabular/format.rb index 7125da8..8a67c2e 100644 --- a/lib/rdf/tabular/format.rb +++ b/lib/rdf/tabular/format.rb @@ -24,10 +24,10 @@ module RDF::Tabular # # @see http://www.w3.org/TR/rdf-testcases/#ntriples class Format < RDF::Format - content_type 'text/csv', + content_type 'text/csv;q=0.4', extensions: [:csv, :tsv], alias: %w{ - text/tab-separated-values + text/tab-separated-values;q=0.4 application/csvm+json } content_encoding 'utf-8' From c8890fed79fa597255f0da100c604a1ee942af19 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sun, 9 Oct 2016 14:55:37 -0700 Subject: [PATCH 7/9] Refactor UAX35 bits into the UAX35 module. Add more year formats "y" and "yy". --- .gitignore | 1 + lib/rdf/tabular/metadata.rb | 33 ++--- lib/rdf/tabular/uax35.rb | 181 +++++++++++++++++++++------ spec/metadata_spec.rb | 16 +-- spec/suite_spec.rb | 2 +- spec/uax35_spec.rb | 239 ++++++++++++++++++++++++++++++++++++ 6 files changed, 399 insertions(+), 73 deletions(-) create mode 100644 spec/uax35_spec.rb diff --git a/.gitignore b/.gitignore index 570c242..50d4322 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /coverage/ Gemfile.lock /.byebug_history +/spec/w3c-csvw/ diff --git a/lib/rdf/tabular/metadata.rb b/lib/rdf/tabular/metadata.rb index 7d89897..87a3962 100644 --- a/lib/rdf/tabular/metadata.rb +++ b/lib/rdf/tabular/metadata.rb @@ -2171,33 +2171,13 @@ def value_matching_datatype(value, datatype, expanded_dt, language) decimalChar = format["decimalChar"] || '.' pattern = format["pattern"] - if !datatype.parse_uax35_number(pattern, value, groupChar || ",", decimalChar) + begin + value = datatype.parse_uax35_number(pattern, value, groupChar || ",", decimalChar) + rescue UAX35::ParseError value_errors << "#{value} does not match numeric pattern #{pattern ? pattern.inspect : 'default'}" end - # pattern facet failed - value_errors << "#{value} has repeating #{groupChar.inspect}" if groupChar && value.include?(groupChar*2) - value = value.gsub(groupChar || ',', '') - value = value.sub(decimalChar, '.') - - # Extract percent or per-mille sign - percent = permille = false - case value - when /%/ - value = value.sub('%', '') - percent = true - when /‰/ - value = value.sub('‰', '') - permille = true - end - lit = RDF::Literal(value, datatype: expanded_dt) - if percent || permille - o = lit.object - o = o / 100 if percent - o = o / 1000 if permille - lit = RDF::Literal(o, datatype: expanded_dt) - end if !lit.plain? && datatype.minimum && lit < datatype.minimum value_errors << "#{value} < minimum #{datatype.minimum}" @@ -2238,10 +2218,11 @@ def value_matching_datatype(value, datatype, expanded_dt, language) end end when :date, :time, :dateTime, :dateTimeStamp, :datetime - if value = datatype.parse_uax35_date(format, value) + begin + value = datatype.parse_uax35_date(format, value) lit = RDF::Literal(value, datatype: expanded_dt) - else - value_errors << "#{original_value} does not match format #{format}" + rescue UAX35::ParseError + value_errors << "#{value} does not match format #{format}" end when :duration, :dayTimeDuration, :yearMonthDuration # SPEC CONFUSION: surely format also includes that for other duration types? diff --git a/lib/rdf/tabular/uax35.rb b/lib/rdf/tabular/uax35.rb index 411a25f..d835167 100644 --- a/lib/rdf/tabular/uax35.rb +++ b/lib/rdf/tabular/uax35.rb @@ -7,50 +7,99 @@ module RDF::Tabular module UAX35 ## - # Parse the date format (if provided), and match against the value (if provided) - # Otherwise, validate format and raise an error + # Parse the date pattern (if provided), and match against the value (if provided) + # Otherwise, validate pattern and raise an error. # - # @param [String] format + # Supported patterns are: + # + # * yyyy-MM-dd + # * yyyyMMdd + # * dd-MM-yyyy + # * d-M-yyyy + # * d-M-yy + # * d-M-y + # * MM-dd-yyyy + # * M-d-yyyy + # * M-d-yy + # * M-d-y + # * dd/MM/yyyy + # * d/M/yyyy + # * d/M/yy + # * d/M/y + # * MM/dd/yyyy + # * M/d/yyyy + # * M/d/yy + # * M/d/y + # * dd.MM.yyyy + # * d.M.yyyy + # * d.M.yy + # * d.M.y + # * MM.dd.yyyy + # * M.d.yyyy + # * M.d.yy + # * M.d.y + # * yyyy-MM-ddTHH:mm + # * yyyy-MM-ddTHH:mm:ss + # * yyyy-MM-ddTHH:mm:ss.S+ + # + # Year comonents less than four digits are normalized to 1900 or 2000 based on if the value is <= 99 or >= 70, it is considered to be in the 1900 range, otherwise, based on 2000. + # + # @param [String] pattern # @param [String] value # @return [String] XMLSchema version of value - # @raise [ArgumentError] if format is not valid, or nil, if value does not match - def parse_uax35_date(format, value) - date_format, time_format = nil, nil - return value unless format - value ||= "" + # @raise [ArgumentError] if pattern is not valid, or nil + # @raise [ParseError] if value does not match + def parse_uax35_date(pattern, value) + date_pattern, time_pattern = nil, nil + return value unless pattern + orig_value = value ||= "" + orig_pattern = pattern # Extract tz info - if md = format.match(/^(.*[dyms])+(\s*[xX]+)$/) - format, tz_format = md[1], md[2] + if md = pattern.match(/^(.*[dyms])+(\s*[xX]+)$/) + pattern, tz_pattern = md[1], md[2] end - date_format, time_format = format.split(' ') - date_format, time_format = nil, date_format if self.base.to_sym == :time + date_pattern, time_pattern = pattern.split(' ') + # Snuff out if this is a Time pattern + date_pattern, time_pattern = nil, date_pattern if time_pattern.nil? && !date_pattern.match(/[TyMd]/) # Extract date, of specified - date_part = case date_format + date_part = case date_pattern when 'yyyy-MM-dd' then value.match(/^(?\d{4})-(?\d{2})-(?\d{2})/) when 'yyyyMMdd' then value.match(/^(?\d{4})(?\d{2})(?\d{2})/) when 'dd-MM-yyyy' then value.match(/^(?\d{2})-(?\d{2})-(?\d{4})/) when 'd-M-yyyy' then value.match(/^(?\d{1,2})-(?\d{1,2})-(?\d{4})/) + when 'd-M-yy' then value.match(/^(?\d{1,2})-(?\d{1,2})-(?\d{2})/) + when 'd-M-y' then value.match(/^(?\d{1,2})-(?\d{1,2})-(?\d{1,4})/) when 'MM-dd-yyyy' then value.match(/^(?\d{2})-(?\d{2})-(?\d{4})/) when 'M-d-yyyy' then value.match(/^(?\d{1,2})-(?\d{1,2})-(?\d{4})/) - when 'dd/MM/yyyy' then value.match(/^(?\d{2})\/(?\d{2})\/(?\d{4})/) + when 'M-d-yy' then value.match(/^(?\d{1,2})-(?\d{1,2})-(?\d{2})/) + when 'M-d-y' then value.match(/^(?\d{1,2})-(?\d{1,2})-(?\d{1,4})/) + when 'dd/MM/yyyy' then value.match(/^(?\d{2})\/(?\d{2})\/(?\d{1,4})/) when 'd/M/yyyy' then value.match(/^(?\d{1,2})\/(?\d{1,2})\/(?\d{4})/) - when 'MM/dd/yyyy' then value.match(/^(?\d{2})\/(?\d{2})\/(?\d{4})/) + when 'd/M/yy' then value.match(/^(?\d{1,2})\/(?\d{1,2})\/(?\d{2})/) + when 'd/M/y' then value.match(/^(?\d{1,2})\/(?\d{1,2})\/(?\d{1,4})/) + when 'MM/dd/yyyy' then value.match(/^(?\d{2})\/(?\d{2})\/(?\d{1,4})/) when 'M/d/yyyy' then value.match(/^(?\d{1,2})\/(?\d{1,2})\/(?\d{4})/) + when 'M/d/yy' then value.match(/^(?\d{1,2})\/(?\d{1,2})\/(?\d{2})/) + when 'M/d/y' then value.match(/^(?\d{1,2})\/(?\d{1,2})\/(?\d{1,4})/) when 'dd.MM.yyyy' then value.match(/^(?\d{2})\.(?\d{2})\.(?\d{4})/) when 'd.M.yyyy' then value.match(/^(?\d{1,2})\.(?\d{1,2})\.(?\d{4})/) + when 'd.M.yy' then value.match(/^(?\d{1,2})\.(?\d{1,2})\.(?\d{2})/) + when 'd.M.y' then value.match(/^(?\d{1,2})\.(?\d{1,2})\.(?\d{1,4})/) when 'MM.dd.yyyy' then value.match(/^(?\d{2})\.(?\d{2})\.(?\d{4})/) when 'M.d.yyyy' then value.match(/^(?\d{1,2})\.(?\d{1,2})\.(?\d{4})/) + when 'M.d.yy' then value.match(/^(?\d{1,2})\.(?\d{1,2})\.(?\d{2})/) + when 'M.d.y' then value.match(/^(?\d{1,2})\.(?\d{1,2})\.(?\d{1,4})/) when 'yyyy-MM-ddTHH:mm' then value.match(/^(?\d{4})-(?\d{2})-(?\d{2})T(?
\d{2}):(?\d{2})(?(?))/) when 'yyyy-MM-ddTHH:mm:ss' then value.match(/^(?\d{4})-(?\d{2})-(?\d{2})T(?
\d{2}):(?\d{2}):(?\d{2})(?)/) when /yyyy-MM-ddTHH:mm:ss\.S+/ md = value.match(/^(?\d{4})-(?\d{2})-(?\d{2})T(?
\d{2}):(?\d{2}):(?\d{2})\.(?\d+)/) - num_ms = date_format.match(/S+/).to_s.length + num_ms = date_pattern.match(/S+/).to_s.length md if md && md[:ms].length <= num_ms else - raise ArgumentError, "unrecognized date/time format #{date_format}" if date_format + raise ArgumentError, "unrecognized date/time pattern #{date_pattern}" if date_pattern nil end @@ -61,25 +110,25 @@ def parse_uax35_date(format, value) end # Extract time, of specified - time_part = case time_format + time_part = case time_pattern when 'HH:mm:ss' then value.match(/^(?
\d{2}):(?\d{2}):(?\d{2})(?)/) when 'HHmmss' then value.match(/^(?
\d{2})(?\d{2})(?\d{2})(?)/) when 'HH:mm' then value.match(/^(?
\d{2}):(?\d{2})(?)(?)/) when 'HHmm' then value.match(/^(?
\d{2})(?\d{2})(?)(?)/) when /HH:mm:ss\.S+/ md = value.match(/^(?
\d{2}):(?\d{2}):(?\d{2})\.(?\d+)/) - num_ms = time_format.match(/S+/).to_s.length + num_ms = time_pattern.match(/S+/).to_s.length md if md && md[:ms].length <= num_ms else - raise ArgumentError, "unrecognized date/time format #{time_format}" if time_format + raise ArgumentError, "unrecognized date/time pattern #{pattern}" if time_pattern nil end - # If there's a date_format but no date_part, match fails - return nil if date_format && date_part.nil? + # If there's a date_pattern but no date_part, match fails + raise ParseError, "#{orig_value} does not match pattern #{orig_pattern}" if !orig_value.empty? && date_pattern && date_part.nil? - # If there's a time_format but no time_part, match fails - return nil if time_format && time_part.nil? + # If there's a time_pattern but no time_part, match fails + raise ParseError, "#{orig_value} does not match pattern #{orig_pattern}" if !orig_value.empty? && time_pattern && time_part.nil? # Forward past time part value = value[time_part.to_s.length..-1] if time_part @@ -88,8 +137,8 @@ def parse_uax35_date(format, value) time_part = date_part if date_part && date_part.names.include?("hr") # If there's a timezone, it may optionally start with whitespace - value = value.lstrip if tz_format.to_s.start_with?(' ') - tz_part = case tz_format.to_s.lstrip + value = value.lstrip if tz_pattern.to_s.start_with?(' ') + tz_part = case tz_pattern.to_s.lstrip when 'x' then value.match(/^(?:(?
[+-]\d{2})(?\d{2})?)$/) when 'X' then value.match(/^(?:(?:(?
[+-]\d{2})(?\d{2})?)|(?Z))$/) when 'xx' then value.match(/^(?:(?
[+-]\d{2})(?\d{2}))|$/) @@ -97,15 +146,30 @@ def parse_uax35_date(format, value) when 'xxx' then value.match(/^(?:(?
[+-]\d{2}):(?\d{2}))$/) when 'XXX' then value.match(/^(?:(?:(?
[+-]\d{2}):(?\d{2}))|(?Z))$/) else - raise ArgumentError, "unrecognized timezone format #{tz_format.to_s.lstrip}" if tz_format + raise ArgumentError, "unrecognized timezone pattern #{tz_pattern.to_s.lstrip}" if tz_pattern nil end - # If there's a tz_format but no time_part, match fails - return nil if tz_format && tz_part.nil? + # If there's a tz_pattern but no time_part, match fails + raise ParseError, "#{orig_value} does not match pattern #{orig_pattern}" if !orig_value.empty? && tz_pattern && tz_part.nil? # Compose normalized value - vd = ("%04d-%02d-%02d" % [date_part[:yr].to_i, date_part[:mo].to_i, date_part[:da].to_i]) if date_part + vd = if date_part + yr, mo, da = [date_part[:yr], date_part[:mo], date_part[:da]].map(&:to_i) + + if date_part[:yr].length < 4 + # Make sure that yr makes sense, if given + yr = case yr + when 0..69 then yr + 2000 + when 100..999 then yr + 2000 + when 70..99 then yr + 1900 + else yr + end + end + + ("%04d-%02d-%02d" % [yr, mo, da]) + end + vt = ("%02d:%02d:%02d" % [time_part[:hr].to_i, time_part[:mi].to_i, time_part[:se].to_i]) if time_part # Add milliseconds, if matched @@ -117,37 +181,74 @@ def parse_uax35_date(format, value) end ## - # Parse the date format (if provided), and match against the value (if provided) - # Otherwise, validate format and raise an error + # Parse the date pattern (if provided), and match against the value (if provided) + # Otherwise, validate pattern and raise an error # # @param [String] pattern # @param [String] value # @param [String] groupChar # @param [String] decimalChar # @return [String] XMLSchema version of value or nil, if value does not match - # @raise [ArgumentError] if format is not valid + # @raise [ArgumentError] if pattern is not valid def parse_uax35_number(pattern, value, groupChar=",", decimalChar=".") value ||= "" re = build_number_re(pattern, groupChar, decimalChar) + raise ParseError, "#{value} has repeating #{groupChar.inspect}" if groupChar.length == 1 && value.include?(groupChar*2) + # Upcase value and remove internal spaces value = value.upcase if value =~ re - # Upcase value and remove internal spaces value = value. - upcase. gsub(/\s+/, ''). gsub(groupChar, ''). gsub(decimalChar, '.') # result re-assembles parts removed from value value - else + elsif !value.empty? # no match - nil + raise ParseError, "#{value.inspect} does not match #{pattern.inspect}" + end + + # Extract percent or per-mille sign + case value + when /%/ + value = value.sub('%', '') + lhs, rhs = value.split('.') + + # Shift decimal + value = case lhs.length + when 0 then "0.00#{rhs}".sub('E', 'e') + when 1 then "0.0#{lhs}#{rhs}".sub('E', 'e') + when 2 then "0.#{lhs}#{rhs}".sub('E', 'e') + else + ll, lr = lhs[0..lhs.length-3], lhs[-2..-1] + ll = ll + "0" unless ll =~ /\d+/ + "#{ll}.#{lr}#{rhs}".sub('E', 'e') + end + when /‰/ + value = value.sub('‰', '') + lhs, rhs = value.split('.') + + # Shift decimal + value = case lhs.length + when 0 then "0.000#{rhs}".sub('E', 'e') + when 1 then "0.00#{lhs}#{rhs}".sub('E', 'e') + when 2 then "0.0#{lhs}#{rhs}".sub('E', 'e') + when 3 then "0.#{lhs}#{rhs}".sub('E', 'e') + else + ll, lr = lhs[0..lhs.length-4], lhs[-3..-1] + ll = ll + "0" unless ll =~ /\d+/ + "#{ll}.#{lr}#{rhs}".sub('E', 'e') + end + when /NAN/ then value.sub('NAN', 'NaN') + when /E/ then value.sub('E', 'e') + else + value end end @@ -157,9 +258,10 @@ def parse_uax35_number(pattern, value, groupChar=",", decimalChar=".") # @param [String] groupChar # @param [String] decimalChar # @return [Regexp] Regular expression matching value - # @raise [ArgumentError] if format is not valid + # @raise [ArgumentError] if pattern is not valid def build_number_re(pattern, groupChar, decimalChar) # pattern must be composed of only 0, #, decimalChar, groupChar, E, %, and ‰ + ge = Regexp.escape groupChar de = Regexp.escape decimalChar @@ -320,5 +422,8 @@ def build_number_re(pattern, groupChar, decimalChar) Regexp.new("^(?#{prefix})(?#{integer_str}#{fractional_str}#{exponent_str})(?#{suffix})$") end + + # ParseError is raised when a value does not match the pattern + class ParseError < RuntimeError; end end end diff --git a/spec/metadata_spec.rb b/spec/metadata_spec.rb index ad3e036..8fed82e 100644 --- a/spec/metadata_spec.rb +++ b/spec/metadata_spec.rb @@ -1136,7 +1136,7 @@ format: {"groupChar" => ";"}, value: "123;;456.789", result: "123;;456.789", - errors: [/has repeating/] + errors: [/does not match numeric pattern/] }, "decimal with explicit decimalChar" => { base: "decimal", @@ -1184,19 +1184,19 @@ "invalid nonPositiveInteger" => {base: "nonPositiveInteger", value: "1", errors: ["1 is not a valid nonPositiveInteger"]}, "valid nonNegativeInteger" => {base: "nonNegativeInteger", value: "0"}, "invalid nonNegativeInteger" => {base: "nonNegativeInteger", value: "-1", errors: ["-1 is not a valid nonNegativeInteger"]}, - "valid double" => {base: "double", value: "1234.456E789"}, + "valid double" => {base: "double", value: "1234.456e789"}, "invalid double" => {base: "double", value: "1z", errors: ["1z is not a valid double"]}, - "NaN double" => {base: "double", value: "NaN"}, + "NaN double" => {base: "double", value: "NaN", result: "NaN"}, "INF double" => {base: "double", value: "INF"}, "-INF double" => {base: "double", value: "-INF"}, - "valid number" => {base: "number", value: "1234.456E789"}, + "valid number" => {base: "number", value: "1234.456e789"}, "invalid number" => {base: "number", value: "1z", errors: ["1z is not a valid number"]}, - "NaN number" => {base: "number", value: "NaN"}, + "NaN number" => {base: "number", value: "NaN", result: "NaN"}, "INF number" => {base: "number", value: "INF"}, "-INF number" => {base: "number", value: "-INF"}, - "valid float" => {base: "float", value: "1234.456E7"}, + "valid float" => {base: "float", value: "1234.456e7"}, "invalid float" => {base: "float", value: "1z", errors: ["1z is not a valid float"]}, - "NaN float" => {base: "float", value: "NaN"}, + "NaN float" => {base: "float", value: "NaN", result: "NaN"}, "INF float" => {base: "float", value: "INF"}, "-INF float" => {base: "float", value: "-INF"}, @@ -1327,7 +1327,7 @@ "valid NMTOKEN" => {base: "NMTOKEN", value: "someThing", result: RDF::Literal("someThing", datatype: RDF::XSD.NMTOKEN)}, # Aliases - "number is alias for double" => {base: "number", value: "1234.456E789", result: RDF::Literal("1234.456E789", datatype: RDF::XSD.double)}, + "number is alias for double" => {base: "number", value: "1234.456e789", result: RDF::Literal("1234.456e789", datatype: RDF::XSD.double)}, "binary is alias for base64Binary" => {base: "binary", value: "Tm93IGlzIHRoZSB0aW1lIGZvciBhbGwgZ29vZCBjb2RlcnMKdG8gbGVhcm4g", result: RDF::Literal("Tm93IGlzIHRoZSB0aW1lIGZvciBhbGwgZ29vZCBjb2RlcnMKdG8gbGVhcm4g", datatype: RDF::XSD.base64Binary)}, "datetime is alias for dateTime" => {base: "dateTime", value: "15-3-2015 1502", format: "d-M-yyyy HHmm", result: RDF::Literal("2015-03-15T15:02:00", datatype: RDF::XSD.dateTime)}, "any is alias for anyAtomicType" => {base: "any", value: "some thing", result: RDF::Literal("some thing", datatype: RDF::XSD.anyAtomicType)}, diff --git a/spec/suite_spec.rb b/spec/suite_spec.rb index fe06e0c..e39f1f4 100644 --- a/spec/suite_spec.rb +++ b/spec/suite_spec.rb @@ -18,7 +18,7 @@ m.entries.each do |t| next if t.approval =~ /Rejected/ specify "#{t.id.split("/").last}: #{t.name} - #{t.comment}" do - pending "rdf#test158 should be isomorphic" if t.id.include?("rdf#test158") + pending "rdf#test283 literal normalization" if t.id.include?("rdf#test283") t.logger = RDF::Spec.logger t.logger.formatter = lambda {|severity, datetime, progname, msg| "#{severity}: #{msg}\n"} t.logger.info t.inspect diff --git a/spec/uax35_spec.rb b/spec/uax35_spec.rb new file mode 100644 index 0000000..7527768 --- /dev/null +++ b/spec/uax35_spec.rb @@ -0,0 +1,239 @@ +# encoding: UTF-8 +$:.unshift "." +require 'spec_helper' + +describe RDF::Tabular::UAX35 do + subject {"".extend(RDF::Tabular::UAX35)} + + describe "parse_uax35_date" do + { + # Dates + "valid date yyyy-MM-dd" => {value: "2015-03-22", pattern: "yyyy-MM-dd", result: "2015-03-22"}, + "valid date yyyyMMdd" => {value: "20150322", pattern: "yyyyMMdd", result: "2015-03-22"}, + "valid date dd-MM-yyyy" => {value: "22-03-2015", pattern: "dd-MM-yyyy", result: "2015-03-22"}, + "valid date d-M-yyyy" => {value: "22-3-2015", pattern: "d-M-yyyy", result: "2015-03-22"}, + "valid date d-M-yy" => {value: "22-3-15", pattern: "d-M-yy", result: "2015-03-22"}, + "valid date d-M-y" => {value: "22-3-15", pattern: "d-M-y", result: "2015-03-22"}, + "valid date MM-dd-yyyy" => {value: "03-22-2015", pattern: "MM-dd-yyyy", result: "2015-03-22"}, + "valid date M-d-yyyy" => {value: "3-22-2015", pattern: "M-d-yyyy", result: "2015-03-22"}, + "valid date M-d-yy" => {value: "3-22-70", pattern: "M-d-yy", result: "1970-03-22"}, + "valid date M-d-y" => {value: "3-22-70", pattern: "M-d-y", result: "1970-03-22"}, + "valid date dd/MM/yyyy" => {value: "22/03/2015", pattern: "dd/MM/yyyy", result: "2015-03-22"}, + "valid date d/M/yyyy" => {value: "22/3/2015", pattern: "d/M/yyyy", result: "2015-03-22"}, + "valid date d/M/yy" => {value: "22/3/15", pattern: "d/M/yy", result: "2015-03-22"}, + "valid date d/M/y" => {value: "22/3/15", pattern: "d/M/y", result: "2015-03-22"}, + "valid date MM/dd/yyyy" => {value: "03/22/2015", pattern: "MM/dd/yyyy", result: "2015-03-22"}, + "valid date M/d/yyyy" => {value: "3/22/2015", pattern: "M/d/yyyy", result: "2015-03-22"}, + "valid date M/d/yy" => {value: "3/22/15", pattern: "M/d/yy", result: "2015-03-22"}, + "valid date M/d/y" => {value: "3/22/15", pattern: "M/d/y", result: "2015-03-22"}, + "valid date dd.MM.yyyy" => {value: "22.03.2015", pattern: "dd.MM.yyyy", result: "2015-03-22"}, + "valid date d.M.yyyy" => {value: "22.3.2015", pattern: "d.M.yyyy", result: "2015-03-22"}, + "valid date d.M.yy" => {value: "22.3.15", pattern: "d.M.yy", result: "2015-03-22"}, + "valid date d.M.y" => {value: "22.3.15", pattern: "d.M.y", result: "2015-03-22"}, + "valid date MM.dd.yyyy" => {value: "03.22.2015", pattern: "MM.dd.yyyy", result: "2015-03-22"}, + "valid date M.d.yyyy" => {value: "3.22.2015", pattern: "M.d.yyyy", result: "2015-03-22"}, + "valid date M.d.yy" => {value: "3.22.15", pattern: "M.d.yy", result: "2015-03-22"}, + "valid date M.d.y" => {value: "3.22.15", pattern: "M.d.y", result: "2015-03-22"}, + + # Times + "valid time HH:mm:ss.S" => {value: "15:02:37.1", pattern: "HH:mm:ss.S", result: "15:02:37.1"}, + "valid time HH:mm:ss" => {value: "15:02:37", pattern: "HH:mm:ss", result: "15:02:37"}, + "valid time HHmmss" => {value: "150237", pattern: "HHmmss", result: "15:02:37"}, + "valid time HH:mm" => {value: "15:02", pattern: "HH:mm", result: "15:02:00"}, + "valid time HHmm" => {value: "1502", pattern: "HHmm", result: "15:02:00"}, + + # DateTimes + "valid dateTime yyyy-MM-ddTHH:mm:ss" => {value: "2015-03-15T15:02:37", pattern: "yyyy-MM-ddTHH:mm:ss", result: "2015-03-15T15:02:37"}, + "valid dateTime yyyy-MM-ddTHH:mm:ss.S"=> {value: "2015-03-15T15:02:37.1", pattern: "yyyy-MM-ddTHH:mm:ss.S", result: "2015-03-15T15:02:37.1"}, + "valid dateTime yyyy-MM-dd HH:mm:ss" => {value: "2015-03-15 15:02:37", pattern: "yyyy-MM-dd HH:mm:ss", result: "2015-03-15T15:02:37"}, + "valid dateTime yyyyMMdd HHmmss" => {value: "20150315 150237", pattern: "yyyyMMdd HHmmss", result: "2015-03-15T15:02:37"}, + "valid dateTime dd-MM-yyyy HH:mm" => {value: "15-03-2015 15:02", pattern: "dd-MM-yyyy HH:mm", result: "2015-03-15T15:02:00"}, + "valid dateTime d-M-yyyy HHmm" => {value: "15-3-2015 1502", pattern: "d-M-yyyy HHmm", result: "2015-03-15T15:02:00"}, + "valid dateTime yyyy-MM-ddTHH:mm" => {value: "2015-03-15T15:02", pattern: "yyyy-MM-ddTHH:mm", result: "2015-03-15T15:02:00"}, + "valid dateTimeStamp d-M-yyyy HHmm X" => {value: "15-3-2015 1502 Z", pattern: "d-M-yyyy HHmm X", result: "2015-03-15T15:02:00Z"}, + "valid datetime yyyy-MM-ddTHH:mm:ss" => {value: "2015-03-15T15:02:37", pattern: "yyyy-MM-ddTHH:mm:ss", result: "2015-03-15T15:02:37"}, + "valid datetime yyyy-MM-dd HH:mm:ss" => {value: "2015-03-15 15:02:37", pattern: "yyyy-MM-dd HH:mm:ss", result: "2015-03-15T15:02:37"}, + "valid datetime yyyyMMdd HHmmss" => {value: "20150315 150237", pattern: "yyyyMMdd HHmmss", result: "2015-03-15T15:02:37"}, + "valid datetime dd-MM-yyyy HH:mm" => {value: "15-03-2015 15:02", pattern: "dd-MM-yyyy HH:mm", result: "2015-03-15T15:02:00"}, + "valid datetime d-M-yyyy HHmm" => {value: "15-3-2015 1502", pattern: "d-M-yyyy HHmm", result: "2015-03-15T15:02:00"}, + "valid datetime yyyy-MM-ddTHH:mm" => {value: "2015-03-15T15:02", pattern: "yyyy-MM-ddTHH:mm", result: "2015-03-15T15:02:00"}, + + # Timezones + "valid w/TZ yyyy-MM-ddX" => {value: "2015-03-22Z", pattern: "yyyy-MM-ddX", result: "2015-03-22Z"}, + "valid w/TZ HH:mm:ssX" => {value: "15:02:37-05", pattern: "HH:mm:ssX", result: "15:02:37-05:00"}, + "valid w/TZ yyyy-MM-dd HH:mm:ss X" => {value: "2015-03-15 15:02:37 +0800", pattern: "yyyy-MM-dd HH:mm:ss X", result: "2015-03-15T15:02:37+08:00"}, + "valid w/TZ HHmm XX" => {value: "1502 +0800", pattern: "HHmm XX", result: "15:02:00+08:00"}, + "valid w/TZ yyyy-MM-dd HH:mm:ss XX" => {value: "2015-03-15 15:02:37 -0800", pattern: "yyyy-MM-dd HH:mm:ss XX", result: "2015-03-15T15:02:37-08:00"}, + "valid w/TZ HHmm XXX" => {value: "1502 +08:00", pattern: "HHmm XXX", result: "15:02:00+08:00"}, + "valid w/TZ yyyy-MM-ddTHH:mm:ssXXX" => {value: "2015-03-15T15:02:37-05:00", pattern: "yyyy-MM-ddTHH:mm:ssXXX", result: "2015-03-15T15:02:37-05:00"}, + "invalid w/TZ HH:mm:ssX" => {value: "15:02:37-05:00", pattern: "HH:mm:ssX", error: "15:02:37-05:00 does not match pattern HH:mm:ssX"}, + "invalid w/TZ HH:mm:ssXX" => {value: "15:02:37-05", pattern: "HH:mm:ssXX", error: "15:02:37-05 does not match pattern HH:mm:ssXX"}, + }.each do |name, props| + context name do + let(:base) {props[:base]} + let(:pattern) {props[:pattern]} + let(:value) {props[:value]} + let(:result) {props.fetch(:result, value)} + if props[:error] + it "finds error" do + expect {subject.parse_uax35_date(pattern, value)}.to raise_error(RDF::Tabular::UAX35::ParseError, props[:error]) + end + else + it "generates #{props[:result] || props[:value]}" do + expect(subject.parse_uax35_date(pattern, value)).to eql result + end + end + end + end + end + + describe "parse_uax35_number" do + { + # Numbers + "default no constraints" => {valid: %w(4)}, + "default matching pattern" => {pattern: "000", valid: %w(123)}, + "default explicit groupChar" => {groupChar: ";", valid: {"123;456.789" => "123456.789"}}, + "default repeated groupChar" => {groupChar: ";", invalid: %w(123;;456.789)}, + "default explicit decimalChar" => {decimalChar: ";", valid: {"123456;789" => "123456.789"}}, + "default percent" => {groupChar: ",", valid: {"123456.789%" => "1234.56789"}}, + "default per-mille" => {groupChar: ",", valid: {"123456.789‰" => "123.456789"}}, + + "0" => {pattern: "0", valid: %w(1 -1 12), invalid: %w(1.2)}, + "00" => {pattern: "00", valid: %w(12 123), invalid: %w(1 1,2)}, + "#" => {pattern: "#", valid: %w(1 12 123), invalid: %w(1.2)}, + "##" => {pattern: "##", valid: %w(1 12 123), invalid: %w(1.2)}, + "#0" => {pattern: "#0", valid: %w(1 12 123), invalid: %w(1.2)}, + '0.0' => {pattern: "0.0", valid: %w(1.1 -1.1 12.1), invalid: %w(1.12)}, + '0.00' => {pattern: '0.00', valid: %w(1.12 +1.12 12.12), invalid: %w(1.1 1.123)}, + '0.#' => {pattern: '0.#', valid: %w(1 1.1 12.1), invalid: %w(1.12)}, + '-0' => {pattern: '-0', valid: %w(-1 -10), invalid: %w(1 +1)}, + '%000' => {pattern: '%000', valid: {"%123" => "1.23", "%+123" => "+1.23", "%1234" => "12.34"}, invalid: %w(%12 123%)}, + '‰000' => {pattern: '‰000', valid: {"‰123" => "0.123", "‰+123" => "+0.123", "‰1234" => "1.234"}, invalid: %w(‰12 123‰)}, + '000%' => {pattern: '000%', valid: {"123%" => "1.23", "+123%" => "+1.23", "1234%" => "12.34"}, invalid: %w(12% %123)}, + '000‰' => {pattern: '000‰', valid: {"123‰" => "0.123", "+123‰" => "+0.123", "1234‰" => "1.234"}, invalid: %w(12‰ ‰123)}, + '000.0%' => {pattern: '000.0%', valid: {"123.4%" => "1.234", "+123.4%" => "+1.234"}, invalid: %w(123.4‰ 123.4 1.234% 12.34% 123.45%)}, + + '###0.#####' => {pattern: '###0.#####', valid: %w(1 1.1 12345.12345), invalid: %w(1,234.1 1.123456)}, + '###0.0000#' => {pattern: '###0.0000#', valid: %w(1.1234 1.12345 12345.12345), invalid: %w(1,234.1234 1.12)}, + '00000.0000' => {pattern: '00000.0000', valid: %w(12345.1234), invalid: %w(1.2 1,234.123,4)}, + + '#0.0#E#0' => {pattern: '#0.0#E#0', valid: %w(1.2e3 12.34e56)}, + '#0.0#E+#0' => {pattern: '#0.0#E+#0', valid: %w(1.2e+3 12.34e+56), invalid: %w(1.2e3 12.34e56)}, + '#0.0#E#0%' => {pattern: '#0.0#E#0%', valid: {"1.2e3%" => "0.012e3", "12.34e56%" => "0.1234e56"}, invalid: %w(1.2e+3 12.34e+56 1.2e3 12.34e56)}, + + # Grouping + '#,##,##0' => {pattern: '#,##,##0', valid: {"1" => "1", "12" => "12", "123" => "123", "1,234" => "1234", "12,345" => "12345", "1,23,456" => "123456"}, invalid: %w(1,2 12,34)}, + '#,##,#00' => {pattern: '#,##,#00', valid: {"12" => "12", "123" => "123", "1,234" => "1234", "12,345" => "12345"}, invalid: %w(1)}, + '#,##,000' => {pattern: '#,##,000', valid: {"123" => "123", "1,234" => "1234", "12,345" => "12345"}, invalid: %w(1 12)}, + '#,#0,000' => {pattern: '#,#0,000', valid: {"1,234" => "1234", "12,345" => "12345"}, invalid: %w(1 12 123)}, + '#,00,000' => {pattern: '#,00,000', valid: {"12,345" => "12345"}, invalid: %w(1 12 123 1,234)}, + '0,00,000' => {pattern: '0,00,000'}, + + '0.0##,###' => {pattern: '0.0##,###'}, + '0.00#,###' => {pattern: '0.00#,###'}, + '0.000,###' => {pattern: '0.000,###'}, + '0.000,0##' => {pattern: '0.000,0##'}, + '0.000,00#' => {pattern: '0.000,00#'}, + '0.000,000' => {pattern: '0.000,000'}, + + # Jeni's + '##0' => {pattern: '##0', valid: %w(1 12 123 1234), invalid: %w(1,234 123.4)}, + '#,#00' => {pattern: '#,#00', valid: {"12" => "12", "123" => "123", "1,234" => "1234", "1,234,567" => "1234567"}, invalid: %w(1 1234 12,34 12,34,567)}, + '#0.#' => {pattern: '#0.#', valid: %w(1 1.2 1234.5), invalid: %w(12.34 1,234.5)}, + '#0.0#,#' => {pattern: '#0.0#,#', valid: {"12.3" => "12.3", "12.34" => "12.34", "12.34,5" => "12.345"}, invalid: %w(1 12.345 12.34,56,7 12.34,567)}, + }.each do |name, props| + context name do + let(:pattern) {props[:pattern]} + let(:groupChar) {props.fetch(:groupChar, ',')} + let(:decimalChar) {props.fetch(:decimalChar, '.')} + + describe "valid" do + case props[:valid] + when Hash + props[:valid].each do |value, result| + it "with #{props[:pattern].inspect} #{value.inspect} => #{result.inspect}" do + expect(subject.parse_uax35_number(pattern, value, groupChar, decimalChar)).to eql result + end + end + when Array + props[:valid].each do |value| + it "with #{props[:pattern].inspect} #{value.inspect} => #{value.inspect}" do + expect(subject.parse_uax35_number(pattern, value, groupChar, decimalChar)).to eql value + end + end + end + end + describe "invalid" do + Array(props[:invalid]).each do |value| + it "with #{props[:pattern].inspect} #{value.inspect} invalid" do + expect {subject.parse_uax35_number(pattern, value, groupChar, decimalChar)}.to raise_error RDF::Tabular::UAX35::ParseError + end + end + end + + it "recognizes bad pattern #{pattern.inspect}" do + expect{subject.parse_uax35_number(pattern, "", groupChar, decimalChar)}.to raise_error(ArgumentError) + end if props[:exception] + end + end + end + + describe "#build_number_re" do + { + '0' => {valid: %w(1 -1 +1 12), invalid: %w(1.2), base: "integer", re: /^(?[+-]?)(?\d{1,})(?)$/}, + '00' => {valid: %w(12 123), invalid: %w(1 1,2), base: "integer", re: /^(?[+-]?)(?\d{2,})(?)$/}, + '#' => {valid: %w(1 12 123), invalid: %w(1.2), base: "integer", re: /^(?[+-]?)(?\d{0,})(?)$/}, + '##' => {re: /^(?[+-]?)(?\d{0,})(?)$/}, + '#0' => {re: /^(?[+-]?)(?\d{1,})(?)$/}, + + '0.0' => {valid: %w(1.1 -1.1 12.1), invalid: %w(1.12), base: "decimal", re: /^(?[+-]?)(?\d{1,}\.\d{1})(?)$/}, + '0.00' => {valid: %w(1.12 +1.12 12.12), invalid: %w(1.1 1.123), base: "decimal", re: /^(?[+-]?)(?\d{1,}\.\d{2})(?)$/}, + '0.#' => {valid: %w(1 1.1 12.1), invalid: %w(1.12), base: "decimal", re: /^(?[+-]?)(?\d{1,}(?:\.\d{0,1})?)(?)$/}, + '-0' => {valid: %w(-1 -10), invalid: %w(1 +1), base: "decimal", re: /^(?\-)(?\d{1,})(?)$/}, + '%000' => {valid: %w(%123 %+123 %-123 %1234), invalid: %w(%12 123%), base: "decimal", re: /^(?%[+-]?)(?\d{3,})(?)$/}, + '‰000' => {valid: %w(‰123 ‰+123 ‰-123 ‰1234), invalid: %w(‰12 123‰), base: "decimal", re: /^(?‰[+-]?)(?\d{3,})(?)$/}, + '000%' => {valid: %w(123% +123% -123% 1234%), invalid: %w(12% %123), base: "decimal", re: /^(?[+-]?)(?\d{3,})(?%)$/}, + '000‰' => {valid: %w(123‰ +123‰ -123‰ 1234‰), invalid: %w(12‰ ‰123), base: "decimal", re: /^(?[+-]?)(?\d{3,})(?‰)$/}, + '000.0%' => {base: "decimal", re: /^(?[+-]?)(?\d{3,}\.\d{1})(?%)$/}, + + '###0.#####' => {valid: %w(1 1.1 12345.12345), invalid: %w(1,234.1 1.123456), base: "decimal", re: /^(?[+-]?)(?\d{1,}(?:\.\d{0,5})?)(?)$/}, + '###0.0000#' => {valid: %w(1.1234 1.12345 12345.12345), invalid: %w(1,234.1234 1.12), base: "decimal", re: /^(?[+-]?)(?\d{1,}\.\d{4,5})(?)$/}, + '00000.0000' => {valid: %w(12345.1234), invalid: %w(1.2 1,234.123,4), base: "decimal", re: /^(?[+-]?)(?\d{5,}\.\d{4})(?)$/}, + + '#0.0#E#0' => {base: "double", re: /^(?[+-]?)(?\d{1,}\.\d{1,2}E[+-]?\d{1,2})(?)$/}, + '#0.0#E+#0' => {base: "double", re: /^(?[+-]?)(?\d{1,}\.\d{1,2}E\+\d{1,2})(?)$/}, + '#0.0#E#0%' => {base: "double", re: /^(?[+-]?)(?\d{1,}\.\d{1,2}E[+-]?\d{1,2})(?%)$/}, + + # Grouping + '#,##,##0' => {base: "integer", re: /^(?[+-]?)(?(?:(?:(?:\d{1,2},)?(?:\d{2},)*\d)?\d)?\d{1})(?)$/}, + '#,##,#00' => {base: "integer", re: /^(?[+-]?)(?(?:(?:\d{1,2},)?(?:\d{2},)*\d)?\d{2})(?)$/}, + '#,##,000' => {base: "integer", re: /^(?[+-]?)(?(?:\d{1,2},)?(?:\d{2},)*\d{3})(?)$/}, + '#,#0,000' => {base: "integer", re: /^(?[+-]?)(?(?:(?:\d{1,2},)?(?:\d{2},)*\d)?\d{1},\d{3})(?)$/}, + '#,00,000' => {base: "integer", re: /^(?[+-]?)(?(?:\d{1,2},)?(?:\d{2},)*\d{2},\d{3})(?)$/}, + '0,00,000' => {base: "integer", re: /^(?[+-]?)(?(?:(?:\d{1,2},)?(?:\d{2},)*\d)?\d{1},\d{2},\d{3})(?)$/}, + + '0.0##,###' => {base: "decimal", re: /^(?[+-]?)(?\d{1,}\.\d{1}(?:\d(?:\d(?:,\d(?:\d(?:\d)?)?)?)?)?)(?)$/}, + '0.00#,###' => {base: "decimal", re: /^(?[+-]?)(?\d{1,}\.\d{2}(?:\d(?:,\d(?:\d(?:\d)?)?)?)?)(?)$/}, + '0.000,###' => {base: "decimal", re: /^(?[+-]?)(?\d{1,}\.\d{3}(?:,\d(?:\d(?:\d)?)?)?)(?)$/}, + '0.000,0##' => {base: "decimal", re:/^(?[+-]?)(?\d{1,}\.\d{3},\d{1}(?:\d(?:\d)?)?)(?)$/}, + '0.000,00#' => {base: "decimal", re: /^(?[+-]?)(?\d{1,}\.\d{3},\d{2}(?:\d)?)(?)$/}, + '0.000,000' => {base: "decimal", re: /^(?[+-]?)(?\d{1,}\.\d{3},\d{3})(?)$/}, + + # Jeni's + '##0' => {valid: %w(1 12 123 1234), invalid: %w(1,234 123.4), base: "integer", re: /^(?[+-]?)(?\d{1,})(?)$/}, + '#,#00' => {valid: %w(12 123 1,234 1,234,567), invalid: %w(1 1234 12,34 12,34,567), base: "integer", re: /^(?[+-]?)(?(?:(?:\d{1,3},)?(?:\d{3},)*\d)?\d{2})(?)$/}, + '#0.#' => {valid: %w(1 1.2 1234.5), invalid: %w(12.34 1,234.5), base: "decimal", re: /^(?[+-]?)(?\d{1,}(?:\.\d{0,1})?)(?)$/}, + '#0.0#,#' => {valid: %w(12.3 12.34 12.34,5), invalid: %w(1 12.345 12.34,56,7 12.34,567), base: "decimal", re: /^(?[+-]?)(?\d{1,}\.\d{1}(?:\d(?:,\d)?)?)(?)$/}, + }.each do |pattern, props| + context pattern do + it "generates #{props[:re]} for #{pattern}" do + expect(subject.build_number_re(pattern, ",", ".")).to eql props[:re] + end if props[:re].is_a?(Regexp) + + it "recognizes bad pattern #{pattern}" do + expect{subject.build_number_re(pattern, ",", ".")}.to raise_error(ArgumentError) + end if props[:re] == ArgumentError + end + end + end +end From bcafa6d14cdc691a7d299dc258446cb52a5f31a1 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Fri, 30 Dec 2016 17:17:10 -0800 Subject: [PATCH 8/9] Update webmock ~> 2.3. Fix use in format_spec. --- .travis.yml | 5 +++-- rdf-tabular.gemspec | 2 +- spec/format_spec.rb | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 36a642d..fa303e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,9 @@ script: "bundle exec rspec spec" env: - CI=true rvm: - - 2.2.5 - - 2.3.1 + - 2.2.6 + - 2.3.3 + - 2.4.0 - jruby - rbx cache: bundler diff --git a/rdf-tabular.gemspec b/rdf-tabular.gemspec index da1ed4f..6b3e4d6 100755 --- a/rdf-tabular.gemspec +++ b/rdf-tabular.gemspec @@ -36,7 +36,7 @@ Gem::Specification.new do |gem| gem.add_development_dependency 'rdf-spec', '~> 2.0' gem.add_development_dependency 'rdf-turtle', '~> 2.0' gem.add_development_dependency 'sparql', '~> 2.0' - gem.add_development_dependency 'webmock', '~> 1.17' + gem.add_development_dependency 'webmock', '~> 2.3' gem.add_development_dependency 'yard' , '~> 0.8' gem.post_install_message = nil diff --git a/spec/format_spec.rb b/spec/format_spec.rb index 245c746..10b682e 100644 --- a/spec/format_spec.rb +++ b/spec/format_spec.rb @@ -54,13 +54,14 @@ end }) end - after(:each) {|example| puts logger.to_s if example.exception} require 'rdf/cli' - let(:input) {File.expand_path("../data/countries.json", __FILE__)} + let(:input) {"http://example.org/data/countries.json"} describe "#tabular-json" do it "serializes to JSON" do - expect {RDF::CLI.exec(["tabular-json", input], format: :tabular)}.to write.to(:output) + expect { + RDF::CLI.exec(["tabular-json", input], format: :tabular) + }.to write.to(:output) end end end From e9d598c75fe7cbea152a18e7cbac5f98f1938130 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 31 Dec 2016 13:29:27 -0800 Subject: [PATCH 9/9] Version 1.0.0. --- VERSION | 2 +- rdf-tabular.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 1d0ba9e..3eefcb9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.0 +1.0.0 diff --git a/rdf-tabular.gemspec b/rdf-tabular.gemspec index 6b3e4d6..def33eb 100755 --- a/rdf-tabular.gemspec +++ b/rdf-tabular.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |gem| gem.required_ruby_version = '>= 2.2.2' gem.requirements = [] gem.add_runtime_dependency 'bcp47', '~> 0.3', '>= 0.3.3' - gem.add_runtime_dependency 'rdf', '~> 2.0' + gem.add_runtime_dependency 'rdf', '~> 2.1' gem.add_runtime_dependency 'rdf-vocab', '~> 2.0' gem.add_runtime_dependency 'rdf-xsd', '~> 2.0' gem.add_runtime_dependency 'json-ld', '~> 2.0'