From 21ca602adb79d92289330f86285ba85e928d9ed8 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 24 Mar 2022 20:59:11 +0300 Subject: [PATCH 01/10] implemented FormulaRule --- .../checker/Checker.kt | 10 --- .../checker/rule/formula/FormulaRule.kt | 76 +++++++++++++++++++ .../pdfdocument/list/PDFList.kt | 7 +- .../pdfdocument/text/Font.kt | 12 ++- .../pdfdocument/text/Word.kt | 2 +- .../mundaneassignmentpolice/wrapper/PDFBox.kt | 8 +- .../wrapper/PDFStripper.kt | 20 +++-- 7 files changed, 107 insertions(+), 28 deletions(-) create mode 100644 src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt index 636c4313..68721174 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt @@ -1,18 +1,8 @@ package com.github.darderion.mundaneassignmentpolice.checker import com.github.darderion.mundaneassignmentpolice.checker.rule.Rule -import com.github.darderion.mundaneassignmentpolice.checker.rule.list.ListRuleBuilder -import com.github.darderion.mundaneassignmentpolice.checker.rule.symbol.SymbolRule -import com.github.darderion.mundaneassignmentpolice.checker.rule.symbol.SymbolRuleBuilder -import com.github.darderion.mundaneassignmentpolice.checker.rule.symbol.and -import com.github.darderion.mundaneassignmentpolice.checker.rule.symbol.or -import com.github.darderion.mundaneassignmentpolice.checker.rule.tableofcontent.TableOfContentRuleBuilder -import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFArea.* -import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFRegion.Companion.EVERYWHERE -import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFRegion.Companion.NOWHERE import com.github.darderion.mundaneassignmentpolice.rules.RuleSet import com.github.darderion.mundaneassignmentpolice.wrapper.PDFBox -import java.util.* class Checker { fun getRuleViolations(pdfName: String, ruleSet: RuleSet) = getRuleViolations(pdfName, ruleSet.rules) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt new file mode 100644 index 00000000..c44e3c52 --- /dev/null +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt @@ -0,0 +1,76 @@ +package com.github.darderion.mundaneassignmentpolice.checker.rule.formula + +import com.github.darderion.mundaneassignmentpolice.checker.RuleViolation +import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType +import com.github.darderion.mundaneassignmentpolice.checker.rule.Rule +import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFArea +import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument +import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFRegion +import com.github.darderion.mundaneassignmentpolice.pdfdocument.inside +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.* +import com.github.darderion.mundaneassignmentpolice.utils.nearby +import kotlin.math.absoluteValue + +abstract class FormulaRule( + type: RuleViolationType, + name: String +): Rule(PDFRegion.NOWHERE.except(PDFArea.SECTION), name, type) { + abstract fun getLinesOfViolation(document: PDFDocument, formula: List): List + + override fun process(document: PDFDocument): List { + val rulesViolations = mutableListOf() + val formulas = getAllFormulas(document) + + formulas.forEach { + rulesViolations.add(RuleViolation(getLinesOfViolation(document, it), name, type)) + } + + return rulesViolations + } + + private fun getAllFormulas(document: PDFDocument): List> { + val normalLineIndents = getNormalIndents(document) + val normalLineInterval = getNormalInterval(document) + + val text = document.text.filter { it.area!! inside area }.toMutableList() + // Adding a line to process a text that has no lines after a formula + val additionalLine = Line(-1, -1, -1, + listOf(Word("NOT A FORMULA LINE", Font(), Coordinate(normalLineIndents.first(), -normalLineInterval - 1))) + ) + text += additionalLine + + val formulas = mutableListOf>() + val formula = mutableListOf() + var lastNormalLine = additionalLine + for (line in text) { + if (normalLineIndents.any { line.position.x nearby it }) { + lastNormalLine = line + if (formula.isNotEmpty()) { + formulas.add(formula) + formula.clear() + } + } + else { + val interval = lastNormalLine.text.maxOf { it.position.y } - line.text.minOf { it.position.y } + // Excludes captions to figures and entries in tables + if (line.text.any { it.font.type == PostScriptFontType.TYPE2 }) { + // Excludes multiline formulas within a single line of common text + if (interval > normalLineInterval || lastNormalLine.page != line.page) + formula.add(line) + } + } + } + return formulas + } + + private fun getNormalIndents(document: PDFDocument) = document.areas!!.sections.first() + .run { + listOf(titleIndex, contentIndex) + + document.areas.lists.map { it.getText() }.flatten().map { it.documentIndex } + }.map { document.text[it].position.x }.toSet() + + private fun getNormalInterval(document: PDFDocument) = document.areas!!.sections.first() + .run { listOf(contentIndex + 1, contentIndex) } + .map { document.text[it].position.y } + .run { first() - last() }.absoluteValue +} diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/list/PDFList.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/list/PDFList.kt index c55826eb..bf3592be 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/list/PDFList.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/list/PDFList.kt @@ -1,9 +1,6 @@ package com.github.darderion.mundaneassignmentpolice.pdfdocument.list -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Coordinate -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Font -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Word +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.* import java.util.* data class PDFList(val value: MutableList = mutableListOf(), val nodes: MutableList> = mutableListOf()) { @@ -54,7 +51,7 @@ data class PDFList(val value: MutableList = mutableListOf(), val nodes: Mu */ fun getLists(lines: List): List> { // Adding a line to process a text that has no lines after a list - val lines = lines + Line(-1, -1, -1, listOf(Word("NOT A LIST ITEM", Font(0.0f), Coordinate(1000, -1)))) + val lines = lines + Line(-1, -1, -1, listOf(Word("NOT A LIST ITEM", Font(), Coordinate(1000, -1)))) val lists: MutableList> = mutableListOf() val stack: Stack> = Stack() diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Font.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Font.kt index 20580b21..1164f35b 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Font.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Font.kt @@ -2,9 +2,15 @@ package com.github.darderion.mundaneassignmentpolice.pdfdocument.text import com.github.darderion.mundaneassignmentpolice.utils.floatEquals -class Font(val size: Float) { +enum class PostScriptFontType { + TYPE0, TYPE1, TYPE2, TYPE3, NONE +} + +class Font(val type: PostScriptFontType, val size: Float) { + constructor(): this(PostScriptFontType.NONE, 0.0f) + override fun equals(other: Any?) = this === other || - (other is Font && floatEquals(size, other.size)) + (other is Font && type == other.type && floatEquals(size, other.size)) - override fun hashCode() = size.hashCode() + override fun hashCode() = (type to size).hashCode() } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Word.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Word.kt index 6663a344..3fb28309 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Word.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Word.kt @@ -5,6 +5,6 @@ data class Word(val text: String, val font: Font, val position: Coordinate) { companion object { val spaceCharacter: Word - get() = Word(" ", Font(0.0f), Coordinate(0, 0)) + get() = Word(" ", Font(), Coordinate(0, 0)) } } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt index 395b3426..a446d809 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFBox.kt @@ -100,8 +100,8 @@ class PDFBox { val strippers = listOf(stripper, textStripper) - var lineIndex = 0 - for(pageIndex in (0..document.pages.count)) { + var lineIndex = -1 + for (pageIndex in (0 until document.pages.count)) { // For each page strippers.forEach { it.startPage = pageIndex + 1 @@ -142,7 +142,7 @@ class PDFBox { contentIndex += contentItem.length if (contentItem == " ") { - words.add(Word(word, font?: Font(0.0f), coordinates)) + words.add(Word(word, font?: Font(), coordinates)) words.add(Word.spaceCharacter) font = null word = "" @@ -163,7 +163,7 @@ class PDFBox { stripperIndex++ } } - if (font == null && word.isEmpty()) font = Font(0.0f) + if (font == null && word.isEmpty()) font = Font() words.add(Word(word, font!!, coordinates)) Line(line, pageIndex, lineIndex, words.toList()) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFStripper.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFStripper.kt index 00baf046..107c8cd0 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFStripper.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFStripper.kt @@ -1,9 +1,10 @@ package com.github.darderion.mundaneassignmentpolice.wrapper -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Coordinate -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Font -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Symbol -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Word +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.* +import org.apache.pdfbox.pdmodel.font.PDType0Font +import org.apache.pdfbox.pdmodel.font.PDType1CFont +import org.apache.pdfbox.pdmodel.font.PDType1Font +import org.apache.pdfbox.pdmodel.font.PDType3Font import org.apache.pdfbox.text.PDFTextStripper import org.apache.pdfbox.text.TextPosition @@ -16,10 +17,19 @@ class PDFStripper: PDFTextStripper() { override fun writeString(string: String?, textPositions: List) { for (text: TextPosition in textPositions) { val symbol = text.unicode + + val fontType = when(text.font) { + is PDType0Font -> PostScriptFontType.TYPE0 + is PDType1Font -> PostScriptFontType.TYPE1 + is PDType1CFont -> PostScriptFontType.TYPE2 + is PDType3Font -> PostScriptFontType.TYPE3 + else -> PostScriptFontType.NONE + } + if (symbol != null && symbol != " ") { symbol.forEach { symbols.add( - Symbol(it, Font(text.fontSize), Coordinate(text.xDirAdj to text.yDirAdj)) + Symbol(it, Font(fontType, text.fontSize), Coordinate(text.xDirAdj to text.yDirAdj)) ) } } From c4934bdef523c94edd90548a0f940b5d418382ca Mon Sep 17 00:00:00 2001 From: Pavel Saltykov Date: Sun, 3 Apr 2022 17:23:24 +0300 Subject: [PATCH 02/10] Change formula detection --- .../checker/Checker.kt | 10 ++++ .../checker/rule/formula/FormulaRule.kt | 51 +++++++++++++------ .../pdfdocument/text/Formula.kt | 4 ++ .../pdfdocument/text/Line.kt | 4 ++ .../wrapper/PDFStripper.kt | 7 ++- 5 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Formula.kt diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt index 68721174..7b2e99fc 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt @@ -1,14 +1,24 @@ package com.github.darderion.mundaneassignmentpolice.checker import com.github.darderion.mundaneassignmentpolice.checker.rule.Rule +import com.github.darderion.mundaneassignmentpolice.checker.rule.formula.FormulaRule +import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Formula +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line import com.github.darderion.mundaneassignmentpolice.rules.RuleSet import com.github.darderion.mundaneassignmentpolice.wrapper.PDFBox +class Helper(type: RuleViolationType = RuleViolationType.Error, name: String = "") : FormulaRule(type, name) { + override fun getLinesOfViolation(document: PDFDocument, formula: Formula) = emptyList() +} + class Checker { fun getRuleViolations(pdfName: String, ruleSet: RuleSet) = getRuleViolations(pdfName, ruleSet.rules) fun getRuleViolations(pdfName: String, rules: List): List { val document = PDFBox().getPDF(pdfName) + val formulaRule = Helper().process(document) + if (document.areas == null) return listOf( RuleViolation( listOf(document.text.first()), diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt index c44e3c52..a3696737 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt @@ -7,15 +7,17 @@ import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFArea import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFRegion import com.github.darderion.mundaneassignmentpolice.pdfdocument.inside -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.* -import com.github.darderion.mundaneassignmentpolice.utils.nearby +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Formula +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.PostScriptFontType +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Word import kotlin.math.absoluteValue abstract class FormulaRule( type: RuleViolationType, name: String ): Rule(PDFRegion.NOWHERE.except(PDFArea.SECTION), name, type) { - abstract fun getLinesOfViolation(document: PDFDocument, formula: List): List + abstract fun getLinesOfViolation(document: PDFDocument, formula: Formula): List override fun process(document: PDFDocument): List { val rulesViolations = mutableListOf() @@ -28,19 +30,19 @@ abstract class FormulaRule( return rulesViolations } - private fun getAllFormulas(document: PDFDocument): List> { - val normalLineIndents = getNormalIndents(document) + private fun getAllFormulas(document: PDFDocument): List { + /*val normalLineIndents = getNormalIndents(document) val normalLineInterval = getNormalInterval(document) - val text = document.text.filter { it.area!! inside area }.toMutableList() // Adding a line to process a text that has no lines after a formula val additionalLine = Line(-1, -1, -1, listOf(Word("NOT A FORMULA LINE", Font(), Coordinate(normalLineIndents.first(), -normalLineInterval - 1))) - ) - text += additionalLine + )*/ + val text = document.text.filter { it.area!! inside area && it.isNotEmpty() } /*+ additionalLine*/ - val formulas = mutableListOf>() + /*val formulas = mutableListOf>() val formula = mutableListOf() + val isFormula = false var lastNormalLine = additionalLine for (line in text) { if (normalLineIndents.any { line.position.x nearby it }) { @@ -49,8 +51,7 @@ abstract class FormulaRule( formulas.add(formula) formula.clear() } - } - else { + } else { val interval = lastNormalLine.text.maxOf { it.position.y } - line.text.minOf { it.position.y } // Excludes captions to figures and entries in tables if (line.text.any { it.font.type == PostScriptFontType.TYPE2 }) { @@ -59,18 +60,38 @@ abstract class FormulaRule( formula.add(line) } } + }*/ + + val formulas = mutableListOf() + val formulaText = mutableListOf() + val formulaLines = mutableSetOf() + + text.forEach { line -> + line.text.forEach { word -> + if (word.font.type == PostScriptFontType.TYPE2 || word.text == " " && formulaText.isNotEmpty()) { + formulaText.add(word) + formulaLines.add(line) + } else if (formulaText.isNotEmpty()) { + formulas.add(Formula(formulaText.dropLastWhile { it.text == " " }, formulaLines.toSet())) + formulaText.clear() + formulaLines.clear() + } + } } + if (formulaText.isNotEmpty()) + formulas.add(Formula(formulaText.dropLastWhile { it.text == " " }, formulaLines.toSet())) + return formulas } private fun getNormalIndents(document: PDFDocument) = document.areas!!.sections.first() - .run { - listOf(titleIndex, contentIndex) + + .let { section -> + listOf(section.titleIndex, section.contentIndex) + document.areas.lists.map { it.getText() }.flatten().map { it.documentIndex } }.map { document.text[it].position.x }.toSet() private fun getNormalInterval(document: PDFDocument) = document.areas!!.sections.first() - .run { listOf(contentIndex + 1, contentIndex) } + .let { listOf(it.contentIndex + 1, it.contentIndex) } .map { document.text[it].position.y } - .run { first() - last() }.absoluteValue + .let { it.first() - it.last() }.absoluteValue } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Formula.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Formula.kt new file mode 100644 index 00000000..9b7f49f6 --- /dev/null +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Formula.kt @@ -0,0 +1,4 @@ +package com.github.darderion.mundaneassignmentpolice.pdfdocument.text + +data class Formula(val text: List, val lines: Set) { +} diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Line.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Line.kt index a7003cc6..0c194910 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Line.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Line.kt @@ -20,4 +20,8 @@ data class Line(val index: Int, val page: Int, val documentIndex: Int, override fun toString() = "[$documentIndex -- $index, p.$page, $area, ${position.x}] --> '$content'" fun drop(numberOfItems: Int) = Line(index, page, documentIndex, text.drop(numberOfItems), area) + + fun isEmpty() = text.isEmpty() || text.size == 1 && text.first().text == "" + + fun isNotEmpty() = !isEmpty() } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFStripper.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFStripper.kt index 107c8cd0..bb949539 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFStripper.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/wrapper/PDFStripper.kt @@ -1,6 +1,9 @@ package com.github.darderion.mundaneassignmentpolice.wrapper -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.* +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Coordinate +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Font +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.PostScriptFontType +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Symbol import org.apache.pdfbox.pdmodel.font.PDType0Font import org.apache.pdfbox.pdmodel.font.PDType1CFont import org.apache.pdfbox.pdmodel.font.PDType1Font @@ -18,7 +21,7 @@ class PDFStripper: PDFTextStripper() { for (text: TextPosition in textPositions) { val symbol = text.unicode - val fontType = when(text.font) { + val fontType = when (text.font) { is PDType0Font -> PostScriptFontType.TYPE0 is PDType1Font -> PostScriptFontType.TYPE1 is PDType1CFont -> PostScriptFontType.TYPE2 From f3c7c5e0497652efd234f79d2fe6d4fef777586b Mon Sep 17 00:00:00 2001 From: Pavel Saltykov Date: Fri, 8 Apr 2022 22:34:29 +0300 Subject: [PATCH 03/10] Implement FormulaPunctuationRule --- .../checker/Checker.kt | 10 ---- .../checker/PunctuationMark.kt | 8 ++++ .../rule/formula/FormulaPunctuationRule.kt | 48 +++++++++++++++++++ .../checker/rule/formula/FormulaRule.kt | 48 ++----------------- .../pdfdocument/PDFDocument.kt | 5 +- 5 files changed, 65 insertions(+), 54 deletions(-) create mode 100644 src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/PunctuationMark.kt create mode 100644 src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt index 7b2e99fc..68721174 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/Checker.kt @@ -1,24 +1,14 @@ package com.github.darderion.mundaneassignmentpolice.checker import com.github.darderion.mundaneassignmentpolice.checker.rule.Rule -import com.github.darderion.mundaneassignmentpolice.checker.rule.formula.FormulaRule -import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Formula -import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line import com.github.darderion.mundaneassignmentpolice.rules.RuleSet import com.github.darderion.mundaneassignmentpolice.wrapper.PDFBox -class Helper(type: RuleViolationType = RuleViolationType.Error, name: String = "") : FormulaRule(type, name) { - override fun getLinesOfViolation(document: PDFDocument, formula: Formula) = emptyList() -} - class Checker { fun getRuleViolations(pdfName: String, ruleSet: RuleSet) = getRuleViolations(pdfName, ruleSet.rules) fun getRuleViolations(pdfName: String, rules: List): List { val document = PDFBox().getPDF(pdfName) - val formulaRule = Helper().process(document) - if (document.areas == null) return listOf( RuleViolation( listOf(document.text.first()), diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/PunctuationMark.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/PunctuationMark.kt new file mode 100644 index 00000000..df0a0e8e --- /dev/null +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/PunctuationMark.kt @@ -0,0 +1,8 @@ +package com.github.darderion.mundaneassignmentpolice.checker + +enum class PunctuationMark(val value: Char) { + FULL_STOP('.'), + COMMA(',') +} + +fun Char.isPunctuationMark() = PunctuationMark.values().map { it.value }.contains(this) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt new file mode 100644 index 00000000..eb62dc81 --- /dev/null +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt @@ -0,0 +1,48 @@ +package com.github.darderion.mundaneassignmentpolice.checker.rule.formula + +import com.github.darderion.mundaneassignmentpolice.checker.PunctuationMark +import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType +import com.github.darderion.mundaneassignmentpolice.checker.isPunctuationMark +import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Formula +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line + +class FormulaPunctuationRule( + type: RuleViolationType, + name: String, + val expectedPunctuationMark: PunctuationMark, + val indicatorWords: List, + val ignoredSymbols: List +) : FormulaRule(type, name) { + override fun getLinesOfViolation(document: PDFDocument, formula: Formula): List { + var textAfterFormula = formula.lines.last().text.takeLastWhile { it != formula.text.last() } + .joinToString("") { it.text } + "\n" + + textAfterFormula += + document.getLines(formula.lines.last().documentIndex + 1, 2, area) + .joinToString("\n") { it.content } + + val lastFormulaSymbol = formula.text.last().text.last() + val firstSymbolAfterFormula = textAfterFormula[0] + + val actualPunctuation = when { + lastFormulaSymbol.isPunctuationMark() -> lastFormulaSymbol + firstSymbolAfterFormula.isPunctuationMark() -> { + textAfterFormula = textAfterFormula.drop(1) + firstSymbolAfterFormula + } + else -> ' ' + } + + val firstWordAfterFormula = textAfterFormula.filterNot { ignoredSymbols.contains(it) } + .split(" ").first() + + if (indicatorWords.any { regex -> regex.matches(firstWordAfterFormula) }) { + if (expectedPunctuationMark.value != actualPunctuation) { + return listOf(formula.lines.last()) + } + } + + return emptyList() + } +} diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt index a3696737..0ffd119a 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt @@ -11,7 +11,6 @@ import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Formula import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.PostScriptFontType import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Word -import kotlin.math.absoluteValue abstract class FormulaRule( type: RuleViolationType, @@ -24,43 +23,17 @@ abstract class FormulaRule( val formulas = getAllFormulas(document) formulas.forEach { - rulesViolations.add(RuleViolation(getLinesOfViolation(document, it), name, type)) + val linesOfViolation = getLinesOfViolation(document, it) + if (linesOfViolation.isNotEmpty()) { + rulesViolations.add(RuleViolation(linesOfViolation, name, type)) + } } return rulesViolations } private fun getAllFormulas(document: PDFDocument): List { - /*val normalLineIndents = getNormalIndents(document) - val normalLineInterval = getNormalInterval(document) - - // Adding a line to process a text that has no lines after a formula - val additionalLine = Line(-1, -1, -1, - listOf(Word("NOT A FORMULA LINE", Font(), Coordinate(normalLineIndents.first(), -normalLineInterval - 1))) - )*/ - val text = document.text.filter { it.area!! inside area && it.isNotEmpty() } /*+ additionalLine*/ - - /*val formulas = mutableListOf>() - val formula = mutableListOf() - val isFormula = false - var lastNormalLine = additionalLine - for (line in text) { - if (normalLineIndents.any { line.position.x nearby it }) { - lastNormalLine = line - if (formula.isNotEmpty()) { - formulas.add(formula) - formula.clear() - } - } else { - val interval = lastNormalLine.text.maxOf { it.position.y } - line.text.minOf { it.position.y } - // Excludes captions to figures and entries in tables - if (line.text.any { it.font.type == PostScriptFontType.TYPE2 }) { - // Excludes multiline formulas within a single line of common text - if (interval > normalLineInterval || lastNormalLine.page != line.page) - formula.add(line) - } - } - }*/ + val text = document.text.filter { it.area!! inside area && it.isNotEmpty() } val formulas = mutableListOf() val formulaText = mutableListOf() @@ -83,15 +56,4 @@ abstract class FormulaRule( return formulas } - - private fun getNormalIndents(document: PDFDocument) = document.areas!!.sections.first() - .let { section -> - listOf(section.titleIndex, section.contentIndex) + - document.areas.lists.map { it.getText() }.flatten().map { it.documentIndex } - }.map { document.text[it].position.x }.toSet() - - private fun getNormalInterval(document: PDFDocument) = document.areas!!.sections.first() - .let { listOf(it.contentIndex + 1, it.contentIndex) } - .map { document.text[it].position.y } - .let { it.first() - it.last() }.absoluteValue } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt index 280a9ff2..439dab36 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt @@ -2,7 +2,6 @@ package com.github.darderion.mundaneassignmentpolice.pdfdocument import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line import mu.KotlinLogging -import java.lang.Exception class PDFDocument(val name: String = "PDF", val text: List, @@ -29,6 +28,10 @@ class PDFDocument(val name: String = "PDF", index in fromIndex..toIndex }.joinToString("\n ") { it.content } + fun getLines(fromIndex: Int, count: Int, region: PDFRegion) = text.filter { it.area!! inside region } + .dropWhile { it.documentIndex < fromIndex } + .take(count) + fun print() { text.map { "${it.area} | ${it.text.joinToString("--") { "${it.font.size}-${it.text}"}}" }.forEach(::println) } From 32ae076382f9c6a478d5b6ec9d9bc612312c6f14 Mon Sep 17 00:00:00 2001 From: Pavel Saltykov Date: Sat, 9 Apr 2022 00:48:44 +0300 Subject: [PATCH 04/10] Add FormulaPunctuationRuleBuilder --- .../formula/FormulaPunctuationRuleBuilder.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt new file mode 100644 index 00000000..bbcf1573 --- /dev/null +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt @@ -0,0 +1,32 @@ +package com.github.darderion.mundaneassignmentpolice.checker.rule.formula + +import com.github.darderion.mundaneassignmentpolice.checker.PunctuationMark +import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType + +class FormulaPunctuationRuleBuilder { + private var type: RuleViolationType = RuleViolationType.Error + private var name: String = "Rule name" + private var punctuationMark: PunctuationMark = PunctuationMark.FULL_STOP + private var indicatorWords: MutableList = mutableListOf() + private var ignoredSymbols: MutableList = mutableListOf() + + infix fun called(name: String) = this.also { this.name = name } + + infix fun type(type: RuleViolationType) = this.also { this.type = type } + + infix fun requiredPunctuationMark(punctuationMark: PunctuationMark) = this.also { + this.punctuationMark = punctuationMark + } + + fun indicatorWords(vararg words: Regex) = this.also { indicatorWords.addAll(words.toList()) } + + fun ignoringAdjusting(vararg symbols: Char) = this.also { ignoredSymbols.addAll(symbols.toList()) } + + fun getRule() = FormulaPunctuationRule( + type, + name, + punctuationMark, + indicatorWords, + ignoredSymbols + ) as FormulaRule +} From 9567a15c6671f0a0e7e20cf441133535c2dfa161 Mon Sep 17 00:00:00 2001 From: Pavel Saltykov Date: Sat, 9 Apr 2022 13:30:51 +0300 Subject: [PATCH 05/10] Add rules of punctuation after formula --- .../rule/formula/FormulaPunctuationRule.kt | 4 ++-- .../checker/rule/formula/FormulaRule.kt | 2 +- .../pdfdocument/text/Formula.kt | 3 +-- .../mundaneassignmentpolice/rules/RuleSet.kt | 5 +++-- .../mundaneassignmentpolice/rules/Rules.kt | 22 ++++++++++++++++++- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt index eb62dc81..a1eb54d2 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt @@ -16,11 +16,11 @@ class FormulaPunctuationRule( ) : FormulaRule(type, name) { override fun getLinesOfViolation(document: PDFDocument, formula: Formula): List { var textAfterFormula = formula.lines.last().text.takeLastWhile { it != formula.text.last() } - .joinToString("") { it.text } + "\n" + .joinToString("") { it.text } + " " textAfterFormula += document.getLines(formula.lines.last().documentIndex + 1, 2, area) - .joinToString("\n") { it.content } + .joinToString(" ") { it.content } val lastFormulaSymbol = formula.text.last().text.last() val firstSymbolAfterFormula = textAfterFormula[0] diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt index 0ffd119a..ad861bd8 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt @@ -15,7 +15,7 @@ import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Word abstract class FormulaRule( type: RuleViolationType, name: String -): Rule(PDFRegion.NOWHERE.except(PDFArea.SECTION), name, type) { +) : Rule(PDFRegion.NOWHERE.except(PDFArea.SECTION), name, type) { abstract fun getLinesOfViolation(document: PDFDocument, formula: Formula): List override fun process(document: PDFDocument): List { diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Formula.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Formula.kt index 9b7f49f6..901f2baa 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Formula.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/text/Formula.kt @@ -1,4 +1,3 @@ package com.github.darderion.mundaneassignmentpolice.pdfdocument.text -data class Formula(val text: List, val lines: Set) { -} +data class Formula(val text: List, val lines: Set) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt index eb2fa858..f7bf89eb 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/RuleSet.kt @@ -3,7 +3,7 @@ package com.github.darderion.mundaneassignmentpolice.rules import com.github.darderion.mundaneassignmentpolice.checker.rule.Rule val RULE_SET_RU = RuleSet( - mutableListOf( + listOf( RULE_LITLINK, RULE_SHORT_DASH, RULE_MEDIUM_DASH, @@ -21,6 +21,7 @@ val RULE_SET_RU = RuleSet( ) + RULES_SPACE_AROUND_BRACKETS + RULES_SMALL_NUMBERS + + RULES_FORMULA_PUNCTUATION ) -class RuleSet(val rules: List) {} +class RuleSet(val rules: List) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt index e3ecfa20..4883ac97 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt @@ -1,8 +1,9 @@ package com.github.darderion.mundaneassignmentpolice.rules +import com.github.darderion.mundaneassignmentpolice.checker.PunctuationMark import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType +import com.github.darderion.mundaneassignmentpolice.checker.rule.formula.FormulaPunctuationRuleBuilder import com.github.darderion.mundaneassignmentpolice.checker.rule.list.ListRuleBuilder -import com.github.darderion.mundaneassignmentpolice.checker.rule.symbol.SymbolRule import com.github.darderion.mundaneassignmentpolice.checker.rule.regex.RegexRuleBuilder import com.github.darderion.mundaneassignmentpolice.checker.rule.symbol.SymbolRuleBuilder import com.github.darderion.mundaneassignmentpolice.checker.rule.symbol.and @@ -265,3 +266,22 @@ val RULE_ORDER_OF_REFERENCES = RegexRuleBuilder() referencesInIntList != referencesInIntList.sorted() }.map { it.second } }.getRule() + +private val ignoringSymbolsAfterFormula = " \n($numbers)" + +val fullStopAfterFormulaRule = FormulaPunctuationRuleBuilder() + .called("Отсутствует точка после формулы") + .requiredPunctuationMark(PunctuationMark.FULL_STOP) + .indicatorWords("""[A-ZА-Я].*?""".toRegex()) + .indicatorWords("""$""".toRegex()) + .ignoringAdjusting(*ignoringSymbolsAfterFormula.toCharArray()) + .getRule() + +val commaAfterFormulaRule = FormulaPunctuationRuleBuilder() + .called("Отсутствует запятая после формулы") + .requiredPunctuationMark(PunctuationMark.COMMA) + .indicatorWords("""где""".toRegex()) + .ignoringAdjusting(*ignoringSymbolsAfterFormula.toCharArray()) + .getRule() + +val RULES_FORMULA_PUNCTUATION = listOf(fullStopAfterFormulaRule, commaAfterFormulaRule) From 214bc2ec8f45314af471ce025af65f4135c98712 Mon Sep 17 00:00:00 2001 From: Pavel Saltykov Date: Fri, 15 Apr 2022 22:59:09 +0300 Subject: [PATCH 06/10] Fix rule of formula punctuation, add tests --- .../rule/formula/FormulaPunctuationRule.kt | 52 +++++++++--------- .../formula/FormulaPunctuationRuleBuilder.kt | 6 +- .../checker/rule/formula/FormulaRule.kt | 9 +-- .../pdfdocument/PDFDocument.kt | 4 -- .../mundaneassignmentpolice/rules/Rules.kt | 11 +++- .../checker/RulesTests.kt | 8 ++- .../FormulaRuleTestsFormulaPunctuation.pdf | Bin 0 -> 36503 bytes ...Urls.pdf => URLRuleTestsShortenedUrls.pdf} | Bin 8 files changed, 50 insertions(+), 40 deletions(-) create mode 100644 src/test/resources/com/github/darderion/mundaneassignmentpolice/checker/FormulaRuleTestsFormulaPunctuation.pdf rename src/test/resources/com/github/darderion/mundaneassignmentpolice/checker/{URLRuleShortenedUrls.pdf => URLRuleTestsShortenedUrls.pdf} (100%) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt index a1eb54d2..1338d1a9 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt @@ -4,6 +4,7 @@ import com.github.darderion.mundaneassignmentpolice.checker.PunctuationMark import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType import com.github.darderion.mundaneassignmentpolice.checker.isPunctuationMark import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument +import com.github.darderion.mundaneassignmentpolice.pdfdocument.inside import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Formula import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line @@ -12,35 +13,36 @@ class FormulaPunctuationRule( name: String, val expectedPunctuationMark: PunctuationMark, val indicatorWords: List, - val ignoredSymbols: List + val ignoredWords: List ) : FormulaRule(type, name) { override fun getLinesOfViolation(document: PDFDocument, formula: Formula): List { - var textAfterFormula = formula.lines.last().text.takeLastWhile { it != formula.text.last() } - .joinToString("") { it.text } + " " - - textAfterFormula += - document.getLines(formula.lines.last().documentIndex + 1, 2, area) - .joinToString(" ") { it.content } - - val lastFormulaSymbol = formula.text.last().text.last() - val firstSymbolAfterFormula = textAfterFormula[0] - - val actualPunctuation = when { - lastFormulaSymbol.isPunctuationMark() -> lastFormulaSymbol - firstSymbolAfterFormula.isPunctuationMark() -> { - textAfterFormula = textAfterFormula.drop(1) - firstSymbolAfterFormula - } - else -> ' ' + val textAfterFormula = formula.lines.last().text + .takeLastWhile { it != formula.text.last() } + .toMutableList() + + textAfterFormula.addAll( + document.text.asSequence().drop(formula.lines.last().documentIndex + 1) + .filter { it.area!! inside area && it.isNotEmpty() } + .take(2) // take a line with formula reference and a line with words after the formula + .map { it.text }.flatten() + ) + + val filteredText = textAfterFormula.filterNot { word -> ignoredWords.any { it.matches(word.text) } } + + val isExpectedSymbolMissing = listOf( + formula.text.last().text.last(), // if a punctuation mark is at the end of the formula + filteredText.getOrNull(0)?.text?.first() + ).none { it == expectedPunctuationMark.value } + + val wordAfterFormula = when { + filteredText.isEmpty() -> "" + filteredText.first().text.first().isPunctuationMark() -> + filteredText.getOrNull(1)?.text ?: "" + else -> filteredText.first().text } - val firstWordAfterFormula = textAfterFormula.filterNot { ignoredSymbols.contains(it) } - .split(" ").first() - - if (indicatorWords.any { regex -> regex.matches(firstWordAfterFormula) }) { - if (expectedPunctuationMark.value != actualPunctuation) { - return listOf(formula.lines.last()) - } + if (indicatorWords.any { regex -> regex.matches(wordAfterFormula) } && isExpectedSymbolMissing) { + return listOf(formula.lines.last()) } return emptyList() diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt index bbcf1573..2e4d7862 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt @@ -8,7 +8,7 @@ class FormulaPunctuationRuleBuilder { private var name: String = "Rule name" private var punctuationMark: PunctuationMark = PunctuationMark.FULL_STOP private var indicatorWords: MutableList = mutableListOf() - private var ignoredSymbols: MutableList = mutableListOf() + private var ignoredWords: MutableList = mutableListOf() infix fun called(name: String) = this.also { this.name = name } @@ -20,13 +20,13 @@ class FormulaPunctuationRuleBuilder { fun indicatorWords(vararg words: Regex) = this.also { indicatorWords.addAll(words.toList()) } - fun ignoringAdjusting(vararg symbols: Char) = this.also { ignoredSymbols.addAll(symbols.toList()) } + fun ignoredWords(vararg symbols: Regex) = this.also { ignoredWords.addAll(symbols.toList()) } fun getRule() = FormulaPunctuationRule( type, name, punctuationMark, indicatorWords, - ignoredSymbols + ignoredWords ) as FormulaRule } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt index ad861bd8..0c1db66b 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt @@ -22,10 +22,11 @@ abstract class FormulaRule( val rulesViolations = mutableListOf() val formulas = getAllFormulas(document) - formulas.forEach { - val linesOfViolation = getLinesOfViolation(document, it) - if (linesOfViolation.isNotEmpty()) { - rulesViolations.add(RuleViolation(linesOfViolation, name, type)) + formulas.forEach { formula -> + getLinesOfViolation(document, formula).let { + if (it.isNotEmpty()) { + rulesViolations.add(RuleViolation(it, name, type)) + } } } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt index 439dab36..a7c7e71b 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/pdfdocument/PDFDocument.kt @@ -28,10 +28,6 @@ class PDFDocument(val name: String = "PDF", index in fromIndex..toIndex }.joinToString("\n ") { it.content } - fun getLines(fromIndex: Int, count: Int, region: PDFRegion) = text.filter { it.area!! inside region } - .dropWhile { it.documentIndex < fromIndex } - .take(count) - fun print() { text.map { "${it.area} | ${it.text.joinToString("--") { "${it.font.size}-${it.text}"}}" }.forEach(::println) } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt index 4883ac97..ae0ad3e3 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt @@ -267,21 +267,26 @@ val RULE_ORDER_OF_REFERENCES = RegexRuleBuilder() }.map { it.second } }.getRule() -private val ignoringSymbolsAfterFormula = " \n($numbers)" +private val ignoringAfterFormula = listOf( + """\s""".toRegex(), + """\([0-9]+\)""".toRegex() // ignore formula reference +) val fullStopAfterFormulaRule = FormulaPunctuationRuleBuilder() .called("Отсутствует точка после формулы") .requiredPunctuationMark(PunctuationMark.FULL_STOP) + // if after a formula there is a capitalized word that indicates the beginning of a new sentence .indicatorWords("""[A-ZА-Я].*?""".toRegex()) + // if no sentence after a formula .indicatorWords("""$""".toRegex()) - .ignoringAdjusting(*ignoringSymbolsAfterFormula.toCharArray()) + .ignoredWords(*ignoringAfterFormula.toTypedArray()) .getRule() val commaAfterFormulaRule = FormulaPunctuationRuleBuilder() .called("Отсутствует запятая после формулы") .requiredPunctuationMark(PunctuationMark.COMMA) .indicatorWords("""где""".toRegex()) - .ignoringAdjusting(*ignoringSymbolsAfterFormula.toCharArray()) + .ignoredWords(*ignoringAfterFormula.toTypedArray()) .getRule() val RULES_FORMULA_PUNCTUATION = listOf(fullStopAfterFormulaRule, commaAfterFormulaRule) diff --git a/src/test/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RulesTests.kt b/src/test/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RulesTests.kt index 3c4c908d..f422ae87 100644 --- a/src/test/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RulesTests.kt +++ b/src/test/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RulesTests.kt @@ -51,6 +51,10 @@ class RulesTests : StringSpec({ "Regex rule should detect incorrect order of literature references"{ RULE_ORDER_OF_REFERENCES.process(PDFBox().getPDF(filePathOrderOfReferences)).count() shouldBeExactly 3 } + "Formula punctuation rules should detect the absence of a full stop or a comma after a formula"{ + fullStopAfterFormulaRule.process(PDFBox().getPDF(filePathFormulaPunctuation)).count() shouldBeExactly 3 + commaAfterFormulaRule.process(PDFBox().getPDF(filePathFormulaPunctuation)).count() shouldBeExactly 3 + } }) { companion object { const val filePathQuestionMarkAndDashes = @@ -63,10 +67,12 @@ class RulesTests : StringSpec({ const val filePathSpaceAroundBrackets = "${TestsConfiguration.resourceFolder}checker/SymbolRuleTestsSpaceAroundBrackets.pdf" const val filePathCitation = "${TestsConfiguration.resourceFolder}checker/SymbolRuleTestsCitation.pdf" - const val filePathShortenedUrls = "${TestsConfiguration.resourceFolder}checker/URLRuleShortenedUrls.pdf" + const val filePathShortenedUrls = "${TestsConfiguration.resourceFolder}checker/URLRuleTestsShortenedUrls.pdf" const val filePathSymbolsInSectionNames = "${TestsConfiguration.resourceFolder}checker/RulesTestsSymbolsInSectionNames.pdf" const val filePathOrderOfReferences = "${TestsConfiguration.resourceFolder}checker/RegexRuleTestsOrderOfReferences.pdf" + const val filePathFormulaPunctuation = + "${TestsConfiguration.resourceFolder}checker/FormulaRuleTestsFormulaPunctuation.pdf" } } diff --git a/src/test/resources/com/github/darderion/mundaneassignmentpolice/checker/FormulaRuleTestsFormulaPunctuation.pdf b/src/test/resources/com/github/darderion/mundaneassignmentpolice/checker/FormulaRuleTestsFormulaPunctuation.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5a48aca54c37a5d7142f80ce39eb9c6ca389d5a4 GIT binary patch literal 36503 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 IN(({$e~cCzO8@`> literal 0 HcmV?d00001 diff --git a/src/test/resources/com/github/darderion/mundaneassignmentpolice/checker/URLRuleShortenedUrls.pdf b/src/test/resources/com/github/darderion/mundaneassignmentpolice/checker/URLRuleTestsShortenedUrls.pdf similarity index 100% rename from src/test/resources/com/github/darderion/mundaneassignmentpolice/checker/URLRuleShortenedUrls.pdf rename to src/test/resources/com/github/darderion/mundaneassignmentpolice/checker/URLRuleTestsShortenedUrls.pdf From 0fc6f8a746fc21755886d81c3d15ecbe5531ef21 Mon Sep 17 00:00:00 2001 From: Pavel Saltykov Date: Sat, 7 May 2022 15:16:37 +0300 Subject: [PATCH 07/10] Add filter of formulas --- .../checker/rule/formula/FormulaRule.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt index 0c1db66b..b68d771b 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt @@ -55,6 +55,11 @@ abstract class FormulaRule( if (formulaText.isNotEmpty()) formulas.add(Formula(formulaText.dropLastWhile { it.text == " " }, formulaLines.toSet())) - return formulas + return filterFormulas(formulas) } + + private fun filterFormulas(formulas: List) = + formulas.filterNot { + it.text.size == 1 && it.text[0].text == "∗" // remove single special characters + } } From 8ae26f4e9d1353fb3b37b092bb3fd68426971fbd Mon Sep 17 00:00:00 2001 From: Pavel Saltykov Date: Sat, 14 May 2022 15:17:09 +0300 Subject: [PATCH 08/10] Modify formula punctuation rules for correct processing of two consecutive formulas --- .../checker/PunctuationMark.kt | 2 + .../rule/formula/FormulaPunctuationRule.kt | 65 ++++++++--------- .../formula/FormulaPunctuationRuleBuilder.kt | 23 +++--- .../checker/rule/formula/FormulaRule.kt | 18 +---- .../mundaneassignmentpolice/rules/Rules.kt | 72 ++++++++++++++++--- 5 files changed, 111 insertions(+), 69 deletions(-) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/PunctuationMark.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/PunctuationMark.kt index df0a0e8e..bd7d7077 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/PunctuationMark.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/PunctuationMark.kt @@ -6,3 +6,5 @@ enum class PunctuationMark(val value: Char) { } fun Char.isPunctuationMark() = PunctuationMark.values().map { it.value }.contains(this) + +fun String.isPunctuationMark() = this.length == 1 && this.single().isPunctuationMark() diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt index 1338d1a9..0e41e282 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRule.kt @@ -1,50 +1,47 @@ package com.github.darderion.mundaneassignmentpolice.checker.rule.formula -import com.github.darderion.mundaneassignmentpolice.checker.PunctuationMark +import com.github.darderion.mundaneassignmentpolice.checker.RuleViolation import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType -import com.github.darderion.mundaneassignmentpolice.checker.isPunctuationMark import com.github.darderion.mundaneassignmentpolice.pdfdocument.PDFDocument import com.github.darderion.mundaneassignmentpolice.pdfdocument.inside import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Formula import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Word class FormulaPunctuationRule( type: RuleViolationType, name: String, - val expectedPunctuationMark: PunctuationMark, - val indicatorWords: List, - val ignoredWords: List + private val ignoredWords: List, + private val ruleBody: + (formula: Formula, filteredText: List, nextFormula: Formula?) -> List ) : FormulaRule(type, name) { - override fun getLinesOfViolation(document: PDFDocument, formula: Formula): List { - val textAfterFormula = formula.lines.last().text - .takeLastWhile { it != formula.text.last() } - .toMutableList() - - textAfterFormula.addAll( - document.text.asSequence().drop(formula.lines.last().documentIndex + 1) - .filter { it.area!! inside area && it.isNotEmpty() } - .take(2) // take a line with formula reference and a line with words after the formula - .map { it.text }.flatten() - ) - - val filteredText = textAfterFormula.filterNot { word -> ignoredWords.any { it.matches(word.text) } } - - val isExpectedSymbolMissing = listOf( - formula.text.last().text.last(), // if a punctuation mark is at the end of the formula - filteredText.getOrNull(0)?.text?.first() - ).none { it == expectedPunctuationMark.value } - - val wordAfterFormula = when { - filteredText.isEmpty() -> "" - filteredText.first().text.first().isPunctuationMark() -> - filteredText.getOrNull(1)?.text ?: "" - else -> filteredText.first().text + override fun getViolations(document: PDFDocument, formulas: List): List { + val violations = mutableListOf() + + formulas.forEachIndexed { index, formula -> + val textAfterFormula = formula.lines.last().text + .takeLastWhile { it != formula.text.last() } + .toMutableList() + + textAfterFormula.addAll( + document.text.asSequence().drop(formula.lines.last().documentIndex + 1) + .filter { it.area!! inside area && it.isNotEmpty() } + .take(2) // take a line with formula reference and a line with words after the formula + .map { it.text }.flatten() + ) + + val filteredText = textAfterFormula.filterNot { word -> ignoredWords.any { it.matches(word.text) } } + + val nextFormula = formulas.getOrNull(index + 1) + + val violationLines = ruleBody(formula, filteredText, nextFormula) + if (violationLines.isNotEmpty()) { + violations.add( + RuleViolation(violationLines, name, type) + ) + } } - if (indicatorWords.any { regex -> regex.matches(wordAfterFormula) } && isExpectedSymbolMissing) { - return listOf(formula.lines.last()) - } - - return emptyList() + return violations } } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt index 2e4d7862..cd0aef4b 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaPunctuationRuleBuilder.kt @@ -1,32 +1,31 @@ package com.github.darderion.mundaneassignmentpolice.checker.rule.formula -import com.github.darderion.mundaneassignmentpolice.checker.PunctuationMark import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Formula +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Line +import com.github.darderion.mundaneassignmentpolice.pdfdocument.text.Word class FormulaPunctuationRuleBuilder { private var type: RuleViolationType = RuleViolationType.Error private var name: String = "Rule name" - private var punctuationMark: PunctuationMark = PunctuationMark.FULL_STOP - private var indicatorWords: MutableList = mutableListOf() private var ignoredWords: MutableList = mutableListOf() + private var ruleBody: (formula: Formula, filteredText: List, nextFormula: Formula?) -> List = + { _, _, _ -> emptyList() } infix fun called(name: String) = this.also { this.name = name } infix fun type(type: RuleViolationType) = this.also { this.type = type } - infix fun requiredPunctuationMark(punctuationMark: PunctuationMark) = this.also { - this.punctuationMark = punctuationMark - } + fun ignoredWords(vararg regexes: Regex) = this.also { ignoredWords.addAll(regexes) } - fun indicatorWords(vararg words: Regex) = this.also { indicatorWords.addAll(words.toList()) } - - fun ignoredWords(vararg symbols: Regex) = this.also { ignoredWords.addAll(symbols.toList()) } + infix fun rule( + ruleBody: (formula: Formula, filteredText: List, nextFormula: Formula?) -> List + ) = this.also { this.ruleBody = ruleBody } fun getRule() = FormulaPunctuationRule( type, name, - punctuationMark, - indicatorWords, - ignoredWords + ignoredWords, + ruleBody ) as FormulaRule } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt index b68d771b..27858945 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt @@ -16,22 +16,10 @@ abstract class FormulaRule( type: RuleViolationType, name: String ) : Rule(PDFRegion.NOWHERE.except(PDFArea.SECTION), name, type) { - abstract fun getLinesOfViolation(document: PDFDocument, formula: Formula): List + abstract fun getViolations(document: PDFDocument, formulas: List): List - override fun process(document: PDFDocument): List { - val rulesViolations = mutableListOf() - val formulas = getAllFormulas(document) - - formulas.forEach { formula -> - getLinesOfViolation(document, formula).let { - if (it.isNotEmpty()) { - rulesViolations.add(RuleViolation(it, name, type)) - } - } - } - - return rulesViolations - } + override fun process(document: PDFDocument) = + getViolations(document, getAllFormulas(document)) private fun getAllFormulas(document: PDFDocument): List { val text = document.text.filter { it.area!! inside area && it.isNotEmpty() } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt index ae0ad3e3..e9ec7f5d 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt @@ -2,6 +2,7 @@ package com.github.darderion.mundaneassignmentpolice.rules import com.github.darderion.mundaneassignmentpolice.checker.PunctuationMark import com.github.darderion.mundaneassignmentpolice.checker.RuleViolationType +import com.github.darderion.mundaneassignmentpolice.checker.isPunctuationMark import com.github.darderion.mundaneassignmentpolice.checker.rule.formula.FormulaPunctuationRuleBuilder import com.github.darderion.mundaneassignmentpolice.checker.rule.list.ListRuleBuilder import com.github.darderion.mundaneassignmentpolice.checker.rule.regex.RegexRuleBuilder @@ -269,24 +270,79 @@ val RULE_ORDER_OF_REFERENCES = RegexRuleBuilder() private val ignoringAfterFormula = listOf( """\s""".toRegex(), - """\([0-9]+\)""".toRegex() // ignore formula reference + """\([0-9]+\)""".toRegex() // ignore formula reference, e.g. "(1)" ) val fullStopAfterFormulaRule = FormulaPunctuationRuleBuilder() .called("Отсутствует точка после формулы") - .requiredPunctuationMark(PunctuationMark.FULL_STOP) - // if after a formula there is a capitalized word that indicates the beginning of a new sentence - .indicatorWords("""[A-ZА-Я].*?""".toRegex()) - // if no sentence after a formula - .indicatorWords("""$""".toRegex()) .ignoredWords(*ignoringAfterFormula.toTypedArray()) + .rule { formula, filteredText, nextFormula -> + val violationLines = listOf(formula.lines.last()) + val lastFormulaSymbol = formula.text.last().text.last() + + if (filteredText.isEmpty()) { + return@rule if (lastFormulaSymbol != PunctuationMark.FULL_STOP.value) violationLines else emptyList() + } + + val (firstAfterFormula, secondAfterFormula) = filteredText.first() to filteredText.getOrNull(1) + if (nextFormula != null && + (firstAfterFormula == nextFormula.text.first() || + firstAfterFormula.text.isPunctuationMark() && secondAfterFormula == nextFormula.text.first()) + ) { + return@rule emptyList() + } + + val indicator = """[A-ZА-Я].*?""".toRegex() + if (indicator.matches(firstAfterFormula.text)) { + return@rule if (lastFormulaSymbol != PunctuationMark.FULL_STOP.value) violationLines else emptyList() + } + + if (firstAfterFormula.text.isPunctuationMark() && + secondAfterFormula != null && indicator.matches(secondAfterFormula.text) + ) { + return@rule if (firstAfterFormula.text.single() != PunctuationMark.FULL_STOP.value) violationLines + else emptyList() + } + + return@rule emptyList() + } .getRule() val commaAfterFormulaRule = FormulaPunctuationRuleBuilder() .called("Отсутствует запятая после формулы") - .requiredPunctuationMark(PunctuationMark.COMMA) - .indicatorWords("""где""".toRegex()) .ignoredWords(*ignoringAfterFormula.toTypedArray()) + .rule { formula, filteredText, nextFormula -> + val violationLines = listOf(formula.lines.last()) + val lastFormulaSymbol = formula.text.last().text.last() + + if (filteredText.isEmpty()) return@rule emptyList() + + val (firstAfterFormula, secondAfterFormula) = filteredText.first() to filteredText.getOrNull(1) + if (nextFormula != null) { + if (firstAfterFormula == nextFormula.text.first()) { + return@rule if (lastFormulaSymbol != PunctuationMark.COMMA.value) violationLines else emptyList() + } + + if (firstAfterFormula.text.isPunctuationMark() && secondAfterFormula == nextFormula.text.first()) { + return@rule if (firstAfterFormula.text.single() != PunctuationMark.COMMA.value) violationLines + else emptyList() + } + } + + val indicator = """где""".toRegex() + if (indicator.matches(firstAfterFormula.text)) { + return@rule if (lastFormulaSymbol != PunctuationMark.COMMA.value) violationLines else emptyList() + } + + if (firstAfterFormula.text.isPunctuationMark() && + secondAfterFormula != null && indicator.matches(secondAfterFormula.text) + ) { + return@rule if (firstAfterFormula.text.single() != PunctuationMark.COMMA.value) violationLines + else emptyList() + } + + return@rule emptyList() + } .getRule() val RULES_FORMULA_PUNCTUATION = listOf(fullStopAfterFormulaRule, commaAfterFormulaRule) From 7097fba8895a094fc370ad2ee567a1feaf00fbce Mon Sep 17 00:00:00 2001 From: Pavel Saltykov Date: Sat, 14 May 2022 21:00:52 +0300 Subject: [PATCH 09/10] Update formula filter, correct processing consecutive formulas --- .../checker/rule/formula/FormulaRule.kt | 4 +++- .../darderion/mundaneassignmentpolice/rules/Rules.kt | 11 ++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt index 27858945..8f5c5314 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt @@ -48,6 +48,8 @@ abstract class FormulaRule( private fun filterFormulas(formulas: List) = formulas.filterNot { - it.text.size == 1 && it.text[0].text == "∗" // remove single special characters + it.text.size == 1 && it.text.first().text == "∗" // remove single special characters + }.filterNot { + it.text.size == 1 && it.text.first().text.toDoubleOrNull() != null // remove numbers } } diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt index e9ec7f5d..b9729683 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt @@ -320,12 +320,17 @@ val commaAfterFormulaRule = FormulaPunctuationRuleBuilder() val (firstAfterFormula, secondAfterFormula) = filteredText.first() to filteredText.getOrNull(1) if (nextFormula != null) { if (firstAfterFormula == nextFormula.text.first()) { - return@rule if (lastFormulaSymbol != PunctuationMark.COMMA.value) violationLines else emptyList() + return@rule if (lastFormulaSymbol == PunctuationMark.COMMA.value || + lastFormulaSymbol == PunctuationMark.FULL_STOP.value + ) emptyList() + else violationLines } if (firstAfterFormula.text.isPunctuationMark() && secondAfterFormula == nextFormula.text.first()) { - return@rule if (firstAfterFormula.text.single() != PunctuationMark.COMMA.value) violationLines - else emptyList() + return@rule if (firstAfterFormula.text.single() == PunctuationMark.COMMA.value || + firstAfterFormula.text.single() == PunctuationMark.FULL_STOP.value + ) emptyList() + else violationLines } } From 44a1beee4d2a19159a22622968baba6859bbda01 Mon Sep 17 00:00:00 2001 From: Pavel Saltykov Date: Sat, 14 May 2022 23:52:31 +0300 Subject: [PATCH 10/10] Update tests, add some comments --- .../checker/rule/formula/FormulaRule.kt | 1 + .../mundaneassignmentpolice/rules/Rules.kt | 6 +++++- .../checker/RulesTests.kt | 2 +- .../FormulaRuleTestsFormulaPunctuation.pdf | Bin 36503 -> 37028 bytes 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt index 8f5c5314..04a4133b 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/checker/rule/formula/FormulaRule.kt @@ -31,6 +31,7 @@ abstract class FormulaRule( text.forEach { line -> line.text.forEach { word -> if (word.font.type == PostScriptFontType.TYPE2 || word.text == " " && formulaText.isNotEmpty()) { + // also captures some records in tables and code listings formulaText.add(word) formulaLines.add(line) } else if (formulaText.isNotEmpty()) { diff --git a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt index b9729683..b4236f2b 100644 --- a/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt +++ b/src/main/kotlin/com/github/darderion/mundaneassignmentpolice/rules/Rules.kt @@ -284,6 +284,7 @@ val fullStopAfterFormulaRule = FormulaPunctuationRuleBuilder() return@rule if (lastFormulaSymbol != PunctuationMark.FULL_STOP.value) violationLines else emptyList() } + // full stop is not required if there is another formula after the formula val (firstAfterFormula, secondAfterFormula) = filteredText.first() to filteredText.getOrNull(1) if (nextFormula != null && (firstAfterFormula == nextFormula.text.first() || @@ -292,11 +293,12 @@ val fullStopAfterFormulaRule = FormulaPunctuationRuleBuilder() return@rule emptyList() } - val indicator = """[A-ZА-Я].*?""".toRegex() + val indicator = """[A-ZА-Я].*?""".toRegex() // capitalized word that indicates the beginning of a new sentence if (indicator.matches(firstAfterFormula.text)) { return@rule if (lastFormulaSymbol != PunctuationMark.FULL_STOP.value) violationLines else emptyList() } + // case when a punctuation mark is after the formula and not the last symbol of the formula if (firstAfterFormula.text.isPunctuationMark() && secondAfterFormula != null && indicator.matches(secondAfterFormula.text) ) { @@ -317,6 +319,7 @@ val commaAfterFormulaRule = FormulaPunctuationRuleBuilder() if (filteredText.isEmpty()) return@rule emptyList() + // comma is required if there is another formula after the formula val (firstAfterFormula, secondAfterFormula) = filteredText.first() to filteredText.getOrNull(1) if (nextFormula != null) { if (firstAfterFormula == nextFormula.text.first()) { @@ -339,6 +342,7 @@ val commaAfterFormulaRule = FormulaPunctuationRuleBuilder() return@rule if (lastFormulaSymbol != PunctuationMark.COMMA.value) violationLines else emptyList() } + // case when a punctuation mark is after the formula and not the last symbol of the formula if (firstAfterFormula.text.isPunctuationMark() && secondAfterFormula != null && indicator.matches(secondAfterFormula.text) ) { diff --git a/src/test/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RulesTests.kt b/src/test/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RulesTests.kt index f422ae87..31902758 100644 --- a/src/test/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RulesTests.kt +++ b/src/test/kotlin/com/github/darderion/mundaneassignmentpolice/checker/RulesTests.kt @@ -53,7 +53,7 @@ class RulesTests : StringSpec({ } "Formula punctuation rules should detect the absence of a full stop or a comma after a formula"{ fullStopAfterFormulaRule.process(PDFBox().getPDF(filePathFormulaPunctuation)).count() shouldBeExactly 3 - commaAfterFormulaRule.process(PDFBox().getPDF(filePathFormulaPunctuation)).count() shouldBeExactly 3 + commaAfterFormulaRule.process(PDFBox().getPDF(filePathFormulaPunctuation)).count() shouldBeExactly 4 } }) { companion object { diff --git a/src/test/resources/com/github/darderion/mundaneassignmentpolice/checker/FormulaRuleTestsFormulaPunctuation.pdf b/src/test/resources/com/github/darderion/mundaneassignmentpolice/checker/FormulaRuleTestsFormulaPunctuation.pdf index 5a48aca54c37a5d7142f80ce39eb9c6ca389d5a4..e70c6c0ed2914c115ffef4a985d3f715898ba308 100644 GIT binary patch delta 17306 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 E02sOYL;wH) delta 16803 zcmZ6xQ*fqF)b2g8F~P*PZQHhO+qq-=$;37%w(U%8XC}5M-2ZpicksQtkJdp~uj=k= zRd@ZaRZ<9^o(rDDz{1JF%tp*4O{_=E!pX%(%*w$+Y(UJUM9juT%p^g~!o|YQO3b84 z%tOqiPRz#1%*m5%m<`4PN`}V*F(mSUWWf-@*;ql^VB`QN2QibftA*$POygoE{(lFP zj5x6#zlkNYIj1EL8#AZ5xdj^s4-YpBCy(WSSR;SU4@2 zO}JQCEG;-qS=dcjS$H^DxlIHOhy?}V-P~O*OdR07bIeT5_pFRfjk#Ij$hEJ*;YR6* z54fc15u};Nz~IV$3yb!ACr^e*Q8qI+<#$6LHZ{hlkc6W}|8^Y+_p6B>JQ4zQ0fh<9 z$^(LcW&mRbu|PY6v$KJUp|t?^|DDI!_`fX1=jO(i#>P3n`^UaP8FSL1vNG4x8=Dw= zUk!N?Pr1aO!1X{8BJK6OpCvZxM?sQw(2_U}HCU~%9O(oQ?T-@1OXR@{Hc~)AP2mAb*sD3a zMzuaFj1^W&VB{}L6lWuug&`KeHy~FfoRe2pCQ%bH2$JWh%nrFFM-YwByV+$tRl^&x zkvbhCCTfOE{>geHjDSj`5)vw!1j!&AV=XDA$!rjTj}Zz<4MsDE_9s*`eKf|-%tCl} z{M$MYf9XbKPzW2VLHwv;mh)c`;WW6!$n@ONYEIZbaJ24A08_)l`))_rLH9VdlfxAY9 zZQNjNDJH+ySHE(~im4y-1?N1#+~YuDywLUg8F!D7m*^y{nw zZ)X9=p*$XCnE3{)3FKnJ#9qeJ%LsbLZ5GaK%`swr4?8d|P1)YBS6C#`U@7YIXFiED zZC^{f*%}{x*0;G|Jjt4Zc_P=cg0xf=6e9DNp(#bCI9W?xchZisCuJH}SS46z3Im4! zJOfSS8@!rqVQzn_X9CY`>dzdWfmAUHs!aGTw(^W4LuU6%iVe328%%b$AN+2qp$c;D zdyBSzAa(#^@bZn@etsD-x|=wzD1nW8a8_YILe+cg-^FOecxPjL(y*?mBK zj_D6vh>8f%E@+=9DZ^pd$1#i zK*WG{2T#MrccQNj)87-9uk(r(ELl*V-?5jwa{#E?&Hn@kg)ov{1!Hh2RcQY zEzr0p%9u?&X>MZv-;jOYZeCm+Wn2~T?#BxW#=A?y7g;iUcRp_~7+}ozI}()mkFl_$ zt82nZB)Iqad0-Ao$9rCYg^OT2`DB8ev>nRdU51?kOK!R+)|yj1caKQXk#h0A>yx+~ zj+hgj=5({6V7-7ip$b3e_`agB*o-|J5DBGf`#67R?m=02Gq0@Q?fNKvNz4cG1D?-J zX+A?IW^U-7zU9A3`k~^jiQyFp4dZVWPY-C#?E8qj4|`hv3)=c3$}IV%bN%qmb_&uE zYbD^s44Ros%{Lho-z#Mm8wfvS>0@YhsId!Hn3y)j9omBOAu*mOIcggz%LC6w%^byG z$epB?Y*`@tVp#jp--@Qg${`+`S=2uL7FWc1W$MWnJwesL9@dTnB~}WQ;y9TF>e(xq z*hqH8kq#~_#4Z2N^5ymHWDd~OrG+@PjbmhVFdq)zN<}n>hOIS1o$;KMce@4?}>kr_*?N3^iixx7NB#Ug?a@f?_ehE!y*0%)De%{kk6$haHW#(RAD8wZR{?M-j#?PSH436lFzFTu zU!U89k>67DzP8@(+1?L_HDg}=^NB`!8u%Sv@_w{5HrB<}(z+Yxv?&R#T`?u)1a+i1 z){EyTgdUvV-mtG!$5nQZf4*1$1jY#@AcZrvZ1nQ&RcK3jh;v?dUpvp~ zatBqyxT(GM%aTynRmOD67+B`vvQ_dL%S^P^4a)tKvLamr&Be9ul;_481opXek(f0F zDBYB6@Msj=nTf*(vUOn9VYA#TMkLUeMiABK#xuJ0#E7m?^e3TJA+;TCG+UdR+j<%` z;!=qf0Kk>Id+xtwVob9A1I}Q~Fc489Rw6IVj?U+x$`pI){d_*@>}~L;D9O5VhVM;u z>yTo)c2?TTq?3k6HjDkFqCLXUi*zKH9P*_j|8V8T;;K3^VJ(D~=p94SmLosAuIOFY z?Tz*J+B~!j$Jh^)I;1KH)wNpsTXBS%dOVaGQlN&GguqavpmrL(;ds`kD52KD{6f@d zo3NHawHjYN`oz9=7HT?acKs~emn*NMWu2GcN`*&aYNV^)jW>rJ)wvk7axL z_$dtzUG9Y=O=CUHVEwy8vcyvET=Y3~kF>5Wjw%;~ZT!?yRzqJ$p%yV9#2wsvzB)fY2yS(^~{iN6V&(7oOI8rA;>^ zst~`OGZ}7Ddy(Pcf?GWw>`GVOm=tRG%rQC8%vV@HaSHGC!-h$uMB>aM-Kn{LE@L)E zQOuT4CIPv}PP^V)0!DO2(Gi*ISsU)88CZ)i-;337;IIER7xd%M*b34zo=)dBGh}CRLvA(gXgSTM@s9u7S`gcfqJ{@iI!}Ctn5>uK>IJh=?{%tDKxVdyT?h)vev^j?(uF86;FR&y_;)1pi}5dv z3`~PRdVyWsS}TM?S>dMfF&r(iUWYnY&qQ;d#O1b?oLzq=GEhBbC8uW}m3ksS)0dM3x*Y_w5jJc zmpa^Pxg#^K8^Oz<-^^V5>MiJs#A2&p0sR{_vD_Udc-&8{qd@?YIP; zOjGP8BZX6^`w~)CRI26B55anO)ugB6#a{2uid0A9Qa{>La9Lh8St*`VSc+Wz67#5XOPjB4{i+jdt3`hI%ZRMvm(GMlP zfZ>OcEb=S$&t4&H|LXO2VPKB=uDv=eN^^==QX{Z<>es2wUy?f_gKS5inFiluX`nYj zijA4hP`_vLI~f&`;Kgpwq||CYnuCi#9I4_Y!R|wy)$|X%3m6y>6EjFagze6-8>Jdz zdkFCs8Zj9^(b2WjN}pYUqM2LAkLJdFrw<1yFR_j#rl9TKC-99e7my`>6o!~$V{N)M zk037xT!Q|_XnkIenb`WkYw#?@wBn50^(6kL$mGtfE~Z=b9wQw)bG`JfIHPD5LAD}x z>||2l^4iaGr`Q_Y&84z2zL4xQS8q`**@WJr2~xlW6m)|IWJ#-G58HR5BT*el?KYQ5idQ_*837k8P4Uqu+tBTFF-V85r^<5q@?X<%5;oPP;i!HchxJe+?o!#q&#+(dFwB43HY7{GnP9_b%+Cz9T_UwDFM|jFsF*$`l&-{NAXNpD$B5KU=7g ztqEo$rgCas()HEL>TQjLV~Ae!rvn}AJFNqiD{6FQ&u7EGsyxoa?O=|Ep@A!;2Eg-M zE_kS(S^ErpTnj0=>52m39{b*(iEVPHMRP-EH9Wtim6v4R`sdBlcxc2AQH-9;PM1-c z!~*PH#aQ~=gqt162MS$OhFiHxUr`3TF-5#A(dio zVPbwz#63%@3zjLi)QDVO!YMh4x$vofSRoNdT`aF*dgMAp7_&8So)KaP_2qU$RV?}hb19O z6V~k+j%qD2_z*IFoB!tQ1>qt}0~Ob*{fTEB+rgatC`X(kGm^ckYIP#ZX%oRK2gb{! zYBb3HY{FGq&#LZd>?h^UYBOJq%Y-~0u)*~AcEKq>5F!N*%lbbBg9c@!!Pz-LC6WwH zJhJ!@80=jCuV%Se|3|Y;VG6wvfISYGi?JCDI`#xOp*x+zl&^<91qXerOODuQU<01- znh3Z+57jA6wf9UsauPw$PX_3d78OD&KELR>{Q{&yC~Afw%~E=)eQ8aCus1X^(WfDy z^TpdrUVnX?Pk$b+2TY77uSp*l*b&Y9PJ^-_GOiw|QCMM5T`I)^rg}duqt>B-;oct6 zx|wfXa?6w@xw&s6-xY##QY~<$uDN8Yon1wXuq=fK@46vZ*eOJcKOZ?IR;RLW!g{)E zr;7^MUAWk*iuxi2c>MaV6qKYB4qP2>ZmFyc)JtC=>fs!uK*XvRFbK~Nu5j>kO>wHR zV313V8K)pTbp>#CE|8W2LsPoCF694|uB|%be^pEkaIpSw0NSH2;It%+dif7iZ3i=N zdMt37^Z-0syePIEnfj_dECQo)@0hL2kc#sTE)_givx)1?a^PcFL$h9O#>EqgPg?c^)t~g`2^bDnhon$PshO!wlIH4CZ&X)g?PO*~D%oEp z{B8egadrED=CF+X`J+qLazK}S)H(fpcDr;Ku>DMFvK*#T!?pggp%euUC6}`FO#G$Uo;Cgk-X4&Y_}#qP8jNN^1^;p1fxdTIF{Q*7y;ZWZ%0VnV!Hg$>K7 zB@dr!foBG%Sj9EoB&Bpx*|f>6(~1Kj)LCQk44S8J6i+Y4(mL6o1*1kqfYX%-`34fZ z3HKfH=^p~3m~;Z|%?VsOOZo9M$_u2|;ufRdYNzP@a>BTyf&;Lz05JN>LM4zVXFIiLU~y6`_nT+wYI|eY<%YK` z=lM6<=0%p_cAB1%Ybq+UNKgk^cR zOBveQqVdfJ;{uN6eCPx4HYAz zZB-8_%9Z)_n-?G!<}hUx!G?8ETYCLRi_)vdC`(K;5&yB392>{&;ST&a+h&g=Y{7mH zrgadblp|yur*Z6!0;@5`923T$rnmm*d>txk{XgWC~XJ z!GUj9vaX)bb$vfHHo!W;7X#wrm&762OwkgPA~oVL`R{-(@{3{eF`X7f4?1Xa9sJ{@ zpB9SBH-OWd^8DjP`7*hm;Knp}+40t$-QJ*UmUrtYE0R1xd9d2l8`kd?Z9z!EmrOk` zuI*Fz^k{4b-OZ1l3`3mfXq7z!CyJldcgHB)@P3}4XH$Q|BTkq}vq{KYjEZJsqK9o= zhSG(FytE&$Ns$jfcnv|xW=y~z6nNQKl*{!DBkh;lvY9Cm!C1#i9~o1YX1&w*3*8ih z^nO)vP6cU!4Z3$O8WRGMq>xn~io80Vm0d%<6)*4af^sBbe8r3!qIZ&tCCJ$j@sMRH5h>Ov+09r0@hr` z9s#N`Yj#UHj1BvrWltYSI;O??ROso_EvoS%x@0@Ws%OJ*K(!z<$> z>@wJQfcnq9&j7+W3{@`s62~>e&Zad4S9>zs9wH=G;4odKIj5=BT1#A{OlHPdsas|G zjYqfcpXQ*q5}7()T_0@apN{qk5$jA1%)C6VPL>bg1$+cP$hiCfLpi_Wl84;VT3sGB z#Bl9iO2?zV<en-W+xy7dyN|MH7o@>Dd5@+om#W*U!iqlY0MyPpS~m3w9` zZhb#Zs>|UXza%%DQ$J4P)RC8}g>(CYx%fU1R^+Sm4u*8BTP*Ob%h&1j#^*oKVRQ|i zWtosRuUvSbX+6mflc0_dMmXEA!8%Q9hckFB5Xbtbg`yj1CpVT`dnBcVov(+Oes~%_ zX_|c-sM|Pb=nae#siE0YKWwBK2#GUjrDpT@^jBT*O09yF>1i0HZ(2QTXMa#Bnm|4p zJ+_NX8(|v#`ADT9(#qMq$@(RB^rO`%5)dPe4Pnm zMRvCFP2XKtgc(>px=%D7cy1sOP8UUEKzg;ztreZEm211w3iL(IDF722v-Wewo_$X& zc3@tTvy@j`!oIoFBMe%)B8_6^LNWhYnvaVe6=`kIm{qZ^bvhV`=g;Ln0 zEF~sZygRrxbWW0P|5eq%`Uk|QZpe1iG@=LKib%XKDipPu0#{A zjYX!enX2}Vvi8b=qp!0~fCf&;_fUv`ONkT2Wvv7ZF(dZ8X`QUGNajF}!jmYYu@0Va zX8q_aHlxclX^02mzg{0F)o{98hTJwaGFj`RmT`=^H1>YUe!K@`xrDtCPjueSB=-&0DxQb#+{@Lgf}8{bz~V$l!E-#mU6oVmqgd z0Z9z-bOhgrS z&>U7|BO4<7e*RZw9_2k-N7bAuL8cM``I>BpFJ~33YtPF5;z6macH$$8KUThz#q2lJ z6Y@6FL3PX3gRvR&$SxD+PA=z$w}y8gm3JVR6e!%r5HONgv%((jtE;`Vs)co&N#jvU zj3%{*Oh7f);0&c0qNdP|5^2SzCA1;9wAp}Q*j@`#uny3=VIl>W_@UjDxzAbbqJqcX zPC@p$+3fhe$Z*EK-N&R?O^|8DwJCf1J>@w7bR3WeLlzHh5|{2s$F!-TcUv}MtEZJ= zh{30G4RlkjcS!35r>-_Ms+Kz2{>7Y;4V$~KVgJ%jI~tm1Uo9zM)Jb5+1|1n|qsH7p z70!|R==RJ?Mou5I|G7J#K1$*5RYslm&y;)M#k+9au}vo^nqn-EpzPJPb&heR%U3%~ zviKbl0^YZq@8tFGj>74XufwPNKWoZ6`vkp*0XQ)HG?-wx*YHdqXY!skDv)sX$+DQA z(ktyLT8&55JT?}2^qf2}Es@f(=JADdkuDd-wwd!g;Wtd&yixmE5XPD+zrx3Agg)X1 zvVFxn=M+T|4+*vVsw}fCcog>f6y{PDeH=>^f%F~U|0T`!%#y+2@gHzzMX|@h$TOEa z0Ny<;GuAg?N}iSf)Sn9J;7Xqcijhf5yU$JA%C@S7`g9UVduhD{!HJ?0B7Wuu#AgOC zLu~#N+#H8p^l}%)7X8y-=*+Z=!q%ss)pci|N?HJ&QrO%g4yu#5?SK@>Ql{g0&?1}m z6tI@R$ZW~^6-uCdwey^&zHR~)u@_I44Ny(?ZMsikMUFZ15N4C2Ka;vKOK1yp*}Z>j zMTVkw{Q8dOjAyxbRe2uD^~>Lbus58>r@eg_x>OlV&sfb56OLKKq95$^3stQ*(LmY) zC7_ryrETDG+nziZ+s%A}`81KzxheN-49rnNnYc_7@^8*RrLXK2Eb@B7i zs?e{>aY-?GMq026tjz8A&Y2*F(At#O2KKW-SmnX|ap_nY#sLPI4tSe0x`vN5+Ud!0 z%2)I=0ZJ)Z-(7E4*ocTwft%#h5i$oox@Kr-ll~HZQ;jcR!Wu_~9zvZr&|Q4$ zBA_!P&{yUmV}LnevOaauPPQmw0^q$*e(o9ZCr>f^A(HKIwQAB?GH*aYd%K6T3}>jNGBVXLCk1U zQ&7CW{K$Dqq5f!Spf26*8nrq$=eE1Gu`H&RLQSx>9<&9aAe8Amp>c3}DkF3dHH`e( zI-nw?qC_f79Uc3qX)vMd6{4PZXlo1f?raEH zH-*6~=w)5NVCR;I7Mo6!0K3+MgucqVasP7;nzwXZyuwd^YwcP33*zs%H%H{c2&Eig zr$lUU5@@$w(Gtux$hIOgP#Zh_E_ZebtIPJ|1h+f0az*y*)jF3+{z%Q#UGl=@wByiY zK!J_-a*go3U@9_AKE0s&9Pspaa90k;{ipvk^t(oKAF%HvKRzWS?Dy;Va;WETV-9`iF(~YYRW0e`=?}?ThKYpvIsqgHVI##hi}U;AET9f2UtRpAKP-g^41Lxwu%0 zNQyYFScI!)=2G+Ns?xql^&KH+L3Frbdl4R!Lg9b!&&5RZ;~5kp6(Nr>xtJjPLKqoVY8K{Y zB1#}1S^^2et}&v>!2-6GxzuQNTwszKL^$Le48Mv`YiSHm4}%WRjE`>W_M-7FaE$5( zQXpiU4P|nGaURIgHhnw~7zuhY7R*UJU>d2l0#Z6nYOT!tgj)&>!l7<_$1KONBDsP^ zW-XmjJx|V@mo{B%{!s+QCD62C@J|Pwm)d2zAPef5PegrE^$VlmI=OE|0k##@E=vXY zfzk_(;j_G(Js~OoIZT7EkH@Oo*%m|Hb(TA|FX`VL;>R~j=2q5MmJ^2cUxk#O23S9R zLmf8YjP;Ng$hY_GiLJ-2E6?mtZO_2zZ1Kb9HOiJ1h3%Tkw&{?8z8!iL!OA{#0h@0 zwBXU1S)Rb6*NMg`t$GiPI1TL~>pw$|NdJ`x0ynZlR+~)NGd3hKD@xS6D9kM;z1jynTWuQX5Z?5U2`5(TS?{xdz zd&?}Z)TSV(Fjf0RB12b(jp#6?kv(wBN-Q9T{bAJHKSF0CuVkF|uZJV?`Qpj%J3l^i zRXCmcSShI^c3#72TP8Mda6EU+B~YxzW&f{)rGI7bU0lGIa~1me->sEnT9@LQ?z`7@ z2D!(w^l=xu3d`s~ceu9=xAd$Hq&g^XIpZcb{}ntEU1u7c&UT zix#L1s}&tNDY8EssO4!(u$l=ks+_o>pktR{Ta|^=jAB=bB9}}RhcmR+2}#A0^fd{) z6*~8xFXIyO-XY?@79);^{0{kD&9|_<{cMRPv_W4=3Yp3_dw;=!?5xg}%F0w2 z5vlwn?lF;EG*snVXg0}927Ze1~-Yh(V=d8;r+P}_cPkZqgg_Tt>WbtI@+<%56!kGXu82ah zn6C*He3ZfdIYYx2H21{W%h0tMKm)Ar1n=)9z^I(@SPFI@JViy`Ac3L-{S+i!5|XP^ z`}qua2OE9ebMxua3iP`8H7+E$9djY~LEEH9Ua*WpiRFJj-`CUFZ^y$k4^R0Cv%igJ z*F^GWkNT*sDfRMgq{HwFgAZ{rIwu5~*g3dfiP_7!a}ar(N`g>SyI>uHxj^lz@a)Kz zY_T$pzJk3I$|j|1$IO()Z|CIHpB|f?RZgw$9(mAK-T^1y72%B?g1qr~8If^rVr;!J zx_2zCmoMxmb+ZTl;)a_b^^vGoMm)*KS{|p@So}t`G0hd+VGhBAxq34fohjHNYC*j` z*P{N>9fR$WNOPIvgmY;9$(f$x)5elH!OG^e z!*|@nwZzk-zWw*D?fQ$c*4X#S90NYw_v&Do?xQ#aV)S=3>|@hzrUjn8A0fVY_V^lk z_)KY}L)60265w|8Y}FNkV!8YAiMM=@SbVbi`!kDsPo`-|`PC+=dWiWgUORFhv0T|1 zSrNjEu)@Fd#J&&K=!@&)eW(rEX}MD|gLS~eQQW%*SkeT9u#9S_E}Z+Qf!e7oIL0ae zK{8VV@FLPdea(MeU6A5oPVsqrjjW{8yA;goT@=(CzzEgku7%ognlW#a-&)QbHjw_4 zWTV{7py*xfBcKS-_q?5iTjET2Jk-D7);}6^dPI3HPSg?)`xo~0NFw(2%90S*&&`56 znu#;cKZa-;!9teFGA({2VTsI^o;fyc^@AB9vn^v}8qJcXDNbj$#$1!7I+G)guc>#e zhj8}+@UBp&{yCW^tGO<>E_GUSS|9%H@|u%ax<)DG*f#@Au6Yfg`_9p(KsPr(1v4`};o7>dGG!DM2iulX9mZiA zfEn(MTBwD?E@u71Sy#|6=$e)|1!mRO7m?t>7qOOiM^xWlt6g=CfQB-ZP}Orrn+4G{ zIMHl52*Tdw$Cx611cLHIJ?t}Yu0vBHZZW@bX{?-1vzeCb2EFvRyf|})cq-iuTc3Tf z#x_RnJp{rIEJ0E+>D}8{BYn&5!1}UE;2{H+%KypfcMb1al2gRyTraBS0Qgt<{qe0i z*{uo%rOG##Zr%>4#8nf^;$qA(P%m|1)bGXT+d(lp*(|I+-56Yb@4wk!sQd0O4y@f- zSiPOvuBGUX_p6q9<$6zomWb98d^Nmeylk2Hs2v3>nq7rH3;V*Du}!ola_ud5}V0P@OU={ zNMr@94(K$EcUk|A^Td(@!s?tE=^UX5Y0vsTCU!i}Hc`ic?|7q*PC2%uon?=t`8 z7|!9d zs66U$?~H3<(RSDhsFK!0iGNNmSSdd0OiSzMC92yac1mV{exDKGa8ztv>uuvU?7wYY zQhU1V3^KBfLQ!~~w;nF77z`Z8yiRG0j!puPaOfI$E4fbj&P-5-um6@HWwoI3g1Ey; za)0%9-2+g4L9SH;iNQ>kX=no&Oy^TULgaWTGJ^t$`^dt1n)3g(Q+i;19kDJSm^ooE zl!V)nyK^0oRrWz3r0LWz74;``V^Hbd_{28OcMLpy)1}dx>pY>1718#Ldt?3W*J?gM zMtE_de$8^gOD}=-!ziK==S`Yv2fQ$2<&hx>1`5I;9~GJ!u;>B%vAMl+HS)7%cBZVZ zFxSvXtSLZa#S=>h;kX(P3Gb8x*I%I)|^Ey4Sz&!}N zt~T1^n1VTFprmeL0y=8H(9V5TSucs!CsCwKUAlQG<(C`%FgD`%PU~Dk(x61sW$w^d z`ZgTC&HzqqJZFEd&N>>fX5~fPi!zps*n1vX)Az_V1i!e~RC!RRVJnk&Iy3!Qy;vv# zchnFIv)dJahRCSw8K%t4x0)SpY8Ury#XD)So~SRqc)|Jea7C6_0Uz$G3U^X zmy{?M2&+5Ght0a@*`xAMi8=Sy`omk!R^{B2V>R)=g^q|mW0 zK{K3Fgb0D=T`8em;pO5lsSbCxbJ%q&&QAB1d%A`oyD+?go&nJZlL2u-``zk4Hm%ZbH~gY z1kPQ;!Z|4=XN$tYaamb}VKQWBO#XWu5b4gsn74eI3(=c~&&r-QxG3kh#VhKNC1kuY zKL5jM#>d_d#k|O$7l)GEl+=sDeR1nz*pUIHlT^H$mxGIP+wEoWUO2=-F(U=BS(Zo{ zhoTZqX6fQ-+PnuLr+RDP?$Mp_tAvYnuxZsn=sk@%{BG_S5&N=hqzpl|QAC>6=NAgO$6Sue-Vyn5cy`SO*_eIp)~gN9jz5s-Ts9L7c0mk&V%}4<7ZcQ*NMy-7!LX@`tzc*Cm)= zd*s#3khqq17o)4$R!=k~aQ4y(Ow9B!f9iOmkPh`ww2?s-q!RTW{t8h+!n)-%kX+ z#64(q%mzp-vj@=@#3w>ZNB)p+)liXf^htDv*}>Uv%TT~3{A5k-P_aTZQPfTyly=g1#tg!&9^OGlMrHUHdbDsYRvUzwJO5;@{++1mQ3gH>tb!QR=A*T-pW{;LV%&In}ZR!-D=qC!RVLcsPe99Gh zY%ucwvr}-PfC?STJF-6qoVfbCix7`gvtcWaaD4bQL zT{Mw^tRf;HF|Hr@NytuyM7ElUTY8wkK%3IYGy*W@x*DVBPIK+*K!uo;2(_&HsZ^_#W7l15WJbg5JQYB{5}2N z%defP9EVK4sX}~KuVak6M3>)YjmPQFC((Z!XziRVuXOeRhAqv23=-Vdphy}?lT4?cwc~(WOs?AwBT!)gX)^bwLNVH<6b_yIeKRj7Bh0&eDRL6>{OvS zcMCBPV;wCnnf&eh*!5yR)M9yUpIzW_=v`B@J(DDR&w4$z8#H~`KOB(%5AWGbq9)uI z3+D2L=fjy}HKxVIy}r!B&o(s=<57(k?#YXnD9vm93yuia!(v`5|HzT($5kciIl?*K zh8)@mF`w0`J^CdlmO9!ex*L{P<)%sS=cY@b;}tu4Yp?kg(aQxd*3kkt=Q9JH(16zU z`612&(9j)wJ&&hA^L-MhQe>QDUl_85p)YT9ObZoLf|jE&Qq57%mr;d`~_;Jv}k|*9Did!u9^NCGs^TmgOWpmJedx zx8s+etle{}LylEdgZdzzej&|yhPJh3`WZMeZg;=j6YE{@i2k;tK^5fbsm*DZH*37T zRw?BoUGx~~Rgz$n8a;E9p)XS?RxSqcO-{joPjL2bs^9~OQ;ehyJ8_a>>7Qj6I2)A< z9-^b+< z{Fn3KQ*Ba%s{FguRnPDQrj1Us#s!zxzlCN&iv4(ObUKAf!@&Us1}5HJ5Gpg4O=p=e6;TY%)zmwtiu9MEl7q$?P#N z@hb7y{cyCm8PPA&MQ_>ZF9-F_dqh3ps_cuv;n}F9Jxvd8T_*ybkjd@sZw~JFTpc#^ zv&)-07nMfGQ=ysLa^#av?DoWu79I=pTFl6-Tc(wnnQixF2HNwAimiU&13c6NjPWn- z;5oYDBH`npSDOoUXI!@!v@DxRfVcQoVv(x|t^GL6azl5sbLG_VykNZ+1m!xcBq0R% zcyP|NeHG&~h8O$7%7@Ar|6odV-M>0dJ)auMS0TRi+0vb(bIhNb8<;w&HIuURi`wfK z=c8`V!9w)qb`*DhjGA>oKE)_>Fofg_wKvg7N;P626lt+!eJ zPjNLUKexEYnNLo553k31Q6B7j+iJt8)U4x6L#UQKw~f^CeqtH}$o7L4+dYd3P87Tb#r*DDD}Ubk)C4Xdc7|K$X7o;u;Dj7MVJ7!8 z$A&*FSF#z86g-Y7*V7#h$6JoqaIQ$LqQ(`Cz`D~gTfP25+TKuip*#%<{tLVsIlTD} z@b8(iN|7V-)?;Sg^@)O?cq)h18U5{L(8fp0I^h)6ousq`uwyHr+1`hZ_8&bVdP_5N z3XBeIrfhspX#<7u_ zA>L$X39!ZBLPe~7h5C+a?+y;ivjstgQua;VJM8f3{TW2aGr# zGb@2^O%b(YSD4dhHl@sOh&brbbd~YQSLV#wTuC>Zd19H8ZZ<`1E7&OS;8enNVu}eH SgW3}QZIgAF(kAy$5Cs5DgBf`M