From c713994b9322c965ceca26428dbeeee3e45b0160 Mon Sep 17 00:00:00 2001 From: fomichev Date: Fri, 17 Apr 2026 16:37:54 +0300 Subject: [PATCH] =?utf8?q?feat(ERP-292):=20RuleBasedParser=20=E2=80=94=20?= =?utf8?q?=D1=81=D0=BB=D0=BE=D0=B2=D0=B0=D1=80=D0=BD=D1=8B=D0=B9=20=D0=BF?= =?utf8?q?=D0=B0=D1=80=D1=81=D0=B8=D0=BD=D0=B3=20=D0=B8=D0=BC=D0=B5=D0=BD?= =?utf8?q?=D0=B8=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- erp24/services/automark/RuleBasedParser.php | 141 ++++++++++++++++++ .../services/automark/RuleBasedParserTest.php | 76 ++++++++++ 2 files changed, 217 insertions(+) create mode 100644 erp24/services/automark/RuleBasedParser.php create mode 100644 erp24/tests/unit/services/automark/RuleBasedParserTest.php diff --git a/erp24/services/automark/RuleBasedParser.php b/erp24/services/automark/RuleBasedParser.php new file mode 100644 index 00000000..2b4d08f2 --- /dev/null +++ b/erp24/services/automark/RuleBasedParser.php @@ -0,0 +1,141 @@ + 'Роза', 'хризантема' => 'Хризантема', 'гербера' => 'Гербера', + 'тюльпан' => 'Тюльпан', 'лилия' => 'Лилия', 'пион' => 'Пион', + 'эустома' => 'Эустома', 'альстромерия' => 'Альстромерия', + 'гипсофила' => 'Гипсофила', 'ирис' => 'Ирис', 'гвоздика' => 'Гвоздика', + 'нарцисс' => 'Нарцисс', 'фрезия' => 'Фрезия', 'лизиантус' => 'Лизиантус', + ]; + + private const SORT_KEYWORDS = [ + 'premium' => 'Premium', 'экстра' => 'Экстра', 'extra' => 'Extra', + 'select' => 'Select', 'стандарт' => 'Стандарт', 'премиум' => 'Премиум', + 'супер' => 'Супер', 'элит' => 'Элит', + ]; + + private const COLOR_KEYWORDS = [ + 'красная' => 'Красная', 'красный' => 'Красный', + 'белая' => 'Белая', 'белый' => 'Белый', + 'розовая' => 'Розовая', 'розовый' => 'Розовый', + 'желтая' => 'Желтая', 'желтый' => 'Желтый', + 'оранжевая' => 'Оранжевая', 'оранжевый' => 'Оранжевый', + 'фиолетовая' => 'Фиолетовая', 'фиолетовый' => 'Фиолетовый', + 'синяя' => 'Синяя', 'синий' => 'Синий', + 'кремовая' => 'Кремовая', 'кремовый' => 'Кремовый', + 'бордовая' => 'Бордовая', 'бордовый' => 'Бордовый', + 'микс' => 'Микс', 'коралловая' => 'Коралловая', + ]; + + private const POTTED_KEYWORDS = ['горшок', 'горшечн', 'кашпо', 'вазон']; + + private const PACKAGING_KEYWORDS = ['пакет', 'коробк', 'упаков', 'лента', 'бумага', 'сетка', 'сизаль']; + + public function parse(string $name): ?ParseResult + { + $nameLower = mb_strtolower($name); + + $category = $this->detectCategory($nameLower); + $species = $this->detectSpecies($nameLower); + $sort = $this->detectSort($nameLower); + $color = $this->detectColor($nameLower); + $size = $this->detectSize($name); + + if ($category === null && $species === null) { + return null; + } + + if ($species !== null && $category === null) { + $category = 'Срезы'; + } + + $confidence = $this->calcConfidence($category, $species, $sort, $color, $size); + + return new ParseResult( + category: $category, + subcategory: null, + species: $species, + sort: $sort, + type: null, + size: $size, + color: $color, + confidence: $confidence, + method: 'rule', + ); + } + + private function detectCategory(string $nameLower): ?string + { + foreach (self::POTTED_KEYWORDS as $kw) { + if (str_contains($nameLower, $kw)) { + return 'Горшечные'; + } + } + foreach (self::PACKAGING_KEYWORDS as $kw) { + if (str_contains($nameLower, $kw)) { + return 'Упаковка'; + } + } + return null; + } + + private function detectSpecies(string $nameLower): ?string + { + foreach (self::SPECIES_SREZY as $keyword => $label) { + if (str_contains($nameLower, $keyword)) { + return $label; + } + } + return null; + } + + private function detectSort(string $nameLower): ?string + { + foreach (self::SORT_KEYWORDS as $keyword => $label) { + if (str_contains($nameLower, $keyword)) { + return $label; + } + } + return null; + } + + private function detectColor(string $nameLower): ?string + { + foreach (self::COLOR_KEYWORDS as $keyword => $label) { + if (str_contains($nameLower, $keyword)) { + return $label; + } + } + return null; + } + + private function detectSize(string $name): ?int + { + if (preg_match('/(\d+)\s*(?:см|cm|СМ|CM)/iu', $name, $m)) { + return (int) $m[1]; + } + return null; + } + + private function calcConfidence( + ?string $category, + ?string $species, + ?string $sort, + ?string $color, + ?int $size + ): float { + $score = 0.5; + if ($category !== null) $score += 0.2; + if ($species !== null) $score += 0.15; + if ($color !== null) $score += 0.1; + if ($size !== null) $score += 0.05; + if ($sort !== null) $score += 0.05; + return min(1.0, $score); + } +} diff --git a/erp24/tests/unit/services/automark/RuleBasedParserTest.php b/erp24/tests/unit/services/automark/RuleBasedParserTest.php new file mode 100644 index 00000000..a4ca3465 --- /dev/null +++ b/erp24/tests/unit/services/automark/RuleBasedParserTest.php @@ -0,0 +1,76 @@ +parser = new RuleBasedParser(); + } + + public function testParsesRoseWithSizeAndColor(): void + { + $result = $this->parser->parse('Роза Premium 50см Красная'); + + $this->assertInstanceOf(ParseResult::class, $result); + $this->assertSame('Срезы', $result->category); + $this->assertSame('Роза', $result->species); + $this->assertSame('Premium', $result->sort); + $this->assertSame(50, $result->size); + $this->assertSame('Красная', $result->color); + $this->assertGreaterThanOrEqual(0.9, $result->confidence); + $this->assertSame('rule', $result->method); + } + + public function testParsesPottedPlant(): void + { + $result = $this->parser->parse('Орхидея Фаленопсис горшок 12'); + + $this->assertSame('Горшечные', $result->category); + $this->assertGreaterThanOrEqual(0.7, $result->confidence); + } + + public function testParsesPackaging(): void + { + $result = $this->parser->parse('Пакет целлофановый 60х80'); + + $this->assertSame('Упаковка', $result->category); + $this->assertGreaterThanOrEqual(0.7, $result->confidence); + } + + public function testExtractsSizeInCm(): void + { + $result = $this->parser->parse('Хризантема Экстра 70 см Белая'); + + $this->assertSame(70, $result->size); + $this->assertSame('Белая', $result->color); + $this->assertSame('Хризантема', $result->species); + } + + public function testReturnsNullForUnknownName(): void + { + $result = $this->parser->parse('XYZ-неизвестный-товар-123'); + + $this->assertNull($result); + } + + public function testChrysanthemumIsSrezy(): void + { + $result = $this->parser->parse('Хризантема Белая 60см'); + + $this->assertSame('Срезы', $result->category); + $this->assertSame('Хризантема', $result->species); + } +} -- 2.39.5