From 9f655b3f434c49d7509fb2e7ffd9d6e423303b2c Mon Sep 17 00:00:00 2001 From: Andriy Rysin Date: Fri, 1 Feb 2019 20:06:56 -0500 Subject: [PATCH] [uk] dynamic tagging improvements --- .../tagging/uk/CompoundTagger.java | 19 ++- .../languagetool/tagging/uk/PosTagHelper.java | 7 + .../tagging/uk/UkrainianTagger.java | 142 ++++++++++++------ .../tagging/uk/UkrainianTaggerTest.java | 37 +++-- 4 files changed, 144 insertions(+), 61 deletions(-) diff --git a/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/CompoundTagger.java b/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/CompoundTagger.java index eea824746c60..9ce86ca1d045 100644 --- a/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/CompoundTagger.java +++ b/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/CompoundTagger.java @@ -77,7 +77,7 @@ class CompoundTagger { private static final String ADJ_TAG_FOR_PO_ADV_NAZ = "adj:m:v_naz"; private static final List LEFT_O_ADJ = Arrays.asList( - "австро", "адиго", "американо", "англо", "афро", "еко", "етно", "іспано", "києво", "марокано", "угро" + "австро", "адиго", "американо", "англо", "афро", "еко", "етно", "іспано", "італо", "києво", "марокано", "угро" ); private static final List LEFT_INVALID = Arrays.asList( @@ -665,18 +665,20 @@ private String getTryPrefix(String rightWord) { private List tagMatch(String word, List leftAnalyzedTokens, List rightAnalyzedTokens) { List newAnalyzedTokens = new ArrayList<>(); List newAnalyzedTokensAnimInanim = new ArrayList<>(); - + String animInanimNotTagged = null; - + for (AnalyzedToken leftAnalyzedToken : leftAnalyzedTokens) { String leftPosTag = leftAnalyzedToken.getPOSTag(); - + if( leftPosTag == null || IPOSTag.contains(leftPosTag, IPOSTag.abbr.getText()) ) continue; - // we don't want to mess with v_kly, e.g. no v_kly у рибо-полювання - if( leftPosTag.startsWith("noun") && leftPosTag.contains("v_kly") ) + // we don't want to have v_kly for рибо-полювання + // but we do for пане-товаришу + if( leftPosTag.startsWith("noun:inanim") + && leftPosTag.contains("v_kly") ) continue; String leftPosTagExtra = ""; @@ -700,13 +702,14 @@ private List tagMatch(String word, List leftAnalyz for (AnalyzedToken rightAnalyzedToken : rightAnalyzedTokens) { String rightPosTag = rightAnalyzedToken.getPOSTag(); - + if( rightPosTag == null // || rightPosTag.contains("v_kly") || rightPosTag.contains(IPOSTag.abbr.getText()) ) continue; - if( rightPosTag.startsWith("noun") && rightPosTag.contains("v_kly") ) + if( rightPosTag.startsWith("noun:inanim") + && rightPosTag.contains("v_kly") ) continue; String extraNvTag = ""; diff --git a/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/PosTagHelper.java b/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/PosTagHelper.java index 1ed69d017e98..5b31063c2160 100644 --- a/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/PosTagHelper.java +++ b/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/PosTagHelper.java @@ -222,6 +222,13 @@ public static List generateTokensForNv(String word, String gender return newAnalyzedTokens; } + @NotNull + public static String addIfNotContains(@NotNull String tag, @NotNull String part) { + if( ! tag.contains(part) ) + return tag + part; + return tag; + } + //private static String getNumAndConj(String posTag) { // Matcher pos4matcher = GENDER_CONJ_REGEX.matcher(posTag); // if( pos4matcher.matches() ) { diff --git a/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/UkrainianTagger.java b/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/UkrainianTagger.java index 04992a207d72..9b365638b876 100644 --- a/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/UkrainianTagger.java +++ b/languagetool-language-modules/uk/src/main/java/org/languagetool/tagging/uk/UkrainianTagger.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Locale; import java.util.regex.Pattern; +import java.util.regex.Matcher; import org.apache.commons.lang3.StringUtils; import org.languagetool.AnalyzedToken; @@ -44,6 +45,7 @@ public class UkrainianTagger extends BaseTagger { private static final Pattern DATE = Pattern.compile("[\\d]{2}\\.[\\d]{2}\\.[\\d]{4}"); private static final Pattern TIME = Pattern.compile("([01]?[0-9]|2[0-3])[.:][0-5][0-9]"); private static final Pattern ALT_DASHES_IN_WORD = Pattern.compile("[а-яіїєґ0-9a-z]\u2013[а-яіїєґ]|[а-яіїєґ]\u2013[0-9]", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + private static final Pattern NAPIV_ALLOWED_TAGS_REGEX = Pattern.compile("(noun|adj(?!.*?:comp[cs])|adv(?!.*?:comp[cs])).*"); private final CompoundTagger compoundTagger = new CompoundTagger(this, wordTagger, conversionLocale); // private BufferedWriter taggedDebugWriter; @@ -76,12 +78,12 @@ public List additionalTags(String word, WordTagger wordTagger) { additionalTaggedTokens.add(new AnalyzedToken(word, IPOSTag.date.getText(), word)); return additionalTaggedTokens; } - + if ( word.indexOf('-') > 0 ) { List guessedCompoundTags = compoundTagger.guessCompoundTag(word); return guessedCompoundTags; } - + return guessOtherTags(word); } @@ -92,59 +94,102 @@ private List guessOtherTags(String word) { || word.endsWith("штрасе")) ) { return PosTagHelper.generateTokensForNv(word, "f", ":prop"); } - + return null; } + private List getAdjustedAnalyzedTokens(String word, String adjustedWord, Pattern posTagRegex) { + List newTokens = super.getAnalyzedTokens(adjustedWord); + + if( newTokens.get(0).hasNoTag() ) + return new ArrayList<>(); + + List newTokens2 = new ArrayList<>(); + + for (int i = 0; i < newTokens.size(); i++) { + AnalyzedToken analyzedToken = newTokens.get(i); + String posTag = analyzedToken.getPOSTag(); + + if( adjustedWord.equals(analyzedToken.getToken()) // filter out tokens with accents etc with null pos tag + && (posTagRegex == null || posTagRegex.matcher(posTag).matches()) ) { + String lemma = analyzedToken.getLemma(); + AnalyzedToken newToken = new AnalyzedToken(word, posTag, lemma); + newTokens2.add(newToken); + } + } + + return newTokens2; + } + + @Override protected List getAnalyzedTokens(String word) { List tokens = super.getAnalyzedTokens(word); - if( tokens.get(0).getPOSTag() == null ) { - char otherHyphen = getOtherHyphen(word); - if( otherHyphen != '\u0000' - && ALT_DASHES_IN_WORD.matcher(word).find() ) { + if( tokens.get(0).hasNoTag() ) { + String origWord = word; - String newWord = word.replace(otherHyphen, '-'); + if( word.endsWith("м²") || word.endsWith("м³") ) { + word = origWord.substring(0, word.length()-1); + List newTokens = getAdjustedAnalyzedTokens(origWord, word, Pattern.compile("noun:inanim.*")); + return newTokens.size() > 0 ? newTokens : tokens; + } - List newTokens = super.getAnalyzedTokens(newWord); + + if( word.indexOf('\u2013') > 0 + && ALT_DASHES_IN_WORD.matcher(word).find() ) { - if( ! newTokens.get(0).hasNoTag() ) { - for (int i = 0; i < newTokens.size(); i++) { - AnalyzedToken analyzedToken = newTokens.get(i); - if( newWord.equals(analyzedToken.getToken()) ) { - String lemma = analyzedToken.getLemma(); - // new lemma with regular dash allows rules to match -// if( lemma != null ) { -// lemma = lemma.replace('-', otherHyphen); -// } - AnalyzedToken newToken = new AnalyzedToken(word, analyzedToken.getPOSTag(), lemma); - newTokens.set(i, newToken); - } - } + word = origWord.replace('\u2013', '-'); + List newTokens = getAdjustedAnalyzedTokens(origWord, word, null); + + if( newTokens.size() > 0 ) { tokens = newTokens; } } - // try УКРАЇНА as Україна - else if( StringUtils.isAllUpperCase(word) ) { - String newWord = StringUtils.capitalize(StringUtils.lowerCase(word)); - List newTokens = super.getAnalyzedTokens(newWord); + + if( word.length() > 7 && word.startsWith("напів") ) { + String addPosTag = ""; + + Matcher matcher = Pattern.compile("(напів['-]?)(.*)").matcher(word); + matcher.matches(); + + String prefix = matcher.group(1); + String adjustedWord = matcher.group(2); + + List newTokens = getAdjustedAnalyzedTokens(origWord, adjustedWord, NAPIV_ALLOWED_TAGS_REGEX); + + +// System.out.println(":: " + word + " -> " + adjustedWord + " - " + newTokens); + + if( newTokens.size() > 0 ) { + if( ! addPosTag.contains(":bad:") ) { + if( word.charAt(5) == '-' + && ! adjustedWord.matches("[А-ЯІЇЄҐ].*") ) { + addPosTag += ":bad"; + } + else if( word.charAt(5) != '\'' + && adjustedWord.matches("[єїюя].*") ) { + addPosTag += ":bad"; + } + } - if( ! newTokens.get(0).hasNoTag() ) { for (int i = 0; i < newTokens.size(); i++) { AnalyzedToken analyzedToken = newTokens.get(i); + String lemma = analyzedToken.getLemma(); - AnalyzedToken newToken = new AnalyzedToken(word, analyzedToken.getPOSTag(), lemma); + String posTag = analyzedToken.getPOSTag(); + + posTag = posTag.replaceAll(":comp.|:&adjp(:(actv|pasv|perf|imperf))*", ""); + + posTag = PosTagHelper.addIfNotContains(posTag, addPosTag); + + AnalyzedToken newToken = new AnalyzedToken(origWord, posTag, prefix+lemma); newTokens.set(i, newToken); } - tokens = newTokens; } } - else if( word.endsWith("м²") || word.endsWith("м³") ) { - tokens = super.getAnalyzedTokens(word.substring(0, word.length()-1)); - } // try г instead of ґ else if( word.contains("ґ") ) { tokens = convertTokens(tokens, word, "ґ", "г", ":alt"); @@ -157,10 +202,28 @@ else if( word.endsWith("тер") ) { } } + // try УКРАЇНА as Україна and СИРІЮ as Сирію + if( word.length() > 2 && StringUtils.isAllUpperCase(word) ) { + + String newWord = StringUtils.capitalize(StringUtils.lowerCase(word)); + + List newTokens = getAdjustedAnalyzedTokens(word, newWord, Pattern.compile("noun.*?:prop.*")); + if( newTokens.size() > 0 ) { + if( tokens.get(0).hasNoTag() ) { + //TODO: add special tags if necessary + tokens = newTokens; + } + else { + tokens.addAll(newTokens); + } + } + } + + // if( taggedDebugWriter != null && ! tkns.isEmpty() ) { // debug_tagged_write(tkns, taggedDebugWriter); // } - + return tokens; } @@ -188,23 +251,14 @@ private List convertTokens(List origTokens, String return newTokens; } - private static char getOtherHyphen(String word) { - if( word.indexOf('\u2013') != -1 ) - return '\u2013'; -// we normalize \u2011 to \u002D in tokenizer -// if( word.indexOf('\u2011') != -1 ) -// return '\u2011'; - - return '\u0000'; - } List asAnalyzedTokenListForTaggedWordsInternal(String word, List taggedWords) { return super.asAnalyzedTokenListForTaggedWords(word, taggedWords); } - + // we need to expose this as some rules want to know if the word is in the dictionary public WordTagger getWordTagger() { return super.getWordTagger(); } - + } diff --git a/languagetool-language-modules/uk/src/test/java/org/languagetool/tagging/uk/UkrainianTaggerTest.java b/languagetool-language-modules/uk/src/test/java/org/languagetool/tagging/uk/UkrainianTaggerTest.java index b1aa56b5a0bb..dfd94fdf7b55 100644 --- a/languagetool-language-modules/uk/src/test/java/org/languagetool/tagging/uk/UkrainianTaggerTest.java +++ b/languagetool-language-modules/uk/src/test/java/org/languagetool/tagging/uk/UkrainianTaggerTest.java @@ -27,10 +27,10 @@ import org.languagetool.tokenizers.uk.UkrainianWordTokenizer; public class UkrainianTaggerTest { - + private UkrainianTagger tagger; private UkrainianWordTokenizer tokenizer; - + @Before public void setUp() { tagger = new UkrainianTagger(); @@ -93,9 +93,9 @@ public void testNumberTagging() throws IOException { @Test public void testSpecialSymbols() throws IOException { - TestTools.myAssert("км²", "км/[км]noun:inanim:m:v_dav:nv:abbr|км/[км]noun:inanim:m:v_kly:nv:abbr|км/[км]noun:inanim:m:v_mis:nv:abbr|км/[км]noun:inanim:m:v_naz:nv:abbr|км/[км]noun:inanim:m:v_oru:nv:abbr" - + "|км/[км]noun:inanim:m:v_rod:nv:abbr|км/[км]noun:inanim:m:v_zna:nv:abbr|км/[км]noun:inanim:p:v_dav:nv:abbr|км/[км]noun:inanim:p:v_kly:nv:abbr" - + "|км/[км]noun:inanim:p:v_mis:nv:abbr|км/[км]noun:inanim:p:v_naz:nv:abbr|км/[км]noun:inanim:p:v_oru:nv:abbr|км/[км]noun:inanim:p:v_rod:nv:abbr|км/[км]noun:inanim:p:v_zna:nv:abbr", tokenizer, tagger); + TestTools.myAssert("км²", "км²/[км]noun:inanim:m:v_dav:nv:abbr|км²/[км]noun:inanim:m:v_kly:nv:abbr|км²/[км]noun:inanim:m:v_mis:nv:abbr|км²/[км]noun:inanim:m:v_naz:nv:abbr|км²/[км]noun:inanim:m:v_oru:nv:abbr" + + "|км²/[км]noun:inanim:m:v_rod:nv:abbr|км²/[км]noun:inanim:m:v_zna:nv:abbr|км²/[км]noun:inanim:p:v_dav:nv:abbr|км²/[км]noun:inanim:p:v_kly:nv:abbr" + + "|км²/[км]noun:inanim:p:v_mis:nv:abbr|км²/[км]noun:inanim:p:v_naz:nv:abbr|км²/[км]noun:inanim:p:v_oru:nv:abbr|км²/[км]noun:inanim:p:v_rod:nv:abbr|км²/[км]noun:inanim:p:v_zna:nv:abbr", tokenizer, tagger); } @Test @@ -139,6 +139,7 @@ public void testTaggingWithDots() throws IOException { @Test public void testProperNameAllCaps() throws IOException { TestTools.myAssert("УКРАЇНА", "УКРАЇНА/[Україна]noun:inanim:f:v_naz:prop:geo", tokenizer, tagger); + TestTools.myAssert("СИРІЮ", "СИРІЮ/[Сирія]noun:inanim:f:v_zna:prop:geo|СИРІЮ/[сиріти]verb:imperf:pres:s:1", tokenizer, tagger); assertNotTagged("УКРАЇ"); } @@ -169,7 +170,7 @@ public void testDynamicTaggingNums() throws IOException { + "|120-мм/[120-мм]adj:m:v_dav|120-мм/[120-мм]adj:m:v_mis|120-мм/[120-мм]adj:m:v_naz|120-мм/[120-мм]adj:m:v_oru|120-мм/[120-мм]adj:m:v_rod|120-мм/[120-мм]adj:m:v_zna" + "|120-мм/[120-мм]adj:n:v_dav|120-мм/[120-мм]adj:n:v_mis|120-мм/[120-мм]adj:n:v_naz|120-мм/[120-мм]adj:n:v_oru|120-мм/[120-мм]adj:n:v_rod|120-мм/[120-мм]adj:n:v_zna" + "|120-мм/[120-мм]adj:p:v_dav|120-мм/[120-мм]adj:p:v_mis|120-мм/[120-мм]adj:p:v_naz|120-мм/[120-мм]adj:p:v_oru|120-мм/[120-мм]adj:p:v_rod|120-мм/[120-мм]adj:p:v_zna", tokenizer, tagger); - } + } @Test public void testNumberedEntities() throws IOException { @@ -321,6 +322,8 @@ public void testDynamicTaggingFullTagMatch() throws IOException { TestTools.myAssert("шмкр-гомеопат", "шмкр-гомеопат/[null]null", tokenizer, tagger); TestTools.myAssert("лікар-ткр", "лікар-ткр/[null]null", tokenizer, tagger); + TestTools.myAssert("пане-товаришу", "пане-товаришу/[пан-товариш]noun:anim:m:v_kly", tokenizer, tagger); + TestTools.myAssert("вчинок-приклад", "вчинок-приклад/[вчинок-приклад]noun:inanim:m:v_naz|вчинок-приклад/[вчинок-приклад]noun:inanim:m:v_zna", tokenizer, tagger); TestTools.myAssert("міста-фортеці", "міста-фортеці/[місто-фортеця]noun:inanim:n:v_rod|міста-фортеці/[місто-фортеця]noun:inanim:p:v_naz|міста-фортеці/[місто-фортеця]noun:inanim:p:v_zna", tokenizer, tagger); @@ -346,7 +349,8 @@ public void testDynamicTaggingFullTagMatch() throws IOException { assertNotTagged("авто-салон"); assertNotTagged("квазі-держави"); assertNotTagged("мульти-візу"); - assertNotTagged("напів-люкс"); + // handled by different logic +// assertNotTagged("напів-люкс"); assertNotTagged("контр-міри"); assertNotTagged("кіно-критика"); assertNotTagged("пів–качана"); @@ -515,7 +519,6 @@ public void testDynamicTaggingSkip() throws IOException { TestTools.myAssert("транс-все", "транс-все/[null]null", tokenizer, tagger); assertNotTagged("спа-салоне"); - // \n may happen in words when we have soft-hyphen wrap: \u00AD\n // in this case we strip \u00AD but leave \n in the word @@ -530,7 +533,23 @@ public void testInvalidSpelling() throws IOException { assertNotTagged("австріях"); TestTools.myAssert("фотометер", "фотометер/[фотометер]noun:inanim:m:v_naz:alt|фотометер/[фотометер]noun:inanim:m:v_zna:alt", tokenizer, tagger); } - + + @Test + public void testNapiv() throws IOException { + TestTools.myAssert("напів'японка", "напів'японка/[напів'японка]noun:anim:f:v_naz", tokenizer, tagger); + TestTools.myAssert("напівяпонка", "напівяпонка/[напівяпонка]noun:anim:f:v_naz:bad", tokenizer, tagger); + TestTools.myAssert("напів-японка", "напів-японка/[напів-японка]noun:anim:f:v_naz:bad", tokenizer, tagger); + TestTools.myAssert("напів-Європа", "напів-Європа/[напів-Європа]noun:inanim:f:v_naz:prop:geo", tokenizer, tagger); + TestTools.myAssert("напівсправедливий", "напівсправедливий/[напівсправедливий]adj:m:v_kly|напівсправедливий/[напівсправедливий]adj:m:v_naz|напівсправедливий/[напівсправедливий]adj:m:v_zna:rinanim", tokenizer, tagger); + TestTools.myAssert("напіврозслабленого", "напіврозслабленого/[напіврозслаблений]adj:m:v_rod:&&adjp:pasv:perf|напіврозслабленого/[напіврозслаблений]adj:m:v_zna:ranim:&&adjp:pasv:perf|напіврозслабленого/[напіврозслаблений]adj:n:v_rod:&&adjp:pasv:perf", tokenizer, tagger); + TestTools.myAssert("напів\u2013фантастичних", "напів–фантастичних/[напів-фантастичний]adj:p:v_mis:bad|напів–фантастичних/[напів-фантастичний]adj:p:v_rod:bad|напів–фантастичних/[напів-фантастичний]adj:p:v_zna:ranim:bad", tokenizer, tagger); + //TODO: +// TestTools.myAssert("напівпольської-напіванглійської", "", tokenizer, tagger); +// TestTools.myAssert("красунями-напівптахами", "", tokenizer, tagger); + assertNotTagged("напіврозслабеному"); // typo + assertNotTagged("напіви"); + } + // @Test // public void testSpecialChars() throws IOException { // AnalyzedSentence analyzedSentence = new JLanguageTool(new Ukrainian()).getAnalyzedSentence("і карт\u00ADками.");