From cc7c455095d8c76410f3a53e3a7dbab76f5856da Mon Sep 17 00:00:00 2001 From: Erick Guan <297343+erickguan@users.noreply.github.com> Date: Sat, 2 Mar 2024 01:50:42 +0100 Subject: [PATCH 01/10] Update document files - Use simpler syntax for README - Update LICENSE date - Set up changelog --- .document | 3 ++- CHANGELOG.md | 13 ++++++++++ LICENSE | 2 +- README.md | 67 ++++++++++++++-------------------------------------- Rakefile | 1 - 5 files changed, 34 insertions(+), 52 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.document b/.document index ecf3673..f2fe4ab 100644 --- a/.document +++ b/.document @@ -1,4 +1,5 @@ -README.rdoc +# Documentation listing for RDoc +README.md lib/**/*.rb bin/* features/**/*.feature diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..39a01a6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +## [Unreleased](https://github.com/erickguan/ffi-icu/compare/v0.5.3...master) ## + +### Added + + +### Changed + + +### Fixed + + +### Removed + diff --git a/LICENSE b/LICENSE index 50cfbae..6a8b89d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010-2015 Jari Bakken +Copyright (c) 2010-2024 Jari Bakken Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 72ff61a..ac23c28 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,20 @@ -ffi-icu [![Build Status](https://app.travis-ci.com/erickguan/ffi-icu.svg?branch=master)](https://app.travis-ci.com/erickguan/ffi-icu) -======= +# ffi-icu -Simple FFI wrappers for ICU. Checkout the renovated [ICU gem](https://github.com/fantasticfears/icu4r) instead which supports various of encoding and distributed with packaged source. FFI-ICU needs some love with ICU gem's transcoding method. +Simple FFI wrappers for [International Components for Unicode (ICU)][icu]. -Gem ---- +## Gem [Rubygem](http://rubygems.org/gems/ffi-icu "ffi-icu") - gem install ffi-icu +``` +gem install ffi-icu +``` -Dependencies ------------- +## Dependencies ICU. -If you get messages that the library or functions are not found, you can -set some environment variables to tell ffi-icu where to find it, e.g.: +If you get messages that the library or functions are not found, you can set some environment variables to tell ffi-icu where to find it, e.g.: ```sh $ export FFI_ICU_LIB="icui18n.so" @@ -24,11 +22,9 @@ $ export FFI_ICU_VERSION_SUFFIX="_3_8" $ ruby -r ffi-icu program.rb ``` -Features -======== +# Features -Character Encoding Detection ----------------------------- +## Character Encoding Detection Examples: @@ -49,8 +45,7 @@ Why not just use rchardet? * speed -Locale Sensitive Collation --------------------------- +## Locale Sensitive Collation Examples: @@ -67,8 +62,7 @@ collator.greater?("z", "a") #=> true collator.collate(%w[å æ ø]) #=> ["æ", "ø", "å"] ``` -Text Boundary Analysis ----------------------- +## Text Boundary Analysis Examples: @@ -78,8 +72,7 @@ iterator.text = "This is a sentence." iterator.to_a #=> [0, 4, 5, 7, 8, 9, 10, 18, 19] ``` -Number/Currency Formatting --------------------------- +## Number/Currency Formatting Examples: @@ -99,8 +92,7 @@ curf = ICU::NumberFormatting.create('en-US', :currency) curf.format(1234.56, 'USD') #=> "$1,234.56" ``` -Time Formatting/Parsing ------------------------ +## Time Formatting/Parsing Examples: @@ -130,8 +122,7 @@ formatter = ICU::TimeFormatting.create(:locale => 'cs_CZ', :date => :pattern, :t formatter.format(Time.now) #=> "2015" ``` -Duration Formatting -------------------- +## Duration Formatting ```ruby # What the various styles look like @@ -187,8 +178,7 @@ formatter = ICU::DurationFormatting::DurationFormatter.new(locale: 'ru', style: formatter.format({hours: 10, minutes: 20, seconds: 30}) #=> "10 ч 20 мин 30 с" ``` -Transliteration ---------------- +## Transliteration Example: @@ -196,8 +186,7 @@ Example: ICU::Transliteration.transliterate('Traditional-Simplified', '沈從文') # => "沈从文" ``` -Locale ------- +## Locale Examples: @@ -210,24 +199,4 @@ locale.display_name_with_context('en-US', [:length_short]) #=> "English (US)" locale.display_name_with_context('en-US', [:length_long]) #=> "English (United States)" ``` -TODO: -===== - -* Any other useful part of ICU? -* Windows?! - -Note on Patches/Pull Requests -============================= - -* Fork the project. -* Make your feature addition or bug fix. -* Add tests for it. This is important so I don't break it in a - future version unintentionally. -* Commit, do not mess with rakefile, version, or history. - (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) -* Send me a pull request. Bonus points for topic branches. - -Copyright -========= - -Copyright (c) 2010-2015 Jari Bakken. See LICENSE for details. +[icu]: https://github.com/unicode-org/icu diff --git a/Rakefile b/Rakefile index e7c25d2..75a55ac 100644 --- a/Rakefile +++ b/Rakefile @@ -25,4 +25,3 @@ rescue LoadError abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard" end end - From 8852b8c65e5584ee8e1c37afddcbb96d70b91be7 Mon Sep 17 00:00:00 2001 From: Erick Guan <297343+erickguan@users.noreply.github.com> Date: Sat, 2 Mar 2024 01:50:42 +0100 Subject: [PATCH 02/10] Add editorconfig --- .editorconfig | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..df5f542 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# Top-level setting to indicate this is the root .editorconfig file +root = true + +# Default settings for all files +[*] +# Enforce LF (\n) as the line ending across all files for consistency +end_of_line = lf +# Ensure every file ends with a newline +insert_final_newline = true +charset = utf-8 +# Use spaces for indentation (as opposed to tabs) +indent_style = space +# Set the width of a single indentation level to 2 spaces +indent_size = 2 +trim_trailing_whitespace = true + +# Specific settings for Markdown files +[*.md] +# Disable trimming of trailing whitespace in Markdown files, +# important for Markdown syntax where trailing spaces can be meaningful +trim_trailing_whitespace = false From 4a77a8ca30eee70b6d133c02265ffbd447754c30 Mon Sep 17 00:00:00 2001 From: Erick Guan <297343+erickguan@users.noreply.github.com> Date: Sat, 2 Mar 2024 01:50:42 +0100 Subject: [PATCH 03/10] Modernize gemspec --- ffi-icu.gemspec | 52 +++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/ffi-icu.gemspec b/ffi-icu.gemspec index e965567..9d25151 100644 --- a/ffi-icu.gemspec +++ b/ffi-icu.gemspec @@ -1,26 +1,36 @@ -require File.expand_path("../lib/ffi-icu/version", __FILE__) +require_relative "lib/ffi-icu/version" -Gem::Specification.new do |s| - s.name = %q{ffi-icu} - s.version = ICU::VERSION +Gem::Specification.new do |spec| + spec.name = "ffi-icu" + spec.version = ICU::VERSION + spec.platform = Gem::Platform::RUBY # rely on FFI library, but being platform-independent - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Jari Bakken"] - s.date = %q{2019-10-15} - s.licenses = ['MIT'] - s.description = %q{Provides charset detection, locale sensitive collation and more. Depends on libicu.} - s.email = %q{jari.bakken@gmail.com} - s.extra_rdoc_files = ["LICENSE", "README.md"] - s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } - s.require_paths = ["lib"] + spec.required_rubygems_version = Gem::Requirement.new(">= 2.5.0") + spec.authors = ["Jari Bakken"] + spec.date = "2019-10-15" + spec.licenses = ["MIT"] + spec.summary = "Simple Ruby FFI wrappers for International Components for Unicode (ICU)." + spec.description = "Provides charset detection, locale sensitive collation and more. Depends on libicu." + spec.email = "jari.bakken@gmail.com" + spec.homepage = "https://github.com/erickguan/ffi-icu" - s.homepage = %q{http://github.com/jarib/ffi-icu} - s.rdoc_options = ["--charset=UTF-8"] - s.summary = %q{Simple Ruby FFI wrappers for things I need from ICU.} + spec.metadata["source_code_uri"] = spec.homepage + spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" - s.add_runtime_dependency "ffi", "~> 1.0", ">= 1.0.9" - s.add_development_dependency 'rspec', '~> 3.9' - s.add_development_dependency "rake", [">= 12.3.3"] + spec.files = `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{^(test|spec|features)/}) + end + spec.bindir = "bin" + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.extra_rdoc_files = ["LICENSE", "README.md"] + spec.rdoc_options = ["--charset=UTF-8"] + + spec.add_runtime_dependency "ffi", "~> 1.0", ">= 1.0.9" + spec.add_runtime_dependency "bigdecimal", "~> 3.1" + + spec.add_development_dependency "rspec", "~> 3.9" + spec.add_development_dependency "rake", ">= 12.3.3" + spec.add_development_dependency "rubocop", "~> 1.60" end From c16afe82b8c9a11284afbd7ab24332c5c8373cf2 Mon Sep 17 00:00:00 2001 From: Erick Guan <297343+erickguan@users.noreply.github.com> Date: Sat, 2 Mar 2024 01:50:42 +0100 Subject: [PATCH 04/10] Recommends a ruby version for development --- .ruby-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .ruby-version diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..b347b11 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.2.3 From 2922514503c6424b92392eac8a1dfd296dfa6ce0 Mon Sep 17 00:00:00 2001 From: Erick Guan <297343+erickguan@users.noreply.github.com> Date: Sat, 2 Mar 2024 01:50:42 +0100 Subject: [PATCH 05/10] Configure rubocops for linting --- .rubocop.yml | 166 ++++++++++++++++++++++++++++++++++++++++++++++++ ffi-icu.gemspec | 14 ++-- 2 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 .rubocop.yml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..644d04e --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,166 @@ +# merge the default 'Exclude' key with ours +inherit_mode: + merge: + - Exclude + +require: + - rubocop-rspec + - rubocop-minitest + - rubocop-packaging + +AllCops: + NewCops: enable + StyleGuideBaseURL: https://rubystyle.guide + TargetRubyVersion: 3.0 + + Exclude: + - bin/**/* + - tmp/**/* + +Bundler/OrderedGems: + Include: + - '*.gemspec' + +# Documentation +Style/Documentation: + Enabled: false + +# Allow more flexibility with string literals +Style/StringLiterals: + Enabled: true + EnforcedStyle: single_quotes + +# Enforce dots on the next line for multi-line method calls +Layout/DotPosition: + EnforcedStyle: trailing + +Layout/LineLength: + Max: 120 + +# Enforce comma after the last item of a multiline array or hash +Style/TrailingCommaInArrayLiteral: + Enabled: true + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInHashLiteral: + Enabled: true + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInArguments: + Enabled: true + EnforcedStyleForMultiline: comma + +Layout/ArgumentAlignment: + Enabled: true + EnforcedStyle: with_fixed_indentation + +Layout/FirstArrayElementIndentation: + Enabled: true + EnforcedStyle: consistent + +Layout/MultilineMethodCallIndentation: + Enabled: true + EnforcedStyle: indented + +Style/WordArray: + # difficult to figure out source text + EnforcedStyle: brackets + +# Encourage the use of method call with args parentheses +Style/MethodCallWithArgsParentheses: + Enabled: true + EnforcedStyle: require_parentheses + +# Allow guard clauses +Style/GuardClause: + Enabled: true + +Style/FrozenStringLiteralComment: + Enabled: false + +Layout/EmptyLineAfterMagicComment: + Enabled: true + +# Exclude test files from BlockComments check +Style/BlockComments: + Exclude: + - 'test/**/*' + +# Naming conventions +Naming/PredicateName: + Enabled: true + +Metrics/ClassLength: + Max: 500 + +Metrics/BlockLength: + Max: 50 + + Exclude: + # allows longer block for RSpec + - spec/**/*.rb + +Metrics/AbcSize: + Max: 110 + +Metrics/BlockNesting: + Max: 5 + +Metrics/CyclomaticComplexity: + Max: 40 + +Metrics/MethodLength: + Max: 100 + +Metrics/ModuleLength: + Max: 250 + Exclude: + # allows longer block for RSpec + - spec/**/*.rb + - lib/ffi-icu/lib.rb + +Metrics/PerceivedComplexity: + Max: 50 + +RSpec/ExampleLength: + Max: 20 + +RSpec/MultipleExpectations: + Max: 10 + +RSpec/NestedGroups: + Max: 5 + +RSpec/FilePath: + Enabled: false + +RSpec/SpecFilePathFormat: + Enabled: false + +RSpec/ContextWording: + Enabled: false + +RSpec/NamedSubject: + Enabled: false + +RSpec/MessageSpies: + Enabled: false + +RSpec/NoExpectationExample: + Enabled: false + +RSpec/MultipleDescribes: + Enabled: false + +RSpec/DescribeClass: + Enabled: false + +RSpec/PredicateMatcher: + Enabled: false + +Naming/MethodParameterName: + Enabled: false + +Naming/FileName: + Exclude: + - lib/ffi-icu.rb diff --git a/ffi-icu.gemspec b/ffi-icu.gemspec index 9d25151..11f20b6 100644 --- a/ffi-icu.gemspec +++ b/ffi-icu.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |spec| spec.metadata["source_code_uri"] = spec.homepage spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" - spec.files = `git ls-files -z`.split("\x0").reject do |f| + spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) end spec.bindir = "bin" @@ -25,12 +25,16 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.extra_rdoc_files = ["LICENSE", "README.md"] - spec.rdoc_options = ["--charset=UTF-8"] + spec.rdoc_options = ["--charset=UTF-8"] spec.add_runtime_dependency "ffi", "~> 1.0", ">= 1.0.9" spec.add_runtime_dependency "bigdecimal", "~> 3.1" - spec.add_development_dependency "rspec", "~> 3.9" - spec.add_development_dependency "rake", ">= 12.3.3" - spec.add_development_dependency "rubocop", "~> 1.60" + spec.add_development_dependency('rake', '>= 12.3.3') + spec.add_development_dependency('rspec', '~> 3.9') + spec.add_development_dependency('rubocop', '~> 1.60') + spec.add_development_dependency('rubocop-rspec', '~> 2.27') + spec.add_development_dependency('rubocop-minitest', '~> 0.34.5') + spec.add_development_dependency('rubocop-packaging', '~> 0.5.2') + spec.metadata['rubygems_mfa_required'] = 'true' end From 95f4d54f7f0b0ac2651fe12933070a89212deab8 Mon Sep 17 00:00:00 2001 From: Erick Guan <297343+erickguan@users.noreply.github.com> Date: Sat, 2 Mar 2024 01:50:42 +0100 Subject: [PATCH 06/10] Set up linting and tests workflow --- .github/workflows/linting.yml | 30 +++++++++++++++++++++ .github/workflows/tests.yml | 43 ++++++++++++++++++++++++++++++ .rubocop.yml | 50 +++++++++-------------------------- .yamllint | 35 ++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/linting.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .yamllint diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 0000000..681f8a4 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,30 @@ +name: Linting + +on: + pull_request: + push: + branches: + - master + +permissions: + contents: read + +jobs: + linting: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Lint YAML + run: yamllint . + + - name: Set up Ruby and install dependencies + uses: ruby/setup-ruby@v1 + with: + # uses .ruby-version implicitly + bundler-cache: true + + - name: Lint Ruby code + run: bundle exec rubocop diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..05ffd1b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,43 @@ +name: Tests + +on: + pull_request: + push: + branches: + - master + +permissions: + contents: read + +jobs: + tests: + strategy: + matrix: + # test a range of Ruby to ensure gem works + # keep ruby until EOL. Read more on https://endoflife.date/ruby + ruby-version: + - '3.0' + - '3.1' + - '3.2' + - '3.3' + - head + # test distributions up to 4 years + runner: + - ubuntu-22.04 + - ubuntu-20.04 + fail-fast: false # allow contributors understand failure builds + + runs-on: ${{ matrix.runner }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Ruby and install dependencies + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + + - name: Run tests + run: bundle exec rake spec diff --git a/.rubocop.yml b/.rubocop.yml index 644d04e..85c4676 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,6 +12,7 @@ AllCops: NewCops: enable StyleGuideBaseURL: https://rubystyle.guide TargetRubyVersion: 3.0 + SuggestExtensions: false # reduce noise. consider add rubocop-benchmark Exclude: - bin/**/* @@ -21,39 +22,27 @@ Bundler/OrderedGems: Include: - '*.gemspec' -# Documentation +# Allow no documentation. Style/Documentation: Enabled: false -# Allow more flexibility with string literals +# Enforce single quotes in the gem Style/StringLiterals: Enabled: true EnforcedStyle: single_quotes +Style/HashSyntax: + EnforcedStyle: no_mixed_keys # consistent hash syntax + EnforcedShorthandSyntax: consistent # enforce explicit hash syntax + # Enforce dots on the next line for multi-line method calls Layout/DotPosition: EnforcedStyle: trailing +# Project maximum code line length Layout/LineLength: Max: 120 -# Enforce comma after the last item of a multiline array or hash -Style/TrailingCommaInArrayLiteral: - Enabled: true - EnforcedStyleForMultiline: comma - -Style/TrailingCommaInHashLiteral: - Enabled: true - EnforcedStyleForMultiline: comma - -Style/TrailingCommaInArguments: - Enabled: true - EnforcedStyleForMultiline: comma - -Layout/ArgumentAlignment: - Enabled: true - EnforcedStyle: with_fixed_indentation - Layout/FirstArrayElementIndentation: Enabled: true EnforcedStyle: consistent @@ -62,40 +51,25 @@ Layout/MultilineMethodCallIndentation: Enabled: true EnforcedStyle: indented -Style/WordArray: - # difficult to figure out source text - EnforcedStyle: brackets +Style/SymbolArray: + EnforcedStyle: brackets # Prefer brackets -# Encourage the use of method call with args parentheses -Style/MethodCallWithArgsParentheses: - Enabled: true - EnforcedStyle: require_parentheses - -# Allow guard clauses -Style/GuardClause: - Enabled: true +Style/WordArray: + EnforcedStyle: brackets # Prefer brackets Style/FrozenStringLiteralComment: Enabled: false -Layout/EmptyLineAfterMagicComment: - Enabled: true - # Exclude test files from BlockComments check Style/BlockComments: Exclude: - 'test/**/*' -# Naming conventions -Naming/PredicateName: - Enabled: true - Metrics/ClassLength: Max: 500 Metrics/BlockLength: Max: 50 - Exclude: # allows longer block for RSpec - spec/**/*.rb diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..54c6342 --- /dev/null +++ b/.yamllint @@ -0,0 +1,35 @@ +extends: default + +locale: en_US.UTF-8 + +# ignored directories +ignore: | + .git/ + +rules: + octal-values: enable + + document-start: disable + document-end: disable + + # allow 120 characters in a line + line-length: + max: 120 + level: error + + indentation: + spaces: 2 + indent-sequences: true + + # allow one space indent + comments: + level: error + min-spaces-from-content: 1 + + # disallow boolean values to avoid surprise + truthy: + level: error + + # github workflows uses `on` as trigger + ignore: | + .github/workflows/*.yml From 058e994c54a05533b549004f310b2d8a9a75fd3d Mon Sep 17 00:00:00 2001 From: Erick Guan <297343+erickguan@users.noreply.github.com> Date: Sat, 2 Mar 2024 20:07:43 +0100 Subject: [PATCH 07/10] Remove travis config --- .travis.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ef17dd8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: ruby -os: linux -dist: focal - -arch: - - amd64 - - arm64 - -rvm: - - 2.7 - - 3.0 - - 3.1 - - ruby-head - - truffleruby - -before_script: - - sudo apt install -y icu-devtools g++ - - sudo chmod +x build_icu.sh - - sudo $PWD/build_icu.sh versions - - sudo $PWD/build_icu.sh install 71.1 - - export LD_LIBRARY_PATH=/usr/local/lib - - icuinfo - - yes | gem update --system --force - - gem install bundler - -jobs: - allow_failures: - - rvm: truffleruby From 2a3ff63779de0d3f940ba6433790515737cdd72f Mon Sep 17 00:00:00 2001 From: Erick Guan <297343+erickguan@users.noreply.github.com> Date: Sat, 2 Mar 2024 20:32:50 +0100 Subject: [PATCH 08/10] Update gitignore --- .gitignore | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 5461953..42290d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,10 @@ -## MAC OS +## macOS .DS_Store -## TEXTMATE -*.tmproj -tmtags - -## EMACS -*~ -\#* -.\#* - -## VIM -*.swp - -## PROJECT::GENERAL +## Project specific coverage rdoc pkg - -## PROJECT::SPECIFIC *.rbc Gemfile.lock +vendor/ From ef924be233805007e16781acece90c0e5de662fe Mon Sep 17 00:00:00 2001 From: Erick Guan <297343+erickguan@users.noreply.github.com> Date: Sat, 2 Mar 2024 19:29:14 +0100 Subject: [PATCH 09/10] Apply linting to source and test code --- Gemfile | 9 + Rakefile | 8 +- benchmark/detect.rb | 16 +- benchmark/shared.rb | 26 +- ffi-icu.gemspec | 52 ++- lib/ffi-icu.rb | 34 +- lib/ffi-icu/break_iterator.rb | 37 +- lib/ffi-icu/chardet.rb | 21 +- lib/ffi-icu/collation.rb | 107 +++--- lib/ffi-icu/core_ext/string.rb | 10 +- lib/ffi-icu/duration_formatting.rb | 560 +++++++++++++++-------------- lib/ffi-icu/lib.rb | 473 +++++++++++++----------- lib/ffi-icu/lib/util.rb | 14 +- lib/ffi-icu/locale.rb | 20 +- lib/ffi-icu/normalization.rb | 12 +- lib/ffi-icu/normalizer.rb | 15 +- lib/ffi-icu/number_formatting.rb | 68 ++-- lib/ffi-icu/time_formatting.rb | 209 ++++++----- lib/ffi-icu/transliteration.rb | 36 +- lib/ffi-icu/uchar.rb | 31 +- lib/ffi-icu/version.rb | 4 +- spec/break_iterator_spec.rb | 102 +++--- spec/chardet_spec.rb | 43 ++- spec/collation_spec.rb | 86 +++-- spec/duration_formatting_spec.rb | 280 +++++++-------- spec/lib/version_info_spec.rb | 8 +- spec/lib_spec.rb | 69 ++-- spec/locale_spec.rb | 219 +++++------ spec/normalization_spec.rb | 19 +- spec/normalizer_spec.rb | 74 ++-- spec/number_formatting_spec.rb | 91 ++--- spec/spec_helper.rb | 8 +- spec/time_spec.rb | 237 ++++++------ spec/transliteration_spec.rb | 37 +- spec/uchar_spec.rb | 24 +- 35 files changed, 1612 insertions(+), 1447 deletions(-) diff --git a/Gemfile b/Gemfile index fa75df1..3c8d089 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,12 @@ source 'https://rubygems.org' gemspec + +group :development, :test do + gem 'rake', '>= 12.3.3' + gem 'rspec', '~> 3.9' + gem 'rubocop', '~> 1.60' + gem 'rubocop-minitest', '~> 0.34.5' + gem 'rubocop-packaging', '~> 0.5.2' + gem 'rubocop-rspec', '~> 2.27' +end diff --git a/Rakefile b/Rakefile index 75a55ac..eafbf44 100644 --- a/Rakefile +++ b/Rakefile @@ -15,13 +15,13 @@ RSpec::Core::RakeTask.new(:rcov) do |spec| spec.rcov = true end -task :default => :spec +task default: :spec begin - require 'yard' + require('yard') YARD::Rake::YardocTask.new rescue LoadError - task :yardoc do - abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard" + task(:yardoc) do + abort('YARD is not available. In order to run yardoc, you must: sudo gem install yard') end end diff --git a/benchmark/detect.rb b/benchmark/detect.rb index fa87ef7..de95ae1 100644 --- a/benchmark/detect.rb +++ b/benchmark/detect.rb @@ -1,14 +1,12 @@ -# encoding: utf-8 +require 'benchmark' -require "benchmark" - -$LOAD_PATH.unshift "lib" -require "ffi-icu" -require "rchardet" +$LOAD_PATH.unshift('lib') +require 'ffi-icu' +require 'rchardet' TESTS = 1000 Benchmark.bmbm do |results| - results.report("rchardet:") { TESTS.times { CharDet.detect("æåø") } } - results.report("ffi-icu:") { TESTS.times { ICU::CharDet.detect("æåø") } } -end \ No newline at end of file + results.report('rchardet:') { TESTS.times { CharDet.detect('æåø') } } + results.report('ffi-icu:') { TESTS.times { ICU::CharDet.detect('æåø') } } +end diff --git a/benchmark/shared.rb b/benchmark/shared.rb index 942554d..14abd52 100644 --- a/benchmark/shared.rb +++ b/benchmark/shared.rb @@ -1,17 +1,21 @@ -# encoding: utf-8 +require 'benchmark' -require "benchmark" - -$LOAD_PATH.unshift "lib" -require "ffi-icu" -require "rchardet" +$LOAD_PATH.unshift('lib') +require 'ffi-icu' +require 'rchardet' TESTS = 1000 -$rchardet = CharDet::UniversalDetector.new -$icu = ICU::CharDet::Detector.new +rchardet = CharDet::UniversalDetector.new +icu = ICU::CharDet::Detector.new Benchmark.bmbm do |results| - results.report("rchardet instance:") { TESTS.times { $rchardet.reset; $rchardet.feed("æåø"); $rchardet.result } } - results.report("ffi-icu instance:") { TESTS.times { $icu.detect("æåø") } } -end \ No newline at end of file + results.report('rchardet instance:') do + TESTS.times do + rchardet.reset + rchardet.feed('æåø') + rchardet.result + end + end + results.report('ffi-icu instance:') { TESTS.times { icu.detect('æåø') } } +end diff --git a/ffi-icu.gemspec b/ffi-icu.gemspec index 11f20b6..7f53272 100644 --- a/ffi-icu.gemspec +++ b/ffi-icu.gemspec @@ -1,40 +1,34 @@ -require_relative "lib/ffi-icu/version" +require_relative 'lib/ffi-icu/version' Gem::Specification.new do |spec| - spec.name = "ffi-icu" + spec.name = 'ffi-icu' spec.version = ICU::VERSION spec.platform = Gem::Platform::RUBY # rely on FFI library, but being platform-independent - spec.required_rubygems_version = Gem::Requirement.new(">= 2.5.0") - spec.authors = ["Jari Bakken"] - spec.date = "2019-10-15" - spec.licenses = ["MIT"] - spec.summary = "Simple Ruby FFI wrappers for International Components for Unicode (ICU)." - spec.description = "Provides charset detection, locale sensitive collation and more. Depends on libicu." - spec.email = "jari.bakken@gmail.com" - spec.homepage = "https://github.com/erickguan/ffi-icu" - - spec.metadata["source_code_uri"] = spec.homepage - spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" - - spec.files = `git ls-files -z`.split("\x0").reject do |f| - f.match(%r{^(test|spec|features)/}) - end - spec.bindir = "bin" + spec.required_rubygems_version = Gem::Requirement.new('>= 2.5.0') + spec.authors = ['Jari Bakken'] + spec.licenses = ['MIT'] + spec.summary = 'Simple Ruby FFI wrappers for International Components for Unicode (ICU).' + spec.description = 'Provides charset detection, locale sensitive collation and more. Depends on libicu.' + spec.email = 'jari.bakken@gmail.com' + spec.homepage = 'https://github.com/erickguan/ffi-icu' + + spec.metadata['source_code_uri'] = spec.homepage + spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/master/CHANGELOG.md" + + spec.files = Dir['lib/**/*.rb', 'Gemfile', 'ffi-icu.gemspec', 'Rakefile'] + spec.bindir = 'bin' spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.require_paths = ["lib"] + spec.require_paths = ['lib'] + + spec.extra_rdoc_files = ['LICENSE', 'README.md'] + spec.rdoc_options = ['--charset=UTF-8'] - spec.extra_rdoc_files = ["LICENSE", "README.md"] - spec.rdoc_options = ["--charset=UTF-8"] + spec.required_ruby_version = '>= 3.0.0' - spec.add_runtime_dependency "ffi", "~> 1.0", ">= 1.0.9" - spec.add_runtime_dependency "bigdecimal", "~> 3.1" + spec.add_runtime_dependency('bigdecimal', '~> 3.1') + spec.add_runtime_dependency('ffi', '~> 1.0', '>= 1.0.9') + spec.add_runtime_dependency('stringio', '~> 3.0') - spec.add_development_dependency('rake', '>= 12.3.3') - spec.add_development_dependency('rspec', '~> 3.9') - spec.add_development_dependency('rubocop', '~> 1.60') - spec.add_development_dependency('rubocop-rspec', '~> 2.27') - spec.add_development_dependency('rubocop-minitest', '~> 0.34.5') - spec.add_development_dependency('rubocop-packaging', '~> 0.5.2') spec.metadata['rubygems_mfa_required'] = 'true' end diff --git a/lib/ffi-icu.rb b/lib/ffi-icu.rb index f132d36..be8db33 100644 --- a/lib/ffi-icu.rb +++ b/lib/ffi-icu.rb @@ -1,9 +1,9 @@ -require "rbconfig" -require "ffi" +require 'rbconfig' +require 'ffi' module ICU def self.platform - os = RbConfig::CONFIG["host_os"] + os = RbConfig::CONFIG['host_os'] case os when /darwin/ @@ -20,17 +20,17 @@ def self.platform end end -require "ffi-icu/core_ext/string" -require "ffi-icu/lib" -require "ffi-icu/lib/util" -require "ffi-icu/uchar" -require "ffi-icu/chardet" -require "ffi-icu/collation" -require "ffi-icu/locale" -require "ffi-icu/transliteration" -require "ffi-icu/normalization" -require "ffi-icu/normalizer" -require "ffi-icu/break_iterator" -require "ffi-icu/number_formatting" -require "ffi-icu/time_formatting" -require "ffi-icu/duration_formatting" +require 'ffi-icu/core_ext/string' +require 'ffi-icu/lib' +require 'ffi-icu/lib/util' +require 'ffi-icu/uchar' +require 'ffi-icu/chardet' +require 'ffi-icu/collation' +require 'ffi-icu/locale' +require 'ffi-icu/transliteration' +require 'ffi-icu/normalization' +require 'ffi-icu/normalizer' +require 'ffi-icu/break_iterator' +require 'ffi-icu/number_formatting' +require 'ffi-icu/time_formatting' +require 'ffi-icu/duration_formatting' diff --git a/lib/ffi-icu/break_iterator.rb b/lib/ffi-icu/break_iterator.rb index f5030a3..6817b95 100644 --- a/lib/ffi-icu/break_iterator.rb +++ b/lib/ffi-icu/break_iterator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ICU class BreakIterator include Enumerable @@ -8,7 +10,7 @@ class BreakIterator def self.available_locales (0...Lib.ubrk_countAvailable).map do |idx| - Lib.ubrk_getAvailable idx + Lib.ubrk_getAvailable(idx) end end @@ -20,25 +22,25 @@ def initialize(type, locale) def text=(str) @text = str - Lib.check_error { |err| - Lib.ubrk_setText @iterator, UCharPointer.from_string(str), str.jlength, err - } + Lib.check_error do |err| + Lib.ubrk_setText(@iterator, UCharPointer.from_string(str), str.jlength, err) + end end - def each(&blk) + def each return to_enum(:each) unless block_given? int = first while int != DONE - yield int + yield(int) int = self.next end self end - def each_substring(&blk) + def each_substring return to_enum(:each_substring) unless block_given? # each_char needed for 1.8, where String#[] works on bytes, not characters @@ -46,7 +48,7 @@ def each_substring(&blk) low = first while (high = self.next) != DONE - yield chars[low...high].join + yield(chars[low...high].join) low = high end @@ -58,36 +60,35 @@ def substrings end def next - Lib.ubrk_next @iterator + Lib.ubrk_next(@iterator) end def previous - Lib.ubrk_next @iterator + Lib.ubrk_next(@iterator) end def first - Lib.ubrk_first @iterator + Lib.ubrk_first(@iterator) end def last - Lib.ubrk_last @iterator + Lib.ubrk_last(@iterator) end def preceding(offset) - Lib.ubrk_preceding @iterator, Integer(offset) + Lib.ubrk_preceding(@iterator, Integer(offset)) end def following(offset) - Lib.ubrk_following @iterator, Integer(offset) + Lib.ubrk_following(@iterator, Integer(offset)) end def current - Lib.ubrk_current @iterator + Lib.ubrk_current(@iterator) end def boundary?(offset) Lib.ubrk_isBoundary(@iterator, Integer(offset)) != 0 end - - end # BreakIterator -end # ICU \ No newline at end of file + end +end diff --git a/lib/ffi-icu/chardet.rb b/lib/ffi-icu/chardet.rb index 6446307..1da82d4 100644 --- a/lib/ffi-icu/chardet.rb +++ b/lib/ffi-icu/chardet.rb @@ -1,20 +1,21 @@ +# frozen_string_literal: true + module ICU module CharDet - def self.detect(string) - Detector.new.detect string + Detector.new.detect(string) end class Detector Match = Struct.new(:name, :confidence, :language) def initialize - ptr = Lib.check_error { |err| Lib.ucsdet_open err } + ptr = Lib.check_error { |err| Lib.ucsdet_open(err) } @detector = FFI::AutoPointer.new(ptr, Lib.method(:ucsdet_close)) end def input_filter_enabled? - Lib.ucsdet_isInputFilterEnabled @detector + Lib.ucsdet_isInputFilterEnabled(@detector) end def input_filter_enabled=(bool) @@ -37,7 +38,7 @@ def detect(str) def detect_all(str) set_text(str) - matches_found_ptr = FFI::MemoryPointer.new :int32_t + matches_found_ptr = FFI::MemoryPointer.new(:int32_t) array_ptr = Lib.check_error do |status| Lib.ucsdet_detectAll(@detector, matches_found_ptr, status) end @@ -71,14 +72,12 @@ def match_ptr_to_ruby(match_ptr) result end - def set_text(text) + def set_text(text) # rubocop:disable Naming/AccessorMethodName Lib.check_error do |status| data = FFI::MemoryPointer.from_string(text) Lib.ucsdet_setText(@detector, data, text.bytesize, status) end end - - end # Detector - end # CharDet -end # ICU - + end + end +end diff --git a/lib/ffi-icu/collation.rb b/lib/ffi-icu/collation.rb index d7b9fe8..21979cf 100644 --- a/lib/ffi-icu/collation.rb +++ b/lib/ffi-icu/collation.rb @@ -1,37 +1,38 @@ +# frozen_string_literal: true + module ICU module Collation - ATTRIBUTES = { - french_collation: 0, - alternate_handling: 1, - case_first: 2, - case_level: 3, - normalization_mode: 4, - strength: 5, - hiragana_quaternary_mode: 6, - numeric_collation: 7, + :french_collation => 0, + :alternate_handling => 1, + :case_first => 2, + :case_level => 3, + :normalization_mode => 4, + :strength => 5, + :hiragana_quaternary_mode => 6, + :numeric_collation => 7 }.freeze ATTRIBUTE_VALUES = { - nil => -1, - primary: 0, - secondary: 1, - default_strength: 2, - tertiary: 2, - quaternary: 3, - identical: 15, - - false => 16, - true => 17, - - shifted: 20, - non_ignorable: 21, - - lower_first: 24, - upper_first: 25, + nil => -1, + :primary => 0, + :secondary => 1, + :default_strength => 2, + :tertiary => 2, + :quaternary => 3, + :identical => 15, + + false => 16, + true => 17, + + :shifted => 20, + :non_ignorable => 21, + + :lower_first => 24, + :upper_first => 25 }.freeze - ATTRIBUTE_VALUES_INVERSE = Hash[ATTRIBUTE_VALUES.map {|k,v| [v, k]}].freeze + ATTRIBUTE_VALUES_INVERSE = ATTRIBUTE_VALUES.to_h { |k, v| [v, k] }.freeze def self.collate(locale, arr) Collator.new(locale).collate(arr) @@ -40,7 +41,7 @@ def self.collate(locale, arr) def self.keywords enum_ptr = Lib.check_error { |error| Lib.ucol_getKeywords(error) } keywords = Lib.enum_ptr_to_array(enum_ptr) - Lib.uenum_close enum_ptr + Lib.uenum_close(enum_ptr) hash = {} keywords.each do |keyword| @@ -54,7 +55,7 @@ def self.keywords def self.available_locales (0...Lib.ucol_countAvailable).map do |idx| - Lib.ucol_getAvailable idx + Lib.ucol_getAvailable(idx) end end @@ -80,40 +81,36 @@ def compare(a, b) def greater?(a, b) Lib.ucol_greater(@c, UCharPointer.from_string(a), a.jlength, - UCharPointer.from_string(b), b.jlength) + UCharPointer.from_string(b), b.jlength) end def greater_or_equal?(a, b) Lib.ucol_greaterOrEqual(@c, UCharPointer.from_string(a), a.jlength, - UCharPointer.from_string(b), b.jlength) + UCharPointer.from_string(b), b.jlength) end def equal?(*args) return super() if args.empty? - if args.size != 2 - raise ArgumentError, "wrong number of arguments (#{args.size} for 2)" - end + raise(ArgumentError, "wrong number of arguments (#{args.size} for 2)") if args.size != 2 a, b = args Lib.ucol_equal(@c, UCharPointer.from_string(a), a.jlength, - UCharPointer.from_string(b), b.jlength) + UCharPointer.from_string(b), b.jlength) end def collate(sortable) - unless sortable.respond_to?(:sort) - raise ArgumentError, "argument must respond to :sort with arity of 2" - end + raise(ArgumentError, 'argument must respond to :sort with arity of 2') unless sortable.respond_to?(:sort) - sortable.sort { |a, b| compare a, b } + sortable.sort { |a, b| compare(a, b) } end def rules @rules ||= begin length = FFI::MemoryPointer.new(:int) ptr = Lib.ucol_getRules(@c, length) - ptr.read_array_of_uint16(length.read_int).pack("U*") + ptr.read_array_of_uint16(length.read_int).pack('U*') end end @@ -135,22 +132,28 @@ def []=(attribute, value) Lib.check_error do |error| Lib.ucol_setAttribute(@c, ATTRIBUTES[attribute], ATTRIBUTE_VALUES[value], error) end - value end # create friendly named methods for setting attributes ATTRIBUTES.each_key do |attribute| - class_eval <<-CODE - def #{attribute} - self[:#{attribute}] - end - - def #{attribute}=(value) - self[:#{attribute}] = value - end + class_eval <<-CODE, __FILE__, __LINE__ + 1 + # def case_first + # self[:case_first] + # end + # + # def case_first=(value) + # self[:case_first] = value + # end + + def #{attribute} + self[:#{attribute}] + end + + def #{attribute}=(value) + self[:#{attribute}] = value + end CODE end - end # Collator - - end # Collate -end # ICU + end + end +end diff --git a/lib/ffi-icu/core_ext/string.rb b/lib/ffi-icu/core_ext/string.rb index 0742598..ffafc86 100644 --- a/lib/ffi-icu/core_ext/string.rb +++ b/lib/ffi-icu/core_ext/string.rb @@ -1,9 +1,7 @@ +# frozen_string_literal: true + class String - unless method_defined?(:bytesize) - alias_method :bytesize, :length - end + alias bytesize length unless method_defined?(:bytesize) - unless method_defined?(:jlength) - alias_method :jlength, :length - end + alias jlength length unless method_defined?(:jlength) end diff --git a/lib/ffi-icu/duration_formatting.rb b/lib/ffi-icu/duration_formatting.rb index 6a9e4bc..32af60c 100644 --- a/lib/ffi-icu/duration_formatting.rb +++ b/lib/ffi-icu/duration_formatting.rb @@ -1,282 +1,308 @@ +# frozen_string_literal: true + +require 'stringio' + module ICU - module DurationFormatting - VALID_FIELDS = %i[ - years - months - weeks - days - hours - minutes - seconds - milliseconds - microseconds - nanoseconds - ] - HMS_FIELDS = %i[ - hours - minutes - seconds - milliseconds - microseconds - nanoseconds - ] - ROUNDABLE_FIELDS = %i[ - seconds - milliseconds - microseconds - nanoseconds - ] - VALID_STYLES = %i[long short narrow digital] - STYLES_TO_LIST_JOIN_FORMAT = { - long: :wide, - short: :short, - narrow: :narrow, - digital: :narrow, - } - UNIT_FORMAT_STRINGS = { - years: 'measure-unit/duration-year', - months: 'measure-unit/duration-month', - weeks: 'measure-unit/duration-week', - days: 'measure-unit/duration-day', - hours: 'measure-unit/duration-hour', - minutes: 'measure-unit/duration-minute', - seconds: 'measure-unit/duration-second', - milliseconds: 'measure-unit/duration-millisecond', - microseconds: 'measure-unit/duration-microsecond', - nanoseconds: 'measure-unit/duration-nanosecond', - } - STYLES_TO_NUMBER_FORMAT_WIDTH = { - long: 'unit-width-full-name', - short: 'unit-width-short', - narrow: 'unit-width-narrow', - # digital for hours:minutes:seconds has some special casing. - digital: 'unit-width-narrow', - } - - def self.format(fields, locale:, style: :long) - DurationFormatter.new(locale: locale, style: style).format(fields) + module DurationFormatting + VALID_FIELDS = [ + :years, + :months, + :weeks, + :days, + :hours, + :minutes, + :seconds, + :milliseconds, + :microseconds, + :nanoseconds + ].freeze + + HMS_FIELDS = [ + :hours, + :minutes, + :seconds, + :milliseconds, + :microseconds, + :nanoseconds + ].freeze + + ROUNDABLE_FIELDS = [ + :seconds, + :milliseconds, + :microseconds, + :nanoseconds + ].freeze + + VALID_STYLES = [:long, :short, :narrow, :digital].freeze + + STYLES_TO_LIST_JOIN_FORMAT = { + :long => :wide, + :short => :short, + :narrow => :narrow, + :digital => :narrow + }.freeze + + UNIT_FORMAT_STRINGS = { + :years => 'measure-unit/duration-year', + :months => 'measure-unit/duration-month', + :weeks => 'measure-unit/duration-week', + :days => 'measure-unit/duration-day', + :hours => 'measure-unit/duration-hour', + :minutes => 'measure-unit/duration-minute', + :seconds => 'measure-unit/duration-second', + :milliseconds => 'measure-unit/duration-millisecond', + :microseconds => 'measure-unit/duration-microsecond', + :nanoseconds => 'measure-unit/duration-nanosecond' + }.freeze + + STYLES_TO_NUMBER_FORMAT_WIDTH = { + :long => 'unit-width-full-name', + :short => 'unit-width-short', + :narrow => 'unit-width-narrow', + # digital for hours:minutes:seconds has some special casing. + :digital => 'unit-width-narrow' + }.freeze + + def self.format(fields, locale:, style: :long) + DurationFormatter.new(locale: locale, style: style).format(fields) + end + + class DurationFormatter + def initialize(locale:, style: :long) + if !Lib.respond_to?(:unumf_openForSkeletonAndLocale) || !Lib.respond_to?(:ulistfmt_openForType) + raise('ICU::DurationFormatting requires ICU >= 67') end - class DurationFormatter - def initialize(locale:, style: :long) - if !Lib.respond_to?(:unumf_openForSkeletonAndLocale) || !Lib.respond_to?(:ulistfmt_openForType) - raise "ICU::DurationFormatting requires ICU >= 67" - end - - raise ArgumentError, "Unknown style #{style}" unless VALID_STYLES.include?(style) - - @locale = locale - @style = style - # These are created lazily based on what parts are actually included - @number_formatters = {} - - list_join_format = STYLES_TO_LIST_JOIN_FORMAT.fetch(style) - @list_formatter = FFI::AutoPointer.new( - Lib.check_error { |error| - Lib.ulistfmt_openForType(@locale, :units, list_join_format, error) - }, - Lib.method(:ulistfmt_close) - ) - end + raise(ArgumentError, "Unknown style #{style}") unless VALID_STYLES.include?(style) - def format(fields) - fields.each_key do |field| - raise "Unknown field #{field}" unless VALID_FIELDS.include?(field) - end - fields = fields.dup # we might modify this argument. - - # Intl.js spec says that rounding options affect only the smallest unit, and only - # if that unit is sub-second. All other fields therefore need to be truncated. - smallest_unit = VALID_FIELDS[fields.keys.map { |k| VALID_FIELDS.index(k) }.max] - fields.each_key do |k| - raise ArgumentError, "Negative durations are not yet supported" if fields[k] < 0 - fields[k] = fields[k].to_i unless k == smallest_unit && ROUNDABLE_FIELDS.include?(smallest_unit) - end - - formatted_hms = nil - if @style == :digital - # icu::MeasureFormat contains special casing for hours/minutes/seconds formatted - # at numeric width, to render it as h:mm:s, essentially. This involves using - # a pattern called durationUnits defined in the ICU data for the locale. - # If we have data for this combination of hours/mins/seconds in this locale, - # use that and emulate ICU's special casing. - formatted_hms = format_hms(fields) - if formatted_hms - # We've taken care of all these fields now. - HMS_FIELDS.each do |f| - fields.delete f - end - end - end - - formatted_fields = VALID_FIELDS.map do |f| - next unless fields.key?(f) - next unless fields[f] != 0 - - format_number(fields[f], [ - UNIT_FORMAT_STRINGS[f], STYLES_TO_NUMBER_FORMAT_WIDTH[@style], - (".#########" if f == smallest_unit), - ].compact.join(' ')) - end - formatted_fields << formatted_hms - formatted_fields.compact! - - format_list(formatted_fields) - end + @locale = locale + @style = style + # These are created lazily based on what parts are actually included + @number_formatters = {} - private - - def hms_duration_units_pattern(fields) - return nil unless HMS_FIELDS.any? { |k| fields.key?(k) } - @unit_res_bundle ||= FFI::AutoPointer.new( - Lib.check_error { |error| Lib.ures_open(Lib.resource_bundle_name(:unit), @locale, error) }, - Lib.method(:ures_close) - ) - - resource_key = "durationUnits/" - resource_key << "h" if fields.key?(:hours) - resource_key << "m" if fields.key?(:minutes) - resource_key << "s" if [:seconds, :milliseconds, :microseconds, :nanoseconds].any? { |f| fields.key?(f) } - - begin - pattern_resource = FFI::AutoPointer.new( - Lib.check_error { |error| Lib.ures_getBykeyWithFallback(@unit_res_bundle, resource_key, nil, error) }, - Lib.method(:ures_close) - ) - rescue MissingResourceError - # This combination of h,m,s not present for this locale. - return nil - end - # Read the resource as a UChar (whose memory we _do not own_ - it's static data) and - # convert it to a Ruby string. - pattern_uchar_len = FFI::MemoryPointer.new(:int32_t) - pattern_uchar = Lib.check_error { |error| - Lib.ures_getString(pattern_resource, pattern_uchar_len, error) - } - pattern_str = pattern_uchar.read_array_of_uint16(pattern_uchar_len.read_int32).pack("U*") - - # For some reason I can't comprehend, loadNumericDateFormatterPattern in ICU wants to turn - # h's into H's here. I guess we have to do it too because the pattern data could in theory - # now contain either. - pattern_str.gsub('h', 'H') - end + list_join_format = STYLES_TO_LIST_JOIN_FORMAT.fetch(style) + @list_formatter = FFI::AutoPointer.new( + Lib.check_error do |error| + Lib.ulistfmt_openForType(@locale, :units, list_join_format, error) + end, + Lib.method(:ulistfmt_close) + ) + end - def format_hms(fields) - pattern = hms_duration_units_pattern(fields) - return nil if pattern.nil? - - # According to the Intl.js spec, when formatting in digital, everything < seconds - # should be coalesced into decimal seconds - seconds_incl_fractional = fields.fetch(:seconds, 0) - second_precision = 0 - if fields.key?(:milliseconds) - seconds_incl_fractional += fields[:milliseconds] / 1e3 - second_precision = 3 - end - if fields.key?(:microseconds) - seconds_incl_fractional += fields[:microseconds] / 1e6 - second_precision = 6 - end - if fields.key?(:nanoseconds) - seconds_incl_fractional += fields[:nanoseconds] / 1e9 - second_precision = 9 - end - - # Follow the rules in ICU measfmt.cpp formatNumeric to fill in the patterns here with - # the appropriate values. - enum = pattern.each_char - protect = false - result = "" - loop do - char = enum.next - next_char = enum.peek rescue nil - - if protect - # In literal mode - if char == "'" - protect = false - next - end - result << char - next - end - - value = case char - when 'H' then fields[:hours] - when 'm' then fields[:minutes] - when 's' then seconds_incl_fractional - end - - case char - when 'H', 'm', 's' - skeleton = "." - if char == 's' && second_precision > 0 - skeleton << ("0" * second_precision) - else - skeleton << ("#" * 9) - end - if char == next_char - # It's doubled - means format it at zero fill - skeleton << " integer-width/00" - enum.next - end - result << format_number(value, skeleton) - when "'" - if next_char == char - # double-apostrophe, means literal ' - result << "'" - enum.next - else - protect = true - end - else - result << char - end - end - - result - end + def format(fields) + fields.each_key do |field| + raise("Unknown field #{field}") unless VALID_FIELDS.include?(field) + end + fields = fields.dup # we might modify this argument. + + # Intl.js spec says that rounding options affect only the smallest unit, and only + # if that unit is sub-second. All other fields therefore need to be truncated. + smallest_unit = VALID_FIELDS[fields.keys.map { |k| VALID_FIELDS.index(k) }.max] + fields.each_key do |k| + raise(ArgumentError, 'Negative durations are not yet supported') if (fields[k]).negative? - def number_formatter(skeleton) - @number_formatters[skeleton] ||= begin - skeleton_uchar = UCharPointer.from_string(skeleton) - FFI::AutoPointer.new( - Lib.check_error { |error| - Lib.unumf_openForSkeletonAndLocale(skeleton_uchar, skeleton_uchar.length_in_uchars, @locale, error) - }, - Lib.method(:unumf_close) - ) - end + fields[k] = fields[k].to_i unless k == smallest_unit && ROUNDABLE_FIELDS.include?(smallest_unit) + end + + formatted_hms = nil + if @style == :digital + # icu::MeasureFormat contains special casing for hours/minutes/seconds formatted + # at numeric width, to render it as h:mm:s, essentially. This involves using + # a pattern called durationUnits defined in the ICU data for the locale. + # If we have data for this combination of hours/mins/seconds in this locale, + # use that and emulate ICU's special casing. + formatted_hms = format_hms(fields) + if formatted_hms + # We've taken care of all these fields now. + HMS_FIELDS.each do |f| + fields.delete(f) end + end + end + + formatted_fields = VALID_FIELDS.map do |f| + next unless fields.key?(f) + next unless fields[f] != 0 + + format_number(fields[f], [ + UNIT_FORMAT_STRINGS[f], STYLES_TO_NUMBER_FORMAT_WIDTH[@style], + ('.#########' if f == smallest_unit) + ].compact.join(' ')) + end + formatted_fields << formatted_hms + formatted_fields.compact! + + format_list(formatted_fields) + end + + private + + def hms_duration_units_pattern(fields) + return nil unless HMS_FIELDS.any? { |k| fields.key?(k) } - def format_number(value, skeleton) - formatter = number_formatter(skeleton) - result = FFI::AutoPointer.new( - Lib.check_error { |error| Lib.unumf_openResult(error) }, - Lib.method(:unumf_closeResult) - ) - value_str = value.to_s - Lib.check_error do |error| - Lib.unumf_formatDecimal(formatter, value_str, value_str.size, result, error) - end - Lib::Util.read_uchar_buffer(0) do |buf, error| - Lib.unumf_resultToString(result, buf, buf.length_in_uchars, error) - end + @unit_res_bundle ||= FFI::AutoPointer.new( + Lib.check_error { |error| Lib.ures_open(Lib.resource_bundle_name(:unit), @locale, error) }, + Lib.method(:ures_close) + ) + + resource_key_builder = StringIO.new + resource_key_builder.write('durationUnits/') + resource_key_builder.putc('h') if fields.key?(:hours) + resource_key_builder.putc('m') if fields.key?(:minutes) + if [:seconds, :milliseconds, :microseconds, :nanoseconds].any? { |f| fields.key?(f) } + resource_key_builder.putc('s') + end + + resource_key = resource_key_builder.string + + begin + pattern_resource = FFI::AutoPointer.new( + Lib.check_error do |error| + Lib.ures_getBykeyWithFallback(@unit_res_bundle, resource_key, nil, error) + end, + Lib.method(:ures_close) + ) + rescue MissingResourceError + # This combination of h,m,s not present for this locale. + return nil + end + # Read the resource as a UChar (whose memory we _do not own_ - it's static data) and + # convert it to a Ruby string. + pattern_uchar_len = FFI::MemoryPointer.new(:int32_t) + pattern_uchar = Lib.check_error do |error| + Lib.ures_getString(pattern_resource, pattern_uchar_len, error) + end + pattern_str = pattern_uchar.read_array_of_uint16(pattern_uchar_len.read_int32).pack('U*') + + # For some reason I can't comprehend, loadNumericDateFormatterPattern in ICU wants to turn + # h's into H's here. I guess we have to do it too because the pattern data could in theory + # now contain either. + pattern_str.gsub('h', 'H') + end + + def format_hms(fields) + pattern = hms_duration_units_pattern(fields) + return nil if pattern.nil? + + # According to the Intl.js spec, when formatting in digital, everything < seconds + # should be coalesced into decimal seconds + seconds_incl_fractional = fields.fetch(:seconds, 0) + second_precision = 0 + if fields.key?(:milliseconds) + seconds_incl_fractional += fields[:milliseconds] / 1e3 + second_precision = 3 + end + if fields.key?(:microseconds) + seconds_incl_fractional += fields[:microseconds] / 1e6 + second_precision = 6 + end + if fields.key?(:nanoseconds) + seconds_incl_fractional += fields[:nanoseconds] / 1e9 + second_precision = 9 + end + + # Follow the rules in ICU measfmt.cpp formatNumeric to fill in the patterns here with + # the appropriate values. + enum = pattern.each_char + protect = false + result = StringIO.new + loop do + char = enum.next + next_char = begin + enum.peek + rescue StandardError + nil + end + + if protect + # In literal mode + if char == "'" + protect = false + next end + result.write(char) + next + end + + value = case char + when 'H' then fields[:hours] + when 'm' then fields[:minutes] + when 's' then seconds_incl_fractional + end - def format_list(values) - value_uchars = values.map(&UCharPointer.method(:from_string)) - value_uchars_array = FFI::MemoryPointer.new(:pointer, value_uchars.size) - value_uchars_array.put_array_of_pointer(0, value_uchars) - value_lengths_array = FFI::MemoryPointer.new(:int32_t, value_uchars.size) - value_lengths_array.put_array_of_int32(0, value_uchars.map(&:length_in_uchars)) - Lib::Util.read_uchar_buffer(0) do |buf, error| - Lib.ulistfmt_format( - @list_formatter, value_uchars_array, value_lengths_array, - value_uchars.size, buf, buf.length_in_uchars, error - ) - end + case char + when 'H', 'm', 's' + skeleton_builder = StringIO.new + skeleton_builder.write('.') + if char == 's' && second_precision.positive? + skeleton_builder.write('0' * second_precision) + else + skeleton_builder.write('#' * 9) + end + if char == next_char + # It's doubled - means format it at zero fill + skeleton_builder.write(' integer-width/00') + enum.next end + skeleton = skeleton_builder.string + result.write(format_number(value, skeleton)) + when "'" + if next_char == char + # double-apostrophe, means literal ' + result.write("'") + enum.next + else + protect = true + end + else + result.write(char) + end + end + + result.string + end + + def number_formatter(skeleton) + @number_formatters[skeleton] ||= begin + skeleton_uchar = UCharPointer.from_string(skeleton) + FFI::AutoPointer.new( + Lib.check_error do |error| + Lib.unumf_openForSkeletonAndLocale(skeleton_uchar, skeleton_uchar.length_in_uchars, + @locale, error) + end, + Lib.method(:unumf_close) + ) + end + end + + def format_number(value, skeleton) + formatter = number_formatter(skeleton) + result = FFI::AutoPointer.new( + Lib.check_error { |error| Lib.unumf_openResult(error) }, + Lib.method(:unumf_closeResult) + ) + value_str = value.to_s + Lib.check_error do |error| + Lib.unumf_formatDecimal(formatter, value_str, value_str.size, result, error) + end + Lib::Util.read_uchar_buffer(0) do |buf, error| + Lib.unumf_resultToString(result, buf, buf.length_in_uchars, error) + end + end + + def format_list(values) + value_uchars = values.map(&UCharPointer.method(:from_string)) + value_uchars_array = FFI::MemoryPointer.new(:pointer, value_uchars.size) + value_uchars_array.put_array_of_pointer(0, value_uchars) + value_lengths_array = FFI::MemoryPointer.new(:int32_t, value_uchars.size) + value_lengths_array.put_array_of_int32(0, value_uchars.map(&:length_in_uchars)) + Lib::Util.read_uchar_buffer(0) do |buf, error| + Lib.ulistfmt_format( + @list_formatter, value_uchars_array, value_lengths_array, + value_uchars.size, buf, buf.length_in_uchars, error + ) end + end end + end end diff --git a/lib/ffi-icu/lib.rb b/lib/ffi-icu/lib.rb index a6f4f58..0ac824d 100644 --- a/lib/ffi-icu/lib.rb +++ b/lib/ffi-icu/lib.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ICU class Error < StandardError end @@ -12,63 +14,60 @@ module Lib extend FFI::Library def self.search_paths - @search_paths ||= begin - if ENV['FFI_ICU_LIB'] - [ ENV['FFI_ICU_LIB'] ] - elsif FFI::Platform::IS_WINDOWS - ENV['PATH'].split(File::PATH_SEPARATOR) - else - [ - '/usr/local/{lib64,lib}', - '/opt/local/{lib64,lib}', - '/usr/{lib64,lib}', - ] + Dir['/usr/lib/*-linux-gnu'] # for Debian Multiarch http://wiki.debian.org/Multiarch - end - end + @search_paths ||= if ENV['FFI_ICU_LIB'] + [ENV['FFI_ICU_LIB']] + elsif FFI::Platform::IS_WINDOWS + ENV['PATH'].split(File::PATH_SEPARATOR) + else + [ + '/usr/local/{lib64,lib}', + '/opt/local/{lib64,lib}', + '/usr/{lib64,lib}' + ] + Dir['/usr/lib/*-linux-gnu'] # for Debian Multiarch http://wiki.debian.org/Multiarch + end end def self.find_lib(lib) - Dir.glob(search_paths.map { |path| + Dir.glob(search_paths.map do |path| File.expand_path(File.join(path, lib)) - }).first + end).first end def self.load_icu - # First find the library - lib_names = case ICU.platform - when :bsd - [find_lib("libicui18n.#{FFI::Platform::LIBSUFFIX}.??"), - find_lib("libicutu.#{FFI::Platform::LIBSUFFIX}.??")] - when :osx - if ENV.key?('FFI_ICU_LIB') - # Ensure we look in the user-supplied override dir for a user-compiled libicu - [find_lib("libicui18n.??.#{FFI::Platform::LIBSUFFIX}"), - find_lib("libicutu.??.#{FFI::Platform::LIBSUFFIX}")] - elsif Gem::Version.new(`sw_vers -productVersion`) >= Gem::Version.new('11') - # See https://developer.apple.com/documentation/macos-release-notes/macos-big-sur-11_0_1-release-notes (62986286) - ["libicucore.#{FFI::Platform::LIBSUFFIX}"] - else - [find_lib("libicucore.#{FFI::Platform::LIBSUFFIX}")] - end - when :linux - [find_lib("libicui18n.#{FFI::Platform::LIBSUFFIX}.??"), - find_lib("libicutu.#{FFI::Platform::LIBSUFFIX}.??")] - when :windows - [find_lib("icuuc??.#{FFI::Platform::LIBSUFFIX}"), - find_lib("icuin??.#{FFI::Platform::LIBSUFFIX}")] - end - - lib_names.compact! if lib_names - - if not lib_names or lib_names.length == 0 - raise LoadError, "Could not find ICU on #{ICU.platform.inspect}. Patches welcome, or you can add the containing directory yourself: #{self}.search_paths << '/path/to/lib'" + lib_names = + case ICU.platform + when :linux, :bsd + [find_lib("libicui18n.#{FFI::Platform::LIBSUFFIX}.??"), + find_lib("libicutu.#{FFI::Platform::LIBSUFFIX}.??")] + when :osx + if ENV.key?('FFI_ICU_LIB') + # Ensure we look in the user-supplied override dir for a user-compiled libicu + [find_lib("libicui18n.??.#{FFI::Platform::LIBSUFFIX}"), + find_lib("libicutu.??.#{FFI::Platform::LIBSUFFIX}")] + elsif Gem::Version.new(`sw_vers -productVersion`) >= Gem::Version.new('11') + ["libicucore.#{FFI::Platform::LIBSUFFIX}"] + else + [find_lib("libicucore.#{FFI::Platform::LIBSUFFIX}")] + end + when :windows + [find_lib("icuuc??.#{FFI::Platform::LIBSUFFIX}"), + find_lib("icuin??.#{FFI::Platform::LIBSUFFIX}")] + end + + lib_names&.compact! + + if !lib_names || lib_names.empty? + raise(LoadError, + "Could not find ICU on #{ICU.platform.inspect}. " \ + 'Patches welcome, or you can add the containing directory yourself: ' \ + "#{self}.search_paths << '/path/to/lib'") end # And now try to load the library begin libs = ffi_lib(*lib_names) - rescue LoadError => ex - raise LoadError, "no idea how to load ICU on #{ICU.platform.inspect}, patches appreciated! (#{ex.message})" + rescue LoadError => e + raise(LoadError, "no idea how to load ICU on #{ICU.platform.inspect}, patches appreciated! (#{e.message})") end icu_version(libs) @@ -82,9 +81,7 @@ def self.icu_version(libs) # we could just call cause this is super fugly! match = lib.name.match(/(\d\d)\.#{FFI::Platform::LIBSUFFIX}/) || lib.name.match(/#{FFI::Platform::LIBSUFFIX}\.(\d\d)/) - if match - version = match[1] - end + version = match[1] if match end # Note this may return nil, like on OSX @@ -107,10 +104,8 @@ def self.figure_suffix(version) # So we need to figure out which one it is. # Here are the possible suffixes - suffixes = [""] - if version - suffixes << "_#{version}" << "_#{version[0].chr}_#{version[1].chr}" << "_#{version.split('.')[0]}" - end + suffixes = [''] + suffixes << "_#{version}" << "_#{version[0].chr}_#{version[1].chr}" << "_#{version.split('.')[0]}" if version # Try to find the u_errorName function using the possible suffixes suffixes.find do |suffix| @@ -128,17 +123,17 @@ def self.check_error ret = yield(ptr) error_code = ptr.read_int - if error_code > 0 - name = Lib.u_errorName error_code - if name == "U_BUFFER_OVERFLOW_ERROR" - raise BufferOverflowError - elsif name == "U_MISSING_RESOURCE_ERROR" - raise MissingResourceError + if error_code.positive? + name = Lib.u_errorName(error_code) + if name == 'U_BUFFER_OVERFLOW_ERROR' + raise(BufferOverflowError) + elsif name == 'U_MISSING_RESOURCE_ERROR' + raise(MissingResourceError) else - raise Error, name + raise(Error, name) end - elsif error_code < 0 - $stderr.puts "ffi-icu: #{Lib.u_errorName error_code}" if $DEBUG || $VERBOSE + elsif error_code.negative? + warn("ffi-icu: #{Lib.u_errorName(error_code)}") if $DEBUG || $VERBOSE end ret @@ -151,37 +146,37 @@ def self.enum_ptr_to_array(enum_ptr) len = FFI::MemoryPointer.new(:int) - (0...length).map do |idx| + (0...length).map do |_idx| check_error { |st| uenum_next(enum_ptr, len, st) } end end def self.not_available(func_name) - self.class.send :define_method, func_name do |*args| - raise Error, "#{func_name} not available on platform #{ICU.platform.inspect}" + self.class.send(:define_method, func_name) do |*_args| + raise(Error, "#{func_name} not available on platform #{ICU.platform.inspect}") end end class VersionInfo < FFI::MemoryPointer extend FFI::DataConverter - MaxLength = 4 - MaxStringLength = 20 + MAX_LENGTH = 4 + MAX_STRING_LENGTH = 20 def self.native_type FFI::Type::POINTER end def initialize - super :uint8, MaxLength + super(:uint8, MAX_LENGTH) end def to_a - read_array_of_uint8(MaxLength) + read_array_of_uint8(MAX_LENGTH) end def to_s - buffer = FFI::MemoryPointer.new(:char, MaxStringLength) + buffer = FFI::MemoryPointer.new(:char, MAX_STRING_LENGTH) Lib.u_versionToString(self, buffer) buffer.read_string_to_null end @@ -198,8 +193,9 @@ def self.version end def self.attach_optional_function(*args) - attach_function *args + attach_function(*args) rescue FFI::NotFoundError + # ignore end version = load_icu @@ -207,12 +203,12 @@ def self.attach_optional_function(*args) typedef VersionInfo, :version - attach_function :u_errorName, "u_errorName#{suffix}", [:int], :string - attach_function :uenum_count, "uenum_count#{suffix}", [:pointer, :pointer], :int + attach_function :u_errorName, "u_errorName#{suffix}", [:int], :string + attach_function :uenum_count, "uenum_count#{suffix}", [:pointer, :pointer], :int attach_function :uenum_close, "uenum_close#{suffix}", [:pointer], :void - attach_function :uenum_next, "uenum_next#{suffix}", [:pointer, :pointer, :pointer], :string - attach_function :u_charsToUChars, "u_charsToUChars#{suffix}", [:string, :pointer, :int32_t], :void - attach_function :u_UCharsToChars, "u_UCharsToChars#{suffix}", [:pointer, :string, :int32_t], :void + attach_function :uenum_next, "uenum_next#{suffix}", [:pointer, :pointer, :pointer], :string + attach_function :u_charsToUChars, "u_charsToUChars#{suffix}", [:string, :pointer, :int32_t], :void + attach_function :u_UCharsToChars, "u_UCharsToChars#{suffix}", [:pointer, :string, :int32_t], :void attach_function :u_getVersion, "u_getVersion#{suffix}", [:version], :void attach_function :u_versionToString, "u_versionToString#{suffix}", [:version, :pointer], :void @@ -225,49 +221,73 @@ def self.attach_optional_function(*args) enum :layout_type, [:ltr, :rtl, :ttb, :btt, :unknown] - attach_function :uloc_canonicalize, "uloc_canonicalize#{suffix}", [:string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_canonicalize, "uloc_canonicalize#{suffix}", [:string, :pointer, :int32_t, :pointer], + :int32_t attach_function :uloc_countAvailable, "uloc_countAvailable#{suffix}", [], :int32_t attach_function :uloc_getAvailable, "uloc_getAvailable#{suffix}", [:int32_t], :string - attach_function :uloc_getBaseName, "uloc_getBaseName#{suffix}", [:string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getCountry, "uloc_getCountry#{suffix}", [:string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_getBaseName, "uloc_getBaseName#{suffix}", [:string, :pointer, :int32_t, :pointer], + :int32_t + attach_function :uloc_getCountry, "uloc_getCountry#{suffix}", [:string, :pointer, :int32_t, :pointer], + :int32_t attach_function :uloc_getDefault, "uloc_getDefault#{suffix}", [], :string attach_function :uloc_getISO3Country, "uloc_getISO3Country#{suffix}", [:string], :string attach_function :uloc_getISO3Language, "uloc_getISO3Language#{suffix}", [:string], :string attach_function :uloc_getISOCountries, "uloc_getISOCountries#{suffix}", [], :pointer attach_function :uloc_getISOLanguages, "uloc_getISOLanguages#{suffix}", [], :pointer - attach_function :uloc_getKeywordValue, "uloc_getKeywordValue#{suffix}", [:string, :string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getLanguage, "uloc_getLanguage#{suffix}", [:string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_getKeywordValue, "uloc_getKeywordValue#{suffix}", + [:string, :string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_getLanguage, "uloc_getLanguage#{suffix}", [:string, :pointer, :int32_t, :pointer], + :int32_t attach_function :uloc_getLCID, "uloc_getLCID#{suffix}", [:string], :uint32 - attach_function :uloc_getName, "uloc_getName#{suffix}", [:string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getParent, "uloc_getParent#{suffix}", [:string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getScript, "uloc_getScript#{suffix}", [:string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getVariant, "uloc_getVariant#{suffix}", [:string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_getName, "uloc_getName#{suffix}", [:string, :pointer, :int32_t, :pointer], + :int32_t + attach_function :uloc_getParent, "uloc_getParent#{suffix}", [:string, :pointer, :int32_t, :pointer], + :int32_t + attach_function :uloc_getScript, "uloc_getScript#{suffix}", [:string, :pointer, :int32_t, :pointer], + :int32_t + attach_function :uloc_getVariant, "uloc_getVariant#{suffix}", [:string, :pointer, :int32_t, :pointer], + :int32_t attach_function :uloc_openKeywords, "uloc_openKeywords#{suffix}", [:string, :pointer], :pointer attach_function :uloc_setDefault, "uloc_setDefault#{suffix}", [:string, :pointer], :void - attach_function :uloc_setKeywordValue, "uloc_setKeywordValue#{suffix}", [:string, :string, :pointer, :int32_t, :pointer], :int32_t - - attach_function :uloc_getDisplayCountry, "uloc_getDisplayCountry#{suffix}", [:string, :string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getDisplayKeyword, "uloc_getDisplayKeyword#{suffix}", [:string, :string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getDisplayKeywordValue, "uloc_getDisplayKeywordValue#{suffix}", [:string, :string, :string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getDisplayLanguage, "uloc_getDisplayLanguage#{suffix}", [:string, :string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getDisplayName, "uloc_getDisplayName#{suffix}", [:string, :string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getDisplayScript, "uloc_getDisplayScript#{suffix}", [:string, :string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getDisplayVariant, "uloc_getDisplayVariant#{suffix}", [:string, :string, :pointer, :int32_t, :pointer], :int32_t - - if Gem::Version.new('3.8') <= Gem::Version.new(self.version) - attach_function :uloc_getLocaleForLCID, "uloc_getLocaleForLCID#{suffix}", [:uint32, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_setKeywordValue, "uloc_setKeywordValue#{suffix}", + [:string, :string, :pointer, :int32_t, :pointer], :int32_t + + attach_function :uloc_getDisplayCountry, "uloc_getDisplayCountry#{suffix}", + [:string, :string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_getDisplayKeyword, "uloc_getDisplayKeyword#{suffix}", + [:string, :string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_getDisplayKeywordValue, "uloc_getDisplayKeywordValue#{suffix}", + [:string, :string, :string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_getDisplayLanguage, "uloc_getDisplayLanguage#{suffix}", + [:string, :string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_getDisplayName, "uloc_getDisplayName#{suffix}", + [:string, :string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_getDisplayScript, "uloc_getDisplayScript#{suffix}", + [:string, :string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_getDisplayVariant, "uloc_getDisplayVariant#{suffix}", + [:string, :string, :pointer, :int32_t, :pointer], :int32_t + + if Gem::Version.new('3.8') <= Gem::Version.new(version) + attach_function :uloc_getLocaleForLCID, "uloc_getLocaleForLCID#{suffix}", + [:uint32, :pointer, :int32_t, :pointer], :int32_t end - if Gem::Version.new('4.0') <= Gem::Version.new(self.version) - attach_function :uloc_addLikelySubtags, "uloc_addLikelySubtags#{suffix}", [:string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_minimizeSubtags, "uloc_minimizeSubtags#{suffix}", [:string, :pointer, :int32_t, :pointer], :int32_t - attach_function :uloc_getCharacterOrientation, "uloc_getCharacterOrientation#{suffix}", [:string, :pointer], :layout_type - attach_function :uloc_getLineOrientation, "uloc_getLineOrientation#{suffix}", [:string, :pointer], :layout_type + if Gem::Version.new('4.0') <= Gem::Version.new(version) + attach_function :uloc_addLikelySubtags, "uloc_addLikelySubtags#{suffix}", + [:string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_minimizeSubtags, "uloc_minimizeSubtags#{suffix}", + [:string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uloc_getCharacterOrientation, "uloc_getCharacterOrientation#{suffix}", [:string, :pointer], + :layout_type + attach_function :uloc_getLineOrientation, "uloc_getLineOrientation#{suffix}", [:string, :pointer], + :layout_type end - if Gem::Version.new('4.2') <= Gem::Version.new(self.version) - attach_function :uloc_forLanguageTag, "uloc_forLanguageTag#{suffix}", [:string, :pointer, :int32_t, :pointer, :pointer], :int32_t - attach_function :uloc_toLanguageTag, "uloc_toLanguageTag#{suffix}", [:string, :pointer, :int32_t, :int8_t, :pointer], :int32_t + if Gem::Version.new('4.2') <= Gem::Version.new(version) + attach_function :uloc_forLanguageTag, "uloc_forLanguageTag#{suffix}", + [:string, :pointer, :int32_t, :pointer, :pointer], :int32_t + attach_function :uloc_toLanguageTag, "uloc_toLanguageTag#{suffix}", + [:string, :pointer, :int32_t, :int8_t, :pointer], :int32_t attach_function :ulocdata_getCLDRVersion, "ulocdata_getCLDRVersion#{suffix}", [:version, :pointer], :void end @@ -279,52 +299,66 @@ def self.attach_optional_function(*args) attach_function :ucsdet_open, "ucsdet_open#{suffix}", [:pointer], :pointer attach_function :ucsdet_close, "ucsdet_close#{suffix}", [:pointer], :void - attach_function :ucsdet_setText, "ucsdet_setText#{suffix}", [:pointer, :pointer, :int32_t, :pointer], :void - attach_function :ucsdet_setDeclaredEncoding, "ucsdet_setDeclaredEncoding#{suffix}", [:pointer, :string, :int32_t, :pointer], :void - attach_function :ucsdet_detect, "ucsdet_detect#{suffix}", [:pointer, :pointer], :pointer - attach_function :ucsdet_detectAll, "ucsdet_detectAll#{suffix}", [:pointer, :pointer, :pointer], :pointer - attach_function :ucsdet_getName, "ucsdet_getName#{suffix}", [:pointer, :pointer], :string - attach_function :ucsdet_getConfidence, "ucsdet_getConfidence#{suffix}", [:pointer, :pointer], :int32_t - attach_function :ucsdet_getLanguage, "ucsdet_getLanguage#{suffix}", [:pointer, :pointer], :string - attach_function :ucsdet_getAllDetectableCharsets, "ucsdet_getAllDetectableCharsets#{suffix}", [:pointer, :pointer], :pointer - attach_function :ucsdet_isInputFilterEnabled, "ucsdet_isInputFilterEnabled#{suffix}", [:pointer], :bool - attach_function :ucsdet_enableInputFilter, "ucsdet_enableInputFilter#{suffix}", [:pointer, :bool], :bool + attach_function :ucsdet_setText, "ucsdet_setText#{suffix}", + [:pointer, :pointer, :int32_t, :pointer], :void + attach_function :ucsdet_setDeclaredEncoding, "ucsdet_setDeclaredEncoding#{suffix}", + [:pointer, :string, :int32_t, :pointer], :void + attach_function :ucsdet_detect, "ucsdet_detect#{suffix}", + [:pointer, :pointer], :pointer + attach_function :ucsdet_detectAll, "ucsdet_detectAll#{suffix}", + [:pointer, :pointer, :pointer], :pointer + attach_function :ucsdet_getName, "ucsdet_getName#{suffix}", + [:pointer, :pointer], :string + attach_function :ucsdet_getConfidence, "ucsdet_getConfidence#{suffix}", + [:pointer, :pointer], :int32_t + attach_function :ucsdet_getLanguage, "ucsdet_getLanguage#{suffix}", + [:pointer, :pointer], :string + attach_function :ucsdet_getAllDetectableCharsets, "ucsdet_getAllDetectableCharsets#{suffix}", + [:pointer, :pointer], :pointer + attach_function :ucsdet_isInputFilterEnabled, "ucsdet_isInputFilterEnabled#{suffix}", [:pointer], :bool + attach_function :ucsdet_enableInputFilter, "ucsdet_enableInputFilter#{suffix}", + [:pointer, :bool], :bool # Collation # # http://icu-project.org/apiref/icu4c/ucol_8h.html # - attach_function :ucol_open, "ucol_open#{suffix}", [:string, :pointer], :pointer - attach_function :ucol_close, "ucol_close#{suffix}", [:pointer], :void - attach_function :ucol_strcoll, "ucol_strcoll#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :int32_t], :int - attach_function :ucol_getKeywords, "ucol_getKeywords#{suffix}", [:pointer], :pointer - attach_function :ucol_getKeywordValues, "ucol_getKeywordValues#{suffix}", [:string, :pointer], :pointer + attach_function :ucol_open, "ucol_open#{suffix}", [:string, :pointer], :pointer + attach_function :ucol_close, "ucol_close#{suffix}", [:pointer], :void + attach_function :ucol_strcoll, "ucol_strcoll#{suffix}", + [:pointer, :pointer, :int32_t, :pointer, :int32_t], :int + attach_function :ucol_getKeywords, "ucol_getKeywords#{suffix}", [:pointer], :pointer + attach_function :ucol_getKeywordValues, "ucol_getKeywordValues#{suffix}", [:string, :pointer], :pointer attach_function :ucol_getAvailable, "ucol_getAvailable#{suffix}", [:int32_t], :string attach_function :ucol_countAvailable, "ucol_countAvailable#{suffix}", [], :int32_t - attach_function :ucol_getLocale, "ucol_getLocale#{suffix}", [:pointer, :int, :pointer], :string - attach_function :ucol_greater, "ucol_greater#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :int32_t], :bool - attach_function :ucol_greaterOrEqual, "ucol_greaterOrEqual#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :int32_t], :bool - attach_function :ucol_equal, "ucol_equal#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :int32_t], :bool - attach_function :ucol_getRules, "ucol_getRules#{suffix}", [:pointer, :pointer], :pointer - attach_function :ucol_getSortKey, "ucol_getSortKey#{suffix}", [:pointer, :pointer, :int, :pointer, :int], :int + attach_function :ucol_getLocale, "ucol_getLocale#{suffix}", [:pointer, :int, :pointer], :string + attach_function :ucol_greater, "ucol_greater#{suffix}", + [:pointer, :pointer, :int32_t, :pointer, :int32_t], :bool + attach_function :ucol_greaterOrEqual, "ucol_greaterOrEqual#{suffix}", + [:pointer, :pointer, :int32_t, :pointer, :int32_t], :bool + attach_function :ucol_equal, "ucol_equal#{suffix}", + [:pointer, :pointer, :int32_t, :pointer, :int32_t], :bool + attach_function :ucol_getRules, "ucol_getRules#{suffix}", [:pointer, :pointer], :pointer + attach_function :ucol_getSortKey, "ucol_getSortKey#{suffix}", + [:pointer, :pointer, :int, :pointer, :int], :int attach_function :ucol_getAttribute, "ucol_getAttribute#{suffix}", [:pointer, :int, :pointer], :int attach_function :ucol_setAttribute, "ucol_setAttribute#{suffix}", [:pointer, :int, :int, :pointer], :void - # Transliteration # # http://icu-project.org/apiref/icu4c/utrans_8h.html # class UParseError < FFI::Struct - layout :line, :int32_t, + layout :line, :int32_t, :offset, :int32_t, :pre_context, :pointer, :post_context, :pointer def to_s - "#<%s:%x line: %d offset: %d" % [self.class, hash*2, self[:line], self[:offset]] + format('#<%s:%x line: %d offset: %d', + class: self.class, hash: hash * 2, line: self[:line], offset: self[:offset]) end end @@ -333,40 +367,45 @@ class UTransPosition < FFI::Struct :context_limit, :int32_t, :start, :int32_t, :end, :int32_t - end enum :trans_direction, [:forward, :reverse] - attach_function :utrans_openIDs, "utrans_openIDs#{suffix}", [:pointer], :pointer - attach_function :utrans_openU, "utrans_openU#{suffix}", [:pointer, :int32_t, :trans_direction, :pointer, :int32_t, :pointer, :pointer], :pointer - attach_function :utrans_open, "utrans_open#{suffix}", [:string, :trans_direction, :pointer, :int32_t, :pointer, :pointer], :pointer - attach_function :utrans_close, "utrans_close#{suffix}", [:pointer], :void - attach_function :utrans_transUChars, "utrans_transUChars#{suffix}", [:pointer, :pointer, :pointer, :int32_t, :int32_t, :pointer, :pointer], :void + attach_function :utrans_openIDs, "utrans_openIDs#{suffix}", [:pointer], :pointer + attach_function :utrans_openU, "utrans_openU#{suffix}", + [:pointer, :int32_t, :trans_direction, :pointer, :int32_t, :pointer, :pointer], :pointer + attach_function :utrans_open, "utrans_open#{suffix}", + [:string, :trans_direction, :pointer, :int32_t, :pointer, :pointer], :pointer + attach_function :utrans_close, "utrans_close#{suffix}", [:pointer], :void + attach_function :utrans_transUChars, "utrans_transUChars#{suffix}", + [:pointer, :pointer, :pointer, :int32_t, :int32_t, :pointer, :pointer], :void # Normalization # # http://icu-project.org/apiref/icu4c/unorm_8h.html # - enum :normalization_mode, [ :none, 1, - :nfd, 2, - :nfkd, 3, - :nfc, 4, - :default, 4, - :nfkc, 5, - :fcd, 6 - ] + enum :normalization_mode, [:none, 1, + :nfd, 2, + :nfkd, 3, + :nfc, 4, + :default, 4, + :nfkc, 5, + :fcd, 6] - attach_function :unorm_normalize, "unorm_normalize#{suffix}", [:pointer, :int32_t, :normalization_mode, :int32_t, :pointer, :int32_t, :pointer], :int32_t + attach_function :unorm_normalize, "unorm_normalize#{suffix}", + [:pointer, :int32_t, :normalization_mode, :int32_t, :pointer, :int32_t, :pointer], :int32_t # http://icu-project.org/apiref/icu4c/unorm2_8h.html - if Gem::Version.new('4.4') <= Gem::Version.new(self.version) - enum :normalization2_mode, [ :compose, :decompose, :fcd, :compose_contiguous ] - attach_function :unorm2_getInstance, "unorm2_getInstance#{suffix}", [:pointer, :pointer, :normalization2_mode, :pointer], :pointer - attach_function :unorm2_normalize, "unorm2_normalize#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :int32_t, :pointer], :int32_t - attach_function :unorm2_isNormalized, "unorm2_isNormalized#{suffix}", [:pointer, :pointer, :int32_t, :pointer], :bool + if Gem::Version.new('4.4') <= Gem::Version.new(version) + enum :normalization2_mode, [:compose, :decompose, :fcd, :compose_contiguous] + attach_function :unorm2_getInstance, "unorm2_getInstance#{suffix}", + [:pointer, :pointer, :normalization2_mode, :pointer], :pointer + attach_function :unorm2_normalize, "unorm2_normalize#{suffix}", + [:pointer, :pointer, :int32_t, :pointer, :int32_t, :pointer], :int32_t + attach_function :unorm2_isNormalized, "unorm2_isNormalized#{suffix}", [:pointer, :pointer, :int32_t, :pointer], + :bool end # @@ -375,34 +414,35 @@ class UTransPosition < FFI::Struct # http://icu-project.org/apiref/icu4c/ubrk_8h.html # - enum :iterator_type, [ :character, :word, :line, :sentence, :title] - enum :word_break, [ :none, 0, - :none_limit, 100, - :number, 100, - :number_limit, 200, - :letter, 200, - :letter_limit, 300, - :kana, 300, - :kana_limit, 400, - :ideo, 400, - :ideo_limit, 400 - ] + enum :iterator_type, [:character, :word, :line, :sentence, :title] + enum :word_break, [:none, 0, + :none_limit, 100, + :number, 100, + :number_limit, 200, + :letter, 200, + :letter_limit, 300, + :kana, 300, + :kana_limit, 400, + :ideo, 400, + :ideo_limit, 400] attach_function :ubrk_countAvailable, "ubrk_countAvailable#{suffix}", [], :int32_t attach_function :ubrk_getAvailable, "ubrk_getAvailable#{suffix}", [:int32_t], :string - attach_function :ubrk_open, "ubrk_open#{suffix}", [:iterator_type, :string, :pointer, :int32_t, :pointer], :pointer - attach_function :ubrk_close, "ubrk_close#{suffix}", [:pointer], :void - attach_function :ubrk_setText, "ubrk_setText#{suffix}", [:pointer, :pointer, :int32_t, :pointer], :void + attach_function :ubrk_open, "ubrk_open#{suffix}", + [:iterator_type, :string, :pointer, :int32_t, :pointer], :pointer + attach_function :ubrk_close, "ubrk_close#{suffix}", [:pointer], :void + attach_function :ubrk_setText, "ubrk_setText#{suffix}", + [:pointer, :pointer, :int32_t, :pointer], :void attach_function :ubrk_current, "ubrk_current#{suffix}", [:pointer], :int32_t attach_function :ubrk_next, "ubrk_next#{suffix}", [:pointer], :int32_t attach_function :ubrk_previous, "ubrk_previous#{suffix}", [:pointer], :int32_t attach_function :ubrk_first, "ubrk_first#{suffix}", [:pointer], :int32_t attach_function :ubrk_last, "ubrk_last#{suffix}", [:pointer], :int32_t - attach_function :ubrk_preceding, "ubrk_preceding#{suffix}", [:pointer, :int32_t], :int32_t - attach_function :ubrk_following, "ubrk_following#{suffix}", [:pointer, :int32_t], :int32_t - attach_function :ubrk_isBoundary, "ubrk_isBoundary#{suffix}", [:pointer, :int32_t], :int32_t + attach_function :ubrk_preceding, "ubrk_preceding#{suffix}", [:pointer, :int32_t], :int32_t + attach_function :ubrk_following, "ubrk_following#{suffix}", [:pointer, :int32_t], :int32_t + attach_function :ubrk_isBoundary, "ubrk_isBoundary#{suffix}", [:pointer, :int32_t], :int32_t enum :number_format_style, [ :pattern_decimal, @@ -422,55 +462,67 @@ class UTransPosition < FFI::Struct :ignore ] enum :number_format_attribute, [ - :parse_int_only, :grouping_used, :decimal_always_show, :max_integer_digits, - :min_integer_digits, :integer_digits, :max_fraction_digits, :min_fraction_digits, - :fraction_digits, :multiplier, :grouping_size, :rounding_mode, - :rounding_increment, :format_width, :padding_position, :secondary_grouping_size, - :significant_digits_used, :min_significant_digits, :max_significant_digits, :lenient_parse + :parse_int_only, :grouping_used, :decimal_always_show, :max_integer_digits, :min_integer_digits, :integer_digits, + :max_fraction_digits, :min_fraction_digits, :fraction_digits, :multiplier, :grouping_size, + :rounding_mode, :rounding_increment, :format_width, :padding_position, :secondary_grouping_size, + :significant_digits_used, :min_significant_digits, :max_significant_digits, :lenient_parse ] - attach_function :unum_open, "unum_open#{suffix}", [:number_format_style, :pointer, :int32_t, :string, :pointer, :pointer ], :pointer + attach_function :unum_open, "unum_open#{suffix}", + [:number_format_style, :pointer, :int32_t, :string, :pointer, :pointer], :pointer attach_function :unum_close, "unum_close#{suffix}", [:pointer], :void - attach_function :unum_format_int32, "unum_format#{suffix}", [:pointer, :int32_t, :pointer, :int32_t, :pointer, :pointer], :int32_t - attach_function :unum_format_int64, "unum_formatInt64#{suffix}", [:pointer, :int64_t, :pointer, :int32_t, :pointer, :pointer], :int32_t - attach_function :unum_format_double, "unum_formatDouble#{suffix}", [:pointer, :double, :pointer, :int32_t, :pointer, :pointer], :int32_t - attach_optional_function :unum_format_decimal, "unum_formatDecimal#{suffix}", [:pointer, :string, :int32_t, :pointer, :int32_t, :pointer, :pointer], :int32_t - attach_function :unum_format_currency, "unum_formatDoubleCurrency#{suffix}", [:pointer, :double, :pointer, :pointer, :int32_t, :pointer, :pointer], :int32_t - attach_function :unum_set_attribute, "unum_setAttribute#{suffix}", [:pointer, :number_format_attribute, :int32_t], :void + attach_function :unum_format_int32, "unum_format#{suffix}", + [:pointer, :int32_t, :pointer, :int32_t, :pointer, :pointer], :int32_t + attach_function :unum_format_int64, "unum_formatInt64#{suffix}", + [:pointer, :int64_t, :pointer, :int32_t, :pointer, :pointer], :int32_t + attach_function :unum_format_double, "unum_formatDouble#{suffix}", + [:pointer, :double, :pointer, :int32_t, :pointer, :pointer], :int32_t + attach_optional_function :unum_format_decimal, "unum_formatDecimal#{suffix}", + [:pointer, :string, :int32_t, :pointer, :int32_t, :pointer, :pointer], :int32_t + attach_function :unum_format_currency, "unum_formatDoubleCurrency#{suffix}", + [:pointer, :double, :pointer, :pointer, :int32_t, :pointer, :pointer], :int32_t + attach_function :unum_set_attribute, "unum_setAttribute#{suffix}", [:pointer, :number_format_attribute, :int32_t], + :void # UResourceBundle attach_function :ures_open, "ures_open#{suffix}", [:string, :string, :pointer], :pointer attach_function :ures_close, "ures_close#{suffix}", [:pointer], :void # This function is marked "internal" but it's fully exported by the library ABI, so we can use it anyway. - attach_function :ures_getBykeyWithFallback, "ures_getByKeyWithFallback#{suffix}", [:pointer, :string, :pointer, :pointer], :pointer + attach_function :ures_getBykeyWithFallback, "ures_getByKeyWithFallback#{suffix}", + [:pointer, :string, :pointer, :pointer], :pointer attach_function :ures_getString, "ures_getString#{suffix}", [:pointer, :pointer, :pointer], :pointer def self.resource_bundle_name(type) - stem = "icudt" + version.read_array_of_char(4)[0].to_s + "l" + "-" + stem = "icudt#{version.read_array_of_char(4)[0]}l-" stem + type.to_s end # UNumberFormatter - attach_optional_function :unumf_openForSkeletonAndLocale, "unumf_openForSkeletonAndLocale#{suffix}", [:pointer, :int32_t, :string, :pointer], :pointer + attach_optional_function :unumf_openForSkeletonAndLocale, "unumf_openForSkeletonAndLocale#{suffix}", + [:pointer, :int32_t, :string, :pointer], :pointer attach_optional_function :unumf_close, "unumf_close#{suffix}", [:pointer], :void attach_optional_function :unumf_openResult, "unumf_openResult#{suffix}", [:pointer], :pointer attach_optional_function :unumf_closeResult, "unumf_closeResult#{suffix}", [:pointer], :void - attach_optional_function :unumf_formatDecimal, "unumf_formatDecimal#{suffix}", [:pointer, :string, :int32_t, :pointer, :pointer], :void - attach_optional_function :unumf_resultToString, "unumf_resultToString#{suffix}", [:pointer, :pointer, :int32_t, :pointer], :int32_t + attach_optional_function :unumf_formatDecimal, "unumf_formatDecimal#{suffix}", + [:pointer, :string, :int32_t, :pointer, :pointer], :void + attach_optional_function :unumf_resultToString, "unumf_resultToString#{suffix}", + [:pointer, :pointer, :int32_t, :pointer], :int32_t # UListFormatter enum :ulistfmt_type, [ :and, 0, :or, 1, - :units, 2, + :units, 2 ] enum :ulistfmt_width, [ :wide, 0, :short, 1, - :narrow, 2, + :narrow, 2 ] - attach_optional_function :ulistfmt_openForType, "ulistfmt_openForType#{suffix}", [:string, :ulistfmt_type, :ulistfmt_width, :pointer], :pointer + attach_optional_function :ulistfmt_openForType, "ulistfmt_openForType#{suffix}", + [:string, :ulistfmt_type, :ulistfmt_width, :pointer], :pointer attach_optional_function :ulistfmt_close, "ulistfmt_close#{suffix}", [:pointer], :void - attach_optional_function :ulistfmt_format, "ulistfmt_format#{suffix}", [:pointer, :pointer, :pointer, :int32_t, :pointer, :int32_t, :pointer], :int32_t + attach_optional_function :ulistfmt_format, "ulistfmt_format#{suffix}", + [:pointer, :pointer, :pointer, :int32_t, :pointer, :int32_t, :pointer], :int32_t # date enum :date_format_style, [ @@ -479,30 +531,49 @@ def self.resource_bundle_name(type) :full, 0, :long, 1, :medium, 2, - :short, 3, + :short, 3 ] enum :uloc_data_locale_type, [ :actual_locale, 0, - :valid_locale, 1, + :valid_locale, 1 ] - attach_function :udat_open, "udat_open#{suffix}", [:date_format_style, :date_format_style, :string, :pointer, :int32_t, :pointer, :int32_t, :pointer ], :pointer + attach_function :udat_open, "udat_open#{suffix}", + [ + :date_format_style, + :date_format_style, + :string, + :pointer, + :int32_t, + :pointer, + :int32_t, + :pointer + ], + :pointer attach_function :udat_close, "unum_close#{suffix}", [:pointer], :void - attach_function :udat_format, "udat_format#{suffix}", [:pointer, :double, :pointer, :int32_t, :pointer, :pointer], :int32_t - attach_function :udat_parse, "udat_parse#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :pointer], :double - attach_function :udat_toPattern, "udat_toPattern#{suffix}", [:pointer, :bool , :pointer, :int32_t , :pointer], :int32_t - attach_function :udat_applyPattern, "udat_applyPattern#{suffix}", [:pointer, :bool , :pointer, :int32_t ], :void + attach_function :udat_format, "udat_format#{suffix}", [:pointer, :double, :pointer, :int32_t, :pointer, :pointer], + :int32_t + attach_function :udat_parse, "udat_parse#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :pointer], :double + attach_function :udat_toPattern, "udat_toPattern#{suffix}", + [:pointer, :bool, :pointer, :int32_t, :pointer], :int32_t + attach_function :udat_applyPattern, "udat_applyPattern#{suffix}", [:pointer, :bool, :pointer, :int32_t], + :void # skeleton pattern attach_function :udatpg_open, "udatpg_open#{suffix}", [:string, :pointer], :pointer attach_function :udatpg_close, "udatpg_close#{suffix}", [:pointer], :void - attach_function :udatpg_getBestPattern, "udatpg_getBestPattern#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :int32_t, :pointer], :int32_t - attach_function :udatpg_getSkeleton, "udatpg_getSkeleton#{suffix}", [:pointer, :pointer, :int32_t, :pointer, :int32_t, :pointer], :int32_t + attach_function :udatpg_getBestPattern, "udatpg_getBestPattern#{suffix}", + [:pointer, :pointer, :int32_t, :pointer, :int32_t, :pointer], :int32_t + attach_function :udatpg_getSkeleton, "udatpg_getSkeleton#{suffix}", + [:pointer, :pointer, :int32_t, :pointer, :int32_t, :pointer], :int32_t # tz attach_function :ucal_setDefaultTimeZone, "ucal_setDefaultTimeZone#{suffix}", [:pointer, :pointer], :int32_t - attach_function :ucal_getDefaultTimeZone, "ucal_getDefaultTimeZone#{suffix}", [:pointer, :int32_t, :pointer], :int32_t + attach_function :ucal_getDefaultTimeZone, "ucal_getDefaultTimeZone#{suffix}", [:pointer, :int32_t, :pointer], + :int32_t # ULocaleDisplayNames - attach_function :uldn_openForContext, "uldn_openForContext#{suffix}", [:string, :pointer, :int32_t, :pointer], :pointer - attach_function :uldn_localeDisplayName, "uldn_localeDisplayName#{suffix}", [:pointer, :string, :pointer, :int32_t, :pointer], :int32_t + attach_function :uldn_openForContext, "uldn_openForContext#{suffix}", [:string, :pointer, :int32_t, :pointer], + :pointer + attach_function :uldn_localeDisplayName, "uldn_localeDisplayName#{suffix}", + [:pointer, :string, :pointer, :int32_t, :pointer], :int32_t attach_function :uldn_close, "uldn_close#{suffix}", [:pointer], :void - end # Lib -end # ICU + end +end diff --git a/lib/ffi-icu/lib/util.rb b/lib/ffi-icu/lib/util.rb index c6a91bf..2a8c328 100644 --- a/lib/ffi-icu/lib/util.rb +++ b/lib/ffi-icu/lib/util.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ICU module Lib module Util @@ -18,11 +20,11 @@ def self.read_string_buffer(length) begin result = FFI::MemoryPointer.new(:char, length) - Lib.check_error { |status| length = yield result, status } + Lib.check_error { |status| length = yield(result, status) } rescue BufferOverflowError attempts += 1 retry if attempts < 2 - raise BufferOverflowError, "needed: #{length}" + raise(BufferOverflowError, "needed: #{length}") end result.read_string(length) @@ -34,22 +36,20 @@ def self.read_uchar_buffer(length, &blk) end def self.read_uchar_buffer_as_ptr(length, &blk) - buf, _ = read_uchar_buffer_as_ptr_impl(length, &blk) + buf, = read_uchar_buffer_as_ptr_impl(length, &blk) buf end - private - def self.read_uchar_buffer_as_ptr_impl(length) attempts = 0 begin result = UCharPointer.new(length) - Lib.check_error { |status| length = yield result, status } + Lib.check_error { |status| length = yield(result, status) } rescue BufferOverflowError attempts += 1 retry if attempts < 2 - raise BufferOverflowError, "needed: #{length}" + raise(BufferOverflowError, "needed: #{length}") end [result, length] diff --git a/lib/ffi-icu/locale.rb b/lib/ffi-icu/locale.rb index 569a4d5..e2ce290 100644 --- a/lib/ffi-icu/locale.rb +++ b/lib/ffi-icu/locale.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ICU class Locale class << self @@ -43,16 +45,16 @@ def iso_languages attr_reader :id DISPLAY_CONTEXT = { - length_full: 512, # UDISPCTX_LENGTH_FULL = (UDISPCTX_TYPE_DISPLAY_LENGTH<<8) + 0 - length_short: 513 # UDISPCTX_LENGTH_SHORT = (UDISPCTX_TYPE_DISPLAY_LENGTH<<8) + 1 - } + length_full: 512, # UDISPCTX_LENGTH_FULL = (UDISPCTX_TYPE_DISPLAY_LENGTH<<8) + 0 + length_short: 513 # UDISPCTX_LENGTH_SHORT = (UDISPCTX_TYPE_DISPLAY_LENGTH<<8) + 1 + }.freeze def initialize(id) @id = id.to_s end def ==(other) - other.is_a?(self.class) && other.id == self.id + other.is_a?(self.class) && other.id == id end def base_name @@ -183,13 +185,13 @@ def script end end - def to_language_tag(strict = false) + def to_language_tag(strict = false) # rubocop:disable Style/OptionalBooleanParameter Lib::Util.read_string_buffer(64) do |buffer, status| Lib.uloc_toLanguageTag(@id, buffer, buffer.size, strict ? 1 : 0, status) end end - alias_method :to_s, :id + alias to_s id def variant Lib::Util.read_string_buffer(64) do |buffer, status| @@ -238,9 +240,11 @@ def with_minimized_subtags def with_locale_display_name(locale, contexts) pointer = FFI::MemoryPointer.new(:int, contexts.length).write_array_of_int(contexts) - locale_display_names = ICU::Lib.check_error { |status| ICU::Lib.uldn_openForContext(locale, pointer, contexts.length, status) } + locale_display_names = ICU::Lib.check_error do |status| + ICU::Lib.uldn_openForContext(locale, pointer, contexts.length, status) + end - yield locale_display_names + yield(locale_display_names) ensure Lib.uldn_close(locale_display_names) if locale_display_names end diff --git a/lib/ffi-icu/normalization.rb b/lib/ffi-icu/normalization.rb index beba68d..7fbe5ae 100644 --- a/lib/ffi-icu/normalization.rb +++ b/lib/ffi-icu/normalization.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + module ICU module Normalization - def self.normalize(input, mode = :default) input_length = input.jlength needed_length = out_length = options = 0 @@ -14,9 +15,9 @@ def self.normalize(input, mode = :default) needed_length = Lib.unorm_normalize(in_ptr, input_length, mode, options, out_ptr, out_length, error) end rescue BufferOverflowError - raise BufferOverflowError, "needed: #{needed_length}" if retried + raise(BufferOverflowError, "needed: #{needed_length}") if retried - out_ptr = out_ptr.resized_to needed_length + out_ptr = out_ptr.resized_to(needed_length) out_length = needed_length + 1 retried = true @@ -25,6 +26,5 @@ def self.normalize(input, mode = :default) out_ptr.string end - - end # Normalization -end # ICU + end +end diff --git a/lib/ffi-icu/normalizer.rb b/lib/ffi-icu/normalizer.rb index f90060e..6c4cf34 100644 --- a/lib/ffi-icu/normalizer.rb +++ b/lib/ffi-icu/normalizer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ICU class Normalizer # support for newer ICU normalization API @@ -20,10 +22,10 @@ def normalize(input) needed_length = Lib.unorm2_normalize(@instance, in_ptr, input_length, out_ptr, capacity, error) end rescue BufferOverflowError - raise BufferOverflowError, "needed: #{needed_length}" if retried + raise(BufferOverflowError, "needed: #{needed_length}") if retried capacity = needed_length - out_ptr = out_ptr.resized_to needed_length + out_ptr = out_ptr.resized_to(needed_length) retried = true retry @@ -32,16 +34,15 @@ def normalize(input) out_ptr.string end - def is_normailzed?(input) + def is_normailzed?(input) # rubocop:disable Naming/PredicateName input_length = input.jlength in_ptr = UCharPointer.from_string(input) Lib.check_error do |error| - result = Lib.unorm2_isNormalized(@instance, in_ptr, input_length, error) + Lib.unorm2_isNormalized(@instance, in_ptr, input_length, error) end result end - - end # Normalizer -end # ICU + end +end diff --git a/lib/ffi-icu/number_formatting.rb b/lib/ffi-icu/number_formatting.rb index 5614dd0..98395bf 100644 --- a/lib/ffi-icu/number_formatting.rb +++ b/lib/ffi-icu/number_formatting.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'bigdecimal' module ICU module NumberFormatting @default_options = {} - + def self.create(locale, type = :decimal, options = {}) case type when :currency @@ -17,7 +19,7 @@ def self.clear_default_options @default_options.clear end - def self.set_default_options(options) + def self.set_default_options(options) # rubocop:disable Naming/AccessorMethodName @default_options.merge!(options) end @@ -38,8 +40,7 @@ def self.spell(locale, number, options = {}) end class BaseFormatter - - def set_attributes(options) + def set_attributes(options) # rubocop:disable Naming/AccessorMethodName options.each { |key, value| Lib.unum_set_attribute(@f, key, value) } self end @@ -47,13 +48,17 @@ def set_attributes(options) private def make_formatter(type, locale) - ptr = Lib.check_error { | error| Lib.unum_open(type, FFI::MemoryPointer.new(4), 0, locale, FFI::MemoryPointer.new(4), error) } + ptr = Lib.check_error do |error| + Lib.unum_open(type, FFI::MemoryPointer.new(4), 0, locale, FFI::MemoryPointer.new(4), error) + end FFI::AutoPointer.new(ptr, Lib.method(:unum_close)) end end class NumberFormatter < BaseFormatter def initialize(locale, type = :decimal) + super() + @f = make_formatter(type, locale) end @@ -75,41 +80,48 @@ def format(number) rescue RangeError # Fall back to stringifying in Ruby and passing that to ICU unless defined? Lib.unum_format_decimal - raise RangeError,"Number #{number} is too big to fit in int64_t and your "\ - "ICU version is too old to have unum_format_decimal" + raise(RangeError, "Number #{number} is too big to fit in int64_t and your " \ + 'ICU version is too old to have unum_format_decimal') end string_version = number.to_s - needed_length = Lib.unum_format_decimal(@f, string_version, string_version.bytesize, out_ptr, needed_length, nil, error) + needed_length = Lib.unum_format_decimal(@f, string_version, string_version.bytesize, out_ptr, + needed_length, nil, error) end when BigDecimal string_version = number.to_s('F') - if Lib.respond_to? :unum_format_decimal - needed_length = Lib.unum_format_decimal(@f, string_version, string_version.bytesize, out_ptr, needed_length, nil, error) - else - needed_length = Lib.unum_format_double(@f, number.to_f, out_ptr, needed_length, nil, error) - end + needed_length = if Lib.respond_to?(:unum_format_decimal) + Lib.unum_format_decimal(@f, string_version, string_version.bytesize, out_ptr, + needed_length, nil, error) + else + Lib.unum_format_double(@f, number.to_f, out_ptr, needed_length, nil, error) + end end end - out_ptr.string needed_length + out_ptr.string(needed_length) rescue BufferOverflowError - raise BufferOverflowError, "needed: #{needed_length}" if retried - out_ptr = out_ptr.resized_to needed_length + raise(BufferOverflowError, "needed: #{needed_length}") if retried + + out_ptr = out_ptr.resized_to(needed_length) retried = true retry end end - end # NumberFormatter + end class CurrencyFormatter < BaseFormatter def initialize(locale, style = :default) - if %w(iso plural).include?((style || '').to_s) + super() + + if ['iso', 'plural'].include?((style || '').to_s) if Lib.version.to_a.first >= 53 - style = "currency_#{style}".to_sym + style = :"currency_#{style}" else - fail "Your version of ICU (#{Lib.version.to_a.join('.')}) does not support #{style} currency formatting (supported only in version >= 53)" + raise("Your version of ICU (#{Lib.version.to_a.join('.')}) does not support " \ + "#{style} currency formatting (supported only in version >= 53)") end elsif style && style.to_sym != :default - fail "The ffi-icu ruby gem does not support :#{default} currency formatting (only :default, :iso, and :plural)" + raise('The ffi-icu ruby gem does not support: ' \ + "#{default} currency formatting (only :default, :iso, and :plural)") else style = :currency end @@ -124,16 +136,18 @@ def format(number, currency) begin Lib.check_error do |error| - needed_length = Lib.unum_format_currency(@f, number, UCharPointer.from_string(currency, 4), out_ptr, needed_length, nil, error) + needed_length = Lib.unum_format_currency(@f, number, UCharPointer.from_string(currency, 4), out_ptr, + needed_length, nil, error) end out_ptr.string rescue BufferOverflowError - raise BufferOverflowError, "needed: #{needed_length}" if retried - out_ptr = out_ptr.resized_to needed_length + raise(BufferOverflowError, "needed: #{needed_length}") if retried + + out_ptr = out_ptr.resized_to(needed_length) retried = true retry end end - end # CurrencyFormatter - end # Formatting -end # ICU + end + end +end diff --git a/lib/ffi-icu/time_formatting.rb b/lib/ffi-icu/time_formatting.rb index c7a4f1e..eb68224 100644 --- a/lib/ffi-icu/time_formatting.rb +++ b/lib/ffi-icu/time_formatting.rb @@ -1,53 +1,65 @@ +# frozen_string_literal: true + require 'date' module ICU module TimeFormatting TZ_MAP = { - :generic_location => 'VVVV',# The generic location format. - # Where that is unavailable, falls back to the long localized GMT format ("OOOO"; - # Note: Fallback is only necessary with a GMT-style Time Zone ID, like Etc/GMT-830.), - # This is especially useful when presenting possible timezone choices for user selection, - # since the naming is more uniform than the "v" format. - # such as "United States Time (New York)", "Italy Time" - :generic_long => 'vvvv', # The long generic non-location format. - # Where that is unavailable, falls back to generic location format ("VVVV")., such as "Eastern Time". - :generic_short => 'v', # The short generic non-location format. - # Where that is unavailable, falls back to the generic location format ("VVVV"), - # then the short localized GMT format as the final fallback., such as "ET". - :specific_long => 'zzzz', # The long specific non-location format. - # Where that is unavailable, falls back to the long localized GMT format ("OOOO"). - :specific_short => 'z', # The short specific non-location format. - # Where that is unavailable, falls back to the short localized GMT format ("O"). - :basic => 'Z', # The ISO8601 basic format with hours, minutes and optional seconds fields. - # The format is equivalent to RFC 822 zone format (when optional seconds field is absent). - # This is equivalent to the "xxxx" specifier. - :localized_long => 'ZZZZ', # The long localized GMT format. This is equivalent to the "OOOO" specifier, such as GMT-8:00 - :extended => 'ZZZZZ', # The ISO8601 extended format with hours, minutes and optional seconds fields. - # The ISO8601 UTC indicator "Z" is used when local time offset is 0. - # This is equivalent to the "XXXXX" specifier, such as -08:00 -07:52:58 - :localized_short => 'O', # The short localized GMT format, such as GMT-8 - :localized_longO => 'OOOO', # The long localized GMT format, such as GMT-08:00 - :tz_id_short => 'V', # The short time zone ID. Where that is unavailable, - # the special short time zone ID unk (Unknown Zone) is used. - # Note: This specifier was originally used for a variant of the short specific non-location format, - # but it was deprecated in the later version of this specification. In CLDR 23, the definition - # of the specifier was changed to designate a short time zone ID, such as uslax - :tz_id_long => 'VV', # The long time zone ID, such as America/Los_Angeles - :city_location => 'VVV', # The exemplar city (location) for the time zone. Where that is unavailable, - # the localized exemplar city name for the special zone Etc/Unknown is used as the fallback - # (for example, "Unknown City"), such as Los Angeles + # The generic location format. + # Where that is unavailable, falls back to the long localized GMT format ("OOOO"; + # Note: Fallback is only necessary with a GMT-style Time Zone ID, like Etc/GMT-830.), + # This is especially useful when presenting possible timezone choices for user selection, + # since the naming is more uniform than the "v" format. + # such as "United States Time (New York)", "Italy Time" + generic_location: 'VVVV', + # The long generic non-location format. + # Where that is unavailable, falls back to generic location format ("VVVV")., such as "Eastern Time". + generic_long: 'vvvv', + # The short generic non-location format. + # Where that is unavailable, falls back to the generic location format ("VVVV"), + # then the short localized GMT format as the final fallback., such as "ET". + generic_short: 'v', + # The long specific non-location format. + # Where that is unavailable, falls back to the long localized GMT format ("OOOO"). + specific_long: 'zzzz', + # The short specific non-location format. + # Where that is unavailable, falls back to the short localized GMT format ("O"). + specific_short: 'z', + # The ISO8601 basic format with hours, minutes and optional seconds fields. + # The format is equivalent to RFC 822 zone format (when optional seconds field is absent). + # This is equivalent to the "xxxx" specifier. + basic: 'Z', + # The long localized GMT format. This is equivalent to the "OOOO" specifier, such as GMT-8:00 + localized_long: 'ZZZZ', + # The ISO8601 extended format with hours, minutes and optional seconds fields. + # The ISO8601 UTC indicator "Z" is used when local time offset is 0. + # This is equivalent to the "XXXXX" specifier, such as -08:00 -07:52:58 + extended: 'ZZZZZ', + localized_short: 'O', # The short localized GMT format, such as GMT-8 + localized_longO: 'OOOO', # The long localized GMT format, such as GMT-08:00 + # The short time zone ID. Where that is unavailable, + # the special short time zone ID unk (Unknown Zone) is used. + # Note: This specifier was originally used for a variant of the short specific non-location format, + # but it was deprecated in the later version of this specification. In CLDR 23, the definition + # of the specifier was changed to designate a short time zone ID, such as uslax + tz_id_short: 'V', + tz_id_long: 'VV', # The long time zone ID, such as America/Los_Angeles + # The exemplar city (location) for the time zone. Where that is unavailable, + # the localized exemplar city name for the special zone Etc/Unknown is used as the fallback + # (for example, "Unknown City"), such as Los Angeles # see: http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns - } + city_location: 'VVV' + }.freeze HOUR_CYCLE_SYMS = { 'h11' => 'K', 'h12' => 'h', 'h23' => 'H', 'h24' => 'k', - :locale => 'j', - } + :locale => 'j' + }.freeze @default_options = {} - + def self.create(options = {}) DateTimeFormatter.new(@default_options.merge(options)) end @@ -56,17 +68,16 @@ def self.clear_default_options @default_options.clear end - def self.set_default_options(options) + def self.set_default_options(options) # rubocop:disable Naming/AccessorMethodName @default_options.merge!(options) end - def self.format(dt, options = {}) - create(@default_options.merge(options)).format(dt) + def self.format(datetime, options = {}) + create(@default_options.merge(options)).format(datetime) end class BaseFormatter - - def set_attributes(options) + def set_attributes(options) # rubocop:disable Naming/AccessorMethodName options.each { |key, value| Lib.unum_set_attribute(@f, key, value) } self end @@ -83,11 +94,11 @@ def make_formatter(time_style, date_style, locale, time_zone_str, skeleton) time_zone = UCharPointer.from_string(time_zone_str) tz_len = time_zone_str.size else - Lib.check_error { | error| - i_len = 150 + Lib.check_error do |error| + i_len = 150 time_zone = UCharPointer.new(i_len) - tz_len = Lib.ucal_getDefaultTimeZone(time_zone, i_len, error) - } + tz_len = Lib.ucal_getDefaultTimeZone(time_zone, i_len, error) + end end if skeleton @@ -97,13 +108,17 @@ def make_formatter(time_style, date_style, locale, time_zone_str, skeleton) pattern_len, pattern_ptr = skeleton_format(skeleton, locale) end - ptr = Lib.check_error { | error| Lib.udat_open(time_style, date_style, locale, time_zone, tz_len, pattern_ptr, pattern_len, error) } + ptr = Lib.check_error do |error| + Lib.udat_open(time_style, date_style, locale, time_zone, tz_len, pattern_ptr, pattern_len, error) + end FFI::AutoPointer.new(ptr, Lib.method(:udat_close)) end end class DateTimeFormatter < BaseFormatter - def initialize(options={}) + def initialize(options = {}) + super() + time_style = options[:time] || :short date_style = options[:date] || :short @locale = options[:locale] || 'C' @@ -113,31 +128,29 @@ def initialize(options={}) @hour_cycle = options[:hour_cycle] if @hour_cycle && !HOUR_CYCLE_SYMS.keys.include?(@hour_cycle) - raise ICU::Error.new("Unknown hour cycle #{@hour_cycle}") + raise(ICU::Error, "Unknown hour cycle #{@hour_cycle}") end @f = make_formatter(time_style, date_style, @locale, time_zone, skeleton) if tz_style f0 = date_format(true) - f1 = update_tz_format(f0, tz_style) - if f1 != f0 - set_date_format(true, f1) - end + f1 = update_tz_format(f0, tz_style) + set_date_format(true, f1) if f1 != f0 end replace_hour_symbol! end def parse(str) - str_u = UCharPointer.from_string(str) - str_l = str.size - Lib.check_error do |error| - ret = Lib.udat_parse(@f, str_u, str_l, nil, error) - Time.at(ret / 1000.0) - end + str_u = UCharPointer.from_string(str) + str_l = str.size + Lib.check_error do |error| + ret = Lib.udat_parse(@f, str_u, str_l, nil, error) + Time.at(ret / 1000.0) + end end - def format(dt) + def format(datetime) needed_length = 0 out_ptr = UCharPointer.new(needed_length) @@ -145,18 +158,23 @@ def format(dt) begin Lib.check_error do |error| - case dt + case datetime when Date - needed_length = Lib.udat_format(@f, Time.mktime( dt.year, dt.month, dt.day, 0, 0, 0, 0 ).to_f * 1000.0, out_ptr, needed_length, nil, error) + needed_length = Lib.udat_format( + @f, + Time.mktime(datetime.year, datetime.month, datetime.day, 0, 0, 0, 0).to_f * 1000.0, + out_ptr, needed_length, nil, error + ) when Time - needed_length = Lib.udat_format(@f, dt.to_f * 1000.0, out_ptr, needed_length, nil, error) + needed_length = Lib.udat_format(@f, datetime.to_f * 1000.0, out_ptr, needed_length, nil, error) end end out_ptr.string rescue BufferOverflowError - raise BufferOverflowError, "needed: #{needed_length}" if retried - out_ptr = out_ptr.resized_to needed_length + raise(BufferOverflowError, "needed: #{needed_length}") if retried + + out_ptr = out_ptr.resized_to(needed_length) retried = true retry end @@ -164,19 +182,23 @@ def format(dt) # time-zone formating def update_tz_format(format, tz_style) - return format if format !~ /(.*?)(\s*(?:[zZOVV]+\s*))(.*?)/ - pre, tz, suff = $1, $2, $3 + return format if format !~ /(.*?)(\s*(?:[zZOV]+\s*))(.*?)/ + + pre = ::Regexp.last_match(1) + tz = ::Regexp.last_match(2) + suff = ::Regexp.last_match(3) if tz_style == :none - tz = ((tz =~ /\s/) && !pre.empty? && !suff.empty?) ? ' ' : '' + tz = (tz =~ /\s/) && !pre.empty? && !suff.empty? ? ' ' : '' else repl = TZ_MAP[tz_style] - raise 'no such tz_style' unless repl - tz.gsub!(/^(\s*)(.*?)(\s*)$/, '\1'+repl+'\3') + raise('no such tz_style') unless repl + + tz.gsub!(/^(\s*)(.*?)(\s*)$/, "\\1#{repl}\\3") end pre + tz + suff end - def date_format(localized=true) + def date_format(localized = true) # rubocop:disable Style/OptionalBooleanParameter needed_length = 0 out_ptr = UCharPointer.new(needed_length) @@ -189,8 +211,9 @@ def date_format(localized=true) out_ptr.string rescue BufferOverflowError - raise BufferOverflowError, "needed: #{needed_length}" if retried - out_ptr = out_ptr.resized_to needed_length + raise(BufferOverflowError, "needed: #{needed_length}") if retried + + out_ptr = out_ptr.resized_to(needed_length) retried = true retry end @@ -205,26 +228,28 @@ def set_date_format(localized, pattern_str) end def skeleton_format(skeleton_pattern_str, locale) - skeleton_pattern_ptr = UCharPointer.from_string(skeleton_pattern_str) - skeleton_pattern_len = skeleton_pattern_str.size + skeleton_pattern_ptr = UCharPointer.from_string(skeleton_pattern_str) + skeleton_pattern_len = skeleton_pattern_str.size - needed_length = 0 - pattern_ptr = UCharPointer.new(needed_length) + needed_length = 0 + pattern_ptr = UCharPointer.new(needed_length) - udatpg_ptr = Lib.check_error { |error| Lib.udatpg_open(locale, error) } - generator = FFI::AutoPointer.new(udatpg_ptr, Lib.method(:udatpg_close)) + udatpg_ptr = Lib.check_error { |error| Lib.udatpg_open(locale, error) } + generator = FFI::AutoPointer.new(udatpg_ptr, Lib.method(:udatpg_close)) - retried = false + retried = false begin Lib.check_error do |error| - needed_length = Lib.udatpg_getBestPattern(generator, skeleton_pattern_ptr, skeleton_pattern_len, pattern_ptr, needed_length, error) + needed_length = Lib.udatpg_getBestPattern(generator, skeleton_pattern_ptr, skeleton_pattern_len, + pattern_ptr, needed_length, error) end - return needed_length, pattern_ptr + [needed_length, pattern_ptr] rescue BufferOverflowError - raise BufferOverflowError, "needed: #{needed_length}" if retried - pattern_ptr = pattern_ptr.resized_to needed_length + raise(BufferOverflowError, "needed: #{needed_length}") if retried + + pattern_ptr = pattern_ptr.resized_to(needed_length) retried = true retry end @@ -255,9 +280,7 @@ def replace_hour_symbol! # Either ensure the skeleton has, or does not have, am/pm, as appropriate if ['h11', 'h12'].include?(@hour_cycle) # Only actually append 'am/pm' if there is an hour in the format string - if skeleton_str =~ /[hHkKjJ]/ && !skeleton_str.include?('a') - skeleton_str << 'a' - end + skeleton_str << 'a' if skeleton_str =~ /[hHkKjJ]/ && !skeleton_str.include?('a') else skeleton_str.gsub!('a', '') end @@ -275,7 +298,7 @@ def replace_hour_symbol! resolved_hour_cycle = @hour_cycle == :locale ? Locale.new(@locale).keyword('hours') : @hour_cycle if HOUR_CYCLE_SYMS.keys.include?(resolved_hour_cycle) - new_pattern_str.gsub!(/[hHkK](?=(?:[^\']|\'[^\']*\')*$)/, HOUR_CYCLE_SYMS[resolved_hour_cycle]) + new_pattern_str.gsub!(/[hHkK](?=(?:[^\']|'[^\']*')*$)/, HOUR_CYCLE_SYMS[resolved_hour_cycle]) end # Finally, set the new pattern onto the date time formatter @@ -325,10 +348,10 @@ def set_date_format_impl(localized, pattern_str) pattern = UCharPointer.from_string(pattern_str) pattern_len = pattern_str.size - Lib.check_error do |error| - needed_length = Lib.udat_applyPattern(@f, localized, pattern, pattern_len) + Lib.check_error do |_error| + Lib.udat_applyPattern(@f, localized, pattern, pattern_len) end end - end # DateTimeFormatter - end # Formatting -end # ICU + end + end +end diff --git a/lib/ffi-icu/transliteration.rb b/lib/ffi-icu/transliteration.rb index 85386da..1ee69e2 100644 --- a/lib/ffi-icu/transliteration.rb +++ b/lib/ffi-icu/transliteration.rb @@ -1,12 +1,13 @@ +# frozen_string_literal: true + module ICU module Transliteration - class << self def transliterate(translit_id, str, rules = nil) - t = Transliterator.new translit_id, rules - t.transliterate str + t = Transliterator.new(translit_id, rules) + t.transliterate(str) end - alias_method :translit, :transliterate + alias translit transliterate def available_ids enum_ptr = Lib.check_error do |error| @@ -21,7 +22,6 @@ def available_ids end class Transliterator - def initialize(id, rules = nil, direction = :forward) rules_length = 0 @@ -33,22 +33,23 @@ def initialize(id, rules = nil, direction = :forward) parse_error = Lib::UParseError.new begin Lib.check_error do |status| - ptr = Lib.utrans_openU(UCharPointer.from_string(id), id.jlength, direction, rules, rules_length, @parse_error, status) + ptr = Lib.utrans_openU(UCharPointer.from_string(id), id.jlength, direction, rules, rules_length, + @parse_error, status) @tr = FFI::AutoPointer.new(ptr, Lib.method(:utrans_close)) end - rescue ICU::Error => ex - raise ex, "#{ex.message} (#{parse_error})" + rescue ICU::Error => e + raise(e, "#{e.message} (#{parse_error})") end end def transliterate(from) # this is a bit unpleasant - unicode_size = from.unpack("U*").size + unicode_size = from.unpack('U*').size capacity = unicode_size + 1 buf = UCharPointer.from_string(from, capacity) - limit = FFI::MemoryPointer.new :int32 - text_length = FFI::MemoryPointer.new :int32 + limit = FFI::MemoryPointer.new(:int32) + text_length = FFI::MemoryPointer.new(:int32) retried = false @@ -63,9 +64,9 @@ def transliterate(from) end rescue BufferOverflowError new_size = text_length.get_int32(0) - $stderr.puts "BufferOverflowError, needs: #{new_size}" if $DEBUG + warn("BufferOverflowError, needs: #{new_size}") if $DEBUG - raise BufferOverflowError, "needed #{new_size}" if retried + raise(BufferOverflowError, "needed #{new_size}") if retried capacity = new_size + 1 @@ -78,9 +79,8 @@ def transliterate(from) retry end - buf.string text_length.get_int32(0) + buf.string(text_length.get_int32(0)) end - - end # Transliterator - end # Translit -end # ICU + end + end +end diff --git a/lib/ffi-icu/uchar.rb b/lib/ffi-icu/uchar.rb index ad8c5c5..fd80976 100644 --- a/lib/ffi-icu/uchar.rb +++ b/lib/ffi-icu/uchar.rb @@ -1,36 +1,35 @@ +# frozen_string_literal: true + module ICU class UCharPointer < FFI::MemoryPointer - UCHAR_TYPE = :uint16 # not sure how platform-dependent this is.. TYPE_SIZE = FFI.type_size(UCHAR_TYPE) def self.from_string(str, capacity = nil) - str = str.encode("UTF-8") if str.respond_to? :encode - chars = str.unpack("U*") + str = str.encode('UTF-8') if str.respond_to?(:encode) + chars = str.unpack('U*') if capacity - if capacity < chars.size - raise ArgumentError, "capacity is too small for string of #{chars.size} UChars" - end + raise(ArgumentError, "capacity is too small for string of #{chars.size} UChars") if capacity < chars.size - ptr = new capacity + ptr = new(capacity) else - ptr = new chars.size + ptr = new(chars.size) end - ptr.write_array_of_uint16 chars + ptr.write_array_of_uint16(chars) ptr end def initialize(size) - super UCHAR_TYPE, size + super(UCHAR_TYPE, size) end def resized_to(new_size) - raise "new_size must be larger than current size" if new_size < size + raise('new_size must be larger than current size') if new_size < size - resized = self.class.new new_size + resized = self.class.new(new_size) resized.put_bytes(0, get_bytes(0, size)) resized @@ -40,13 +39,11 @@ def string(length = nil) length ||= size / TYPE_SIZE wstring = read_array_of_uint16(length) - wstring.pack("U*") + wstring.pack('U*') end def length_in_uchars size / type_size end - - - end # UCharPointer -end # ICU + end +end diff --git a/lib/ffi-icu/version.rb b/lib/ffi-icu/version.rb index e5587de..4c57d93 100644 --- a/lib/ffi-icu/version.rb +++ b/lib/ffi-icu/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ICU - VERSION = "0.5.3" + VERSION = '0.5.3' end diff --git a/spec/break_iterator_spec.rb b/spec/break_iterator_spec.rb index 8faf13f..7396ac8 100644 --- a/spec/break_iterator_spec.rb +++ b/spec/break_iterator_spec.rb @@ -1,77 +1,77 @@ -# encoding: utf-8 - module ICU describe BreakIterator do - - it "should return available locales" do - locales = ICU::BreakIterator.available_locales - expect(locales).to be_an(Array) - expect(locales).to_not be_empty - expect(locales).to include("en_US") + it 'returns available locales' do + locales = described_class.available_locales + expect(locales).to(be_an(Array)) + expect(locales).not_to(be_empty) + expect(locales).to(include('en_US')) end - it "finds all word boundaries in an English string" do - iterator = BreakIterator.new :word, "en_US" - iterator.text = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - expect(iterator.to_a).to eq( - [0, 5, 6, 11, 12, 17, 18, 21, 22, 26, 27, 28, 39, 40, 51, 52, 56, 57, 58, 61, 62, 64, 65, 72, 73, 79, 80, 90, 91, 93, 94, 100, 101, 103, 104, 110, 111, 116, 117, 123, 124] - ) + it 'finds all word boundaries in an English string' do + iterator = described_class.new(:word, 'en_US') + iterator.text = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, ' \ + 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + expect(iterator.to_a).to(eq( + [0, 5, 6, 11, 12, 17, 18, 21, 22, 26, 27, 28, 39, 40, 51, 52, + 56, 57, 58, 61, 62, 64, 65, 72, 73, 79, 80, 90, 91, 93, 94, 100, + 101, 103, 104, 110, 111, 116, 117, 123, 124] + )) end - it "returns each substring" do - iterator = BreakIterator.new :word, "en_US" - iterator.text = "Lorem ipsum dolor sit amet." + it 'returns each substring' do + iterator = described_class.new(:word, 'en_US') + iterator.text = 'Lorem ipsum dolor sit amet.' - expect(iterator.substrings).to eq(["Lorem", " ", "ipsum", " ", "dolor", " ", "sit", " ", "amet", "."]) + expect(iterator.substrings).to(eq(['Lorem', ' ', 'ipsum', ' ', 'dolor', ' ', 'sit', ' ', 'amet', '.'])) end - it "returns the substrings of a non-ASCII string" do - iterator = BreakIterator.new :word, "th_TH" - iterator.text = "รู้อะไรไม่สู้รู้วิชา รู้รักษาตัวรอดเป็นยอดดี" + it 'returns the substrings of a non-ASCII string' do + iterator = described_class.new(:word, 'th_TH') + iterator.text = 'รู้อะไรไม่สู้รู้วิชา รู้รักษาตัวรอดเป็นยอดดี' - expect(iterator.substrings).to eq( - ["รู้", "อะไร", "ไม่สู้", "รู้", "วิชา", " ", "รู้", "รักษา", "ตัว", "รอด", "เป็น", "ยอดดี"] - ) + expect(iterator.substrings).to(eq( + ['รู้', 'อะไร', 'ไม่สู้', 'รู้', 'วิชา', ' ', 'รู้', 'รักษา', 'ตัว', 'รอด', + 'เป็น', 'ยอดดี'] + )) end - it "finds all word boundaries in a non-ASCII string" do - iterator = BreakIterator.new :word, "th_TH" - iterator.text = "การทดลอง" - expect(iterator.to_a).to eq([0, 3, 8]) + it 'finds all word boundaries in a non-ASCII string' do + iterator = described_class.new(:word, 'th_TH') + iterator.text = 'การทดลอง' + expect(iterator.to_a).to(eq([0, 3, 8])) end - it "finds all sentence boundaries in an English string" do - iterator = BreakIterator.new :sentence, "en_US" - iterator.text = "This is a sentence. This is another sentence, with a comma in it." - expect(iterator.to_a).to eq([0, 20, 65]) + it 'finds all sentence boundaries in an English string' do + iterator = described_class.new(:sentence, 'en_US') + iterator.text = 'This is a sentence. This is another sentence, with a comma in it.' + expect(iterator.to_a).to(eq([0, 20, 65])) end - it "can navigate back and forward" do - iterator = BreakIterator.new :word, "en_US" - iterator.text = "Lorem ipsum dolor sit amet." + it 'can navigate back and forward' do + iterator = described_class.new(:word, 'en_US') + iterator.text = 'Lorem ipsum dolor sit amet.' - expect(iterator.first).to eq(0) + expect(iterator.first).to(eq(0)) iterator.next - expect(iterator.current).to eq(5) - expect(iterator.last).to eq(27) + expect(iterator.current).to(eq(5)) + expect(iterator.last).to(eq(27)) end - it "fetches info about given offset" do - iterator = BreakIterator.new :word, "en_US" - iterator.text = "Lorem ipsum dolor sit amet." + it 'fetches info about given offset' do + iterator = described_class.new(:word, 'en_US') + iterator.text = 'Lorem ipsum dolor sit amet.' - expect(iterator.following(3)).to eq(5) - expect(iterator.preceding(6)).to eq(5) + expect(iterator.following(3)).to(eq(5)) + expect(iterator.preceding(6)).to(eq(5)) - expect(iterator).to be_boundary(5) - expect(iterator).to_not be_boundary(10) + expect(iterator).to(be_boundary(5)) + expect(iterator).not_to(be_boundary(10)) end - it "returns an Enumerator if no block was given" do - iterator = BreakIterator.new :word, "nb" + it 'returns an Enumerator if no block was given' do + iterator = described_class.new(:word, 'nb') - expect(iterator.each).to be_kind_of(Enumerator) + expect(iterator.each).to(be_a(Enumerator)) end - - end # BreakIterator -end # ICU + end +end diff --git a/spec/chardet_spec.rb b/spec/chardet_spec.rb index 4043de0..896256d 100644 --- a/spec/chardet_spec.rb +++ b/spec/chardet_spec.rb @@ -1,42 +1,39 @@ -# encoding: UTF-8 - describe ICU::CharDet::Detector do + let(:detector) { described_class.new } - let(:detector) { ICU::CharDet::Detector.new } - - it "should recognize UTF-8" do - m = detector.detect("æåø") - expect(m.name).to eq("UTF-8") - expect(m.language).to be_a(String) + it 'recognizes UTF-8' do + m = detector.detect('æåø') + expect(m.name).to(eq('UTF-8')) + expect(m.language).to(be_a(String)) end - it "has a list of detectable charsets" do + it 'has a list of detectable charsets' do cs = detector.detectable_charsets - expect(cs).to be_an(Array) - expect(cs).to_not be_empty + expect(cs).to(be_an(Array)) + expect(cs).not_to(be_empty) - expect(cs.first).to be_a(String) + expect(cs.first).to(be_a(String)) end - it "should disable / enable the input filter" do - expect(detector.input_filter_enabled?).to be_falsey + it 'disables / enable the input filter' do + expect(detector).not_to(be_input_filter_enabled) detector.input_filter_enabled = true - expect(detector.input_filter_enabled?).to be_truthy + expect(detector).to(be_input_filter_enabled) end - it "should should set declared encoding" do - detector.declared_encoding = "UTF-8" + it 'shoulds set declared encoding' do + detector.declared_encoding = 'UTF-8' end - it "should detect several matching encodings" do - expect(detector.detect_all("foo bar")).to be_an(Array) + it 'detects several matching encodings' do + expect(detector.detect_all('foo bar')).to(be_an(Array)) end - it "should support null bytes" do + it 'supports null bytes' do # Create a utf-16 string and then force it to binary (ascii) to mimic data from net/http - string = "foo".encode("UTF-16").force_encoding("binary") + string = 'foo'.encode('UTF-16').force_encoding('binary') m = detector.detect(string) - expect(m.name).to eq("UTF-16BE") - expect(m.language).to be_a(String) + expect(m.name).to(eq('UTF-16BE')) + expect(m.language).to(be_a(String)) end end diff --git a/spec/collation_spec.rb b/spec/collation_spec.rb index 6f24fdf..3ca8597 100644 --- a/spec/collation_spec.rb +++ b/spec/collation_spec.rb @@ -1,84 +1,82 @@ -# encoding: UTF-8 - module ICU module Collation - describe "Collation" do - it "should collate an array of strings" do - expect(Collation.collate("nb", %w[æ å ø])).to eq(%w[æ ø å]) + describe 'Collation' do + it 'collates an array of strings' do + expect(Collation.collate('nb', ['æ', 'å', 'ø'])).to(eq(['æ', 'ø', 'å'])) end end describe Collator do - let(:collator) { Collator.new("nb") } + let(:collator) { described_class.new('nb') } - it "should collate an array of strings" do - expect(collator.collate(%w[å ø æ])).to eq(%w[æ ø å]) + it 'collates an array of strings' do + expect(collator.collate(['å', 'ø', 'æ'])).to(eq(['æ', 'ø', 'å'])) end - it "raises an error if argument does not respond to :sort" do - expect { collator.collate(1) }.to raise_error(ArgumentError) + it 'raises an error if argument does not respond to :sort' do + expect { collator.collate(1) }.to(raise_error(ArgumentError)) end - it "should return available locales" do + it 'returns available locales' do locales = ICU::Collation.available_locales - expect(locales).to be_an(Array) - expect(locales).to_not be_empty - expect(locales).to include("nb") + expect(locales).to(be_an(Array)) + expect(locales).not_to(be_empty) + expect(locales).to(include('nb')) end - it "should return the locale of the collator" do - expect(collator.locale).to eq('nb') + it 'returns the locale of the collator' do + expect(collator.locale).to(eq('nb')) end - it "should compare two strings" do - expect(collator.compare("blåbærsyltetøy", "blah")).to eq(1) - expect(collator.compare("blah", "blah")).to eq(0) - expect(collator.compare("ba", "bl")).to eq(-1) + it 'compares two strings' do + expect(collator.compare('blåbærsyltetøy', 'blah')).to(eq(1)) + expect(collator.compare('blah', 'blah')).to(eq(0)) + expect(collator.compare('ba', 'bl')).to(eq(-1)) end - it "should know if a string is greater than another" do - expect(collator).to be_greater("z", "a") - expect(collator).to_not be_greater("a", "z") + it 'knows if a string is greater than another' do + expect(collator.greater?('z', 'a')).to(be_truthy) + expect(collator.greater?('a', 'z')).to(be_falsy) end - it "should know if a string is greater or equal to another" do - expect(collator).to be_greater_or_equal("z", "a") - expect(collator).to be_greater_or_equal("z", "z") - expect(collator).to_not be_greater_or_equal("a", "z") + it 'knows if a string is greater or equal to another' do + expect(collator.greater_or_equal?('z', 'a')).to(be_truthy) + expect(collator.greater_or_equal?('z', 'z')).to(be_truthy) + expect(collator.greater_or_equal?('a', 'z')).to(be_falsy) end - it "should know if a string is equal to another" do - expect(collator).to be_equal("a", "a") - expect(collator).to_not be_equal("a", "b") + it 'knows if a string is equal to another' do + expect(collator.equal?('a', 'a')).to(be_truthy) + expect(collator.equal?('a', 'b')).to(be_falsy) end - it "should return rules" do - expect(collator.rules).to_not be_empty + it 'returns rules' do + expect(collator.rules).not_to(be_empty) # ö sorts before Ö - expect(collator.rules).to include('ö<<<Ö') + expect(collator.rules).to(include('ö<<<Ö')) end - it "returns usable collation keys" do - collator.collation_key("abc").should be < collator.collation_key("xyz") + it 'returns usable collation keys' do + collator.collation_key('abc').should(be < collator.collation_key('xyz')) end - context "attributes" do - it "can set and get normalization_mode" do + context 'attributes' do + it 'can set and get normalization_mode' do collator.normalization_mode = true - collator.normalization_mode.should be true + collator.normalization_mode.should(be(true)) - collator[:normalization_mode].should be true + collator[:normalization_mode].should(be(true)) collator[:normalization_mode] = false - collator.normalization_mode.should be false + collator.normalization_mode.should(be(false)) - collator.case_first.should be false + collator.case_first.should(be(false)) collator.case_first = :lower_first - collator.case_first.should == :lower_first + collator.case_first.should collator.strength = :tertiary collator.strength.should == :tertiary end end end - end # Collate -end # ICU + end +end diff --git a/spec/duration_formatting_spec.rb b/spec/duration_formatting_spec.rb index 08f51c3..94262c1 100644 --- a/spec/duration_formatting_spec.rb +++ b/spec/duration_formatting_spec.rb @@ -1,143 +1,143 @@ module ICU - module DurationFormatting - describe 'DurationFormatting::format' do - before(:each) do - skip("Only works on ICU >= 67") if Lib.version.to_a[0] < 67 - end - - it 'produces hours, minutes, and seconds in order' do - result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'C', style: :long) - expect(result).to match(/1.*hour.*2.*minute.*3.*second/i) - end - - it 'rounds down fractional seconds < 0.5' do - result = DurationFormatting.format({seconds: 5.4}, locale: 'C', style: :long) - expect(result).to match(/5.*second/i) - end - - it 'rounds up fractional seconds > 0.5' do - result = DurationFormatting.format({seconds: 5.6}, locale: 'C', style: :long) - expect(result).to match(/6.*second/i) - end - - it 'trims off leading zero values' do - result = DurationFormatting.format({hours: 0, minutes: 1, seconds: 30}, locale: 'C', style: :long) - expect(result).to match(/1.*minute.*30.*second/i) - expect(result).to_not match(/hour/i) - end - - it 'trims off leading missing values' do - result = DurationFormatting.format({minutes: 1, seconds: 30}, locale: 'C', style: :long) - expect(result).to match(/1.*minute.*30.*second/i) - expect(result).to_not match(/hour/i) - end - - it 'trims off non-leading zero values' do - result = DurationFormatting.format({hours: 1, minutes: 0, seconds: 10}, locale: 'C', style: :long) - expect(result).to match(/1.*hour.*10.*second/i) - expect(result).to_not match(/minute/i) - end - - it 'trims off non-leading missing values' do - result = DurationFormatting.format({hours: 1, seconds: 10}, locale: 'C', style: :long) - expect(result).to match(/1.*hour.*10.*second/i) - expect(result).to_not match(/minute/i) - end - - it 'uses comma-based number formatting as appropriate for locale' do - result = DurationFormatting.format({seconds: 90123}, locale: 'en-AU', style: :long) - expect(result).to match(/90,123.*second/i) - expect(result).to_not match(/hour/i) - expect(result).to_not match(/minute/i) - end - - it 'localizes unit names' do - result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'el', style: :long) - expect(result).to match(/1.*ώρα.*2.*λεπτά.*3.*δευτερόλεπτα/i) - end - - it 'can format long' do - result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'en-AU', style: :long) - expect(result).to match(/hour.*minute.*second/i) - end - - it 'can format short' do - result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'en-AU', style: :short) - expect(result).to match(/hr.*min.*sec/i) - expect(result).to_not match(/hour/i) - expect(result).to_not match(/minute/i) - expect(result).to_not match(/second/i) - end - - it 'can format narrow' do - result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'en-AU', style: :narrow) - expect(result).to match(/h.*min.*s/i) - expect(result).to_not match(/hr/i) - expect(result).to_not match(/sec/i) - end - - it 'can format digital' do - result = DurationFormatting.format({hours: 1, minutes: 2, seconds: 3}, locale: 'en-AU', style: :digital) - expect(result).to eql('1:02:03') - end - - it 'can format the full sequence of time units in order' do - duration = { - years: 1, - months: 2, - weeks: 3, - days: 4, - hours: 5, - minutes: 6, - seconds: 7, - milliseconds: 8, - microseconds: 9, - nanoseconds: 10, - } - result = DurationFormatting.format(duration, locale: 'en-AU', style: :short) - expect(result).to match(/1.yr.*2.*mths.*3.*wks.*4.*days.*5.*hrs.*6.*mins.*7.*secs.*8.*ms.*9.*μs.*10.*ns/) - end - - it 'joins ms, us, ns values to seconds in digital format' do - duration = {minutes: 10, seconds: 5, milliseconds: 325, microseconds: 53, nanoseconds: 236} - result = DurationFormatting.format(duration, locale: 'en-AU', style: :digital) - expect(result).to eql('10:05.325053236') - end - - it 'includes trailing zeros as appropriate for the last unit in digital format' do - duration = {minutes: 10, seconds: 5, milliseconds: 325, microseconds: 400} - result = DurationFormatting.format(duration, locale: 'en-AU', style: :digital) - expect(result).to eql('10:05.325400') - end - - it 'joins h:mm:ss and other units in digital format' do - duration = {days: 8, hours: 23, minutes: 10, seconds: 9} - result = DurationFormatting.format(duration, locale: 'en-AU', style: :digital) - expect(result).to match(/8.*d.*23:10:09/ ) - end - - it 'ignores all decimal parts except the last, if it is seconds' do - duration = {hours: 7.3, minutes: 9.7, seconds: 8.93} - result = DurationFormatting.format(duration, locale: 'en-AU', style: :short) - expect(result).to match(/7[^0-9]*hrs.*9[^0-9]*min.*8\.93[^0-9]*secs/) - end - - it 'ignores all decimal parts except the last, if it is milliseconds' do - duration = {hours: 7.3, minutes: 9.7, seconds: 8.93, milliseconds: 632.2} - result = DurationFormatting.format(duration, locale: 'en-AU', style: :short) - expect(result).to match(/7[^0-9]*hrs.*9[^0-9]*min.*8[^0-9]*secs.*632\.2[^0-9]*ms/) - end - - it 'ignores all decimal parts including the last, if it is > seconds' do - duration = {hours: 7.3, minutes: 9.7} - result = DurationFormatting.format(duration, locale: 'en-AU', style: :short) - expect(result).to match(/7[^0-9]*hrs.*9[^0-9]*min/) - end - - it 'raises on durations with any negative component' do - duration = {hours: 7.3, minutes: -9.7} - expect { DurationFormatting.format(duration, locale: 'en-AU') }.to raise_error(ArgumentError) - end - end + module DurationFormatting + describe 'DurationFormatting::format' do + before do + skip('Only works on ICU >= 67') if Lib.version.to_a[0] < 67 + end + + it 'produces hours, minutes, and seconds in order' do + result = DurationFormatting.format({ hours: 1, minutes: 2, seconds: 3 }, locale: 'C', style: :long) + expect(result).to(match(/1.*hour.*2.*minute.*3.*second/i)) + end + + it 'rounds down fractional seconds < 0.5' do + result = DurationFormatting.format({ seconds: 5.4 }, locale: 'C', style: :long) + expect(result).to(match(/5.*second/i)) + end + + it 'rounds up fractional seconds > 0.5' do + result = DurationFormatting.format({ seconds: 5.6 }, locale: 'C', style: :long) + expect(result).to(match(/6.*second/i)) + end + + it 'trims off leading zero values' do + result = DurationFormatting.format({ hours: 0, minutes: 1, seconds: 30 }, locale: 'C', style: :long) + expect(result).to(match(/1.*minute.*30.*second/i)) + expect(result).not_to(match(/hour/i)) + end + + it 'trims off leading missing values' do + result = DurationFormatting.format({ minutes: 1, seconds: 30 }, locale: 'C', style: :long) + expect(result).to(match(/1.*minute.*30.*second/i)) + expect(result).not_to(match(/hour/i)) + end + + it 'trims off non-leading zero values' do + result = DurationFormatting.format({ hours: 1, minutes: 0, seconds: 10 }, locale: 'C', style: :long) + expect(result).to(match(/1.*hour.*10.*second/i)) + expect(result).not_to(match(/minute/i)) + end + + it 'trims off non-leading missing values' do + result = DurationFormatting.format({ hours: 1, seconds: 10 }, locale: 'C', style: :long) + expect(result).to(match(/1.*hour.*10.*second/i)) + expect(result).not_to(match(/minute/i)) + end + + it 'uses comma-based number formatting as appropriate for locale' do + result = DurationFormatting.format({ seconds: 90_123 }, locale: 'en-AU', style: :long) + expect(result).to(match(/90,123.*second/i)) + expect(result).not_to(match(/hour/i)) + expect(result).not_to(match(/minute/i)) + end + + it 'localizes unit names' do + result = DurationFormatting.format({ hours: 1, minutes: 2, seconds: 3 }, locale: 'el', style: :long) + expect(result).to(match(/1.*ώρα.*2.*λεπτά.*3.*δευτερόλεπτα/i)) + end + + it 'can format long' do + result = DurationFormatting.format({ hours: 1, minutes: 2, seconds: 3 }, locale: 'en-AU', style: :long) + expect(result).to(match(/hour.*minute.*second/i)) + end + + it 'can format short' do + result = DurationFormatting.format({ hours: 1, minutes: 2, seconds: 3 }, locale: 'en-AU', style: :short) + expect(result).to(match(/hr.*min.*sec/i)) + expect(result).not_to(match(/hour/i)) + expect(result).not_to(match(/minute/i)) + expect(result).not_to(match(/second/i)) + end + + it 'can format narrow' do + result = DurationFormatting.format({ hours: 1, minutes: 2, seconds: 3 }, locale: 'en-AU', style: :narrow) + expect(result).to(match(/h.*min.*s/i)) + expect(result).not_to(match(/hr/i)) + expect(result).not_to(match(/sec/i)) + end + + it 'can format digital' do + result = DurationFormatting.format({ hours: 1, minutes: 2, seconds: 3 }, locale: 'en-AU', style: :digital) + expect(result).to(eql('1:02:03')) + end + + it 'can format the full sequence of time units in order' do + duration = { + years: 1, + months: 2, + weeks: 3, + days: 4, + hours: 5, + minutes: 6, + seconds: 7, + milliseconds: 8, + microseconds: 9, + nanoseconds: 10 + } + result = DurationFormatting.format(duration, locale: 'en-AU', style: :short) + expect(result).to(match(/1.yr.*2.*mths.*3.*wks.*4.*days.*5.*hrs.*6.*mins.*7.*secs.*8.*ms.*9.*μs.*10.*ns/)) + end + + it 'joins ms, us, ns values to seconds in digital format' do + duration = { minutes: 10, seconds: 5, milliseconds: 325, microseconds: 53, nanoseconds: 236 } + result = DurationFormatting.format(duration, locale: 'en-AU', style: :digital) + expect(result).to(eql('10:05.325053236')) + end + + it 'includes trailing zeros as appropriate for the last unit in digital format' do + duration = { minutes: 10, seconds: 5, milliseconds: 325, microseconds: 400 } + result = DurationFormatting.format(duration, locale: 'en-AU', style: :digital) + expect(result).to(eql('10:05.325400')) + end + + it 'joins h:mm:ss and other units in digital format' do + duration = { days: 8, hours: 23, minutes: 10, seconds: 9 } + result = DurationFormatting.format(duration, locale: 'en-AU', style: :digital) + expect(result).to(match(/8.*d.*23:10:09/)) + end + + it 'ignores all decimal parts except the last, if it is seconds' do + duration = { hours: 7.3, minutes: 9.7, seconds: 8.93 } + result = DurationFormatting.format(duration, locale: 'en-AU', style: :short) + expect(result).to(match(/7[^0-9]*hrs.*9[^0-9]*min.*8\.93[^0-9]*secs/)) + end + + it 'ignores all decimal parts except the last, if it is milliseconds' do + duration = { hours: 7.3, minutes: 9.7, seconds: 8.93, milliseconds: 632.2 } + result = DurationFormatting.format(duration, locale: 'en-AU', style: :short) + expect(result).to(match(/7[^0-9]*hrs.*9[^0-9]*min.*8[^0-9]*secs.*632\.2[^0-9]*ms/)) + end + + it 'ignores all decimal parts including the last, if it is > seconds' do + duration = { hours: 7.3, minutes: 9.7 } + result = DurationFormatting.format(duration, locale: 'en-AU', style: :short) + expect(result).to(match(/7[^0-9]*hrs.*9[^0-9]*min/)) + end + + it 'raises on durations with any negative component' do + duration = { hours: 7.3, minutes: -9.7 } + expect { DurationFormatting.format(duration, locale: 'en-AU') }.to(raise_error(ArgumentError)) + end end + end end diff --git a/spec/lib/version_info_spec.rb b/spec/lib/version_info_spec.rb index bdbca54..059a8bc 100644 --- a/spec/lib/version_info_spec.rb +++ b/spec/lib/version_info_spec.rb @@ -1,19 +1,17 @@ -# encoding: UTF-8 - module ICU module Lib describe VersionInfo do describe '.to_a' do subject { described_class.new.to_a } - it { is_expected.to be_an(Array) } + it { is_expected.to(be_an(Array)) } end describe '.to_s' do subject { described_class.new.to_s } - it { is_expected.to be_a(String) } - it { is_expected.to match(/^[0-9.]+$/) } + it { is_expected.to(be_a(String)) } + it { is_expected.to(match(/^[0-9.]+$/)) } end end end diff --git a/spec/lib_spec.rb b/spec/lib_spec.rb index 90b7c85..1423471 100644 --- a/spec/lib_spec.rb +++ b/spec/lib_spec.rb @@ -1,5 +1,3 @@ -# encoding: UTF-8 - module ICU describe Lib do describe 'error checking' do @@ -7,57 +5,84 @@ module ICU context 'upon success' do it 'returns the block result' do - expect(Lib.check_error { |status| return_value }).to eq(return_value) - expect(Lib.check_error { |status| status.write_int(0); return_value }).to eq(return_value) + expect(described_class.check_error { |_status| return_value }).to(eq(return_value)) + expect(described_class.check_error do |status| + status.write_int(0) + return_value + end).to(eq(return_value)) end end context 'upon failure' do it 'raises an error' do - expect { Lib.check_error { |status| status.write_int(1) } }.to raise_error ICU::Error, /U_.*_ERROR/ + expect do + described_class.check_error do |status| + status.write_int(1) + end + end.to(raise_error(ICU::Error, /U_.*_ERROR/)) end end + # rubocop:disable RSpec/InstanceVariable context 'upon warning' do - before(:each) { @verbose = $VERBOSE } - after(:each) { $VERBOSE = @verbose } + before { @verbose = $VERBOSE } + after { $VERBOSE = @verbose } + # rubocop:disable RSpec/ExpectOutput context 'when warnings are enabled' do - before(:each) { $VERBOSE = true } + before do + @original_stderr = $stderr + $stderr = StringIO.new + $VERBOSE = true + end + + after do + $stderr = @original_stderr + end it 'prints to STDERR and returns the block result' do - expect($stderr).to receive(:puts) { |message| expect(message).to match /U_.*_WARNING/ } - error_check = Lib.check_error { |status| status.write_int(-127); return_value } - expect(error_check).to eq(return_value) + error_check = described_class.check_error do |status| + status.write_int(-127) + return_value + end + + $stderr.rewind + expect($stderr.read).to(match(/U_.*_WARNING/)) + expect(error_check).to(eq(return_value)) end end + # rubocop:enable RSpec/ExpectOutput context 'when warnings are disabled' do - before(:each) { $VERBOSE = false } + before { $VERBOSE = false } it 'returns the block result' do - expect($stderr).to_not receive(:puts) - error_check = Lib.check_error { |status| status.write_int(-127); return_value } - expect(error_check).to eq(return_value) + expect($stderr).not_to(receive(:puts)) + error_check = described_class.check_error do |status| + status.write_int(-127) + return_value + end + expect(error_check).to(eq(return_value)) end end end + # rubocop:enable RSpec/InstanceVariable end - if Gem::Version.new('4.2') <= Gem::Version.new(Lib.version) + if Gem::Version.new('4.2') <= Gem::Version.new(described_class.version) describe 'CLDR version' do - subject { Lib.cldr_version } + subject { described_class.cldr_version } - it { should be_a Lib::VersionInfo } - it('is populated') { expect(subject.to_a).to_not eq([0,0,0,0]) } + it { is_expected.to(be_a(described_class::VersionInfo)) } + it('is populated') { expect(subject.to_a).not_to(eq([0, 0, 0, 0])) } end end describe 'ICU version' do - subject { Lib.version } + subject { described_class.version } - it { is_expected.to be_a Lib::VersionInfo } - it('is populated') { expect(subject.to_a).to_not eq([0,0,0,0]) } + it { is_expected.to(be_a(described_class::VersionInfo)) } + it('is populated') { expect(subject.to_a).not_to(eq([0, 0, 0, 0])) } end end end diff --git a/spec/locale_spec.rb b/spec/locale_spec.rb index dda97c6..76014f5 100644 --- a/spec/locale_spec.rb +++ b/spec/locale_spec.rb @@ -1,92 +1,93 @@ -# encoding: UTF-8 - module ICU describe Locale do describe 'the available locales' do - subject { Locale.available } + subject { described_class.available } - it { is_expected.to be_an Array } - it { is_expected.to_not be_empty } + it { is_expected.to(be_an(Array)) } + it { is_expected.not_to(be_empty) } - it 'should be an array of available Locales' do - expect(subject.first).to be_a(Locale) + it 'is an array of available Locales' do + expect(subject.first).to(be_a(described_class)) end end describe 'the available ISO 639 country codes' do - subject { Locale.iso_countries } + subject { described_class.iso_countries } - it { is_expected.to be_an Array } - it { is_expected.to_not be_empty } + it { is_expected.to(be_an(Array)) } + it { is_expected.not_to(be_empty) } - it 'should be an array of Strings' do - expect(subject.first).to be_a(String) + it 'is an array of Strings' do + expect(subject.first).to(be_a(String)) end end describe 'the available ISO 639 language codes' do - subject { Locale.iso_languages } + subject { described_class.iso_languages } - it { is_expected.to be_an Array } - it { is_expected.to_not be_empty } + it { is_expected.to(be_an(Array)) } + it { is_expected.not_to(be_empty) } - it 'should be an array of Strings' do - expect(subject.first).to be_a(String) + it 'is an array of Strings' do + expect(subject.first).to(be_a(String)) end end describe 'the default' do - subject { Locale.default } + subject { described_class.default } let(:locale) do - locales = Locale.available - locales.delete(Locale.default) + locales = described_class.available + locales.delete(described_class.default) locales.respond_to?(:sample) ? locales.sample : locales.choice end - it { is_expected.to be_a Locale } + it { is_expected.to(be_a(described_class)) } it 'can be assigned using Locale' do - expect(Locale.default = locale).to eq(locale) - expect(Locale.default).to eq(locale) + expect(described_class.default = locale).to(eq(locale)) + expect(described_class.default).to(eq(locale)) end it 'can be assigned using string' do string = locale.to_s - expect(Locale.default = string).to eq(string) - expect(Locale.default).to eq(Locale.new(string)) + expect(described_class.default = string).to(eq(string)) + expect(described_class.default).to(eq(described_class.new(string))) end it 'can be assigned using symbol' do symbol = locale.to_s.to_sym - expect(Locale.default = symbol).to eq(symbol) - expect(Locale.default).to eq(Locale.new(symbol)) + expect(described_class.default = symbol).to(eq(symbol)) + expect(described_class.default).to(eq(described_class.new(symbol))) end end if Gem::Version.new('4.2') <= Gem::Version.new(Lib.version) describe 'BCP 47 language tags' do it 'converts a language tag to a locale' do - expect(Locale.for_language_tag('en-us')).to eq(Locale.new('en_US')) - expect(Locale.for_language_tag('nan-Hant-tw')).to eq(Locale.new('nan_Hant_TW')) + expect(described_class.for_language_tag('en-us')).to(eq(described_class.new('en_US'))) + expect(described_class.for_language_tag('nan-Hant-tw')).to(eq(described_class.new('nan_Hant_TW'))) end it 'returns a language tag for a locale' do if Gem::Version.new('4.4') <= Gem::Version.new(Lib.version) - expect(Locale.new('en_US').to_language_tag).to eq('en-US') - expect(Locale.new('zh_TW').to_language_tag).to eq('zh-TW') - # Support for this "magic" transform was dropped with https://unicode-org.atlassian.net/browse/ICU-20187, so don't test it + expect(described_class.new('en_US').to_language_tag).to(eq('en-US')) + expect(described_class.new('zh_TW').to_language_tag).to(eq('zh-TW')) + # Support for this "magic" transform was dropped with + # https://unicode-org.atlassian.net/browse/ICU-20187, so don't test it if Gem::Version.new(Lib.version) < Gem::Version.new('64') - expect(Locale.new('zh_Hans_CH_PINYIN').to_language_tag).to eq('zh-Hans-CH-u-co-pinyin') + expect(described_class.new('zh_Hans_CH_PINYIN').to_language_tag).to(eq('zh-Hans-CH-u-co-pinyin')) else - expect(Locale.new('zh_Hans_CH@collation=pinyin').to_language_tag).to eq('zh-Hans-CH-u-co-pinyin') + expect(described_class.new('zh_Hans_CH@collation=pinyin').to_language_tag).to( + eq('zh-Hans-CH-u-co-pinyin') + ) end else - expect(Locale.new('en_US').to_language_tag).to eq('en-us') - expect(Locale.new('zh_TW').to_language_tag).to eq('zh-tw') - expect(Locale.new('zh_Hans_CH_PINYIN').to_language_tag).to eq('zh-hans-ch-u-co-pinyin') + expect(described_class.new('en_US').to_language_tag).to(eq('en-us')) + expect(described_class.new('zh_TW').to_language_tag).to(eq('zh-tw')) + expect(described_class.new('zh_Hans_CH_PINYIN').to_language_tag).to(eq('zh-hans-ch-u-co-pinyin')) end end end @@ -94,185 +95,191 @@ module ICU describe 'Win32 locale IDs' do it 'converts an LCID to a locale' do - expect(Locale.for_lcid(1033)).to eq(Locale.new('en_US')) - expect(Locale.for_lcid(1036)).to eq(Locale.new('fr_FR')) + expect(described_class.for_lcid(1033)).to(eq(described_class.new('en_US'))) + expect(described_class.for_lcid(1036)).to(eq(described_class.new('fr_FR'))) end it 'returns an LCID for a locale' do - expect(Locale.new('en_US').lcid).to eq(1033) - expect(Locale.new('es_US').lcid).to eq(21514) + expect(described_class.new('en_US').lcid).to(eq(1033)) + expect(described_class.new('es_US').lcid).to(eq(21_514)) end end describe 'display' do - let(:locale_ids) { Locale.available.map(&:id) } + let(:locale_ids) { described_class.available.map(&:id) } context 'in a specific locale' do it 'returns the country' do - expect(Locale.new('de_DE').display_country('en')).to eq('Germany') - expect(Locale.new('en_US').display_country('fr')).to eq('États-Unis') + expect(described_class.new('de_DE').display_country('en')).to(eq('Germany')) + expect(described_class.new('en_US').display_country('fr')).to(eq('États-Unis')) end it 'returns the language' do - expect(Locale.new('fr_FR').display_language('de')).to eq('Französisch') - expect(Locale.new('zh_CH').display_language('en')).to eq('Chinese') + expect(described_class.new('fr_FR').display_language('de')).to(eq('Französisch')) + expect(described_class.new('zh_CH').display_language('en')).to(eq('Chinese')) end it 'returns the name' do - expect(Locale.new('en_US').display_name('de')).to eq('Englisch (Vereinigte Staaten)') - expect(Locale.new('zh_CH').display_name('fr')).to eq('chinois (Suisse)') + expect(described_class.new('en_US').display_name('de')).to(eq('Englisch (Vereinigte Staaten)')) + expect(described_class.new('zh_CH').display_name('fr')).to(eq('chinois (Suisse)')) end it 'returns the name using display context' do - expect(Locale.new('en_HK').display_name_with_context('en_US', [:length_full])).to eq('English (Hong Kong SAR China)') - expect(Locale.new('en_HK').display_name_with_context('en_US', [:length_short])).to eq('English (Hong Kong)') + expect(described_class.new('en_HK').display_name_with_context('en_US', + [:length_full])).to( + eq('English (Hong Kong SAR China)') + ) + expect(described_class.new('en_HK').display_name_with_context('en_US', + [:length_short])).to(eq('English (Hong Kong)')) end it 'returns the script' do - expect(Locale.new('ja_Hira_JP').display_script('en')).to eq('Hiragana') - expect(Locale.new('ja_Hira_JP').display_script('ru')).to eq('хирагана') + expect(described_class.new('ja_Hira_JP').display_script('en')).to(eq('Hiragana')) + expect(described_class.new('ja_Hira_JP').display_script('ru')).to(eq('хирагана')) end it 'returns the variant' do - expect(Locale.new('be_BY_TARASK').display_variant('de')).to eq('Taraskievica-Orthographie') - expect(Locale.new('zh_CH_POSIX').display_variant('en')).to eq('Computer') + expect(described_class.new('be_BY_TARASK').display_variant('de')).to(eq('Taraskievica-Orthographie')) + expect(described_class.new('zh_CH_POSIX').display_variant('en')).to(eq('Computer')) end # If memory set for 'read_uchar_buffer' is set too low it will throw an out # of bounds memory error, which results in a Segmentation fault error. it 'insures memory sizes is set correctly' do # Currently, testing the longest known locales. May need to be update in the future. - expect(Locale.new('en_VI').display_country('ccp')).to_not be_nil - expect(Locale.new('yue_Hant').display_language('ccp')).to_not be_nil - expect(Locale.new('en_VI').display_name('ccp')).to_not be_nil - expect(Locale.new('en_VI').display_name_with_context('ccp')).to_not be_nil - expect(Locale.new('yue_Hant').display_script('ccp')).to_not be_nil - expect(Locale.new('en_US_POSIX').display_variant('sl')).to_not be_nil + expect(described_class.new('en_VI').display_country('ccp')).not_to(be_nil) + expect(described_class.new('yue_Hant').display_language('ccp')).not_to(be_nil) + expect(described_class.new('en_VI').display_name('ccp')).not_to(be_nil) + expect(described_class.new('en_VI').display_name_with_context('ccp')).not_to(be_nil) + expect(described_class.new('yue_Hant').display_script('ccp')).not_to(be_nil) + expect(described_class.new('en_US_POSIX').display_variant('sl')).not_to(be_nil) end end context 'in the default locale' do - let(:locale) { Locale.new('de_DE') } + let(:locale) { described_class.new('de_DE') } it 'returns the country' do - expect(locale.display_country).to eq(locale.display_country(Locale.default)) + expect(locale.display_country).to(eq(locale.display_country(described_class.default))) end it 'returns the language' do - expect(locale.display_language).to eq(locale.display_language(Locale.default)) + expect(locale.display_language).to(eq(locale.display_language(described_class.default))) end it 'returns the name' do - expect(locale.display_name).to eq(locale.display_name(Locale.default)) + expect(locale.display_name).to(eq(locale.display_name(described_class.default))) end it 'returns the script' do - expect(locale.display_script).to eq(locale.display_script(Locale.default)) + expect(locale.display_script).to(eq(locale.display_script(described_class.default))) end it 'returns the variant' do - expect(locale.display_variant).to eq(locale.display_variant(Locale.default)) + expect(locale.display_variant).to(eq(locale.display_variant(described_class.default))) end end end describe 'formatting' do - let(:locale) { Locale.new('de-de.utf8@collation = phonebook') } + let(:locale) { described_class.new('de-de.utf8@collation = phonebook') } - it('is formatted') { expect(locale.name).to eq('de_DE.utf8@collation=phonebook') } - it('is formatted without keywords') { expect(locale.base_name).to eq('de_DE.utf8') } - it('is formatted for ICU') { expect(locale.canonical).to eq('de_DE@collation=phonebook') } + it('is formatted') { expect(locale.name).to(eq('de_DE.utf8@collation=phonebook')) } + it('is formatted without keywords') { expect(locale.base_name).to(eq('de_DE.utf8')) } + it('is formatted for ICU') { expect(locale.canonical).to(eq('de_DE@collation=phonebook')) } end it 'truncates a properly formatted locale, returning the "parent"' do - expect(Locale.new('es-mx').parent).to eq('') - expect(Locale.new('es_MX').parent).to eq('es') - expect(Locale.new('zh_Hans_CH_PINYIN').parent).to eq('zh_Hans_CH') + expect(described_class.new('es-mx').parent).to(eq('')) + expect(described_class.new('es_MX').parent).to(eq('es')) + expect(described_class.new('zh_Hans_CH_PINYIN').parent).to(eq('zh_Hans_CH')) end describe 'ISO codes' do it 'returns the ISO 3166 alpha-3 country code' do - expect(Locale.new('en_US').iso_country).to eq('USA') - expect(Locale.new('zh_CN').iso_country).to eq('CHN') + expect(described_class.new('en_US').iso_country).to(eq('USA')) + expect(described_class.new('zh_CN').iso_country).to(eq('CHN')) end it 'returns the ISO 639 three-letter language code' do - expect(Locale.new('en_US').iso_language).to eq('eng') - expect(Locale.new('zh_CN').iso_language).to eq('zho') + expect(described_class.new('en_US').iso_language).to(eq('eng')) + expect(described_class.new('zh_CN').iso_language).to(eq('zho')) end end describe 'keywords' do context 'when improperly formatted' do - let(:locale) { Locale.new('de_DE@euro') } + let(:locale) { described_class.new('de_DE@euro') } it 'raises an error' do - expect { locale.keywords }.to raise_error(ICU::Error) + expect { locale.keywords }.to(raise_error(ICU::Error)) end end context 'when properly formatted' do - let(:locale) { Locale.new('de_DE@currency=EUR') } + let(:locale) { described_class.new('de_DE@currency=EUR') } it 'returns the list of keywords' do - expect(locale.keywords).to eq(['currency']) + expect(locale.keywords).to(eq(['currency'])) end end it 'can be read' do - expect(Locale.new('en_US@calendar=chinese').keyword('calendar')).to eq('chinese') - expect(Locale.new('en_US@calendar=chinese').keyword(:calendar)).to eq('chinese') - expect(Locale.new('en_US@some=thing').keyword('missing')).to eq('') + expect(described_class.new('en_US@calendar=chinese').keyword('calendar')).to(eq('chinese')) + expect(described_class.new('en_US@calendar=chinese').keyword(:calendar)).to(eq('chinese')) + expect(described_class.new('en_US@some=thing').keyword('missing')).to(eq('')) end it 'can be added' do - expect(Locale.new('de_DE').with_keyword('currency', 'EUR')).to eq(Locale.new('de_DE@currency=EUR')) - expect(Locale.new('de_DE').with_keyword(:currency, :EUR)).to eq(Locale.new('de_DE@currency=EUR')) + expect(described_class.new('de_DE').with_keyword('currency', + 'EUR')).to(eq(described_class.new('de_DE@currency=EUR'))) + expect(described_class.new('de_DE').with_keyword(:currency, + :EUR)).to(eq(described_class.new('de_DE@currency=EUR'))) end it 'can be added using hash' do - expect(Locale.new('fr').with_keywords(:a => :b, :c => :d)).to eq(Locale.new('fr@a=b;c=d')) + expect(described_class.new('fr').with_keywords(a: :b, c: :d)).to(eq(described_class.new('fr@a=b;c=d'))) end it 'can be removed' do - expect(Locale.new('en_US@some=thing').with_keyword(:some, nil)).to eq(Locale.new('en_US')) - expect(Locale.new('en_US@some=thing').with_keyword(:some, '')).to eq(Locale.new('en_US')) + expect(described_class.new('en_US@some=thing').with_keyword(:some, nil)).to(eq(described_class.new('en_US'))) + expect(described_class.new('en_US@some=thing').with_keyword(:some, '')).to(eq(described_class.new('en_US'))) end end describe 'orientation' do it 'returns the character orientation' do - expect(Locale.new('ar').character_orientation).to eq(:rtl) - expect(Locale.new('en').character_orientation).to eq(:ltr) - expect(Locale.new('fa').character_orientation).to eq(:rtl) + expect(described_class.new('ar').character_orientation).to(eq(:rtl)) + expect(described_class.new('en').character_orientation).to(eq(:ltr)) + expect(described_class.new('fa').character_orientation).to(eq(:rtl)) end it 'returns the line orientation' do - expect(Locale.new('ar').line_orientation).to eq(:ttb) - expect(Locale.new('en').line_orientation).to eq(:ttb) - expect(Locale.new('fa').line_orientation).to eq(:ttb) + expect(described_class.new('ar').line_orientation).to(eq(:ttb)) + expect(described_class.new('en').line_orientation).to(eq(:ttb)) + expect(described_class.new('fa').line_orientation).to(eq(:ttb)) end end describe 'subtags' do - let(:locale) { Locale.new('zh-hans-ch-pinyin') } + let(:locale) { described_class.new('zh-hans-ch-pinyin') } - it('returns the country code') { expect(locale.country).to eq('CH') } - it('returns the language code') { expect(locale.language).to eq('zh') } - it('returns the script code') { expect(locale.script).to eq('Hans') } - it('returns the variant code') { expect(locale.variant).to eq('PINYIN') } + it('returns the country code') { expect(locale.country).to(eq('CH')) } + it('returns the language code') { expect(locale.language).to(eq('zh')) } + it('returns the script code') { expect(locale.script).to(eq('Hans')) } + it('returns the variant code') { expect(locale.variant).to(eq('PINYIN')) } describe 'likely subtags according to UTS #35' do it 'adds likely subtags' do - expect(Locale.new('en').with_likely_subtags).to eq(Locale.new('en_Latn_US')) - expect(Locale.new('sr').with_likely_subtags).to eq(Locale.new('sr_Cyrl_RS')) - expect(Locale.new('zh_TW').with_likely_subtags).to eq(Locale.new('zh_Hant_TW')) + expect(described_class.new('en').with_likely_subtags).to(eq(described_class.new('en_Latn_US'))) + expect(described_class.new('sr').with_likely_subtags).to(eq(described_class.new('sr_Cyrl_RS'))) + expect(described_class.new('zh_TW').with_likely_subtags).to(eq(described_class.new('zh_Hant_TW'))) end it 'removes likely subtags' do - expect(Locale.new('en_US').with_minimized_subtags).to eq(Locale.new('en')) - expect(Locale.new('sr_RS').with_minimized_subtags).to eq(Locale.new('sr')) - expect(Locale.new('zh_Hant_TW').with_minimized_subtags).to eq(Locale.new('zh_TW')) + expect(described_class.new('en_US').with_minimized_subtags).to(eq(described_class.new('en'))) + expect(described_class.new('sr_RS').with_minimized_subtags).to(eq(described_class.new('sr'))) + expect(described_class.new('zh_Hant_TW').with_minimized_subtags).to(eq(described_class.new('zh_TW'))) end end end diff --git a/spec/normalization_spec.rb b/spec/normalization_spec.rb index dc69092..680a84a 100644 --- a/spec/normalization_spec.rb +++ b/spec/normalization_spec.rb @@ -1,22 +1,17 @@ -# encoding: UTF-8 - module ICU module Normalization # http://bugs.icu-project.org/trac/browser/icu/trunk/source/test/cintltst/cnormtst.c - describe "Normalization" do - - it "should normalize a string - decomposed" do - expect(ICU::Normalization.normalize("Å", :nfd).unpack("U*")).to eq([65, 778]) + describe 'Normalization' do + it 'normalizes a string - decomposed' do + expect(ICU::Normalization.normalize('Å', :nfd).unpack('U*')).to(eq([65, 778])) end - it "should normalize a string - composed" do - expect(ICU::Normalization.normalize("Å", :nfc).unpack("U*")).to eq([197]) + it 'normalizes a string - composed' do + expect(ICU::Normalization.normalize('Å', :nfc).unpack('U*')).to(eq([197])) end # TODO: add more normalization tests - - end - end # Normalization -end # ICU + end +end diff --git a/spec/normalizer_spec.rb b/spec/normalizer_spec.rb index 6397d6e..5a552d7 100644 --- a/spec/normalizer_spec.rb +++ b/spec/normalizer_spec.rb @@ -1,57 +1,55 @@ -# encoding: UTF-8 - module ICU describe Normalizer do describe 'NFD: nfc decompose' do - let(:normalizer) { ICU::Normalizer.new(nil, 'nfc', :decompose) } - - it "should normalize a string" do - expect(normalizer.normalize("Å").unpack("U*")).to eq([65, 778]) - expect(normalizer.normalize("ô").unpack("U*")).to eq([111, 770]) - expect(normalizer.normalize("a").unpack("U*")).to eq([97]) - expect(normalizer.normalize("中文").unpack("U*")).to eq([20013, 25991]) - expect(normalizer.normalize("Äffin").unpack("U*")).to eq([65, 776, 102, 102, 105, 110]) - expect(normalizer.normalize("Äffin").unpack("U*")).to eq([65, 776, 64259, 110]) - expect(normalizer.normalize("Henry IV").unpack("U*")).to eq([72, 101, 110, 114, 121, 32, 73, 86]) - expect(normalizer.normalize("Henry Ⅳ").unpack("U*")).to eq([72, 101, 110, 114, 121, 32, 8547]) + let(:normalizer) { described_class.new(nil, 'nfc', :decompose) } + + it 'normalizes a string' do + expect(normalizer.normalize('Å').unpack('U*')).to(eq([65, 778])) + expect(normalizer.normalize('ô').unpack('U*')).to(eq([111, 770])) + expect(normalizer.normalize('a').unpack('U*')).to(eq([97])) + expect(normalizer.normalize('中文').unpack('U*')).to(eq([20_013, 25_991])) + expect(normalizer.normalize('Äffin').unpack('U*')).to(eq([65, 776, 102, 102, 105, 110])) + expect(normalizer.normalize('Äffin').unpack('U*')).to(eq([65, 776, 64_259, 110])) + expect(normalizer.normalize('Henry IV').unpack('U*')).to(eq([72, 101, 110, 114, 121, 32, 73, 86])) + expect(normalizer.normalize('Henry Ⅳ').unpack('U*')).to(eq([72, 101, 110, 114, 121, 32, 8547])) end end describe 'NFC: nfc compose' do - let(:normalizer) { ICU::Normalizer.new(nil, 'nfc', :compose) } - - it "should normalize a string" do - expect(normalizer.normalize("Å").unpack("U*")).to eq([197]) - expect(normalizer.normalize("ô").unpack("U*")).to eq([244]) - expect(normalizer.normalize("a").unpack("U*")).to eq([97]) - expect(normalizer.normalize("中文").unpack("U*")).to eq([20013, 25991]) - expect(normalizer.normalize("Äffin").unpack("U*")).to eq([196, 102, 102, 105, 110]) - expect(normalizer.normalize("Äffin").unpack("U*")).to eq([196, 64259, 110]) - expect(normalizer.normalize("Henry IV").unpack("U*")).to eq([72, 101, 110, 114, 121, 32, 73, 86]) - expect(normalizer.normalize("Henry Ⅳ").unpack("U*")).to eq([72, 101, 110, 114, 121, 32, 8547]) + let(:normalizer) { described_class.new(nil, 'nfc', :compose) } + + it 'normalizes a string' do + expect(normalizer.normalize('Å').unpack('U*')).to(eq([197])) + expect(normalizer.normalize('ô').unpack('U*')).to(eq([244])) + expect(normalizer.normalize('a').unpack('U*')).to(eq([97])) + expect(normalizer.normalize('中文').unpack('U*')).to(eq([20_013, 25_991])) + expect(normalizer.normalize('Äffin').unpack('U*')).to(eq([196, 102, 102, 105, 110])) + expect(normalizer.normalize('Äffin').unpack('U*')).to(eq([196, 64_259, 110])) + expect(normalizer.normalize('Henry IV').unpack('U*')).to(eq([72, 101, 110, 114, 121, 32, 73, 86])) + expect(normalizer.normalize('Henry Ⅳ').unpack('U*')).to(eq([72, 101, 110, 114, 121, 32, 8547])) end end describe 'NFKD: nfkc decompose' do - let(:normalizer) { ICU::Normalizer.new(nil, 'nfkc', :decompose) } + let(:normalizer) { described_class.new(nil, 'nfkc', :decompose) } - it "should normalize a string" do - expect(normalizer.normalize("Äffin").unpack("U*")).to eq([65, 776, 102, 102, 105, 110]) - expect(normalizer.normalize("Äffin").unpack("U*")).to eq([65, 776, 102, 102, 105, 110]) - expect(normalizer.normalize("Henry IV").unpack("U*")).to eq([72, 101, 110, 114, 121, 32, 73, 86]) - expect(normalizer.normalize("Henry Ⅳ").unpack("U*")).to eq([72, 101, 110, 114, 121, 32, 73, 86]) + it 'normalizes a string' do + expect(normalizer.normalize('Äffin').unpack('U*')).to(eq([65, 776, 102, 102, 105, 110])) + expect(normalizer.normalize('Äffin').unpack('U*')).to(eq([65, 776, 102, 102, 105, 110])) + expect(normalizer.normalize('Henry IV').unpack('U*')).to(eq([72, 101, 110, 114, 121, 32, 73, 86])) + expect(normalizer.normalize('Henry Ⅳ').unpack('U*')).to(eq([72, 101, 110, 114, 121, 32, 73, 86])) end end describe 'NFKC: nfkc compose' do - let(:normalizer) { ICU::Normalizer.new(nil, 'nfkc', :compose) } + let(:normalizer) { described_class.new(nil, 'nfkc', :compose) } - it "should normalize a string" do - expect(normalizer.normalize("Äffin").unpack("U*")).to eq([196, 102, 102, 105, 110]) - expect(normalizer.normalize("Äffin").unpack("U*")).to eq([196, 102, 102, 105, 110]) - expect(normalizer.normalize("Henry IV").unpack("U*")).to eq([72, 101, 110, 114, 121, 32, 73, 86]) - expect(normalizer.normalize("Henry Ⅳ").unpack("U*")).to eq([72, 101, 110, 114, 121, 32, 73, 86]) + it 'normalizes a string' do + expect(normalizer.normalize('Äffin').unpack('U*')).to(eq([196, 102, 102, 105, 110])) + expect(normalizer.normalize('Äffin').unpack('U*')).to(eq([196, 102, 102, 105, 110])) + expect(normalizer.normalize('Henry IV').unpack('U*')).to(eq([72, 101, 110, 114, 121, 32, 73, 86])) + expect(normalizer.normalize('Henry Ⅳ').unpack('U*')).to(eq([72, 101, 110, 114, 121, 32, 73, 86])) end end - end # Normalizer -end # ICU + end +end diff --git a/spec/number_formatting_spec.rb b/spec/number_formatting_spec.rb index 03bff01..06d3bd9 100644 --- a/spec/number_formatting_spec.rb +++ b/spec/number_formatting_spec.rb @@ -1,79 +1,80 @@ -# encoding: UTF-8 - module ICU module NumberFormatting describe 'NumberFormatting' do - it 'should format a simple integer' do - expect(NumberFormatting.format_number("en", 1)).to eq("1") - expect(NumberFormatting.format_number("en", 1_000)).to eq("1,000") - expect(NumberFormatting.format_number("de-DE", 1_000_000)).to eq("1.000.000") + it 'formats a simple integer' do + expect(NumberFormatting.format_number('en', 1)).to(eq('1')) + expect(NumberFormatting.format_number('en', 1_000)).to(eq('1,000')) + expect(NumberFormatting.format_number('de-DE', 1_000_000)).to(eq('1.000.000')) end - it 'should format a float' do - expect(NumberFormatting.format_number("en", 1.0)).to eq("1") - expect(NumberFormatting.format_number("en", 1.123)).to eq("1.123") - expect(NumberFormatting.format_number("en", 1_000.1238)).to eq("1,000.124") - expect(NumberFormatting.format_number("en", 1_000.1238, max_fraction_digits: 4)).to eq("1,000.1238") + it 'formats a float' do + expect(NumberFormatting.format_number('en', 1.0)).to(eq('1')) + expect(NumberFormatting.format_number('en', 1.123)).to(eq('1.123')) + expect(NumberFormatting.format_number('en', 1_000.1238)).to(eq('1,000.124')) + expect(NumberFormatting.format_number('en', 1_000.1238, max_fraction_digits: 4)).to(eq('1,000.1238')) NumberFormatting.set_default_options(fraction_digits: 5) - expect(NumberFormatting.format_number("en", 1_000.1238)).to eq("1,000.12380") + expect(NumberFormatting.format_number('en', 1_000.1238)).to(eq('1,000.12380')) NumberFormatting.clear_default_options end - it 'should format a decimal' do - expect(NumberFormatting.format_number("en", BigDecimal("10000.123"))).to eq("10,000.123") + it 'formats a decimal' do + expect(NumberFormatting.format_number('en', BigDecimal('10000.123'))).to(eq('10,000.123')) end - it 'should format a currency' do - expect(NumberFormatting.format_currency("en", 123.45, 'USD')).to eq("$123.45") - expect(NumberFormatting.format_currency("en", 123_123.45, 'USD')).to eq("$123,123.45") - expect(NumberFormatting.format_currency("de-DE", 123_123.45, 'EUR')).to eq("123.123,45\u{A0}€") + it 'formats a currency' do + expect(NumberFormatting.format_currency('en', 123.45, 'USD')).to(eq('$123.45')) + expect(NumberFormatting.format_currency('en', 123_123.45, 'USD')).to(eq('$123,123.45')) + expect(NumberFormatting.format_currency('de-DE', 123_123.45, 'EUR')).to(eq("123.123,45\u{A0}€")) end - it 'should format a percent' do - expect(NumberFormatting.format_percent("en", 1.1)).to eq("110%") - expect(NumberFormatting.format_percent("da", 0.15)).to eq("15\u{A0}%") - expect(NumberFormatting.format_percent("da", -0.1545, max_fraction_digits: 10)).to eq("-15,45\u{A0}%") + it 'formats a percent' do + expect(NumberFormatting.format_percent('en', 1.1)).to(eq('110%')) + expect(NumberFormatting.format_percent('da', 0.15)).to(eq("15\u{A0}%")) + expect(NumberFormatting.format_percent('da', -0.1545, max_fraction_digits: 10)).to(eq("-15,45\u{A0}%")) end - it 'should spell numbers' do - expect(NumberFormatting.spell("en_US", 1_000)).to eq('one thousand') - expect(NumberFormatting.spell("de-DE", 123.456)).to eq("ein\u{AD}hundert\u{AD}drei\u{AD}und\u{AD}zwanzig Komma vier fünf sechs") + it 'spells numbers' do + expect(NumberFormatting.spell('en_US', 1_000)).to(eq('one thousand')) + expect(NumberFormatting.spell('de-DE', + 123.456)).to( + eq("ein\u{AD}hundert\u{AD}drei\u{AD}und\u{AD}zwanzig Komma vier fünf sechs") + ) end - it 'should be able to re-use number formatter objects' do + it 'is able to re-use number formatter objects' do numf = NumberFormatting.create('fr-CA') - expect(numf.format(1_000)).to eq("1\u{A0}000") - expect(numf.format(1_000.123)).to eq("1\u{A0}000,123") + expect(numf.format(1_000)).to(eq("1\u{A0}000")) + expect(numf.format(1_000.123)).to(eq("1\u{A0}000,123")) end - it 'should be able to re-use currency formatter objects' do + it 'is able to re-use currency formatter objects' do curf = NumberFormatting.create('en-US', :currency) - expect(curf.format(1_000.12, 'USD')).to eq("$1,000.12") + expect(curf.format(1_000.12, 'USD')).to(eq('$1,000.12')) end - it 'should allow for various styles of currency formatting if the version is new enough' do + it 'allows for various styles of currency formatting if the version is new enough' do if ICU::Lib.version.to_a.first >= 53 curf = NumberFormatting.create('en-US', :currency, style: :iso) expected = if ICU::Lib.version.to_a.first >= 62 - "USD\u00A01,000.12" - else - "USD1,000.12" - end - expect(curf.format(1_000.12, 'USD')).to eq(expected) + "USD\u00A01,000.12" + else + 'USD1,000.12' + end + expect(curf.format(1_000.12, 'USD')).to(eq(expected)) curf = NumberFormatting.create('en-US', :currency, style: :plural) - expect(curf.format(1_000.12, 'USD')).to eq("1,000.12 US dollars") - expect { NumberFormatting.create('en-US', :currency, style: :fake) }.to raise_error(StandardError) + expect(curf.format(1_000.12, 'USD')).to(eq('1,000.12 US dollars')) + expect { NumberFormatting.create('en-US', :currency, style: :fake) }.to(raise_error(StandardError)) else curf = NumberFormatting.create('en-US', :currency, style: :default) - expect(curf.format(1_000.12, 'USD')).to eq('$1,000.12') - expect { NumberFormatting.create('en-US', :currency, style: :iso) }.to raise_error(StandardError) + expect(curf.format(1_000.12, 'USD')).to(eq('$1,000.12')) + expect { NumberFormatting.create('en-US', :currency, style: :iso) }.to(raise_error(StandardError)) end end - it 'should format a bignum' do - str = NumberFormatting.format_number("en", 1_000_000_000_000_000_000_000_000_000_000_000_000_000) - expect(str).to eq('1,000,000,000,000,000,000,000,000,000,000,000,000,000') + it 'formats a bignum' do + str = NumberFormatting.format_number('en', 1_000_000_000_000_000_000_000_000_000_000_000_000_000) + expect(str).to(eq('1,000,000,000,000,000,000,000,000,000,000,000,000,000')) end end - end # NumberFormatting -end # ICU + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0f84f3b..73016cb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,13 +1,11 @@ $LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -require "rubygems" +require 'rubygems' require 'ffi-icu' require 'rspec' RSpec.configure do |config| - - if ENV['TRAVIS'] - config.filter_run_excluding broken: true - end + config.expect_with(:rspec) { |c| c.syntax = [:should, :expect] } + config.mock_with(:rspec) { |c| c.syntax = :expect } end diff --git a/spec/time_spec.rb b/spec/time_spec.rb index 63c1478..c0d939d 100644 --- a/spec/time_spec.rb +++ b/spec/time_spec.rb @@ -1,98 +1,96 @@ -# encoding: UTF-8 - module ICU describe TimeFormatting do - describe 'the TimeFormatting ' do - t0 = Time.at(1226499676) # in TZ=Europe/Prague Time.mktime(2008, 11, 12, 15, 21, 16) - t1 = Time.at(1224890117) # in TZ=Europe/Prague Time.mktime(2008, 10, 25, 01, 15, 17) - t2 = Time.at(1224893778) # in TZ=Europe/Prague Time.mktime(2008, 10, 25, 02, 16, 18) - t3 = Time.at(1224897439) # in TZ=Europe/Prague Time.mktime(2008, 10, 25, 03, 17, 19) - t4 = Time.at(1224901100) # in TZ=Europe/Prague Time.mktime(2008, 10, 25, 04, 18, 20) - t5 = Time.at(1206750921) # in TZ=Europe/Prague Time.mktime(2008, 03, 29, 01, 35, 21) - t6 = Time.at(1206754582) # in TZ=Europe/Prague Time.mktime(2008, 03, 29, 02, 36, 22) - t7 = Time.at(1206758243) # in TZ=Europe/Prague Time.mktime(2008, 03, 29, 03, 37, 23) - t8 = Time.at(1206761904) # in TZ=Europe/Prague Time.mktime(2008, 03, 29, 04, 38, 24) - - f1 = TimeFormatting.create(:locale => 'cs_CZ', :zone => 'Europe/Prague', :date => :long , :time => :long, :tz_style => :localized_long) + describe 'the TimeFormatting' do + t0 = Time.at(1_226_499_676) # in TZ=Europe/Prague Time.mktime(2008, 11, 12, 15, 21, 16) + t1 = Time.at(1_224_890_117) # in TZ=Europe/Prague Time.mktime(2008, 10, 25, 01, 15, 17) + t2 = Time.at(1_224_893_778) # in TZ=Europe/Prague Time.mktime(2008, 10, 25, 02, 16, 18) + t3 = Time.at(1_224_897_439) # in TZ=Europe/Prague Time.mktime(2008, 10, 25, 03, 17, 19) + t4 = Time.at(1_224_901_100) # in TZ=Europe/Prague Time.mktime(2008, 10, 25, 04, 18, 20) + t5 = Time.at(1_206_750_921) # in TZ=Europe/Prague Time.mktime(2008, 03, 29, 01, 35, 21) + t6 = Time.at(1_206_754_582) # in TZ=Europe/Prague Time.mktime(2008, 03, 29, 02, 36, 22) + t7 = Time.at(1_206_758_243) # in TZ=Europe/Prague Time.mktime(2008, 03, 29, 03, 37, 23) + t8 = Time.at(1_206_761_904) # in TZ=Europe/Prague Time.mktime(2008, 03, 29, 04, 38, 24) + + f1 = described_class.create(locale: 'cs_CZ', zone: 'Europe/Prague', date: :long, time: :long, + tz_style: :localized_long) it 'check date_format for lang=cs_CZ' do - expect(f1.date_format(true)).to eq("d. MMMM y H:mm:ss ZZZZ") - expect(f1.date_format(false)).to eq("d. MMMM y H:mm:ss ZZZZ") + expect(f1.date_format(true)).to(eq('d. MMMM y H:mm:ss ZZZZ')) + expect(f1.date_format(false)).to(eq('d. MMMM y H:mm:ss ZZZZ')) end - it "for lang=cs_CZ zone=Europe/Prague" do - expect(f1).to be_an_instance_of TimeFormatting::DateTimeFormatter - expect(f1.format(t0)).to eq("12. listopadu 2008 15:21:16 GMT+01:00") - expect(f1.format(t1)).to eq("25. října 2008 1:15:17 GMT+02:00") - expect(f1.format(t2)).to eq("25. října 2008 2:16:18 GMT+02:00") - expect(f1.format(t3)).to eq("25. října 2008 3:17:19 GMT+02:00") - expect(f1.format(t4)).to eq("25. října 2008 4:18:20 GMT+02:00") - expect(f1.format(t5)).to eq("29. března 2008 1:35:21 GMT+01:00") - expect(f1.format(t6)).to eq("29. března 2008 2:36:22 GMT+01:00") - expect(f1.format(t7)).to eq("29. března 2008 3:37:23 GMT+01:00") - expect(f1.format(t8)).to eq("29. března 2008 4:38:24 GMT+01:00") + it 'for lang=cs_CZ zone=Europe/Prague' do + expect(f1).to(be_an_instance_of(described_class::DateTimeFormatter)) + expect(f1.format(t0)).to(eq('12. listopadu 2008 15:21:16 GMT+01:00')) + expect(f1.format(t1)).to(eq('25. října 2008 1:15:17 GMT+02:00')) + expect(f1.format(t2)).to(eq('25. října 2008 2:16:18 GMT+02:00')) + expect(f1.format(t3)).to(eq('25. října 2008 3:17:19 GMT+02:00')) + expect(f1.format(t4)).to(eq('25. října 2008 4:18:20 GMT+02:00')) + expect(f1.format(t5)).to(eq('29. března 2008 1:35:21 GMT+01:00')) + expect(f1.format(t6)).to(eq('29. března 2008 2:36:22 GMT+01:00')) + expect(f1.format(t7)).to(eq('29. března 2008 3:37:23 GMT+01:00')) + expect(f1.format(t8)).to(eq('29. března 2008 4:38:24 GMT+01:00')) end - f2 = TimeFormatting.create(:locale => 'en_US', :zone => 'Europe/Moscow', :date => :short , :time => :long, :tz_style => :generic_location) + f2 = described_class.create(locale: 'en_US', zone: 'Europe/Moscow', date: :short, time: :long, + tz_style: :generic_location) cldr_version = Lib.cldr_version.to_s - en_tz = "Moscow Time" - en_sep = "," - if cldr_version <= "2.0.1" - en_tz = "Russia Time (Moscow)" - en_sep = "" + en_tz = 'Moscow Time' + en_sep = ',' + if cldr_version <= '2.0.1' + en_tz = 'Russia Time (Moscow)' + en_sep = '' end en_exp = "M/d/yy#{en_sep} h:mm:ss a VVVV" it 'check date_format for lang=en_US' do - expect(f2.date_format(true)).to eq(en_exp) - expect(f2.date_format(false)).to eq(en_exp) + expect(f2.date_format(true)).to(eq(en_exp)) + expect(f2.date_format(false)).to(eq(en_exp)) end - it "lang=en_US zone=Europe/Moscow" do - expect(f2.format(t0)).to eq("11/12/08#{en_sep} 5:21:16 PM #{en_tz}") - expect(f2.format(t1)).to eq("10/25/08#{en_sep} 3:15:17 AM #{en_tz}") - expect(f2.format(t2)).to eq("10/25/08#{en_sep} 4:16:18 AM #{en_tz}") - expect(f2.format(t3)).to eq("10/25/08#{en_sep} 5:17:19 AM #{en_tz}") - expect(f2.format(t4)).to eq("10/25/08#{en_sep} 6:18:20 AM #{en_tz}") - expect(f2.format(t5)).to eq("3/29/08#{en_sep} 3:35:21 AM #{en_tz}") - expect(f2.format(t6)).to eq("3/29/08#{en_sep} 4:36:22 AM #{en_tz}") - expect(f2.format(t7)).to eq("3/29/08#{en_sep} 5:37:23 AM #{en_tz}") - expect(f2.format(t8)).to eq("3/29/08#{en_sep} 6:38:24 AM #{en_tz}") + it 'lang=en_US zone=Europe/Moscow' do + expect(f2.format(t0)).to(eq("11/12/08#{en_sep} 5:21:16 PM #{en_tz}")) + expect(f2.format(t1)).to(eq("10/25/08#{en_sep} 3:15:17 AM #{en_tz}")) + expect(f2.format(t2)).to(eq("10/25/08#{en_sep} 4:16:18 AM #{en_tz}")) + expect(f2.format(t3)).to(eq("10/25/08#{en_sep} 5:17:19 AM #{en_tz}")) + expect(f2.format(t4)).to(eq("10/25/08#{en_sep} 6:18:20 AM #{en_tz}")) + expect(f2.format(t5)).to(eq("3/29/08#{en_sep} 3:35:21 AM #{en_tz}")) + expect(f2.format(t6)).to(eq("3/29/08#{en_sep} 4:36:22 AM #{en_tz}")) + expect(f2.format(t7)).to(eq("3/29/08#{en_sep} 5:37:23 AM #{en_tz}")) + expect(f2.format(t8)).to(eq("3/29/08#{en_sep} 6:38:24 AM #{en_tz}")) end - f3 = TimeFormatting.create(:locale => 'de_DE', :zone => 'Africa/Dakar', :date => :short , :time => :long) - ge_sep = "" - if cldr_version >= "27.0.1" - ge_sep = "," - end + f3 = described_class.create(locale: 'de_DE', zone: 'Africa/Dakar', date: :short, time: :long) + ge_sep = '' + ge_sep = ',' if cldr_version >= '27.0.1' ge_exp = "dd.MM.yy#{ge_sep} HH:mm:ss z" it 'check date_format for lang=de_DE' do - expect(f3.date_format(true)).to eq(ge_exp) - expect(f3.date_format(false)).to eq(ge_exp) + expect(f3.date_format(true)).to(eq(ge_exp)) + expect(f3.date_format(false)).to(eq(ge_exp)) end - it "lang=de_DE zone=Africa/Dakar" do - expect(f3.format(t0)).to eq("12.11.08#{ge_sep} 14:21:16 GMT") - expect(f3.format(t1)).to eq("24.10.08#{ge_sep} 23:15:17 GMT") - expect(f3.format(t2)).to eq("25.10.08#{ge_sep} 00:16:18 GMT") - expect(f3.format(t3)).to eq("25.10.08#{ge_sep} 01:17:19 GMT") - expect(f3.format(t4)).to eq("25.10.08#{ge_sep} 02:18:20 GMT") - expect(f3.format(t5)).to eq("29.03.08#{ge_sep} 00:35:21 GMT") - expect(f3.format(t6)).to eq("29.03.08#{ge_sep} 01:36:22 GMT") - expect(f3.format(t7)).to eq("29.03.08#{ge_sep} 02:37:23 GMT") - expect(f3.format(t8)).to eq("29.03.08#{ge_sep} 03:38:24 GMT") + it 'lang=de_DE zone=Africa/Dakar' do + expect(f3.format(t0)).to(eq("12.11.08#{ge_sep} 14:21:16 GMT")) + expect(f3.format(t1)).to(eq("24.10.08#{ge_sep} 23:15:17 GMT")) + expect(f3.format(t2)).to(eq("25.10.08#{ge_sep} 00:16:18 GMT")) + expect(f3.format(t3)).to(eq("25.10.08#{ge_sep} 01:17:19 GMT")) + expect(f3.format(t4)).to(eq("25.10.08#{ge_sep} 02:18:20 GMT")) + expect(f3.format(t5)).to(eq("29.03.08#{ge_sep} 00:35:21 GMT")) + expect(f3.format(t6)).to(eq("29.03.08#{ge_sep} 01:36:22 GMT")) + expect(f3.format(t7)).to(eq("29.03.08#{ge_sep} 02:37:23 GMT")) + expect(f3.format(t8)).to(eq("29.03.08#{ge_sep} 03:38:24 GMT")) end context 'skeleton pattern' do - f4 = TimeFormatting.create(:locale => 'fr_FR', :date => :pattern, :time => :pattern, :skeleton => 'MMMy') + f4 = described_class.create(locale: 'fr_FR', date: :pattern, time: :pattern, skeleton: 'MMMy') it 'check format' do - expect(f4.format(t0)).to eq("nov. 2008") - expect(f4.format(t1)).to eq("oct. 2008") + expect(f4.format(t0)).to(eq('nov. 2008')) + expect(f4.format(t1)).to(eq('oct. 2008')) end it 'check date_format' do - expect(f4.date_format(true)).to eq("MMM y") + expect(f4.date_format(true)).to(eq('MMM y')) end end @@ -101,96 +99,107 @@ module ICU ['en_AU', 'fr_FR', 'zh_CN'].each do |locale_name| context "with locale #{locale_name}" do it 'works with hour_cycle: h11' do - t = Time.new(2021, 04, 01, 12, 05, 0, "+00:00") - str = TimeFormatting.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', hour_cycle: 'h11') - expect(str).to match(/0:05/i) - expect(str).to match(/(pm|下午)/i) + t = Time.new(2021, 0o4, 0o1, 12, 0o5, 0, '+00:00') + str = described_class.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', + hour_cycle: 'h11') + expect(str).to(match(/0:05/i)) + expect(str).to(match(/(pm|下午)/i)) end it 'works with hour_cycle: h12' do - t = Time.new(2021, 04, 01, 12, 05, 0, "+00:00") - str = TimeFormatting.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', hour_cycle: 'h12') - expect(str).to match(/12:05/i) - expect(str).to match(/(pm|下午)/i) + t = Time.new(2021, 0o4, 0o1, 12, 0o5, 0, '+00:00') + str = described_class.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', + hour_cycle: 'h12') + expect(str).to(match(/12:05/i)) + expect(str).to(match(/(pm|下午)/i)) end it 'works with hour_cycle: h23' do - t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00") - str = TimeFormatting.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', hour_cycle: 'h23') - expect(str).to match(/0:05/i) - expect(str).to_not match(/(am|pm)/i) + t = Time.new(2021, 0o4, 0o1, 0o0, 0o5, 0, '+00:00') + str = described_class.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', + hour_cycle: 'h23') + expect(str).to(match(/0:05/i)) + expect(str).not_to(match(/(am|pm)/i)) end it 'works with hour_cycle: h24' do - t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00") - str = TimeFormatting.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', hour_cycle: 'h24') - expect(str).to match(/24:05/i) - expect(str).to_not match(/(am|pm)/i) + t = Time.new(2021, 0o4, 0o1, 0o0, 0o5, 0, '+00:00') + str = described_class.format(t, time: :short, date: :none, locale: locale_name, zone: 'UTC', + hour_cycle: 'h24') + expect(str).to(match(/24:05/i)) + expect(str).not_to(match(/(am|pm)/i)) end it 'does not include am/pm if time is not requested' do - t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00") - str = TimeFormatting.format(t, time: :none, date: :short, locale: locale_name, zone: 'UTC', hour_cycle: 'h12') - expect(str).to_not match(/(am|pm|下午|上午)/i) + t = Time.new(2021, 0o4, 0o1, 0o0, 0o5, 0, '+00:00') + str = described_class.format(t, time: :none, date: :short, locale: locale_name, zone: 'UTC', + hour_cycle: 'h12') + expect(str).not_to(match(/(am|pm|下午|上午)/i)) end context '@hours keyword' do - before(:each) do - skip("Only works on ICU >= 67") if Lib.version.to_a[0] < 67 + before do + skip('Only works on ICU >= 67') if Lib.version.to_a[0] < 67 end it 'works with @hours=h11 keyword' do - t = Time.new(2021, 04, 01, 12, 05, 0, "+00:00") + t = Time.new(2021, 0o4, 0o1, 12, 0o5, 0, '+00:00') locale = Locale.new(locale_name).with_keyword('hours', 'h11').to_s - str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale) - expect(str).to match(/0:05/i) - expect(str).to match(/(pm|下午)/i) + str = described_class.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', + hour_cycle: :locale) + expect(str).to(match(/0:05/i)) + expect(str).to(match(/(pm|下午)/i)) end + it 'works with @hours=h12 keyword' do - t = Time.new(2021, 04, 01, 12, 05, 0, "+00:00") + t = Time.new(2021, 0o4, 0o1, 12, 0o5, 0, '+00:00') locale = Locale.new(locale_name).with_keyword('hours', 'h12').to_s - str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale) - expect(str).to match(/12:05/i) - expect(str).to match(/(pm|下午)/i) + str = described_class.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', + hour_cycle: :locale) + expect(str).to(match(/12:05/i)) + expect(str).to(match(/(pm|下午)/i)) end it 'works with @hours=h23 keyword' do - t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00") + t = Time.new(2021, 0o4, 0o1, 0o0, 0o5, 0, '+00:00') locale = Locale.new(locale_name).with_keyword('hours', 'h23').to_s - str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale) - expect(str).to match(/0:05/i) - expect(str).to_not match(/(am|pm)/i) + str = described_class.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', + hour_cycle: :locale) + expect(str).to(match(/0:05/i)) + expect(str).not_to(match(/(am|pm)/i)) end it 'works with @hours=h24 keyword' do - t = Time.new(2021, 04, 01, 00, 05, 0, "+00:00") + t = Time.new(2021, 0o4, 0o1, 0o0, 0o5, 0, '+00:00') locale = Locale.new(locale_name).with_keyword('hours', 'h24').to_s - str = TimeFormatting.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', hour_cycle: :locale) - expect(str).to match(/24:05/i) - expect(str).to_not match(/(am|pm)/i) + str = described_class.format(t, time: :short, date: :none, locale: locale, zone: 'UTC', + hour_cycle: :locale) + expect(str).to(match(/24:05/i)) + expect(str).not_to(match(/(am|pm)/i)) end end end end it 'for lang=fi hour_cycle=h12' do - t = Time.new(2021, 04, 01, 13, 05, 0, "+00:00") - str = TimeFormatting.format(t, locale: 'fi', zone: 'America/Los_Angeles', date: :long, time: :short, hour_cycle: 'h12') - expect(str).to match(/\sklo\s/) + t = Time.new(2021, 0o4, 0o1, 13, 0o5, 0, '+00:00') + str = described_class.format(t, locale: 'fi', zone: 'America/Los_Angeles', date: :long, time: :short, + hour_cycle: 'h12') + expect(str).to(match(/\sklo\s/)) end it 'works with defaults on a h12 locale' do - t = Time.new(2021, 04, 01, 13, 05, 0, "+00:00") - str = TimeFormatting.format(t, time: :short, date: :none, locale: 'en_AU', zone: 'UTC', hour_cycle: :locale) - expect(str).to match(/1:05/i) - expect(str).to match(/pm/i) + t = Time.new(2021, 0o4, 0o1, 13, 0o5, 0, '+00:00') + str = described_class.format(t, time: :short, date: :none, locale: 'en_AU', zone: 'UTC', hour_cycle: :locale) + expect(str).to(match(/1:05/i)) + expect(str).to(match(/pm/i)) end it 'works with defaults on a h23 locale' do - t = Time.new(2021, 04, 01, 0, 05, 0, "+00:00") - str = TimeFormatting.format(t, time: :short, date: :none, locale: 'fr_FR', zone: 'UTC', hour_cycle: :locale) - expect(str).to match(/0:05/i) - expect(str).to_not match(/(am|pm)/i) + t = Time.new(2021, 0o4, 0o1, 0, 0o5, 0, '+00:00') + str = described_class.format(t, time: :short, date: :none, locale: 'fr_FR', zone: 'UTC', hour_cycle: :locale) + expect(str).to(match(/0:05/i)) + expect(str).not_to(match(/(am|pm)/i)) end end end diff --git a/spec/transliteration_spec.rb b/spec/transliteration_spec.rb index 8d968e3..d868718 100644 --- a/spec/transliteration_spec.rb +++ b/spec/transliteration_spec.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - module ICU describe Transliteration::Transliterator do def transliterator_for(*args) @@ -7,30 +5,29 @@ def transliterator_for(*args) end [ - ["Any-Hex", "abcde", "\\u0061\\u0062\\u0063\\u0064\\u0065"], - ["Lower", "ABC", "abc"], - ["Han-Latin", "雙屬性集合之空間分群演算法-應用於地理資料", "shuāng shǔ xìng jí hé zhī kōng jiān fēn qún yǎn suàn fǎ-yīng yòng yú de lǐ zī liào"], - ["Devanagari-Latin", "दौलत", "daulata"] + ['Any-Hex', 'abcde', '\\u0061\\u0062\\u0063\\u0064\\u0065'], + ['Lower', 'ABC', 'abc'], + ['Han-Latin', '雙屬性集合之空間分群演算法-應用於地理資料', + 'shuāng shǔ xìng jí hé zhī kōng jiān fēn qún yǎn suàn fǎ-yīng yòng yú de lǐ zī liào'], + ['Devanagari-Latin', 'दौलत', 'daulata'] ].each do |id, input, output| - it "should transliterate #{id}" do + it "transliterates #{id}" do tl = transliterator_for(id) - expect(tl.transliterate(input)).to eq(output) + expect(tl.transliterate(input)).to(eq(output)) end - end - end # Transliterator + end describe Transliteration do - it "should provide a list of available ids" do - ids = ICU::Transliteration.available_ids + it 'provides a list of available ids' do + ids = described_class.available_ids - expect(ids).to be_an(Array) - expect(ids).to_not be_empty + expect(ids).to(be_an(Array)) + expect(ids).not_to(be_empty) end - it "should transliterate custom rules" do - expect(ICU::Transliteration.translit("NFD; [:Nonspacing Mark:] Remove; NFC", "âêîôû")).to eq("aeiou") - end - - end # Transliteration -end # ICU + it 'transliterates custom rules' do + expect(described_class.translit('NFD; [:Nonspacing Mark:] Remove; NFC', 'âêîôû')).to(eq('aeiou')) + end + end +end diff --git a/spec/uchar_spec.rb b/spec/uchar_spec.rb index 6371b20..f5d8cbe 100644 --- a/spec/uchar_spec.rb +++ b/spec/uchar_spec.rb @@ -1,33 +1,31 @@ -# encoding: UTF-8 - module ICU describe UCharPointer do it 'allocates enough memory for 16-bit characters' do - expect(UCharPointer.new(5).size).to eq(10) + expect(described_class.new(5).size).to(eq(10)) end it 'builds a buffer from a string' do - ptr = UCharPointer.from_string('abc') - expect(ptr).to be_a UCharPointer - expect(ptr.size).to eq(6) - expect(ptr.read_array_of_uint16(3)).to eq([0x61, 0x62, 0x63]) + ptr = described_class.from_string('abc') + expect(ptr).to(be_a(described_class)) + expect(ptr.size).to(eq(6)) + expect(ptr.read_array_of_uint16(3)).to(eq([0x61, 0x62, 0x63])) end it 'takes an optional capacity' do - ptr = UCharPointer.from_string('abc', 5) - expect(ptr.size).to eq(10) + ptr = described_class.from_string('abc', 5) + expect(ptr.size).to(eq(10)) end describe 'converting to string' do - let(:ptr) { UCharPointer.new(3).write_array_of_uint16 [0x78, 0x0, 0x79] } + let(:ptr) { described_class.new(3).write_array_of_uint16([0x78, 0x0, 0x79]) } it 'returns the the entire buffer by default' do - expect(ptr.string).to eq("x\0y") + expect(ptr.string).to(eq("x\0y")) end it 'returns strings of the specified length' do - expect(ptr.string(0)).to eq("") - expect(ptr.string(2)).to eq("x\0") + expect(ptr.string(0)).to(eq('')) + expect(ptr.string(2)).to(eq("x\0")) end end end From 324286adef4504cb64aa1858832f0d5ae01f1dd7 Mon Sep 17 00:00:00 2001 From: Erick Guan <297343+erickguan@users.noreply.github.com> Date: Sat, 2 Mar 2024 21:06:22 +0100 Subject: [PATCH 10/10] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39a01a6..ec8acb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed +- Required Ruby 3.0 and up. ### Fixed