├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bin └── petrovich ├── lib ├── petrovich.rb ├── petrovich │ ├── case │ │ ├── rule.rb │ │ └── rule │ │ │ ├── modifier.rb │ │ │ └── test.rb │ ├── gender.rb │ ├── gender │ │ └── rule.rb │ ├── inflected.rb │ ├── inflector.rb │ ├── name.rb │ ├── rule_set.rb │ ├── unicode.rb │ ├── value.rb │ └── version.rb └── tasks │ └── evaluate.rake ├── petrovich.gemspec ├── petrovich.png └── test ├── bench_core.rb ├── petrovich_gender_test.rb ├── petrovich_test.rb └── test_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "bundler" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | - package-ecosystem: "gitsubmodule" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | submodules: true 17 | - name: Set up Ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: ruby 21 | bundler-cache: true 22 | - name: Publish to GPR 23 | run: | 24 | mkdir -p $HOME/.gem 25 | touch $HOME/.gem/credentials 26 | chmod 0600 $HOME/.gem/credentials 27 | printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 28 | gem build petrovich.gemspec 29 | gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} petrovich-*.gem 30 | env: 31 | GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}" 32 | OWNER: ${{ github.repository_owner }} 33 | - name: Publish to RubyGems 34 | run: | 35 | mkdir -p $HOME/.gem 36 | touch $HOME/.gem/credentials 37 | chmod 0600 $HOME/.gem/credentials 38 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 39 | gem build petrovich.gemspec 40 | gem push petrovich-*.gem 41 | env: 42 | GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}" 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | continue-on-error: ${{ contains(matrix.ruby-version, 'head') }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | ruby-version: ['2.7', '3.0', '3.1', ruby, head, jruby, jruby-head, truffleruby, truffleruby-head] 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | submodules: true 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby-version }} 25 | bundler-cache: true 26 | - name: Build 27 | run: gem build petrovich.gemspec 28 | - name: Run tests 29 | run: rake 30 | - name: Run evaluation 31 | run: rake evaluate 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .rbx 4 | .bundle 5 | .config 6 | .yardoc 7 | .rvmrc 8 | .ruby-version 9 | .ruby-gemset 10 | Gemfile.lock 11 | InstalledFiles 12 | _yardoc 13 | coverage 14 | doc/ 15 | lib/bundler/man 16 | pkg 17 | rdoc 18 | test/reports 19 | test/data/*.txt 20 | test/data/male_surnames_spec.rb 21 | test/data/female_surnames_spec.rb 22 | test/data/generator.rb 23 | test/tmp 24 | test/version_tmp 25 | tmp 26 | test.html 27 | *.tsv 28 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rules"] 2 | path = rules 3 | url = https://github.com/petrovich/petrovich-rules.git 4 | [submodule "eval"] 5 | path = eval 6 | url = https://github.com/petrovich/petrovich-eval.git 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014 Andrew Kozloff & Dmitry Ustalov 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Petrovich](petrovich.png) 2 | 3 | Склонение падежей русских имён, фамилий и отчеств. Вы задаёте начальное имя 4 | в именительном падеже, а получаете в нужном вам. Просто посмотрите на 5 | [демонстрацию](http://petrovich.nlpub.ru/) и сделайте так же. 6 | 7 | Petrovich также позволяет определять пол по имени, фамилии, отчеству. 8 | 9 | [![RubyGems][rubygems_badge]][rubygems_link] [![Build Status][tests_badge]][tests_link] 10 | 11 | [rubygems_badge]: https://badge.fury.io/rb/petrovich.svg 12 | [rubygems_link]: https://rubygems.org/gems/petrovich 13 | [tests_badge]: https://github.com/petrovich/petrovich-ruby/actions/workflows/test.yml/badge.svg 14 | [tests_link]: https://github.com/petrovich/petrovich-ruby/actions/workflows/test.yml 15 | 16 | ## Установка 17 | 18 | Добавьте в Gemfile: 19 | 20 | ```ruby 21 | gem 'petrovich', '~> 1.0' 22 | ``` 23 | 24 | Установите гем cредствами Bundler: 25 | 26 | $ bundle 27 | 28 | Или установите его отдельно: 29 | 30 | $ gem install petrovich 31 | 32 | ## Зависимости 33 | 34 | Для работы гема требуется Ruby не младше версии 1.9.3. Petrovich не 35 | привязан к Ruby on Rails и может свободно использоваться практически 36 | в любых приложениях и библиотеках на языке Ruby. 37 | 38 | ## Использование 39 | 40 | Вы задаёте начальные значения (фамилию, имя и отчество) в именительном 41 | падеже, а библиотека делает всё остальное. Если вам известен пол - укажите его, это повысит скорость работы и даст более точные результаты. Если пол не указан, то Petrovich попытается определить его автоматически. Примеры: 42 | 43 | ```ruby 44 | # Склонение в дательный падеж при помощи метода `dative`. Существуют методы `genitive`, 45 | # `dative`, `accusative`, `instrumental`, `prepositional`. 46 | Petrovich( 47 | lastname: 'Салтыков-Щедрин', 48 | firstname: 'Михаил', 49 | middlename: 'Евграфович', 50 | ).dative.to_s # => Салтыкову-Щедрину Михаилу Евграфовичу 51 | 52 | # Склонение в творительный падеж с использованием метода `to` и возвратом отчества. 53 | # Аналогично можно вызвать метод `firstname`, чтобы получить имя. 54 | Petrovich( 55 | firstname: 'Иван', 56 | middlename: 'Петрович', 57 | ).to(:instrumental).middlename # => Петровича 58 | 59 | # Склонение с указанием пола. В данном случае, по имени и фамилии невозможно определить пол 60 | # человека, поэтому, если вам известен пол, то всегда передавайте его в аргументах, 61 | # чтобы склонение было верным. 62 | # Если пол неизвестен, то гем попытается определить его самостоятельно. 63 | # Пол должен быть указан в виде строки или символа. Возможные значения: male, female. 64 | Petrovich( 65 | lastname: 'Андрейчук', 66 | firstname: 'Саша', 67 | gender: :male 68 | ).to(:instrumental).to_s # => Андрейчуку Саше 69 | ``` 70 | 71 | Полный список поддерживаемых методов склонения приведён 72 | в таблице ниже. 73 | 74 | | Метод | Падеж | Характеризующий вопрос | 75 | |----------------|--------------|------------------------| 76 | | genitive | родительный | Кого? | 77 | | dative | дательный | Кому? | 78 | | accusative | винительный | Кого? | 79 | | instrumental | творительный | Кем? | 80 | | prepositional | предложный | О ком? | 81 | 82 | ### Определение пола 83 | 84 | Примеры: 85 | 86 | ```ruby 87 | Petrovich( 88 | lastname: 'Склифасовский' 89 | ).gender # => :male 90 | 91 | Petrovich( 92 | firstname: 'Александра', 93 | lastname: 'Склифасовская' 94 | ).female? # => true 95 | 96 | Petrovich( 97 | lastname: 'Склифасовский' 98 | ).male? # => true 99 | 100 | Petrovich( 101 | firstname: 'Саша', 102 | lastname: 'Андрейчук' 103 | ).androgynous? # => true 104 | 105 | Petrovich( 106 | firstname: 'Саша', 107 | lastname: 'Андрейчук' 108 | gender: :male, 109 | ).male? # => true 110 | ``` 111 | 112 | ## Интерфейс командной строки 113 | 114 | Примеры вызовов: 115 | 116 | ```bash 117 | petrovich -l Иванов -f Иван -m Иванович -g male -c accusative 118 | petrovich -l Иванов -f Иван -m Иванович -c accusative -n 119 | petrovich -l Иванов -f Иван -m Иванович -c accusative -o 120 | ``` 121 | 122 | Подробное руководство: `petrovich --help`. 123 | 124 | ## Модульные тесты 125 | 126 | Для запуска тестов достаточно выполнить команду `rake`. 127 | Чтобы запустить тесты "аккуратности" по словарю фамилий, выполните команду `rake evaluate`, после выполнения вы увидите подробный отчёт. 128 | 129 | ## Разработчики 130 | 131 | * [Андрей Козлов](https://github.com/tanraya) 132 | * [Дмитрий Усталов](https://github.com/dustalov) 133 | 134 | ## Благодарности 135 | 136 | Эта библиотека не была бы столь замечательна без содействия Павла Скрылёва, 137 | Никиты Помящего, Игоря Бочкарёва, и других хороших людей. 138 | 139 | Отдельная благодарность [Андрею Бильжо](https://ru.wikipedia.org/wiki/%D0%91%D0%B8%D0%BB%D1%8C%D0%B6%D0%BE,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9_%D0%93%D0%B5%D0%BE%D1%80%D0%B3%D0%B8%D0%B5%D0%B2%D0%B8%D1%87) за то, что разрешил нам использовать персонаж Петровича в нашем логотипе. 140 | 141 | ## Портирование 142 | 143 | Существуют официальные порты Petrovich на другие языке и платформы. Их список 144 | доступен по адресу . Ребята, спасибо! 145 | 146 | ## Содействие 147 | 148 | Если вы нашли баги как программной части, так и в базе правил, то вы всегда 149 | можете форкнуть репозиторий и внести необходимые изменения. Ваша помощь не 150 | останется незамеченной! Если вы заметили ошибки при склонении падежей имён, 151 | фамилий или отчеств, можете написать об этом в Issues на GitHub. 152 | Проблема будет сразу же исследована и, по возможности, решена. 153 | 154 | Не стесняйтесь добавлять улучшения. 155 | 156 | 1. Fork it 157 | 2. Create your feature branch (`git checkout -b my-new-feature`) 158 | 3. Commit your changes (`git commit -am 'Add some feature'`) 159 | 4. Push to the branch (`git push origin my-new-feature`) 160 | 5. Create new Pull Request 161 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # encoding: utf-8 3 | 4 | require 'bundler/gem_tasks' 5 | require 'rake/testtask' 6 | 7 | Rake::TestTask.new do |t| 8 | t.test_files = FileList['test/**/*_test.rb'] 9 | end 10 | 11 | task default: :test 12 | 13 | # We need this to avoid loading the library in tasks directly. 14 | task :petrovich do 15 | require 'petrovich' 16 | end 17 | 18 | task :populate do 19 | system 'git submodule update --init --recursive' 20 | end 21 | 22 | task :update do 23 | system 'git submodule update --init --remote' 24 | end 25 | 26 | Rake::TestTask.new(:bench) do |test| 27 | test.libs << 'test' 28 | test.test_files = Dir['test/**/bench_*.rb'] 29 | end 30 | 31 | Dir.glob('lib/tasks/*.rake').each { |r| import r } 32 | -------------------------------------------------------------------------------- /bin/petrovich: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'ostruct' 4 | require 'optparse' 5 | 6 | if File.exists? File.expand_path('../../.git', __FILE__) 7 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 8 | end 9 | 10 | require 'petrovich' 11 | 12 | options = OpenStruct.new 13 | 14 | optparse = OptionParser.new do |opts| 15 | opts.banner = 'Usage: %s [options] command' % $PROGRAM_NAME 16 | 17 | opts.on '-l', '--lastname [LASTNAME]', 'Last name' do |lastname| 18 | options.lastname = lastname 19 | end 20 | 21 | opts.on '-f', '--firstname [FIRSTNAME]', 'First name' do |firstname| 22 | options.firstname = firstname 23 | end 24 | 25 | opts.on '-m', '--middlename [MIDDLENAME]', 'Middle name' do |middlename| 26 | options.middlename = middlename 27 | end 28 | 29 | opts.on '-g', '--gender [GENDER]', Petrovich::GENDERS, 'Gender, if known' do |gender| 30 | options.gender = gender 31 | end 32 | 33 | opts.on '-c', '--case CASE', Petrovich::CASES, 'Required case' do |rcase| 34 | options.case = rcase 35 | end 36 | 37 | opts.on_tail '-n', '--name-gender', 'Print the name and the gender' do 38 | options.name_and_gender = true 39 | end 40 | 41 | opts.on_tail '-o', '--only-gender', 'Print the gender only' do 42 | options.gender_only = true 43 | end 44 | 45 | opts.on_tail '-h', '--help', 'Just display this help' do 46 | puts opts 47 | exit 48 | end 49 | 50 | opts.on_tail '-v', '--version', 'Just print the version infomation' do 51 | puts 'Petrovich %s' % Petrovich::VERSION 52 | exit 53 | end 54 | end 55 | 56 | optparse.parse! 57 | 58 | petrovich = Petrovich( 59 | lastname: options.lastname, 60 | firstname: options.firstname, 61 | middlename: options.middlename, 62 | gender: options.gender 63 | ) 64 | 65 | if options.gender_only 66 | puts petrovich.gender 67 | exit 68 | end 69 | 70 | name = petrovich.to(options.case).to_s 71 | if options.name_and_gender 72 | puts "%s\t%s" % [name, petrovich.gender] 73 | else 74 | puts name 75 | end 76 | -------------------------------------------------------------------------------- /lib/petrovich.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'forwardable' 3 | require 'petrovich/version' 4 | require 'petrovich/value' 5 | require 'petrovich/inflector' 6 | require 'petrovich/inflected' 7 | require 'petrovich/gender' 8 | require 'petrovich/unicode' 9 | require 'petrovich/rule_set' 10 | require 'petrovich/case/rule' 11 | require 'petrovich/case/rule/modifier' 12 | require 'petrovich/case/rule/test' 13 | require 'petrovich/gender/rule' 14 | 15 | # A library to inflect Russian anthroponyms such as first names, last names, and middle names. 16 | module Petrovich 17 | # Possible cases 18 | CASES = [ 19 | :nominative, 20 | :genitive, 21 | :dative, 22 | :accusative, 23 | :instrumental, 24 | :prepositional 25 | ] 26 | 27 | # Possible genders 28 | GENDERS = [ 29 | :androgynous, 30 | :male, 31 | :female 32 | ] 33 | 34 | class << self 35 | # A place that keeps inflection and gender rules loaded from yaml file. 36 | # 37 | # @return Petrovich::RuleSet 38 | attr_accessor :rule_set 39 | 40 | # Checks name that should be value object, instance of {Petrovich::Value} 41 | # 42 | # Raises an ArgumentError if passed argument not of type Petrovich::Value or 43 | # all values is empty in this object. 44 | # 45 | # @example 46 | # Petrovich.assert_name!(name) 47 | # 48 | # @param [Foo] name Value to check 49 | def assert_name!(name) 50 | unless name.is_a?(Value) 51 | fail ArgumentError, 'Passed argument should be Petrovich::Value instace'.freeze 52 | end 53 | 54 | if [name.lastname, name.firstname, name.middlename].compact.size == 0 55 | fail ArgumentError, 'You should set at least one of :lastname, :firstname or :middlename'.freeze 56 | end 57 | end 58 | 59 | # Checks passed argument that should be member of {CASES} 60 | # 61 | # Raises an ArgumentError if passed argument not in {CASES} 62 | # 63 | # @example 64 | # Petrovich.assert_case!(name_case) 65 | # 66 | # @param [Foo] name_case Value to check 67 | def assert_case!(name_case) 68 | return if CASES.include?(name_case) 69 | fail ArgumentError, "Unknown case #{name_case}" 70 | end 71 | 72 | # Converts hash to {Petrovich::Value} value object 73 | # 74 | # @example 75 | # Petrovich.normalize_name(firstname: 'Иван', lastname: 'Иванов') 76 | # 77 | # @param [Hash] name Value hash with lastname, firstname, middlename 78 | # @return [Petrovich::Value] Value object 79 | def normalize_name(name) 80 | name = Value.new(name) if name.is_a?(Hash) 81 | name 82 | end 83 | 84 | # Loads YAML rules into {Petrovich::RuleSet} object 85 | # @return [void] 86 | def load_rules! 87 | self.rule_set ||= RuleSet.new 88 | self.rule_set.load! 89 | end 90 | end 91 | 92 | load_rules! 93 | 94 | require 'petrovich/name' 95 | end 96 | 97 | # Main entry point 98 | def Petrovich(opts) 99 | Petrovich::Name.new(opts) 100 | end 101 | -------------------------------------------------------------------------------- /lib/petrovich/case/rule.rb: -------------------------------------------------------------------------------- 1 | module Petrovich 2 | module Case 3 | # A case rule from the set of rules 4 | class Rule 5 | attr_reader :gender, :modifiers, :tests, :tags, :as, :an_exception 6 | 7 | def initialize(opts) 8 | @gender = opts[:gender].to_sym.downcase 9 | @as = opts[:as] 10 | @an_exception = opts[:section] == :exceptions 11 | @modifiers = opts[:modifiers] 12 | @tests = opts[:tests] 13 | @tags = [] 14 | 15 | @tests_regexp = Regexp.union(Array(@tests).map(&:suffix)) 16 | end 17 | 18 | def match?(name, match_gender, match_as, known_gender = false) 19 | return false unless match_as == as 20 | 21 | if known_gender 22 | return false if match_gender != gender 23 | else 24 | return false if gender != :female && match_gender == :female 25 | return false if gender == :female && match_gender != :female 26 | end 27 | 28 | !!name.match(@tests_regexp) 29 | end 30 | 31 | # Is this exceptional rule? 32 | def an_exception? 33 | an_exception == true 34 | end 35 | 36 | def get_modifier(name_case) 37 | case name_case.to_sym 38 | when :nominative 39 | nil 40 | when :genitive 41 | modifiers[0] 42 | when :dative 43 | modifiers[1] 44 | when :accusative 45 | modifiers[2] 46 | when :instrumental 47 | modifiers[3] 48 | when :prepositional 49 | modifiers[4] 50 | else 51 | fail UnknownCaseError, "Unknown grammatic case: #{name_case}".freeze 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/petrovich/case/rule/modifier.rb: -------------------------------------------------------------------------------- 1 | module Petrovich 2 | module Case 3 | class Rule 4 | # A modifier for the test rule 5 | class Modifier 6 | attr_reader :suffix, :offset 7 | 8 | def initialize(suffix, offset = 0) 9 | @suffix = suffix.to_s 10 | @offset = offset 11 | end 12 | 13 | def inspect 14 | [suffix, offset].inspect 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/petrovich/case/rule/test.rb: -------------------------------------------------------------------------------- 1 | module Petrovich 2 | module Case 3 | class Rule 4 | # A test for the case rule 5 | class Test 6 | attr_reader :suffix 7 | 8 | def initialize(suffix) 9 | @suffix = /#{suffix}$/i 10 | end 11 | 12 | def match?(name) 13 | !!name.match(suffix) 14 | end 15 | 16 | def inspect 17 | suffix.inspect 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/petrovich/gender.rb: -------------------------------------------------------------------------------- 1 | module Petrovich 2 | # Methods of determining gender by the name 3 | module Gender 4 | def self.detect(name) 5 | # Accept hash and convert it to ostruct object 6 | name = Petrovich.normalize_name(name) 7 | rule_set = Petrovich.rule_set 8 | genders = {} 9 | 10 | Petrovich.assert_name!(name) 11 | 12 | [:lastname, :firstname, :middlename].each do |name_part| 13 | next unless name.respond_to?(name_part) && name.send(name_part) 14 | 15 | rules = rule_set.find_all_gender_rules(name.send(name_part), name_part) 16 | 17 | rules.each do |rule| 18 | genders[name_part] = rule.nil? ? :androgynous : rule.gender 19 | end 20 | end 21 | 22 | # Return gender if middlename is specified and gender is determined. 23 | return genders[:middlename] if genders[:middlename] && genders[:middlename] != :androgynous 24 | 25 | if genders.values.uniq.size > 1 26 | if genders[:firstname] != :androgynous && genders[:lastname] == :androgynous 27 | return genders[:firstname] 28 | end 29 | 30 | if genders[:lastname] != :androgynous && genders[:firstname] == :androgynous 31 | return genders[:lastname] 32 | end 33 | end 34 | 35 | # Otherwise, it returns what recognized 36 | return genders.values.uniq.first if genders.values.uniq.size == 1 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/petrovich/gender/rule.rb: -------------------------------------------------------------------------------- 1 | module Petrovich 2 | module Gender 3 | # A gender rule from the set of rules 4 | class Rule 5 | attr_reader :gender, :as, :suffix, :accuracy 6 | 7 | # TODO: check options (see Case::Rule) 8 | def initialize(opts) 9 | @gender = opts[:gender] 10 | @as = opts[:as] 11 | @suffix = /#{opts[:suffix]}$/i 12 | @accuracy = opts[:suffix].length 13 | end 14 | 15 | def match?(name) 16 | !!name.match(suffix) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/petrovich/inflected.rb: -------------------------------------------------------------------------------- 1 | module Petrovich 2 | # Keeps inflected @name 3 | class Inflected 4 | extend Forwardable 5 | 6 | def_delegator :@name, :lastname, :lastname 7 | def_delegator :@name, :firstname, :firstname 8 | def_delegator :@name, :middlename, :middlename 9 | 10 | def initialize(name) 11 | @name = name 12 | end 13 | 14 | def to_s 15 | [lastname, firstname, middlename].compact.join(' ') 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/petrovich/inflector.rb: -------------------------------------------------------------------------------- 1 | module Petrovich 2 | class Inflector 3 | def initialize(name, gender, name_case) 4 | Petrovich.assert_name!(name) 5 | 6 | @name = Petrovich.normalize_name(name) 7 | @gender = gender 8 | @name_case = name_case 9 | end 10 | 11 | def inflect_lastname(rules) 12 | inflect(@name.lastname, rules) 13 | end 14 | 15 | def inflect_firstname(rules) 16 | inflect(@name.firstname, rules) 17 | end 18 | 19 | def inflect_middlename(rules) 20 | inflect(@name.middlename, rules) 21 | end 22 | 23 | private 24 | 25 | def inflect(name, rules) 26 | return name if rules.size == 0 27 | 28 | parts = name.split('-') 29 | parts.map! do |part| 30 | rule = rules.shift 31 | 32 | if rule && (modifier = rule.get_modifier(@name_case)) 33 | part.slice(0, part.size - modifier.offset) + modifier.suffix 34 | else 35 | part 36 | end 37 | end 38 | 39 | parts.join('-') 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/petrovich/name.rb: -------------------------------------------------------------------------------- 1 | module Petrovich 2 | class Name 3 | extend Forwardable 4 | 5 | def_delegator :@name, :lastname, :lastname 6 | def_delegator :@name, :firstname, :firstname 7 | def_delegator :@name, :middlename, :middlename 8 | 9 | def initialize(opts) 10 | @rule_set = Petrovich.rule_set 11 | @gender = opts[:gender] 12 | @name = Petrovich.normalize_name( 13 | lastname: opts[:lastname], 14 | firstname: opts[:firstname], 15 | middlename: opts[:middlename] 16 | ) 17 | end 18 | 19 | def gender 20 | if known_gender? 21 | @gender.to_sym 22 | else 23 | Gender.detect(@name) 24 | end 25 | end 26 | 27 | def known_gender? 28 | !@gender.nil? && Petrovich::GENDERS.include?(@gender.to_sym) 29 | end 30 | 31 | def male? 32 | Gender.detect(@name) == :male 33 | end 34 | 35 | def female? 36 | Gender.detect(@name) == :female 37 | end 38 | 39 | def androgynous? 40 | Gender.detect(@name) == :androgynous 41 | end 42 | 43 | def to(name_case) 44 | Petrovich.assert_case!(name_case) 45 | Inflected.new(inflect(@name.dup, gender, name_case)) 46 | end 47 | 48 | def to_s 49 | [lastname, firstname, middlename].join(' ') 50 | end 51 | 52 | Petrovich::CASES.each do |name_case| 53 | define_method name_case do 54 | to(name_case) 55 | end 56 | end 57 | 58 | private 59 | 60 | def inflect(name, gender, name_case) 61 | inflector = Inflector.new(name, gender, name_case) 62 | known_gender = known_gender? 63 | find = proc { |x| @rule_set.find_all_case_rules(name.send(x), gender, x, known_gender) } 64 | 65 | if !name.lastname.nil? && (rules = find.call(:lastname)) 66 | name.lastname = inflector.inflect_lastname(rules) 67 | end 68 | 69 | if !name.firstname.nil? && (rules = find.call(:firstname)) 70 | name.firstname = inflector.inflect_firstname(rules) 71 | end 72 | 73 | if !name.middlename.nil? && (rules = find.call(:middlename)) 74 | name.middlename = inflector.inflect_middlename(rules) 75 | end 76 | 77 | name 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/petrovich/rule_set.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Petrovich 4 | # A set of loaded rules from YAML file 5 | class RuleSet 6 | def initialize 7 | clear! 8 | end 9 | 10 | def add_case_rule(rule) 11 | unless rule.is_a?(Case::Rule) 12 | fail ArgumentError, 'Expecting rule of type Petrovich::Case::Rule'.freeze 13 | end 14 | 15 | @case_rules << rule 16 | end 17 | 18 | def find_all_case_rules(name, gender, as, known_gender = false) 19 | parts = name.split('-') 20 | parts.map.with_index { |part, index| find_case_rule(part, gender, as, (index == parts.count-1) && known_gender) } 21 | end 22 | 23 | def find_all_gender_rules(name, as) 24 | name.split('-').map { |part| find_gender_rule(part, as) } 25 | end 26 | 27 | def clear! 28 | @case_rules = [] 29 | @gender_rules = {} 30 | @gender_exceptions = {} 31 | end 32 | 33 | def load! 34 | return false if @case_rules.size > 0 35 | 36 | rules = YAML.load_file( 37 | File.expand_path('../../../rules/rules.yml', __FILE__) 38 | ) 39 | gender = YAML.load_file( 40 | File.expand_path('../../../rules/gender.yml', __FILE__) 41 | ) 42 | 43 | load_case_rules!(rules) 44 | load_gender_rules!(gender) 45 | end 46 | 47 | private 48 | 49 | # Load rules for names 50 | def load_case_rules!(rules) 51 | [:lastname, :firstname, :middlename].each do |name_part| 52 | [:exceptions, :suffixes].each do |section| 53 | entries = rules[name_part.to_s][section.to_s] 54 | next if entries.nil? 55 | 56 | entries.each do |entry| 57 | load_case_entry(name_part, section, entry) 58 | end 59 | end 60 | end 61 | end 62 | 63 | # Load rules for genders 64 | def load_gender_rules!(rules) 65 | [:lastname, :firstname, :middlename].each do |name_part| 66 | Petrovich::GENDERS.each do |section| 67 | entries = rules['gender'][name_part.to_s]['suffixes'][section.to_s] 68 | Array(entries).each do |entry| 69 | load_gender_entry(name_part, section, entry) 70 | end 71 | 72 | exceptions = rules['gender'][name_part.to_s]['exceptions'] 73 | @gender_exceptions[name_part] ||= {} 74 | next if exceptions.nil? 75 | Array(exceptions[section.to_s]).each do |exception| 76 | @gender_exceptions[name_part][exception] = Gender::Rule.new(as: name_part, gender: section, suffix: exception) 77 | end 78 | end 79 | end 80 | @gender_rules.each do |_, gender_rules| 81 | gender_rules.sort_by!{ |rule| -rule.accuracy } 82 | end 83 | end 84 | 85 | def find_case_rule(name, gender, as, known_gender = false) 86 | found_rule = @case_rules.find { |rule| rule.match?(name, gender, as, known_gender) } 87 | found_rule || @case_rules.find { |rule| rule.match?(name, :androgynous, as) } 88 | end 89 | 90 | def find_gender_rule(name, as) 91 | @gender_exceptions[as][Unicode.downcase(name)] || @gender_rules[as].find{ |rule| rule.match?(name) } 92 | end 93 | 94 | def load_case_entry(as, section, entry) 95 | modifiers = entry['mods'].map do |mod| 96 | suffix = mod.scan(/[^.-]+/).first 97 | offset = mod.count('-') 98 | Petrovich::Case::Rule::Modifier.new(suffix, offset) 99 | end 100 | 101 | tests = entry['test'].map do |suffix| 102 | suffix = "^#{suffix}" if section == :exceptions 103 | Petrovich::Case::Rule::Test.new(suffix) 104 | end 105 | 106 | add_case_rule Petrovich::Case::Rule.new( 107 | gender: entry['gender'], 108 | as: as, 109 | section: section, 110 | modifiers: modifiers, 111 | tests: tests, 112 | tags: entry['tags'] 113 | ) 114 | end 115 | 116 | def load_gender_entry(as, section, entry) 117 | @gender_rules[as] ||= [] 118 | @gender_rules[as] << Gender::Rule.new(as: as, gender: section, suffix: entry) 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/petrovich/unicode.rb: -------------------------------------------------------------------------------- 1 | module Petrovich 2 | # Custom downcase and upcase methods for russian language. 3 | module Unicode 4 | RU_UPPER = [ 5 | "\u0410", "\u0411", "\u0412", "\u0413", "\u0414", "\u0415", "\u0416", "\u0417", 6 | "\u0418", "\u0419", "\u041A", "\u041B", "\u041C", "\u041D", "\u041E", "\u041F", 7 | "\u0420", "\u0421", "\u0422", "\u0423", "\u0424", "\u0425", "\u0426", "\u0427", 8 | "\u0428", "\u0429", "\u042A", "\u042B", "\u042C", "\u042D", "\u042E", "\u042F", 9 | "\u0401" # Ё 10 | ].join 11 | 12 | RU_LOWER = [ 13 | "\u0430", "\u0431", "\u0432", "\u0433", "\u0434", "\u0435", "\u0436", "\u0437", 14 | "\u0438", "\u0439", "\u043A", "\u043B", "\u043C", "\u043D", "\u043E", "\u043F", 15 | "\u0440", "\u0441", "\u0442", "\u0443", "\u0444", "\u0445", "\u0446", "\u0447", 16 | "\u0448", "\u0449", "\u044A", "\u044B", "\u044C", "\u044D", "\u044E", "\u044F", 17 | "\u0451" # Ё 18 | ].join 19 | 20 | def self.downcase(entry) 21 | entry.to_s.tr(RU_UPPER, RU_LOWER) 22 | end 23 | 24 | def self.upcase(entry) 25 | entry.to_s.tr(RU_LOWER, RU_UPPER) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/petrovich/value.rb: -------------------------------------------------------------------------------- 1 | module Petrovich 2 | # Value-object for name 3 | class Value 4 | attr_accessor :firstname, :lastname, :middlename 5 | 6 | def initialize(name) 7 | @firstname = name[:firstname] 8 | @lastname = name[:lastname] 9 | @middlename = name[:middlename] 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/petrovich/version.rb: -------------------------------------------------------------------------------- 1 | module Petrovich 2 | VERSION = '1.1.5' 3 | end 4 | -------------------------------------------------------------------------------- /lib/tasks/evaluate.rake: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'csv' 4 | 5 | def check!(errors, correct, total, name, gender, gcase, expected) 6 | petrovich = Petrovich(name.merge(gender: gender)) 7 | lemma = name.values.join(' ') 8 | actual = Petrovich::Unicode.upcase(petrovich.public_send(gcase).to_s) 9 | total[[gender, gcase]] += 1 10 | if actual == expected 11 | correct[[gender, gcase]] += 1 12 | true 13 | else 14 | errors << [lemma, expected, actual, [gender, gcase]] 15 | actual 16 | end 17 | end 18 | 19 | def figure_namepart(args) 20 | namepart_filename = args[:namepart] || "surnames" 21 | namepart_filename += 's' unless namepart_filename.end_with?('s') 22 | namepart_symbol = namepart_filename.chop.to_sym 23 | namepart_symbol = :lastname if namepart_symbol == :surname 24 | namepart_symbol = :middlename if namepart_symbol == :midname 25 | namepart_filename += ".#{args[:subset]}" if args[:subset] 26 | [namepart_filename, namepart_symbol] 27 | end 28 | 29 | desc 'Evaluate Petrovich' 30 | task :evaluate, [:namepart, :subset] => [:'evaluate:rules', :'evaluate:gender'] 31 | 32 | namespace :evaluate do 33 | desc 'Evaluate the inflector on lastnames' 34 | task :rules, [:namepart, :subset] => :petrovich do |_, args| 35 | namepart_filename, namepart_symbol = figure_namepart(args) 36 | filename = File.expand_path("../../../eval/#{namepart_filename}.tsv", __FILE__) 37 | unless File.file?(filename) 38 | warn "File #{filename} not found, skipping task" 39 | next 40 | end 41 | errors_filename = ENV['errors'] || 'errors.tsv' 42 | 43 | correct, total = Hash.new(0), Hash.new(0) 44 | 45 | puts 'I will evaluate the inflector on "%s" ' \ 46 | 'and store errors to "%s".' % [filename, errors_filename] 47 | 48 | errors = [] 49 | 50 | CSV.open(filename, "r:BINARY", col_sep: "\t", headers: true).each do |row| 51 | word = row['word'].force_encoding('UTF-8') 52 | lemma = row['lemma'].force_encoding('UTF-8') 53 | 54 | grammemes = if row['grammemes'] 55 | row['grammemes'].force_encoding('UTF-8').split(',') 56 | else 57 | [] 58 | end 59 | 60 | gender = grammemes.include?('мр') ? :male : :female 61 | 62 | if grammemes.include? '0' 63 | # some words are aptotic so we have to ensure that 64 | Petrovich::CASES.each do |gcase| 65 | check!(errors, correct, total, { namepart_symbol => lemma }, gender, gcase, word) 66 | end 67 | elsif grammemes.include? 'им' 68 | check!(errors, correct, total, { namepart_symbol => lemma }, gender, :nominative, word) 69 | elsif grammemes.include? 'рд' 70 | check!(errors, correct, total, { namepart_symbol => lemma }, gender, :genitive, word) 71 | elsif grammemes.include? 'дт' 72 | check!(errors, correct, total, { namepart_symbol => lemma }, gender, :dative, word) 73 | elsif grammemes.include? 'вн' 74 | check!(errors, correct, total, { namepart_symbol => lemma }, gender, :accusative, word) 75 | elsif grammemes.include? 'тв' 76 | check!(errors, correct, total, { namepart_symbol => lemma }, gender, :instrumental, word) 77 | elsif grammemes.include? 'пр' 78 | check!(errors, correct, total, { namepart_symbol => lemma }, gender, :prepositional, word) 79 | end 80 | end 81 | 82 | errors.sort_by!{ |array| array.first.reverse + array.last.first.to_s } 83 | 84 | CSV.open(errors_filename, 'w', col_sep: "\t") do |errors_file| 85 | errors_file << %w(lemma expected actual params) 86 | errors.each do |array| 87 | errors_file << array 88 | end 89 | end 90 | 91 | total.each do |(gender, gcase), correct_count| 92 | accuracy = correct[[gender, gcase]] / correct_count.to_f * 100 93 | puts "\tAc(%s|%s) = %.4f%%" % [gcase, gender, accuracy] 94 | end 95 | 96 | correct_size = correct.values.inject(&:+).to_i 97 | total_size = total.values.inject(&:+).to_i 98 | 99 | puts 'Well, the accuracy on %d examples is about %.4f%%.' % 100 | [total_size, (correct_size / total_size.to_f * 100)] 101 | 102 | puts 'Sum of the %d correct examples and %d mistakes is %d.' % 103 | [correct_size, total_size - correct_size, total_size] 104 | end 105 | 106 | desc 'Evaluate the gender detector' 107 | task :gender, [:namepart, :subset] => :petrovich do |_, args| 108 | GENDER_MAP = { 'мр' => :male, 'жр' => :female, 'мр-жр' => :androgynous } 109 | 110 | namepart_filename, namepart_symbol = figure_namepart(args) 111 | filename = File.expand_path("../../../eval/#{namepart_filename}.gender.tsv", __FILE__) 112 | unless File.file?(filename) 113 | warn "File #{filename} not found, skipping task" 114 | next 115 | end 116 | errors_filename = ENV['errors'] || 'errors.gender.tsv' 117 | 118 | correct, total = Hash.new(0), Hash.new(0) 119 | 120 | puts 'I will evaluate gender detector on "%s" ' \ 121 | 'and store errors to "%s".' % [filename, errors_filename] 122 | 123 | errors = [] 124 | hard_error_count = 0 125 | 126 | CSV.open(filename, "r:BINARY", col_sep: "\t", headers: true).each do |row| 127 | lemma = row['lemma'].force_encoding('UTF-8') 128 | gender_name = row['gender'].force_encoding('UTF-8') 129 | expected_gender = GENDER_MAP[gender_name] 130 | 131 | detected_gender = Petrovich(namepart_symbol => lemma).gender 132 | 133 | total[expected_gender] += 1 134 | if detected_gender == expected_gender 135 | correct[expected_gender] += 1 136 | else 137 | errors << [lemma, expected_gender, detected_gender] 138 | if detected_gender != :androgynous 139 | hard_error_count += 1 140 | warn " - #{Petrovich::Unicode.downcase(lemma)}" 141 | end 142 | end 143 | end 144 | 145 | puts 'Hard error count: %d.' % [hard_error_count] 146 | 147 | PART_INDEX = {:female => 0, :male => 1, :androgynous => 3} 148 | errors.sort_by!{ |array| array.first.reverse + PART_INDEX[array[1]].to_s } 149 | 150 | CSV.open(errors_filename, 'w', col_sep: "\t") do |errors_file| 151 | errors_file << %w(lemma expected actual) 152 | errors.each do |array| 153 | errors_file << array 154 | end 155 | end 156 | 157 | total.each do |gender, correct_count| 158 | accuracy = correct[gender] / correct_count.to_f * 100 159 | puts "\tAc(%s) = %.4f%%" % [gender, accuracy] 160 | end 161 | 162 | correct_size = correct.values.inject(&:+).to_i 163 | total_size = total.values.inject(&:+).to_i 164 | 165 | puts 'Well, the accuracy on %d examples is about %.4f%%.' % 166 | [total_size, (correct_size / total_size.to_f * 100)] 167 | 168 | puts 'Sum of the %d correct examples and %d mistakes is %d.' % 169 | [correct_size, total_size - correct_size, total_size] 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /petrovich.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require File.expand_path('../lib/petrovich/version', __FILE__) 4 | $:.push File.expand_path('../lib', __FILE__) 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'petrovich' 8 | s.version = Petrovich::VERSION 9 | s.authors = ['Andrew Kozlov', 'Dmitry Ustalov'] 10 | s.email = ['demerest@gmail.com', 'dmitry.ustalov@gmail.com'] 11 | s.homepage = 'https://petrovich.nlpub.ru/' 12 | s.summary = 'Petrovich, an inflector for Russian anthroponyms.' 13 | s.description = 'A morphological library for Russian anthroponyms, such as first names, last names, and middle names.' 14 | s.license = 'MIT' 15 | 16 | s.required_ruby_version = '>= 1.9.3' 17 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | s.files = Dir['{lib}/**/*'] + Dir['rules/*.yml'] + ['MIT-LICENSE', 'Rakefile', 'README.md'] 19 | 20 | s.add_development_dependency 'rake', '>= 12.3.3' 21 | s.add_development_dependency 'minitest', '~> 5.9' 22 | end 23 | -------------------------------------------------------------------------------- /petrovich.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petrovich/petrovich-ruby/7610e19d804addd2f15faf4675667d6616f0e90a/petrovich.png -------------------------------------------------------------------------------- /test/bench_core.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'minitest/benchmark' 3 | require 'petrovich' 4 | 5 | module MockBenchmark 6 | module Settings 7 | def bench_range 8 | [20, 80, 320, 1280] 9 | end 10 | 11 | def run(*) 12 | puts 'Running ' + self.name 13 | super 14 | end 15 | end 16 | 17 | def self.included(base) 18 | base.extend Settings 19 | end 20 | 21 | def result_code 22 | '' 23 | end 24 | end 25 | 26 | class Petrovich::Benchmark < Minitest::Benchmark 27 | include MockBenchmark 28 | 29 | def bench_dative_to_s 30 | assert_performance_linear 0.99 do |n| 31 | n.times do 32 | Petrovich( 33 | lastname: 'Салтыков-Щедрин', 34 | firstname: 'Михаил', 35 | middlename: 'Евграфович', 36 | ).dative.to_s 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/petrovich_gender_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require_relative 'test_helper' 3 | 4 | describe Petrovich::Gender do 5 | it 'detects male gender by firstname' do 6 | assert_equal :male, Petrovich(firstname: 'Александр').gender 7 | end 8 | 9 | it 'detects male gender by lastname' do 10 | assert_equal :male, Petrovich(lastname: 'Склифасовский').gender 11 | end 12 | 13 | it 'detects female gender by firstname' do 14 | assert_equal :female, Petrovich(firstname: 'Александра').gender 15 | end 16 | 17 | it 'detects female gender by lastname' do 18 | assert_equal :female, Petrovich(lastname: 'Склифасовская').gender 19 | end 20 | 21 | it 'detects female gender by firstname and lastname' do 22 | assert_equal :female, Petrovich(firstname: 'Александра', lastname: 'Склифасовская').gender 23 | end 24 | 25 | it 'detects androgynous gender by firstname' do 26 | assert_equal :androgynous, Petrovich(firstname: 'Саша').gender 27 | end 28 | 29 | it 'detects androgynous gender by firstname and lastname' do 30 | assert_equal :androgynous, Petrovich(firstname: 'Саша', lastname: 'Андрейчук').gender 31 | end 32 | 33 | it 'detects male gender by firstname and lastname' do 34 | assert_equal :male, Petrovich(firstname: 'Саша', lastname: 'Иванов').gender 35 | end 36 | 37 | it 'detects male gender by firstname, lastname and middlename' do 38 | assert_equal :male, Petrovich(firstname: 'Саша', lastname: 'Андрейчук', middlename: 'Олегович').gender 39 | end 40 | 41 | it 'detects male gender by firstname and middlename' do 42 | assert_equal :male, Petrovich(firstname: 'Саша', middlename: 'Олегович').gender 43 | end 44 | 45 | it 'detects androgynous gender by lastname' do 46 | assert_equal :androgynous, Petrovich(lastname: 'Осипчук').gender 47 | end 48 | 49 | it 'detects male gender by middlename' do 50 | assert_equal :male, Petrovich(middlename: 'Олегович').gender 51 | end 52 | 53 | it 'detects female gender by middlename' do 54 | assert_equal :female, Petrovich(middlename: 'Олеговна').gender 55 | end 56 | 57 | it 'fails with argument error test 1' do 58 | _ { Petrovich::Gender.detect(xxx: 'yyy') }.must_raise ArgumentError 59 | end 60 | 61 | it 'fails with argument error test 2' do 62 | _ { Petrovich::Gender.detect('wrong args') }.must_raise ArgumentError 63 | end 64 | 65 | it 'fails with argument error test 3' do 66 | _ { Petrovich::Gender.detect(firstname: nil, lastname: nil) }.must_raise ArgumentError 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/petrovich_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require_relative 'test_helper' 3 | 4 | describe Petrovich do 5 | it 'have inflect method' do 6 | firstname = Petrovich( 7 | firstname: 'Саша', 8 | gender: 'male' 9 | ).to(:dative).firstname 10 | 11 | assert_equal 'Саше', firstname 12 | end 13 | 14 | it 'inflects firstname 1' do 15 | firstname = Petrovich( 16 | firstname: 'Саша', 17 | gender: 'male' 18 | ).dative.firstname 19 | 20 | assert_equal 'Саше', firstname 21 | end 22 | 23 | it 'inflects lastname 1' do 24 | lastname = Petrovich( 25 | lastname: 'Кочубей', 26 | gender: 'male' 27 | ).dative.lastname 28 | 29 | assert_equal 'Кочубею', lastname 30 | end 31 | 32 | it 'inflects lastname 2' do 33 | lastname = Petrovich( 34 | lastname: 'Козлов', 35 | gender: 'male' 36 | ).dative.lastname 37 | 38 | assert_equal 'Козлову', lastname 39 | end 40 | 41 | it 'inflects lastname 3' do 42 | lastname = Petrovich( 43 | lastname: 'Салтыков-Щедрин', 44 | gender: 'male' 45 | ).dative.lastname 46 | 47 | assert_equal 'Салтыкову-Щедрину', lastname 48 | end 49 | 50 | it 'inflects lastname 4' do 51 | lastname = Petrovich( 52 | lastname: 'Дюма', 53 | gender: 'male' 54 | ).dative.lastname 55 | 56 | assert_equal 'Дюма', lastname 57 | end 58 | 59 | it 'inflects lastname 5' do 60 | lastname = Petrovich( 61 | lastname: 'Воробей', 62 | gender: 'male' 63 | ).dative.lastname 64 | 65 | assert_equal 'Воробью', lastname 66 | end 67 | 68 | it 'inflects lastname 6' do 69 | lastname = Petrovich( 70 | lastname: 'Воробей', 71 | gender: 'female' 72 | ).dative.lastname 73 | 74 | assert_equal 'Воробей', lastname 75 | end 76 | 77 | it 'inflects firstname 2' do 78 | firstname = Petrovich( 79 | firstname: 'Анна-Мария', 80 | gender: 'female' 81 | ).dative.firstname 82 | 83 | assert_equal 'Анне-Марии', firstname 84 | end 85 | 86 | it 'inflects middlename 1' do 87 | middlename = Petrovich( 88 | middlename: 'Борух-Бендитовна', 89 | gender: 'female' 90 | ).dative.middlename 91 | 92 | assert_equal 'Борух-Бендитовне', middlename 93 | end 94 | 95 | it 'inflects middlename 2' do 96 | middlename = Petrovich( 97 | middlename: 'Георгиевна-Авраамовна', 98 | gender: 'female' 99 | ).dative.middlename 100 | 101 | assert_equal 'Георгиевне-Авраамовне', middlename 102 | end 103 | 104 | it 'inflects lastname 2' do 105 | lastname = Petrovich( 106 | firstname: 'Иван', 107 | lastname: 'Плевако', 108 | gender: 'male' 109 | ).dative.lastname 110 | 111 | assert_equal 'Плевако', lastname 112 | end 113 | 114 | # Gender 115 | it 'is androgynous gender' do 116 | p = Petrovich( 117 | firstname: 'Саша', 118 | lastname: 'Андрейчук' 119 | ) 120 | 121 | assert_equal true, p.androgynous? 122 | end 123 | 124 | it 'detects male gender' do 125 | p = Petrovich( 126 | firstname: 'Александр', 127 | lastname: 'Андрейчук' 128 | ) 129 | 130 | assert_equal true, p.male? 131 | end 132 | 133 | it 'detects female gender' do 134 | p = Petrovich( 135 | firstname: 'Александра', 136 | lastname: 'Андрейчук' 137 | ) 138 | 139 | assert_equal true, p.female? 140 | end 141 | 142 | ### 143 | 144 | it 'is androgynous gender returned by gender()' do 145 | p = Petrovich( 146 | firstname: 'Саша', 147 | lastname: 'Андрейчук' 148 | ) 149 | 150 | assert_equal true, p.gender == :androgynous 151 | end 152 | 153 | it 'detects male gender returned by gender()' do 154 | p = Petrovich( 155 | firstname: 'Александр', 156 | lastname: 'Андрейчук' 157 | ) 158 | 159 | assert_equal true, p.gender == :male 160 | end 161 | 162 | it 'detects female gender returned by gender()' do 163 | p = Petrovich( 164 | firstname: 'Александра', 165 | lastname: 'Андрейчук' 166 | ) 167 | 168 | assert_equal true, p.gender == :female 169 | end 170 | 171 | it 'inflects lastname [ая]' do 172 | lastname = Petrovich( 173 | lastname: 'Слуцкая', 174 | ).dative.lastname 175 | 176 | assert_equal 'Слуцкой', lastname 177 | end 178 | 179 | it 'inflects male lastname' do 180 | lastname = Petrovich( 181 | lastname: 'Штеттер', 182 | gender: :male, 183 | ).dative.lastname 184 | 185 | assert_equal 'Штеттеру', lastname 186 | end 187 | 188 | it 'inflects male lastname' do 189 | lastname = Petrovich( 190 | lastname: 'Коломиец', 191 | ).genitive.lastname 192 | 193 | assert_equal 'Коломийца', lastname 194 | end 195 | 196 | it 'inflects male lastname' do 197 | lastname = Petrovich( 198 | lastname: 'Быхун', 199 | ).instrumental.lastname 200 | 201 | assert_equal 'Быхуном', lastname 202 | end 203 | 204 | # Future stuff 205 | # Petrovich.scan do |p| 206 | # name = [p.lastname, p.firstname, p.middlename].join(' ') 207 | # 208 | # "#{name}" 209 | # end 210 | end 211 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | 5 | gem 'minitest' 6 | require 'minitest/autorun' 7 | 8 | begin 9 | require 'petrovich' 10 | rescue Errno::ENOENT => e 11 | if e.message.index('rules.yml') || e.message.index('gender.yml') 12 | warn 'Please, run `git submodule update --init --recursive` to populate the submodules.' 13 | end 14 | end 15 | --------------------------------------------------------------------------------