كيفية إنشاء رمز مميز لتعبير الرياضيات باستخدام JavaScript (أو أي لغة أخرى)

المصدر: ويكيميديا ​​كومنز

منذ بعض الوقت ، أصبحت مصدر إلهام لإنشاء تطبيق لحل أنواع معينة من مشاكل الرياضيات. اكتشفت أنه كان عليّ تحليل التعبير إلى شجرة بناء جملة مجردة ، لذلك قررت إنشاء نموذج أولي في Javascript. أثناء العمل على المحلل اللغوي ، أدركت أن الرمز المميز يجب بناؤه أولاً. سوف أطلعك على كيفية القيام بذلك بنفسك. (تحذير: إنه أسهل مما يبدو في البداية.)

ما هو Tokenizer؟

الرمز المميز هو برنامج يقوم بتقسيم تعبير إلى وحدات تسمى الرموز. على سبيل المثال ، إذا كان لدينا تعبير مثل "أنا مطور للدهون الكبيرة" ، فيمكننا تمييزه بطرق مختلفة ، مثل:

باستخدام الكلمات كرموز ،

0 => أنا
1 => أ
2 => كبير
3 => الدهون
4 => المطور

باستخدام أحرف غير بيضاء مثل الرموز ،

0 => أنا
1 => ‘
2 => م
3 => أ
4 => ب
...
16 => ع
17 => ه
18 => ص

يمكننا أيضًا النظر في جميع الشخصيات كرموز ، للحصول عليها

0 => أنا
1 => ‘
2 => م
3 => (الفضاء)
4 => أ
5 => (الفضاء)
6 => ب
...
20 => ص
21 => ه
22 => ص

تحصل على هذه الفكرة، أليس كذلك؟

تُستخدم الرموز الرمزية (وتسمى أيضًا المُعادلون) في تطوير برامج الترجمة للغات البرمجة. أنها تساعد المترجم المنطقي الهيكلي من ما تحاول قوله. في هذه الحالة ، على الرغم من ذلك ، نحن نبني واحدة لتعبيرات الرياضيات.

الرموز

يتكون تعبير الرياضيات الصحيح من الرموز المميزة الرياضية الصالحة ، والتي قد تكون لأغراض هذا المشروع عبارة عن حرفي أو متغيرات أو عوامل تشغيل أو وظائف أو فواصل وسيطات دالة.
بعض الملاحظات على ما سبق:

  • الحرفي هو اسم يتوهم لعدد (في هذه الحالة). لن نسمح بالأرقام بشكل كلي أو عشري فقط.
  • المتغير هو النوع الذي اعتدت عليه في الرياضيات: أ ، ب ، ج ، س ، ص ، ي. بالنسبة لهذا المشروع ، يتم تقييد جميع المتغيرات بأسماء ذات حرف واحد (لذلك لا شيء مثل var1 أو السعر). هذا حتى نتمكن من الرمز المميز لتعبير مثل ma كمنتج للمتغيرات m و a ، وليس متغير واحد فردي.
  • يعمل المشغلون على القيم الحرفية والمتغيرات ونتائج الوظائف. سنسمح للمشغلين + و - و * و / و و ^.
  • وظائف هي عمليات "أكثر تقدما". وهي تشمل أشياء مثل sin () و cos () و tan () و min () و max () إلخ
  • فاصل وسيطة فاصل هو مجرد اسم فاخر لفاصلة ، يُستخدم في سياق مثل هذا: الحد الأقصى (4 ، 5) (الحد الأقصى لواحد من القيمتين). نحن نسميها فاصل وسيطة دالة ، لأنه يفصل وسيطات الوظيفة (للوظائف التي تأخذ وسيطين أو أكثر ، مثل الحد الأقصى والدقائق).

سنضيف أيضًا رمزين لا يعتبران عادةً رموزًا ، لكنهما سيساعداننا في الوضوح: الأقواس اليمنى واليسرى. أنت تعرف ما هي تلك.

قليل من الاعتبارات

الضرب الضمني

يعني الضرب الضمني ببساطة السماح للمستخدم بكتابة مضاعفات "الاختزال" ، مثل 5x ، بدلاً من 5 * x. بأخذ خطوة للأمام ، يسمح أيضًا بالقيام بذلك مع الوظائف (5sin (x) = 5 * sin (x)).

علاوة على ذلك ، فإنه يسمح بـ 5 (x) و 5 (sin (x)). لدينا خيار السماح بذلك أم لا. المفاضلات؟ عدم السماح بذلك سيجعل عملية الرموز أسهل ويسمح بأسماء المتغيرات المتعددة الحروف (أسماء likeprice). إن السماح بذلك يجعل النظام أكثر سهولة للمستخدم ، كما أنه يوفر تحديًا إضافيًا للتغلب عليه. اخترت السماح بذلك.

بناء الجملة

على الرغم من أننا لا ننشئ لغة برمجة ، إلا أننا نحتاج إلى بعض القواعد حول ما يجعل التعبير صحيحًا ، حتى يعرف المستخدمون ما يجب إدخاله ونعرف ما نخطط له. بعبارات دقيقة ، يجب دمج الرموز المميزة للرياضيات وفقًا لقواعد بناء الجملة هذه حتى يكون التعبير صحيحًا. إليك قواعدي:

  1. يمكن فصل الرموز المميزة عن طريق 0 أو أكثر من أحرف المسافة البيضاء
2 + 3 ، 2 +3 ، 2 + 3 ، 2 + 3 كلها موافق
5 × 22 و 5 × 22 و 5 × 22 كلها موافق

بمعنى آخر ، التباعد ليس مهمًا (باستثناء رمز مميز متعدد الأحرف مثل الحرفي 22).

2. يجب أن تكون وسيطات الوظيفة بين قوسين (sin (y) ، cos (45) ، وليس sin y ، cos 45). (لماذا؟ سنقوم بإزالة جميع المسافات من السلسلة ، لذلك نريد أن نعرف من أين تبدأ وظيفة وتنتهي دون الاضطرار إلى القيام ببعض "الجمباز".)

3. يُسمح بالضرب الضمني فقط بين الحرفي والمتغيرات ، أو الحرفي والوظائف ، بهذا الترتيب (أي ، الحرف تأتي دائمًا أولاً) ، ويمكن أن تكون مع أو بدون أقواس. هذا يعنى:

  • ستتم معاملة a (4) على أنها استدعاء دالة بدلاً من * 4
  • غير مسموح a4
  • 4 أ و 4 (أ) على ما يرام

الآن ، هيا بنا إلى العمل.

نمذجة البيانات

من المفيد أن يكون لديك تعبير عينة في رأسك لاختبار ذلك. سنبدأ بشيء أساسي: 2y + 1

ما نتوقعه هو مجموعة تسرد الرموز المختلفة في التعبير ، إلى جانب أنواعها وقيمها. لذلك في هذه الحالة ، نتوقع:

0 => الحرفي (2)
1 => المتغير (ص)
2 => المشغل (+)
3 => الحرفي (1)

أولاً ، سنقوم بتحديد فئة رمز لجعل الأمور أسهل:

وظيفة رمز (النوع ، القيمة) {
   this.type = النوع ؛
   this.value = القيمة
}

خوارزمية

بعد ذلك ، دعونا نبني الهيكل العظمي لوظيفة الرمز المميز لدينا.

سوف يمر الرمز المميز الخاص بنا عبر كل حرف من صفيف str ويقوم بإنشاء الرموز المميزة استنادًا إلى القيمة التي يجدها.

[لاحظ أننا نفترض أن المستخدم يعطينا تعبيرًا صالحًا ، لذلك سنتخطى أي شكل من أشكال التحقق خلال هذا المشروع.]

وظيفة الرمز المميز (str) {
  فار النتيجة = [] ؛ // مجموعة من الرموز
  
  // إزالة المسافات ؛ تذكر أنهم لا يهم؟
  str.replace (/ \ s + / g، "")؛
  / / تحويل إلى مجموعة من الشخصيات
  شارع = str.split ( "")؛
str.forEach (دالة (char ، idx) {
    if (isDigit (char)) {
      result.push (رمز جديد ("حرفي" ، char)) ؛
    } آخر إذا (isLetter (char)) {
      result.push (رمز جديد ("متغير" ، char)) ؛
    } آخر إذا (isOperator (char)) {
      result.push (رمز جديد ("المشغل" ، char)) ؛
    } if if (isLeftParenthesis (char)) {
      result.push (رمز جديد ("Lent Parenthesis" ، char)) ؛
    } if if (isRightParenthesis (char)) {
      result.push (رمز جديد ("القوس الأيمن" ، char)) ؛
    } آخر إذا (isComma (char)) {
      result.push (رمز جديد ("Function وسيطة فاصل" ، char)) ؛
    }
  })؛
  نتيجة العودة
}

الكود أعلاه أساسي إلى حد ما. كمرجع ، المساعدين هم: Digit () ، isLetter () ، isOperator () ، isLeftParenthesis () ، و isRightParenthesis () يتم تعريفهم على النحو التالي (لا تخافوا من الرموز - إنها تسمى regex ، وهي رائعة حقًا):

الوظيفة isComma (ch) {
 return (ch === "،")؛
}
الوظيفة isDigit (ch) {
 return /\d/.test(ch) ؛
}
الوظيفة isLetter (ch) {
 return / Budapa-zنتج/i.test(ch)؛
}
الوظيفة هي المشغل (الفصل)
 return /\+|-|\*|\/|\^/.test(ch)؛
}
الوظيفة هي LeftParenthesis (ch) {
 عائد (ch === "(") ؛
}
الوظيفة هيالطول الحنطي (الفصل) {
 عائد (ch == ")") ؛
}

[لاحظ أنه لا توجد وظائف isFunction () أو isLiteral () أو isVariable () ، لأننا نختبر الأحرف بشكل فردي.]

حتى الآن لدينا محلل يعمل فعلا. جربه على هذه التعبيرات: 2 + 3 ، 4a + 1 ، 5x + (2y) ، 11 + sin (20.4).

الامور جيدة؟

ليس تماما.

ستلاحظ أنه في التعبير الأخير ، تم الإبلاغ عن 11 كرمزين حرفيين بدلاً من واحد. كما يتم الإبلاغ عن الخطيئة بثلاثة رموز بدلاً من واحدة. لماذا هذا؟

دعونا نتوقف للحظة ونفكر في هذا. لقد قمنا برمز حرف الصفيف بحرف ، ولكن في الواقع ، يمكن أن تحتوي بعض الرموز المميزة على أحرف متعددة. على سبيل المثال ، يمكن أن تكون القيم الحرفية 5 ، 7.9 ، 0.5. يمكن أن تكون الوظائف خطيئة أو cos وما إلى ذلك. المتغيرات هي أحرف مفردة فقط ، ولكن يمكن أن تحدث معًا في الضرب الضمني. كيف يمكننا حل هذا؟

مخازن

يمكننا إصلاح هذا عن طريق تطبيق المخزن المؤقت. اثنان ، في الواقع. سنستخدم مخزنًا مؤقتًا واحدًا لاستيعاب الأحرف الحرفية (الأرقام والفاصلة العشرية) ، والآخر للأحرف (التي تغطي كل من المتغيرات والوظائف).

كيف تعمل المخازن المؤقتة؟ عندما يصادف الرمز المميز رقمًا / رقمًا عشريًا أو حرفًا ، فإنه يدفعه إلى المخزن المؤقت المناسب ، ويواصل القيام بذلك حتى يدخل نوع مختلف من المشغل. ستختلف إجراءاتها بناءً على المشغل.

على سبيل المثال ، في التعبير 456.7xy + 6sin (7.04x) - دقيقة (a ، 7) ، يجب أن تتماشى مع هذه الخطوط:

قراءة 4 => numberBuffer
 قراءة 5 => numberBuffer
 قراءة 6 => numberBuffer
 اقرأ . => numberBuffer
 قراءة 7 => numberBuffer
 x حرف ، لذا ضع كل محتويات numberbuffer معًا كحرف 456.7 => نتيجة
 قراءة س => letterBuffer
 اقرأ y => letterBuffer
 + هو عامل تشغيل ، لذلك قم بإزالة جميع محتويات letterbuffer بشكل منفصل مثل المتغيرات x => نتيجة ، y => نتيجة
 + => النتيجة
 قراءة 6 => numberBuffer
 s حرف ، لذا ضع كل محتويات numberbuffer معًا كحرف 6 => نتيجة
 اقرأ s => letterBuffer
 قرأت أنا => letterBuffer
 قراءة ن => letterBuffer
 (هو أقواس يسارية ، لذا ضع كل محتويات letterbuffer معًا كدالة sin => نتيجة
 قراءة 7 => numberBuffer
 اقرأ . => numberBuffer
 قراءة 0 => numberBuffer
 قراءة 4 => numberBuffer
 x حرف ، لذا ضع كل محتويات numberbuffer معًا كحرف 7.04 => نتيجة
 قراءة س => letterBuffer
 ) هو أقواس يمينية ، لذلك قم بإزالة جميع محتويات letterbuffer بشكل منفصل مثل المتغيرات x => نتيجة
 - هو المشغل ، ولكن كلا المخازن المؤقتة فارغة ، لذلك لا يوجد شيء لإزالته
 قراءة م => letterBuffer
 قرأت أنا => letterBuffer
 قراءة ن => letterBuffer
 (هو أقواس يسارية ، لذا ضع كل محتويات letterbuffer معًا كدالة min => نتيجة
 قراءة => letterBuffer
 ، فاصلة ، لذلك ضع كل محتويات letterbuffer معًا كمتغير a => نتيجة ، ثم اضغط ، باعتبارها دالة Arg Separator => نتيجة
 قراءة 7 => numberBuffer
 ) هو أقواس صحيحة ، لذلك ضع كل محتويات numberbuffer معًا كحرف 7 => نتيجة

اكتمال. تحصل على تعليق منه الآن ، أليس كذلك؟

نحن نصل إلى هناك ، فقط عدد قليل من الحالات للتعامل معها.

هذه هي النقطة التي تجلس فيها وتفكر بعمق في الخوارزمية ونمذجة البيانات. ماذا يحدث إذا كانت شخصيتي الحالية هي عامل ، والرقم Buffer غير فارغ؟ هل يمكن أن يكون كلا المخزن المؤقت غير فارغ في وقت واحد؟

بتجميعها معًا ، إليك ما توصلنا إليه (القيم على يسار السهم توضح نوع الشخصية الحالية (ch) ، NB = numberbuffer ، LB = letterbuffer ، LP = الأقواس اليسرى ، RP = الأقواس اليمنى

حلقة من خلال مجموعة:
  ما هو نوع الفصل؟
رقم => دفع ch إلى NB
  العلامة العشرية => دفع ch إلى NB
  letter => انضم إلى محتويات NB كحرف واحد واضغط على النتيجة ، ثم اضغط ch على LB
  عامل التشغيل>> انضم إلى محتويات NB حرفيًا واحدًا وادفع للنتيجة أو ادفع محتويات LB بشكل منفصل كمتغيرات ، ثم ادفع ch إلى النتيجة
  LP => انضم إلى محتويات LB كوظيفة واحدة والدفع للنتيجة أو (انضم إلى محتويات NB كحرف حرفي واحد وادفع للنتيجة ، وادفع المشغل * للنتيجة) ، ثم ادفع ch للنتيجة
  RP => انضم إلى محتويات NB حرفي واحد وادفع للنتيجة ، وادفع محتويات LB بشكل منفصل كمتغيرات ، ثم ادفع ch إلى النتيجة
  فاصلة => انضم إلى محتويات NB حرفي واحد واضغط على النتيجة ، ادفع محتويات LB بشكل منفصل كمتغيرات ، ثم ادفع ch إلى النتيجة
حلقة النهاية
الانضمام إلى محتويات NB حرفي واحد والضغط على النتيجة ، ودفع محتويات LB بشكل منفصل كمتغيرات ،

شيئين لاحظ.

  1. لاحظ أين أضفت "دفع المشغل * لتحقيق"؟ هذا هو تحويل الضرب الضمني إلى صريح. أيضًا ، عند إفراغ محتويات LB بشكل منفصل كمتغيرات ، نحتاج أن نتذكر إدراج مشغل الضرب بينهما.
  2. في نهاية حلقة الوظيفة ، نحتاج أن نتذكر تفريغ كل ما بقي في المخازن المؤقتة.

ترجمة ذلك إلى رمز

بتجميعها معًا ، يجب أن تبدو وظيفة الرمز المميز كما يلي الآن:

يمكننا تشغيل تجريبي قليلا:

var tokens = tokenize ("89sin (45) + 2.2x / 7")؛
tokens.forEach (دالة (الرمز المميز ، الفهرس) {
  console.log (index + "=>" + token.type + "(" + token.value + ")":
})؛
بلى! لاحظ * المضافة عن الضرب الضمني

قم بتغليفه

هذه هي النقطة التي تقوم فيها بتحليل وظيفتك وقياس ما تقوم به مقابل ما تريد القيام به. اسأل نفسك أسئلة مثل: "هل تعمل الوظيفة كما هو مقصود؟" و "هل غطيت جميع حالات الحافة؟"

الحالات حافة لهذا يمكن أن تشمل الأرقام السلبية وما شابه ذلك. يمكنك أيضا إجراء اختبارات على الوظيفة. إذا كنت راضيًا في النهاية ، فقد تبدأ بعد ذلك في البحث عن كيف يمكنك تحسينه.

شكرا للقراءة. الرجاء النقر فوق القلب الصغير للتوصية بهذه المقالة ، ومشاركتها إذا كنت تستمتع بها وإذا كنت قد جربت طريقة أخرى لبناء رمز رياضي ، فاسمح لي أن أعرف في التعليقات.