
يوم جيد يا زملائي. ما زلت مطورًا لنظام ISPsystem ، ولا يزال اسمي ديمتري سميرنوف. لبعض الوقت (إلى حد ما) لم أستطع اتخاذ قرار بشأن موضوع المنشور التالي ، حيث تراكمت الكثير من المواد خلال الأشهر الماضية من العمل مع Boost.Asio. وحتى في تلك اللحظة التي بدا فيها أنه من السهل رمي عملة معدنية ، فإن مهمة واحدة غيرت كل شيء. كان من الضروري تطوير أداة تسمح للواجهة الأمامية بترشيح البيانات في القوائم المطلوبة. القائمة نفسها من الخلفية هي json_array عادية. مرحبًا بك في كات ، فهناك كل الصعود والهبوط في الأيام الأخيرة.
تنصل
يجب أن أقول على الفور أن آخر مرة "شعرت" المؤلف بها مثل القواعد الخالية من السياق قبل عشر سنوات. بعد ذلك بدا الأمر وكأنه نوع من الأدوات غير المدببة وغير الضرورية ، لكنني علمت عن مكتبة Boost.Spirit في نفس يوم بيان المشكلة.
مهمة
تحتاج إلى تحويل استعلام مثل:
(string_field CP value AND int_field NOT LT 150) OR bool_field EQ true
في بعض الهياكل التي تحقق كائن json والإبلاغ عما إذا كان يفي بالمتطلبات أم لا.
الخطوات الأولى
بادئ ذي بدء ، تحتاج إلى تحديد واجهة عامل التصفية في المستقبل. من الضروري إزالة الكائنات غير الضرورية من الصفيف ، لذلك يجب دمجها مع خوارزميات STL ، وخاصة std :: remove_if.
سيكون المشغّل مثالياً ، وسيتم بناؤه مباشرةً من الطلب المقدم من الأمام. نظرًا لأن المشروع يستخدم nlohmann :: json ، سيكون التصميم أنيقًا تمامًا:
filter = "(string_field CP value AND int_field NOT LT 150) OR bool_field EQ true"; json.erase(std::remove_if(json.begin(), json.end(), std::not_fn(JsonFilter{filter})), json.end());
للاستخدام المريح للمرشح ، اخترت تقسيم الظروف إلى شجرة ثنائية. يجب أن تحتوي أدنى القمم على عوامل مقارنة ، ولكن يجب أن تكون البقية مشغلات منطقية. هذه هي الطريقة التي سيبدو فيها المرشح أعلاه في حالة تفكيكها:

كانت النتيجة شكلاً من أشكال AST ، إذا كنت تستطيع تسميتها. الآن بعد أن أخذت صورة المنطق القادم في التبلور ، فقد حان الوقت للأكثر إثارة للاهتمام والرهيبة. يجب أن يكون هذا مكتوب ... على الروح ...
معرفة
نشأ أصعب سؤال: من أين نبدأ؟ بخلاف Asio ، فإن قراءة رؤوس Spirit لم تقدم أي أدلة واضحة ، بمعنى آخر - هناك "نوع من السحر". وأعقب ذلك دراسة للأمثلة في الوثائق الرسمية للدفعة وجميع أنواع الأمثلة على الشبكة ، والتي لم تجلب بعد ثمارها بعد وقت فحسب ، بل حلًا أقرب ما يكون لاحتياجاتي: AST calculator
لنلقِ نظرة على القواعد الموجودة في المثال:
حاسبة القواعد class ArithmeticGrammar1 : public qi::grammar<std::string::const_iterator, ASTNodePtr(), qi::space_type> { public: using Iterator = std::string::const_iterator; ArithmeticGrammar1() : ArithmeticGrammar1::base_type(start) { start = (product >> '+' >> start) [qi::_val = phx::new_<OperatorNode<'+'>> (qi::_1, qi::_2)] | product[qi::_val = qi::_1]; product = (factor >> '*' >> product) [qi::_val = phx::new_<OperatorNode<'*'>> (qi::_1, qi::_2)] | factor[qi::_val = qi::_1]; factor = group[qi::_val = qi::_1] | qi::int_[qi::_val = phx::new_<ConstantNode>(qi::_1)]; group %= '(' >> start >> ')'; } qi::rule<Iterator, ASTNodePtr(), qi::space_type> start, group, product, factor; };
موروثة القواعد من الأساس :: القواعد. ASTNodePtr () ليست طريقة واضحة ولكنها مريحة للغاية لتمرير كائن النتيجة المتوقعة إلى كائن نحوي.
AST آلة حاسبة العقدة class ASTNode { public: virtual double evaluate() = 0; virtual ~ASTNode() {} }; using ASTNodePtr = ASTNode*; template <char Operator> class OperatorNode : public ASTNode { public: OperatorNode(const ASTNodePtr &left, const ASTNodePtr &right) : left(left) , right(right) {} double evaluate() { if (Operator == '+') return left->evaluate() + right->evaluate(); else if (Operator == '*') return left->evaluate() * right->evaluate(); } ~OperatorNode() { delete left; delete right; } private: ASTNodePtr left, right;
باستخدام مكتبة Boost.Phoenix ، يمكنك إنشاء عقدة AST جاهزة من واحد أو عدة أطراف غير مباشرة أثناء التحليل والكتابة مباشرة إلى النتيجة. دعونا نلقي نظرة فاحصة على ما تتكون آلة حاسبة من:
start = (product >> '+' >> start)[qi::_val = phx::new_<OperatorNode<'+'>> (qi::_1, qi::_2)] | product[qi::_val = qi::_1];
البدء - البدء في تحليل الجملة. نقطة الانطلاق. يمكن التعبير عنها من خلال مجموع المنتج والبدء ، أو من خلال المنتج ببساطة.
لاحظ الإجراء بين قوسين معقوفين لكل تعبير. هذا هو الإجراء الذي يجب تنفيذه إذا نجح التحليل ، إذا كان كل شيء مطابقًا. qi :: _ val هو في الواقع زيادة :: spirit :: qi :: _ val عنصر نائب. مع مساعدتها ، سيتم تسجيل الإجابة في النتيجة. في حالة البدء ، سيكون هذا هو كائن OperatorNode ، وستكون حجهته الأولى نتيجة تحليل المنتج ، والثاني هو نتيجة بدء التحليل.
نحن ننظر أبعد من ذلك. لنفترض أننا صادفنا الخيار الثاني ، فالبدء ليس مبلغًا ، بل منتج. كيف يتم التعبير عنه؟
product = (factor >> '*' >> product) [qi::_val = phx::new_<OperatorNode<'*'>> (qi::_1, qi::_2)] | factor[qi::_val = qi::_1];
يتم تكرار الصورة السابقة مع الحد الأدنى من الاختلافات. مرة أخرى نلتقي نوعًا من التعبير ، ومرة أخرى نكتب كائن OperatorNode في النتيجة ، أو مجرد نوع من العوامل. دعونا ننظر في الأمر.
factor = group[qi::_val = qi::_1] | qi::int_[qi::_val = phx::new_<ConstantNode>(qi::_1)];
ونحن نمضي على طول الطريق الأقصر ، نفترض أننا لم نلتق سوى غير int. الآن ، إذا وصفنا جميع الخطوات السابقة في الشفرة الزائفة ، فسنحصل بشكل موسع على هذا الشكل:
factor1 = ConstantNode(1)
يتم التعبير عن كل عقدة ، بدءًا من الأعلى (باستثناء أقلها ، وهي أعداد صحيحة في الأساس هنا) ، من خلال العقد اللاحقة. والاتصال الوحيد لطريقة التقييم () على عنصر الجذر يحل المشكلة بأكملها ، رائع!
ثم qi :: space_type تلفت انتباهك - هذه الوسيطة هي قائمة بالعناصر التي يتم تجاهلها عند التحليل. هذا لا يزال يلعب خدعة علي :-).
ما يلفت الانتباه هنا هو طريقة إعطاء الأولوية لعملية الضرب على الإضافة من خلال التعبير ببساطة عن البداية غير الطرفية (تحتوي فقط على +) عبر المنتج (*). في صيغة القواعد الخاصة بي ، نظرًا لأنه تقرر أنه سوف يسود على أو ، أنا ببساطة استبدل العوامل المنطقية المطلوبة في الأماكن الصحيحة. إذا كان من الصعب ارتكاب أخطاء في كتابة العوامل الرياضية ، فإن العوامل المنطقية النصية هي قصة مختلفة تمامًا. هناك رغبة في حل جزء على الأقل من المشاكل المحتملة ، على سبيل المثال ، السجل. لهذا ، يحتوي Spirit على نوع مدمج qi :: no_case
علاوة على ذلك ، بدلاً من الأرقام ، سأحتاج إلى أسماء الحقول ، لذلك نضيف غير المطابق المقابل بدلاً من القيمة qi :: int_ المضمنة :
field = qi::char_("a-zA-Z_") >> *qi::char_("a-zA-Z_0-9");
ونصل هنا إلى هذا التعبير البسيط (حتى الآن لا توجد عمليات الدلالية):
start = product >> qi::no_case["OR"] >> start | product; product = factor >> qi::no_case["AND"] >> product | factor; factor = group | field; group %= '(' >> start >> ')';
الآن كل شيء جاهز لتحليل أبسط جملة "field و field2" . نبدأ و ... لا شيء يعمل.
كانت المشكلة في مكان غير متوقع: qi :: space_type لا يتجاهل المسافات فقط ، بل يزيلها من الجملة قبل التحليل ، ويظهر تعبير المرشح الأولي في التحليل بالفعل في النموذج:
"fieldAndfield2" \\ , "(5 * 5) + 11 " \\ "(5*5)+11"
هذا هو حقل واحد فقط. وفقا لذلك ، تحتاج إلى بعض قائد:
skipper = +qi::lit(' ');
بعد تحليل الحقول ، من الممكن معرفة كيفية الحصول على القيم من التعبيرات وفهم كيفية التحقق من صحة الحقل مقابل القيمة. يمكن التعبير عن جميع خيارات المقارنة من خلال العمليات التالية:
enum class Operator { EQ,
والقيم نفسها يتم التعبير عنها في مثل هذه اللامركزية:
value = qi::double_ | qi::int_ | qi::bool_ | string; string = qi::lit("'") >> +qi::char_("a-zA-Z0-9_. ") >> qi::lit("'");
الآن للمشاكل التي تجلبها مثل هذه الطريقة للحصول على القيمة. سوف تقوم شركة Spirit بإرجاعها في صورة تعزيز :: variant <int، double، bool، std :: string> ، وعندما يحين الوقت لمقارنتها ببعض البيانات ، ستكون هناك حاجة إلى بعض الحيل للحصول على قيمة النوع المطلوب. إليك ما الخيار الذي أتيت إليه:
using ValueType = boost::variant<int, double, bool, std::string>; struct ValueGetter : public boost::static_visitor<Json> { template <typename Type> Json operator()(const Type &value) const { return value; } };
لماذا يعيد getter كائن Json؟ وبالتالي ، عند مقارنة القيم أثناء التصفية ، سأتجنب الاضطرار إلى معرفة نوع البيانات التي تمر بها المقارنة ، وترك كل العمل لمكتبة json.
خط النهاية. وصف الأم. سوف نستخدم نفس المثال مع آلة حاسبة. بادئ ذي بدء ، نحن بحاجة إلى تجريد ، سوف نمر به إلى القواعد ، وسيتفضل الروح بتعبئته معنا:
class AbstractMatcher { public: AbstractMatcher() = default; virtual ~AbstractMatcher() = default; virtual bool evaluate(const Json &object) = 0;
العقد المنطقية الأخرى هي عقد التصفية الرئيسية:
العقدة المنطقية enum class Logic { AND, OR }; template <Logic Operator> class LogicNode final : public AbstractMatcher { public: LogicNode(MatcherPtr &left, MatcherPtr &right) : m_left(std::move(left)) , m_right(std::move(right)) { switch (Operator) { case Logic::AND: m_evaluator = &LogicNode::And; break; case Logic::OR: m_evaluator = &LogicNode::Or; } } bool evaluate(const Json &object) final { return std::invoke(m_evaluator, this, object); } private: MatcherPtr m_left; MatcherPtr m_right; using EvaluateType = bool(LogicNode::*)(const Json &); EvaluateType m_evaluator = nullptr; bool And(const Json &object) { return m_left->evaluate(object) && m_right->evaluate(object); } bool Or(const Json &object) { return m_left->evaluate(object) || m_right->evaluate(object); } };
وأخيرا ، العقد السفلي
مقارنة القيمة class ObjectNode final : public AbstractMatcher { public: ObjectNode(std::string field, const ValueType &value, boost::optional<std::string> &unary, Operator oper) : m_field(std::move(field)) , m_json_value(boost::apply_visitor(ValueGetter(), value)) , m_reject(unary.has_value()) { switch (oper) { case Operator::EQ: m_evaluator = &ObjectNode::Equal; break; case Operator::LT: m_evaluator = &ObjectNode::LesserThan; break; case Operator::GT: m_evaluator = &ObjectNode::GreaterThan; break; case Operator::CP: m_evaluator = &ObjectNode::Substr; break; } } bool evaluate(const Json &object) final { const auto &value = object.at(m_field); const bool result = std::invoke(m_evaluator, this, value); return m_reject ? !result : result; } private: using EvaluateType = bool(ObjectNode::*)(const Json &); const std::string m_field; const Json m_json_value; const bool m_reject; EvaluateType m_evaluator = nullptr; bool Equal(const Json &json) { return json == m_json_value; } bool LesserThan(const Json &json) { return json < m_json_value; } bool GreaterThan(const Json &json) { return json > m_json_value; } bool Substr(const Json &json) { return Str(json).find(Str(m_json_value)) != std::string::npos; } };
يبقى فقط أن نضعها معًا:
مرشح جسون struct JsonFilterGrammar : qi::grammar<std::string::const_iterator, MatcherPtr()> { JsonFilterGrammar() : JsonFilterGrammar::base_type(expression) { skipper = +qi::lit(' '); unary = qi::no_case["NOT"]; compare.add ("eq", Operator::EQ) ("lt", Operator::LT) ("gt", Operator::GT) ("cp", Operator::CP); expression = (product >> skipper >> qi::no_case["OR"] >> skipper >> expression) [qi::_val = make_shared_<LogicNode<Logic::OR>>()(qi::_1, qi::_2)] | product[qi::_val = qi::_1]; product = (term >> skipper >> qi::no_case["AND"] >> skipper >> product) [qi::_val = make_shared_<LogicNode<Logic::AND>>()(qi::_1, qi::_2)]| term[qi::_val = qi::_1]; term = group[qi::_val = qi::_1] | (field >> -(skipper >> unary)>> skipper >> qi::no_case[compare] >> skipper >> value) [qi::_val = make_shared_<ObjectNode>()(qi::_1, qi::_4, qi::_2, qi::_3)]; field = qi::char_("a-zA-Z_") >> *qi::char_("a-zA-Z_0-9"); value = qi::double_ | qi::int_ | qi::bool_ | string; string = qi::lit("'") >> +qi::char_("a-zA-Z0-9_. \u20BD€$¥-") >> qi::lit("'"); group %= '(' >> expression >> ')'; } qi::rule<Iterator> skipper; qi::rule<Iterator, MatcherPtr()> product, term, expression, group; qi::rule<Iterator, std::string()> field, unary, string; qi::rule<Iterator, ValueType()> value; qi::symbols<char, Operator> compare;
هذا كل شيء. أصبح الحصول على الفلتر النهائي الآن عملية بسيطة إلى حد ما:
MatcherPtr matcher; std::string filter = "int not LT 15"; JsonFilterGrammar grammar; qi::parse(filter.begin(), filter.end(), grammar, matcher);
سأحذف عملية التفاف القواعد النحوية في جهاز التوجيه (لا أعتقد أنه سيكون ممتعًا لأي شخص). سننظر في الأداة بشكل أفضل باستخدام أبسط مثال:
std::string filter = "int not LT 15"; Json json{ {{"int", 10}}, {{"int", 11}}, {{"int", 20}}, {{"int", 30}}, {{"int", 9}} }; std::cout << json.dump() << std::endl; json.erase(std::remove_if(json.begin(), json.end(), std::not_fn(JsonFilter{filter})), json.end()); std::cout << json.dump() << std::endl;
هنا هو الإخراج:
[{"int":10},{"int":11},{"int":20},{"int":30},{"int":9}] [{"int":20},{"int":30}]
آمل ، أيها القراء الأعزاء ، أنكم مهتمون أيضًا بالتعرف على أساسيات الروح وكذلك أنا. ثم بقيت. اراك قريبا