-# coding: utf-8\r
-\r
-# Porter 2 stemmer in Ruby.\r
-#\r
-# This is the Porter 2 stemming algorithm, as described at \r
-# http://snowball.tartarus.org/algorithms/english/stemmer.html\r
-# The original paper is:\r
-#\r
-# Porter, 1980, "An algorithm for suffix stripping", _Program_, Vol. 14,\r
-# no. 3, pp 130-137\r
-\r
-module Stemmable\r
- # A non-vowel\r
- C = "[^aeiouy]"\r
-\r
- # A vowel\r
- V = "[aeiouy]"\r
-\r
- # A non-vowel other than w, x, or Y\r
- CW = "[^aeiouywxY]"\r
-\r
- # Doubles created when added a suffix: these are undoubled when stemmed\r
- Double = "(bb|dd|ff|gg|mm|nn|pp|rr|tt)"\r
-\r
- # A valid letter that can come before 'li'\r
- Valid_LI = "[cdeghkmnrt]"\r
-\r
- # A specification for a short syllable\r
- SHORT_SYLLABLE = "((#{C}#{V}#{CW})|(^#{V}#{C}))"\r
-\r
- # Suffix transformations used in Step 2.\r
- # (ogi, li endings dealt with in procedure)\r
- STEP_2_MAPS = {"tional" => "tion",\r
- "enci" => "ence",\r
- "anci" => "ance",\r
- "abli" => "able",\r
- "entli" => "ent",\r
- "ization" => "ize",\r
- "izer" => "ize",\r
- "ational" => "ate",\r
- "ation" => "ate",\r
- "ator" => "ate",\r
- "alism" => "al",\r
- "aliti" => "al",\r
- "alli" => "al",\r
- "fulness" => "ful",\r
- "ousli" => "ous",\r
- "ousness" => "ous",\r
- "iveness" => "ive",\r
- "iviti" => "ive",\r
- "biliti" => "ble",\r
- "bli" => "ble",\r
- "fulli" => "ful",\r
- "lessli" => "less" }\r
-\r
- # Suffix transformations used in Step 3.\r
- # (ative ending dealt with in procedure) \r
- STEP_3_MAPS = {"tional" => "tion",\r
- "ational" => "ate",\r
- "alize" => "al",\r
- "icate" => "ic",\r
- "iciti" => "ic",\r
- "ical" => "ic",\r
- "ful" => "",\r
- "ness" => "" }\r
- \r
- # Suffix transformations used in Step 4.\r
- STEP_4_MAPS = {"al" => "",\r
- "ance" => "",\r
- "ence" => "",\r
- "er" => "",\r
- "ic" => "",\r
- "able" => "",\r
- "ible" => "",\r
- "ant" => "",\r
- "ement" => "",\r
- "ment" => "",\r
- "ent" => "",\r
- "ism" => "",\r
- "ate" => "",\r
- "iti" => "",\r
- "ous" => "",\r
- "ive" => "",\r
- "ize" => "" }\r
- \r
- # Special-case stemmings \r
- SPECIAL_CASES = {"skis" => "ski",\r
- "skies" => "sky",\r
- \r
- "dying" => "die",\r
- "lying" => "lie",\r
- "tying" => "tie",\r
- "idly" => "idl",\r
- "gently" => "gentl",\r
- "ugly" => "ugli",\r
- "early" => "earli",\r
- "only" => "onli",\r
- "singly" =>"singl",\r
- \r
- "sky" => "sky",\r
- "news" => "news",\r
- "howe" => "howe",\r
- "atlas" => "atlas",\r
- "cosmos" => "cosmos",\r
- "bias" => "bias",\r
- "andes" => "andes" }\r
- \r
- # Special case words to ignore after step 1a.\r
- STEP_1A_SPECIAL_CASES = %w[ inning outing canning herring earring proceed exceed succeed ]\r
-\r
- # Tidy up the word before we get down to the algorithm\r
- def porter2_tidy\r
- preword = self.to_s.strip.downcase\r
- \r
- # map apostrophe-like characters to apostrophes\r
- preword.gsub!(/‘/, "'")\r
- preword.gsub!(/’/, "'")\r
-\r
- preword\r
- end\r
- \r
- def porter2_preprocess \r
- w = self.dup\r
-\r
- # remove any initial apostrophe\r
- w.gsub!(/^'*(.)/, '\1')\r
- \r
- # set initial y, or y after a vowel, to Y\r
- w.gsub!(/^y/, "Y")\r
- w.gsub!(/(#{V})y/, '\1Y')\r
- \r
- w\r
- end\r
- \r
- # The word after the first non-vowel after the first vowel\r
- def porter2_r1\r
- if self =~ /^(gener|commun|arsen)(?<r1>.*)/\r
- Regexp.last_match(:r1)\r
- else\r
- self =~ /#{V}#{C}(?<r1>.*)$/\r
- Regexp.last_match(:r1) || ""\r
- end\r
- end\r
- \r
- # R1 after the first non-vowel after the first vowel\r
- def porter2_r2\r
- self.porter2_r1 =~ /#{V}#{C}(?<r2>.*)$/\r
- Regexp.last_match(:r2) || ""\r
- end\r
- \r
- # A short syllable in a word is either \r
- # 1. a vowel followed by a non-vowel other than w, x or Y and preceded by a non-vowel, or \r
- # 2. a vowel at the beginning of the word followed by a non-vowel. \r
- def porter2_ends_with_short_syllable?\r
- self =~ /#{SHORT_SYLLABLE}$/ ? true : false\r
- end\r
-\r
- # A word is short if it ends in a short syllable, and if R1 is null\r
- def porter2_is_short_word?\r
- self.porter2_ends_with_short_syllable? and self.porter2_r1.empty?\r
- end\r
- \r
- # Search for the longest among the suffixes, \r
- # * '\r
- # * 's\r
- # * 's'\r
- # and remove if found.\r
- def step_0\r
- self.sub!(/(.)('s'|'s|')$/, '\1') || self\r
- end\r
- \r
- # Remove plural suffixes\r
- def step_1a\r
- if self =~ /sses$/\r
- self.sub(/sses$/, 'ss')\r
- elsif self =~ /..(ied|ies)$/\r
- self.sub(/(ied|ies)$/, 'i')\r
- elsif self =~ /(ied|ies)$/\r
- self.sub(/(ied|ies)$/, 'ie')\r
- elsif self =~ /(us|ss)$/\r
- self\r
- elsif self =~ /s$/\r
- if self =~ /(#{V}.+)s$/\r
- self.sub(/s$/, '') \r
- else\r
- self\r
- end\r
- else\r
- self\r
- end\r
- end\r
- \r
- def step_1b(gb_english = false)\r
- if self =~ /(eed|eedly)$/\r
- if self.porter2_r1 =~ /(eed|eedly)$/\r
- self.sub(/(eed|eedly)$/, 'ee')\r
- else\r
- self\r
- end\r
- else\r
- w = self.dup\r
- if w =~ /#{V}.*(ed|edly|ing|ingly)$/\r
- w.sub!(/(ed|edly|ing|ingly)$/, '')\r
- if w =~ /(at|lb|iz)$/\r
- w += 'e' \r
- elsif w =~ /is$/ and gb_english\r
- w += 'e' \r
- elsif w =~ /#{Double}$/\r
- w.chop!\r
- elsif w.porter2_is_short_word?\r
- w += 'e'\r
- end\r
- end\r
- w\r
- end\r
- end\r
-\r
- \r
- def step_1c\r
- if self =~ /.+#{C}(y|Y)$/\r
- self.sub(/(y|Y)$/, 'i')\r
- else\r
- self\r
- end\r
- end\r
- \r
-\r
- def step_2(gb_english = false)\r
- r1 = self.porter2_r1\r
- s2m = STEP_2_MAPS.dup\r
- if gb_english\r
- s2m["iser"] = "ise"\r
- s2m["isation"] = "ise"\r
- end\r
- step_2_re = Regexp.union(s2m.keys.map {|r| Regexp.new(r + "$")})\r
- if self =~ step_2_re\r
- if r1 =~ /#{$&}$/\r
- self.sub(/#{$&}$/, s2m[$&])\r
- else\r
- self\r
- end\r
- elsif r1 =~ /li$/ and self =~ /(#{Valid_LI})li$/\r
- self.sub(/li$/, '')\r
- elsif r1 =~ /ogi$/ and self =~ /logi$/\r
- self.sub(/ogi$/, 'og')\r
- else\r
- self\r
- end\r
- end\r
- \r
- \r
- def step_3(gb_english = false)\r
- if self =~ /ative$/ and self.porter2_r2 =~ /ative$/\r
- self.sub(/ative$/, '')\r
- else\r
- s3m = STEP_3_MAPS.dup\r
- if gb_english\r
- s3m["alise"] = "al"\r
- end\r
- step_3_re = Regexp.union(s3m.keys.map {|r| Regexp.new(r + "$")})\r
- r1 = self.porter2_r1\r
- if self =~ step_3_re and r1 =~ /#{$&}$/ \r
- self.sub(/#{$&}$/, s3m[$&])\r
- else\r
- self\r
- end\r
- end\r
- end\r
- \r
- \r
- def step_4(gb_english = false)\r
- if self.porter2_r2 =~ /ion$/ and self =~ /(s|t)ion$/\r
- self.sub(/ion$/, '')\r
- else\r
- s4m = STEP_4_MAPS.dup\r
- if gb_english\r
- s4m["ise"] = ""\r
- end\r
- step_4_re = Regexp.union(s4m.keys.map {|r| Regexp.new(r + "$")})\r
- r2 = self.porter2_r2\r
- if self =~ step_4_re\r
- if r2 =~ /#{$&}/\r
- self.sub(/#{$&}$/, s4m[$&])\r
- else\r
- self\r
- end\r
- else\r
- self\r
- end\r
- end\r
- end\r
-\r
- \r
- def step_5\r
- if self =~ /ll$/ and self.porter2_r2 =~ /l$/\r
- self.sub(/ll$/, 'l') \r
- elsif self =~ /e$/ and self.porter2_r2 =~ /e$/ \r
- self.sub(/e$/, '') \r
- else\r
- r1 = self.porter2_r1\r
- if self =~ /e$/ and r1 =~ /e$/ and not self =~ /#{SHORT_SYLLABLE}e$/\r
- self.sub(/e$/, '')\r
- else\r
- self\r
- end\r
- end\r
- end\r
- \r
- \r
- def porter2_postprocess\r
- self.gsub(/Y/, 'y')\r
- end\r
-\r
- \r
- def porter2_stem(gb_english = false)\r
- preword = self.porter2_tidy\r
- return preword if preword.length <= 2\r
-\r
- word = preword.porter2_preprocess\r
- \r
- if SPECIAL_CASES.has_key? word\r
- SPECIAL_CASES[word]\r
- else\r
- w1a = word.step_0.step_1a\r
- if STEP_1A_SPECIAL_CASES.include? w1a \r
- w1a\r
- else\r
- w1a.step_1b(gb_english).step_1c.step_2(gb_english).step_3(gb_english).step_4(gb_english).step_5.porter2_postprocess\r
- end\r
- end\r
- end \r
- \r
- def porter2_stem_verbose(gb_english = false)\r
- preword = self.porter2_tidy\r
- puts "Preword: #{preword}"\r
- return preword if preword.length <= 2\r
-\r
- word = preword.porter2_preprocess\r
- puts "Preprocessed: #{word}"\r
- \r
- if SPECIAL_CASES.has_key? word\r
- puts "Returning #{word} as special case #{SPECIAL_CASES[word]}"\r
- SPECIAL_CASES[word]\r
- else\r
- r1 = word.porter2_r1\r
- r2 = word.porter2_r2\r
- puts "R1 = #{r1}, R2 = #{r2}"\r
- \r
- w0 = word.step_0 ; puts "After step 0: #{w0} (R1 = #{w0.porter2_r1}, R2 = #{w0.porter2_r2})"\r
- w1a = w0.step_1a ; puts "After step 1a: #{w1a} (R1 = #{w1a.porter2_r1}, R2 = #{w1a.porter2_r2})"\r
- \r
- if STEP_1A_SPECIAL_CASES.include? w1a\r
- puts "Returning #{w1a} as 1a special case"\r
- w1a\r
- else\r
- w1b = w1a.step_1b(gb_english) ; puts "After step 1b: #{w1b} (R1 = #{w1b.porter2_r1}, R2 = #{w1b.porter2_r2})"\r
- w1c = w1b.step_1c ; puts "After step 1c: #{w1c} (R1 = #{w1c.porter2_r1}, R2 = #{w1c.porter2_r2})"\r
- w2 = w1c.step_2(gb_english) ; puts "After step 2: #{w2} (R1 = #{w2.porter2_r1}, R2 = #{w2.porter2_r2})"\r
- w3 = w2.step_3(gb_english) ; puts "After step 3: #{w3} (R1 = #{w3.porter2_r1}, R2 = #{w3.porter2_r2})"\r
- w4 = w3.step_4(gb_english) ; puts "After step 4: #{w4} (R1 = #{w4.porter2_r1}, R2 = #{w4.porter2_r2})"\r
- w5 = w4.step_5 ; puts "After step 5: #{w5}"\r
- wpost = w5.porter2_postprocess ; puts "After postprocess: #{wpost}"\r
- wpost\r
- end\r
- end\r
- end \r
- \r
- alias stem porter2_stem\r
-\r
-end\r
-\r
-# Add stem method to all Strings\r
-class String\r
- include Stemmable\r
- \r
- # private :porter2_preprocess, :porter2_r1, :porter2_r2\r
-end\r