From 3cceb4bdd75b2b11f7d0f423e13522528d9162cd Mon Sep 17 00:00:00 2001 From: Florian Brinker Date: Tue, 20 Apr 2021 00:19:48 +0200 Subject: [PATCH] First version --- .gitignore | 6 + composer.json | 14 + composer.lock | 1390 +++++++++++++++++++++++++++++++++++ config.sample.php | 8 + src/CookidooScraper.php | 97 +++ src/Models/Category.php | 21 + src/Models/Ingredient.php | 106 +++ src/Models/ShoppingList.php | 120 +++ src/Normalizer.php | 13 + src/RuleApplicator.php | 17 + src/Rules/AbstractRule.php | 8 + src/Rules/MergeSame.php | 133 ++++ src/Rules/Omit.php | 23 + src/Rules/Simplify.php | 57 ++ src/Rules/Unify.php | 10 + 15 files changed, 2023 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config.sample.php create mode 100644 src/CookidooScraper.php create mode 100644 src/Models/Category.php create mode 100644 src/Models/Ingredient.php create mode 100644 src/Models/ShoppingList.php create mode 100644 src/Normalizer.php create mode 100644 src/RuleApplicator.php create mode 100644 src/Rules/AbstractRule.php create mode 100644 src/Rules/MergeSame.php create mode 100644 src/Rules/Omit.php create mode 100644 src/Rules/Simplify.php create mode 100644 src/Rules/Unify.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af69334 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/*.phar +/list.json +/config.php +/scrape.php + +/vendor \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..83f13d0 --- /dev/null +++ b/composer.json @@ -0,0 +1,14 @@ +{ + "name": "fbrinker/cookidoo-shopping-advanced", + "description": "A Cookidoo Shopping List Scraper and Parser for better results. It can unify and merge all ingredients by different rule sets.", + "type": "project", + "autoload": { + "psr-4": { + "CookidooShoppingAdvanced\\": "src/" + } + }, + "require": { + "fabpot/goutte": "^4.0", + "nadar/stemming": "^1.0" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..fd63388 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1390 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "712553e77e06d74db8baca8e007628a3", + "packages": [ + { + "name": "fabpot/goutte", + "version": "v4.0.1", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/Goutte.git", + "reference": "293e754f0be2f1e85f9b31262cb811de39874e03" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/293e754f0be2f1e85f9b31262cb811de39874e03", + "reference": "293e754f0be2f1e85f9b31262cb811de39874e03", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/browser-kit": "^4.4|^5.0", + "symfony/css-selector": "^4.4|^5.0", + "symfony/dom-crawler": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.0" + }, + "type": "application", + "autoload": { + "psr-4": { + "Goutte\\": "Goutte" + }, + "exclude-from-classmap": [ + "Goutte/Tests" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A simple PHP Web Scraper", + "homepage": "https://github.com/FriendsOfPHP/Goutte", + "keywords": [ + "scraper" + ], + "support": { + "issues": "https://github.com/FriendsOfPHP/Goutte/issues", + "source": "https://github.com/FriendsOfPHP/Goutte/tree/v4.0.1" + }, + "time": "2020-10-14T06:49:09+00:00" + }, + { + "name": "nadar/stemming", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/nadar/stemming.git", + "reference": "9f6c74ff6e3e5cc2e932864293f5d9112d9542aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nadar/stemming/zipball/9f6c74ff6e3e5cc2e932864293f5d9112d9542aa", + "reference": "9f6c74ff6e3e5cc2e932864293f5d9112d9542aa", + "shasum": "" + }, + "require-dev": { + "luyadev/luya-testsuite": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nadar\\Stemming\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Basil Suter", + "homepage": "https://github.com/nadar", + "role": "Creator" + } + ], + "description": "Stemming by Language", + "homepage": "https://github.com/nadar/stemming", + "keywords": [ + "php", + "stemmer", + "stemming" + ], + "support": { + "issues": "https://github.com/nadar/stemming/issues", + "source": "https://github.com/nadar/stemming/tree/1.0.3" + }, + "time": "2020-12-20T08:36:03+00:00" + }, + { + "name": "psr/container", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" + }, + "time": "2021-03-05T17:36:06+00:00" + }, + { + "name": "psr/log", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.3" + }, + "time": "2020-03-23T09:12:05+00:00" + }, + { + "name": "symfony/browser-kit", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "3ca3a57ce9860318b20a924fec5daf5c6db44d93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/3ca3a57ce9860318b20a924fec5daf5c6db44d93", + "reference": "3ca3a57ce9860318b20a924fec5daf5c6db44d93", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/dom-crawler": "^4.4|^5.0" + }, + "require-dev": { + "symfony/css-selector": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0", + "symfony/process": "^4.4|^5.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/browser-kit/tree/v5.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-22T06:48:33+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "f65f217b3314504a1ec99c2d6ef69016bb13490f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/f65f217b3314504a1ec99c2d6ef69016bb13490f", + "reference": "f65f217b3314504a1ec99c2d6ef69016bb13490f", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v5.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-27T10:01:46+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-03-23T23:28:01+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "400e265163f65aceee7e904ef532e15228de674b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/400e265163f65aceee7e904ef532e15228de674b", + "reference": "400e265163f65aceee7e904ef532e15228de674b", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "masterminds/html5": "<2.6" + }, + "require-dev": { + "masterminds/html5": "^2.6", + "symfony/css-selector": "^4.4|^5.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v5.2.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-15T18:55:04+00:00" + }, + { + "name": "symfony/http-client", + "version": "v5.2.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "3c3075467da15bc2edf38d2ac20d34719e794bd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/3c3075467da15bc2edf38d2ac20d34719e794bd8", + "reference": "3c3075467da15bc2edf38d2ac20d34719e794bd8", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/log": "^1.0", + "symfony/http-client-contracts": "^2.2", + "symfony/polyfill-php73": "^1.11", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.0|^2" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "2.2" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/http-kernel": "^4.4.13|^5.1.5", + "symfony/process": "^4.4|^5.0", + "symfony/stopwatch": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-client/tree/v5.2.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-03-28T09:42:18+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "7e82f6084d7cae521a75ef2cb5c9457bbda785f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/7e82f6084d7cae521a75ef2cb5c9457bbda785f4", + "reference": "7e82f6084d7cae521a75ef2cb5c9457bbda785f4", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-04-11T23:07:08+00:00" + }, + { + "name": "symfony/mime", + "version": "v5.2.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "1b2092244374cbe48ae733673f2ca0818b37197b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/1b2092244374cbe48ae733673f2ca0818b37197b", + "reference": "1b2092244374cbe48ae733673f2ca0818b37197b", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<4.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/property-access": "^4.4|^5.1", + "symfony/property-info": "^4.4|^5.1", + "symfony/serializer": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v5.2.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-03-12T13:18:39+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", + "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "2d63434d922daf7da8dd863e7907e67ee3031483" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/2d63434d922daf7da8dd863e7907e67ee3031483", + "reference": "2d63434d922daf7da8dd863e7907e67ee3031483", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-22T09:19:47+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", + "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-07T16:49:33+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-04-01T10:43:52+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/config.sample.php b/config.sample.php new file mode 100644 index 0000000..633509f --- /dev/null +++ b/config.sample.php @@ -0,0 +1,8 @@ + [ + 'username' => 'foo', + 'password' => 'bar', + ] +]; \ No newline at end of file diff --git a/src/CookidooScraper.php b/src/CookidooScraper.php new file mode 100644 index 0000000..9d7ed7e --- /dev/null +++ b/src/CookidooScraper.php @@ -0,0 +1,97 @@ +client = new Client(HttpClient::create(['headers' => [ + 'user-agent' => self::USER_AGENT, + 'accept' => "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + 'accept-language' => "de,en-US;q=0.7,en;q=0.3", + ], 'timeout' => 60])); + } + + public function login(string $user, string $password): void { + try { + $crawler = $this->client->request('GET', self::PAGE_LOGIN); + $form = $crawler->selectButton(self::PAGE_LOGIN_BUTTON_ID)->form(); + $submitResult = $this->client->submit($form, [ + self::PAGE_LOGIN_FIELD_USER => $user, + self::PAGE_LOGIN_FIELD_PASSWORD => $password + ]); + + if (stripos($submitResult->getUri(), 'authentication_error=true') !== false) { + throw new Exception("Authentication error"); + } + + $this->login = true; + } catch (Exception $e) { + throw new Exception(sprintf("Cannot login: %s", $e->getMessage())); + } + } + + public function getShopptingList(): ShoppingList { + if (!$this->login) { + throw new Exception('You need to login first.'); + } + + $crawler = $this->client->request('GET', self::PAGE_SHOPPING_LIST); + $crawler = $crawler->filter(self::PAGE_SHOPPING_LIST_GROUP_TAG); + + $shoppingList = new ShoppingList(); + $crawler->each(function ($parentCrawler) use($shoppingList) { + if (empty($parentCrawler->text(''))) { + return; + } + + $headline = $parentCrawler->filter(self::PAGE_SHOPPING_LIST_GROUP_HEADLINE); + if (empty($headline->text(''))) { + return; + } + $category = new Category($headline->text()); + + $ingredients = $parentCrawler->filter(self::PAGE_SHOPPING_LIST_GROUP_INGREDIENT); + $ingredients->each(function ($ingredient) use($shoppingList, $category) { + $ingredient = new Ingredient( + $category, + $ingredient->filter(self::PAGE_SHOPPING_LIST_GROUP_INGREDIENT_LABEL)->text(''), + $ingredient->filter(self::PAGE_SHOPPING_LIST_GROUP_INGREDIENT_AMOUNT)->text(0), + $ingredient->filter(self::PAGE_SHOPPING_LIST_GROUP_INGREDIENT_UNIT)->text(''), + ); + + if (!$ingredient->isValid()) { + throw new \Exception("Invalid ingredient: $ingredient"); + } + + $shoppingList->add($ingredient); + }); + }); + + return $shoppingList; + } +} \ No newline at end of file diff --git a/src/Models/Category.php b/src/Models/Category.php new file mode 100644 index 0000000..e4273bf --- /dev/null +++ b/src/Models/Category.php @@ -0,0 +1,21 @@ +name = $name; + } + + public function getName(): string { + return $this->name; + } + + public function jsonSerialize() { + return [ + 'name' => $this->name, + ]; + } +} \ No newline at end of file diff --git a/src/Models/Ingredient.php b/src/Models/Ingredient.php new file mode 100644 index 0000000..0efaf2a --- /dev/null +++ b/src/Models/Ingredient.php @@ -0,0 +1,106 @@ +id = uniqid(); + $this->category = $category; + $this->name = $name; + $this->amount = $amount; + $this->unit = $unit; + + $this->merged[] = clone $this; + } + + public function isValid(): bool { + return !empty($this->name); + } + + public function getId(): string { + return $this->id; + } + + public function getCategory(): Category { + return $this->category; + } + + public function getName(): string { + return $this->name; + } + + public function getCleanName(): string { + if (!empty($this->cleanName)) { + return $this->cleanName; + } + + $this->cleanName = Normalizer::normalize($this->name); + return $this->cleanName; + } + + public function setCleanName(string $cleanName): void { + $this->cleanName = $cleanName; + } + + public function getAmount(): string { + return $this->amount; + } + + public function setAmount(string $amount): void { + $this->amount = $amount; + } + + public function getUnit(): string { + return $this->unit; + } + + // debug info + public function addMerged(Ingredient $ingredient) { + $this->merged[] = $ingredient; + } + + public function getMerged(): array { + if (count($this->merged) === 1) { + return []; + } + + return $this->merged; + } + + public function __toString(): string + { + $amount = $this->getAmount() ?: ''; + $unit = $this->getUnit() ?: ''; + $amountAndUnit = trim(sprintf('%s %s', $amount, $unit)); + + if (!empty($amountAndUnit)) { + $amountAndUnit = ' ' . $amountAndUnit; + } + + return $this->getName() . $amountAndUnit; + } + + public function jsonSerialize() { + return [ + 'name' => $this->name, + 'amount' => $this->amount, + 'unit' => $this->unit, + 'category' => $this->category, + ]; + } +} \ No newline at end of file diff --git a/src/Models/ShoppingList.php b/src/Models/ShoppingList.php new file mode 100644 index 0000000..13b61a2 --- /dev/null +++ b/src/Models/ShoppingList.php @@ -0,0 +1,120 @@ +ingredients[] = $ingredient; + } + + public function remove(Ingredient $ingredientToRemove): void { + $result = []; + foreach($this->ingredients as $ingredient) { + if ($ingredient->getId() === $ingredientToRemove->getId()) { + continue; + } + + $result[] = $ingredient; + } + + $this->ingredients = $result; + } + + public function update(Ingredient $ingredientToUpdate): void { + foreach($this->ingredients as $key => $ingredient) { + if ($ingredient->getId() === $ingredientToUpdate->getId()) { + $this->ingredients[$key] = $ingredientToUpdate; + return; + } + } + } + + public function get(): array { + return $this->ingredients; + } + + public function getByCategory(): array { + $result = []; + + foreach($this->ingredients as $ingredient) { + if (!isset($result[$ingredient->getCategory()->getName()])) { + $result[$ingredient->getCategory()->getName()] = []; + } + + $result[$ingredient->getCategory()->getName()][] = $ingredient; + } + + return $result; + } + + public function isEmpty(): bool { + return empty($this->ingredients); + } + + public function rewind() { + $this->index = 0; + } + + public function current() { + return $this->ingredients[$this->index]; + } + + public function key() { + return $this->index; + } + + public function next() { + ++$this->index; + } + + public function valid(): bool { + return isset($this->ingredients[$this->index]); + } + + public function offsetSet($offset, $value) { + if (is_null($offset)) { + $this->ingredients[] = $value; + } else { + $this->ingredients[$offset] = $value; + } + } + + public function offsetExists($offset) { + return isset($this->ingredients[$offset]); + } + + public function offsetUnset($offset) { + unset($this->ingredients[$offset]); + } + + public function offsetGet($offset) { + return isset($this->ingredients[$offset]) ? $this->ingredients[$offset] : null; + } + + public function toJson(): string { + return json_encode($this->ingredients); + } + + public static function fromJson(string $json): self { + $list = new self(); + if (empty($json)) { + return $list; + } + + $data = json_decode($json); + foreach($data as $entry) { + $ingredient = new Ingredient( + new Category($entry->category->name), + $entry->name, + $entry->amount, + $entry->unit, + ); + $list->add($ingredient); + } + + return $list; + } +} \ No newline at end of file diff --git a/src/Normalizer.php b/src/Normalizer.php new file mode 100644 index 0000000..cf39c85 --- /dev/null +++ b/src/Normalizer.php @@ -0,0 +1,13 @@ +applyRule($shoppingList, $rule); + } + } + + public function applyRule(ShoppingList &$shoppingList, AbstractRule $rule): void { + $rule->filter($shoppingList); + } +} \ No newline at end of file diff --git a/src/Rules/AbstractRule.php b/src/Rules/AbstractRule.php new file mode 100644 index 0000000..66a1e46 --- /dev/null +++ b/src/Rules/AbstractRule.php @@ -0,0 +1,8 @@ +isEmpty()) { + return; + } + + $mergableIngredientsByStrategy = $this->groupByMergeStrategy( + $this->extractMergableGroups($shoppingList) + ); + + $this->merge($shoppingList, $mergableIngredientsByStrategy); + } + + private function extractMergableGroups(ShoppingList $shoppingList): array { + $groupedByNames = []; + foreach ($shoppingList as $ingredient) { + $key = Stemm::stem($ingredient->getCleanName(), 'de'); + if (!isset($groupedByNames[$key])) { + $groupedByNames[$key] = []; + } + + $groupedByNames[$key][] = $ingredient; + } + + $mergableIngredients = array_filter($groupedByNames, static function(array $group): bool { + return count($group) > 1; + }); + + return $mergableIngredients; + } + + private function groupByMergeStrategy(array $mergableIngredients): array { + $strategyGrouped = []; + foreach($mergableIngredients as $name => $ingredientGroup) { + if (!isset($strategyGrouped[$name])) { + $strategyGrouped[$name] = []; + } + + foreach($ingredientGroup as $ingredient) { + $strategy = $this->detectStrategy($ingredient); + $strategyGrouped[$name][$strategy][] = $ingredient; + } + } + + return $strategyGrouped; + } + + private function detectStrategy(Ingredient $ingredient): string { + if (empty($ingredient->getAmount())) { + return self::STRATEGY_WITHOUT_AMOUNT; + } + + return self::STRATEGY_BY_UNIT; + } + + private function merge(ShoppingList &$shoppingList, array $mergableIngredientsByStrategy) { + foreach($mergableIngredientsByStrategy as $ingredientGroup) { + foreach($ingredientGroup as $strategy => $ingredientList) { + switch($strategy) { + case self::STRATEGY_WITHOUT_AMOUNT: + $this->applyStrategyWithoutAmount($shoppingList, $ingredientList); + break; + case self::STRATEGY_BY_UNIT: + $this->applyStrategyByUnit($shoppingList, $ingredientList); + break; + } + } + } + } + + /** + * WithoutAmount Strategy + * Keep the first entry only, remove the others + */ + private function applyStrategyWithoutAmount(ShoppingList &$shoppingList, array $ingredientGroup): void { + $keep = reset($ingredientGroup); + $ingredientsToRemove = array_slice($ingredientGroup, 1); + foreach($ingredientsToRemove as $ingredient) { + $keep->addMerged($ingredient); + $shoppingList->remove($ingredient); + } + } + + /** + * By Unit Strategy + * Group ingredients with the same unit + */ + private function applyStrategyByUnit(ShoppingList &$shoppingList, array $ingredientGroup): void { + $byUnit = []; + foreach($ingredientGroup as $ingredient) { + $key = Stemm::stem($ingredient->getUnit(), 'de'); + if (!isset($byUnit[$key])) { + $byUnit[$key] = []; + } + + $byUnit[$key][] = $ingredient; + } + + foreach($byUnit as $ingredients) { + $baseIngredient = $ingredients[0]; + $ingredientsToRemove = array_slice($ingredients, 1); + + foreach($ingredientsToRemove as $ingredientToRemove) { + $amount = $ingredientToRemove->getAmount(); + // check for ranges + if (strpos($amount, '-') !== false) { + $amounts = array_map('trim', explode('-', $amount)); + $amount = array_pop($amounts); + } + + if (!is_numeric($amount)) { + throw new Exception(sprintf('Cannot merge non numeric amounts (%s) of ingredients: %s', $amount, $ingredientToRemove)); + } + + $newAmount = (float)$baseIngredient->getAmount() + (float)$ingredientToRemove->getAmount(); + $baseIngredient->setAmount($newAmount); + $baseIngredient->addMerged($ingredientToRemove); + $shoppingList->remove($ingredientToRemove); + } + } + } +} \ No newline at end of file diff --git a/src/Rules/Omit.php b/src/Rules/Omit.php new file mode 100644 index 0000000..fc6bdcc --- /dev/null +++ b/src/Rules/Omit.php @@ -0,0 +1,23 @@ +getCleanName()])) { + $shoppingList->remove($ingredient); + } + } + } +} \ No newline at end of file diff --git a/src/Rules/Simplify.php b/src/Rules/Simplify.php new file mode 100644 index 0000000..c1432c9 --- /dev/null +++ b/src/Rules/Simplify.php @@ -0,0 +1,57 @@ + '', + 'gemahlen' => '', + 'selbst gemacht' => '', + 'konserviert' => '', + 'vegan' => '', + 'mittelscharf' => '', + ]; + + private const SIMPLIFICATION_MAPPING = [ // obey the replacement order + 'Cayenne-Pfeffer' => 'Pfeffer', + 'Paprikaschote' => 'Paprika', + 'Schalotte' => 'Zwiebel', + 'Erdäpfel' => 'Kartoffel', + 'Paprikapulver' => 'Paprika', + 'Essiggurkerl' => 'Essiggurken', + ]; + + // Words the stemming algorithm cannot reduce + private const SIMPLIFICATION_STEMMS = [ // obey the replacement order + 'Zwiebeln' => 'Zwiebel', + 'Kartoffeln' => 'Kartoffel', + ]; + + public function filter(ShoppingList &$shoppingList): void { + foreach($shoppingList as $key => $ingredient) { + $cleanName = $ingredient->getCleanName(); + $search = $replace = []; + + $mappings = array_merge( + self::SIMPLIFICATION_OMISSION, + self::SIMPLIFICATION_MAPPING, + self::SIMPLIFICATION_STEMMS + ); + + foreach($mappings as $simplify => $with) { + $simplify = Normalizer::normalize($simplify); + if (strpos($cleanName, $simplify) !== false) { + $search[] = $simplify; + $replace[] = Normalizer::normalize($with); + } + } + + if (!empty($search)) { + $ingredient->setCleanName(trim(str_replace($search, $replace, $cleanName))); + $shoppingList[$key] = $ingredient; + } + } + } +} \ No newline at end of file diff --git a/src/Rules/Unify.php b/src/Rules/Unify.php new file mode 100644 index 0000000..b904e15 --- /dev/null +++ b/src/Rules/Unify.php @@ -0,0 +1,10 @@ +