cripslock

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit d34497f54cea576e236f820e4337cc83c3b01522
parent 829a359042a85d37cd9b8d8cd9bd45643913fff6
Author: Daniel Barlow <dan@telent.net>
Date:   Mon, 31 Aug 2026 18:44:30 +0100

add porter stemming algorithm

from
https://gurumeditation.org/wp-content/uploads/sites/6/2022/02/stem.lua
via
https://gurumeditation.org/52/porter-stemming-algorithm-in-lua/

Diffstat:
MMakefile | 4++--
Astem.lua | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 215 insertions(+), 2 deletions(-)

diff --git a/Makefile b/Makefile @@ -3,7 +3,7 @@ PREFIX?=/usr/local NAME?=cripslock MODULES=main.fnl textile.fnl site.fnl html.fnl lib.fnl rss.fnl server.fnl - +SOURCES=$(MODULES) assert.fnl stem.lua default: run: @@ -15,4 +15,4 @@ test: install: mkdir -p $(PREFIX)/bin $(PREFIX)/lib install $(NAME).fnl $(PREFIX)/bin/$(NAME) - cp $(MODULES) assert.fnl $(PREFIX)/lib + cp $(SOURCES) $(PREFIX)/lib diff --git a/stem.lua b/stem.lua @@ -0,0 +1,213 @@ +--[[ + This software is based on the Porter Stemmer which can be found here: + http://tartarus.org/~martin/PorterStemmer/ + + The general algorithm is based on the Javascript version from the above page + (by 'Andargor'?) and then verified against the C reference source. + + MIT LICENSE + + Copyright (C) 2012 Chris Osgood <chris at luadev.com> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +--]] +-- module("stem", package.seeall) + +local lpeg = require("lpeg") + +-- Makes pattern match at end of string +local function eos(p) + return (1 - (p * -1))^0 * lpeg.C(p) * -1 +end + +-- Word patterns +local c = 1 - lpeg.S("aeiou") -- consonant +local v = lpeg.S("aeiouy") -- vowel +local C = c * (1 - v)^0 -- consonant sequence +local V = v * lpeg.S("aeiou")^0 -- vowel sequence + +local mgr0 = C^-1 * V * C -- [C]VC... is m>0 +local meq1 = C^-1 * V * C * V^-1 * -1 -- [C]VC[V] is m=1 +local mgr1 = C^-1 * V * C * V * C -- [C]VCVC... is m>1 +local s_v = C^-1 * v -- vowel in stem +local e_v = C * v * (1 - lpeg.S("aeiouwxy")) * -1 + +-- Suffix patterns +local step2map = { + ["ational"] = "ate", ["ization"] = "ize", ["iveness"] = "ive", + ["fulness"] = "ful", ["ousness"] = "ous", ["tional"] = "tion", + ["biliti"] = "ble", ["entli"] = "ent", ["ousli"] = "ous", + ["ation"] = "ate", ["alism"] = "al", ["aliti"] = "al", + ["iviti"] = "ive", ["enci"] = "ence", ["anci"] = "ance", + ["izer"] = "ize", ["alli"] = "al", ["ator"] = "ate", + ["logi"] = "log", ["bli"] = "ble", ["eli"] = "e" +} + +local step3map = { + ["icate"] = "ic", ["ative"] = "", ["alize"] = "al", + ["iciti"] = "ic", ["ical"] = "ic", ["ful"] = "", + ["ness"] = "" +} + +local step1a1 = eos(lpeg.P("sses") + "ies") +local step1a2 = eos((1 - lpeg.P("s")) * "s") +local step1b1 = eos(lpeg.P("eed")) +local step1b2 = eos(lpeg.P("ed") + "ing") +local step1b3 = eos(lpeg.P("at") + "bl" + "iz") +local step1c = eos(lpeg.P("y")) + +local step2 = eos(lpeg.P("ational") + "ization" + "iveness" + "fulness" + + "ousness" + "tional" + "biliti" + "entli" + "ousli" + + "ation" + "alism" + "aliti" + "iviti" + "enci" + "anci" + + "izer" + "alli" + "ator" + "logi" + "bli" + "eli") + +local step3 = eos(lpeg.P("icate") + "ative" + "alize" + + "iciti" + "ical" + "ness" + "ful") + +local step4a = eos(lpeg.P("ement") + "ance" + "ence" + "able" + "ible" + + "ment" + "ant" + "ent" + "ous" + "ism" + "ate" + "iti" + + "ive" + "ize" + "al" + "er" + "ic" + "ou") + +local step4b = eos(lpeg.P("sion") + "tion") + +local step5a = eos(lpeg.P("e")) +local step5b = eos(lpeg.P("ll")) + +-- Stemming function +function stem(w) + if #w < 3 then return w end + + local firstch = w:sub(1,1) + if firstch == "y" then + w = "Y" .. w:sub(2) + end + + -- Step 1a + if lpeg.match(step1a1, w) then + w = w:sub(1, -3) + elseif lpeg.match(step1a2, w) then + w = w:sub(1, -2) + end + + -- Step 1b + if lpeg.match(step1b1, w) then + if lpeg.match(mgr0, w:sub(1, -4)) then + w = w:sub(1, -2) + end + else + local suffix = lpeg.match(step1b2, w) + + if suffix then + local stem = w:sub(1, -#suffix - 1) + if lpeg.match(s_v, stem) then + w = stem + if lpeg.match(step1b3, w) then + w = w .."e" + -- Don't use lpeg, it's slower with backreferences + elseif w:find("([^aeiouylsz])%1$") then + w = w:sub(1, -2) + elseif lpeg.match(e_v, w) then + w = w .. "e" + end + end + end + end + + -- Step 1c + if lpeg.match(step1c, w) then + local stem = w:sub(1, -2) + if lpeg.match(s_v, stem) then + w = stem .. "i" + end + end + + -- Step 2 + local suffix = lpeg.match(step2, w) + + if suffix then + local stem = w:sub(1, -#suffix - 1) + if lpeg.match(mgr0, stem) then + w = stem .. step2map[suffix] + end + end + + -- Step 3 + suffix = lpeg.match(step3, w) + + if suffix then + local stem = w:sub(1, -#suffix - 1) + if lpeg.match(mgr0, stem) then + w = stem .. step3map[suffix] + end + end + + -- Step 4 + suffix = lpeg.match(step4a, w) + + if suffix then + local stem = w:sub(1, -#suffix - 1) + if lpeg.match(mgr1, stem) then + w = stem + end + elseif lpeg.match(step4b, w) then + local stem = w:sub(1, -4) + if lpeg.match(mgr1, stem) then + w = stem + end + end + + -- Step 5 + if lpeg.match(step5a, w) then + local stem = w:sub(1, -2) + if lpeg.match(mgr1, stem) or (lpeg.match(meq1, stem) and not lpeg.match(e_v, stem)) then + w = stem + end + end + + if lpeg.match(step5b, w) and lpeg.match(mgr1, w) then + w = w:sub(1, -2) + end + + if firstch == "y" then + w = "y" .. w:sub(2) + end + + return w +end + +return { stem = stem } + +--[[ +function file(filename) + local fp = io.open(filename, "r") + if not fp then error("error opening file `"..tostring(filename).."'") end + for l in fp:lines() do + print(stem(l)) + end + fp:close() +end + +local args = {...} +if #args > 0 then + for _, filename in ipairs(args) do + file(filename) + end +end +--]] +