كيف تدير ساعة؟ تحليل المسار الأمامي للبطولة البرمجة الثانية

قصة حب جديدة في سلسلة من تحليلات البطولة الأخيرة. يجب على المشاركين المؤهلين الذين اختاروا قسم الواجهة الأمامية حل العديد من المهام ذات التعقيد المختلف للغاية: الأول (حسب توقعاتنا) استغرق 20 دقيقة ، والأخير - حوالي ساعة. لقد اختبرنا مجموعة واسعة من مهارات مطور الواجهة ، بما في ذلك القدرة على فهم مجال موضوع غير عادي.

إبادة ذلك

المؤلفون: مكسيم سيسوف ، كونستانتين بيترييف

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

حالة


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

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

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

تحتاج إلى معرفة جزيء الوزن الذي سيبقى في نهاية التجربة ، فهذه المعرفة مطلوبة من قبل علماء قسم آخر.

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

رمز الموروثة لك

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

var findLatestWeight = function(weights, i = weights.length - 1) { const cur = weights.length - 1 === i; if (i === 0) return weights[0]; weights.sort((a, b) => a - b); weights[i - 1] = (weights[i] === weights[i-1]) ? 0 : weights[i] - weights[i-1]; return findLatestWeight(weights, i - 1); } 

مثال وملاحظات

مثال


المدخل: [2،7،4،1،8،1]
الإخراج: 1

نأخذ جزيئات يبلغ وزنها 7 و 8 ، ونحول 7 إلى جزيء مضاد ونصطدم به بجزيء من الوزن 8. لا يزال هناك جزيء من الوزن 1. أوزان الجزيئات المتبقية من الصلب [2،4،1،1،1]. نأخذ جزيئات بوزن 2 و 4 ، ونحول 2 إلى جزيء مضاد ونصطدم به بجزيء من الوزن 4. لا يزال هناك جزيء من الوزن 2. أوزان الجزيئات المتبقية من الصلب [2،1،1،1]. نأخذ جزيئات بوزن 2 و 1 ، ونحول 1 إلى جزيء مضاد ونصطدم به بجزيء من الوزن 2. لا يزال هناك جزيء من الوزن 1. أوزان الجزيئات المتبقية من الصلب [1،1،1]. نحن نأخذ جزيئات بوزن 1 و 1 ، ونحول أحدها إلى جزيء مضاد ويصطدم به مع ثاني. هم مبيدون. أوزان الجزيئات المتبقية [1]. جزيء واحد اليسار. والنتيجة هي 1.

الملاحظات


كحل ، قم بتوفير ملف يقوم بتصدير الإصدار الذي تم تصحيحه من دالة findLatestWeight:

 function findLatestWeight(weights) { // ... } module.exports = findLatestWeight; 

سيتم تشغيل الحل في Node.js 12.

قرار


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

 var findLatestWeight = function(weights) { let i = weights.length - 1; do { if (i === 0) return weights[0] || 0; weights.sort((a, b) => a - b); weights[i-1] = (weights[i]=== weights[i-1]) ? 0 : weights[i]-weights[i-1]; i--; } while (true); } 

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

خيار حل هذه المشكلة هو محاولة تقليم الصفيف باستمرار.

 var findLatestWeight = function(weights) { let i = weights.length - 1; do { if (i === 0) return weights[0] || 0; weights.sort((a, b) => a - b); weights[i-1] = (weights[i]=== weights[i-1]) ? 0 : weights[i]-weights[i-1]; weights.length = i; // <---   i--; } while (true); } 

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

 const maximumTwo = (arr) => { let max1 = arr[0]; let max2 = arr[1]; let max1I = 0; let max2I = 1; for(let i = 2; i < arr.length; i++) { if (arr[i] > max1) { if (max1 > max2) { max2 = arr[i]; max2I = i; } else { max1 = arr[i]; max1I = i; } } else if (arr[i] > max2) { max2 = arr[i]; max2I = i; } } if (max1 > max2) return [max2, max1, max2I, max1I]; return [max1, max2, max1I, max2I]; }; 

ونغير وظيفة البحث كما يلي:

 const fn = function(weights) { if (weights.length <= 1) { return weights[0]; } do { const [x, y, xI, yI] = maximumTwo(weights); if (x === 0) { return y; } weights[xI] = 0; weights[yI] = y - x; } while(true); }; 

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

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

ب

المؤلفون: يوجين ميششينكو ، فلاديمير غرينينكو تاداتوتا

حالة


يشارك Layout Alexander في العديد من المشاريع باستخدام منهجية BEM. حتى أنه أنشأ مكونًا مفيدًا لمُعرّف IDE المفضل لديه ، والذي يسمح له بكتابة أسماء الفئات بترميز مختصر ونشرها بالكامل. لكن المشكلة هي أنه بالنسبة لكل مشروع ، يقوم الأشخاص بتعيين محددات مختلفة بين الكتلة والعنصر والمعدل (block__mod__val - elem ، block - mod - val ___ elem) ، وفي كل مرة يتعين عليه تعديل هذا يدويًا في البرنامج المساعد الخاص به. ساعد ألكساندر في كتابة وحدة ستحدد الفاصل للكيانات بناءً على الفصل. قاعدة المحددات هي عدد تعسفي من الأحرف (وليس الحروف). أمثلة للرموز المحتملة (قد تكون المعدلات الخاصة بلوك في بيانات الإدخال بدون قيمة):

 block_mod__elem // ,     block_mod_mod__elem block__elem_mod_mod 

التحسينات:
- يتم كتابة الدروس في المشاريع فقط بأحرف صغيرة.
- يتم تغذية سلسلة مع فئة CSS صالحة لإدخال الوحدة النمطية.

يجب على الوحدة النمطية إرجاع استجابة النموذج:

 { mod: "_", //    elem: "__", //    } 

يجب أن تصدر الوحدة كوحدة نمطية مشتركة:

 module.exports = function(str) { } 

قرار


استغرقت المهمة الثانية حوالي 20 دقيقة. بمساعدتها ، أردنا اختبار معرفة التعبيرات المنتظمة بين المشاركين.

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

الجزء الأول من اسم الفصل سيكون دائمًا اسم الكتلة. هذا تسلسل واحد أو أكثر من الحروف. نكتب التعبير العادي المقابل: [az] +.

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

للبحث عن المحددات ، نحتاج إلى تسلسل غير حرفي ، التعبير: [^ az] + مناسب.

ضعها معًا وحدد المجموعات التي سنستخدم قيمها:

 let [, mod, elem ] = str.match(/[az]+(?:([^az]+)[az]+(?:\1)?[az]+)([^az]+)[az]+(?:\2)?[az]+/); 

أنت الآن بحاجة إلى التأكد من أننا قمنا بتعريف دلالات المجموعات الموجودة بشكل صحيح. يمكنك الاستفادة من حقيقة أن معدّل فقط يمكنه الاجتماع مرتين.

سوف نكتب دالة ستأخذ السلسلة الأصلية وسيجد الفاصل لحساب عدد التكرارات:

 const substringCount = (source, substr) => (source.match(new RegExp('[az]' + substr + '[az]', 'g')) || []).length; 

إذا اتضح أن عنصر delimiter يحدث مرتين ، و mod - مرة واحدة ، في الواقع فإن العكس هو الصحيح. القرار النهائي:

 module.exports = function(str) { let [, mod, elem ] = str.match(/[az]+(?:([^az]+)[az]+(?:\1)?[az]+)([^az]+)[az]+(?:\2)?[az]+/); const substringCount = (source, substr) => (source.match(new RegExp('[az]' + substr + '[az]', 'g')) || []).length; if (substringCount(str, elem) === 2 && substringCount(str, mod) === 1) { [mod, elem] = [elem, mod]; } return { mod, elem }; } 

مصنع جيم استنساخ

المؤلفون: ديمتري أندريانوف ديما 117 ، أليكسي غوسيف

حالة


خارج النافذة هو 2319. الشركات استنساخ الموظفين الناجحين لأداء مهام معقدة.

في إنتاج الحيوانات المستنسخة ، قرروا تسمية "منتجات" جديدة مع وشم الباركود على كتفهم - لتمييز الحيوانات المستنسخة عن بعضها البعض.

ساعد موظفي المصنع في كتابة وظيفة من شأنها رسم رمز شريطي يحتوي على معلومات حول النسخ.

استنساخ تنسيق المعلومات

يتم تخزين المعلومات حول استنساخ على النحو التالي:

 type CloneInfo = { /** *   —  'male'  'female' */ sex: string; /** *   —      *    ,  10  */ id: string; /** *   —      *     ( 0  26 ) */ name: string; } 

خوارزمية تقديم الباركود

الرموز الشريطية المستخدمة في مصنع النسخ تبدو كما يلي:



حجم الباركود ثابت - 148 × 156 بكسل. حول محيط الباركود إطارات سوداء وبيضاء من 3 بكسل لكل منهما. يوجد داخل الإطارات محتوى الباركود ، ويتألف من 18 سطرًا من 17 مربعًا أسود أو أبيض لكل سطر. حجم كل مربع هو 8 × 8 بكسل.

المربعات البيضاء في المحتوى ترميز 0 ، أسود - 1.

خوارزمية إنشاء محتوى الباركود

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

علاوة على ذلك ، يتم تشكيل سطر من النموذج <id> <name> من معرف الحقول واسمها. حقل الاسم مبطن بمسافات في نهاية 26 حرفًا.

يتم تحويل السلسلة الناتجة إلى صفيف بايت - يتم تعيين رمز ASCII المقابل لكل حرف في السلسلة (رقم من 0 إلى 255).

ثم يُترجم كل عنصر من الصفيف الناتج إلى ترميز ثنائي (ثمانية أحرف 0 أو 1) ويتم ترميزه بسلسلة من ثمانية مربعات (0 - ربع أبيض ، 1 - مربع أسود). يتم رسم المربعات في محتوى الباركود بالتتابع والسطر سطراً.

يحتوي السطر الأخير من المحتوى على معلومات التحكم.

خوارزمية التحكم في معلومات التحكم

تحدد كل مربع في سطر معلومات التحكم تكافؤ مجموع قيم المحتوى في العمود المقابل. إذا كان مجموع الأصفار وتلك الموجودة في العمود متساويًا ، فسيتم رسم مربع أبيض في معلومات التحكم ، وإلا ، فإن المربع الأسود.

شكل الحل والأمثلة
تنسيق الحل

يجب أن يحتوي الحل الذي تقوم بتحميله على وظيفة renderBarcode:

 /** *       element * @param cloneInfo {CloneInfo} —    * @param element {HTMLDivElement} — div    * 148x156 ,      */ function renderBarcode(cloneInfo, element) { //   }</source lang="javascript">      Google Chrome 77. <h4> 1</h4>   : <source lang="javascript">{ "sex": "male", "id": "c5j818dyo5", "name": "Oleg Vladimirovich" } 

الباركود:



مثال 2


استنساخ المعلومات:

 { "sex": "female", "id": "0owrgqqwfw", "name": "Dazdraperma Petrovna" } 

الباركود:


قرار


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

لنبدأ بالتمثيل الثنائي. أولاً ، أعلن وظائف المساعد:

 //    ASCII- function charToByte(char) { return char.charCodeAt(0); } //      0  1 (      ) function byteToString(byte) { return byte.toString(2).padStart(8, '0'); } 

نقوم بتكوين سلسلة من البيانات المصدر تتكون من أصفار وأخرى:

 let dataString = (cloneInfo.sex === 'female' ? '0' : '1') + cloneInfo.id.split('').map(charToByte).map(byteToString).join('') + cloneInfo.name.padEnd(26, ' ').split('').map(charToByte).map(byteToString).join(''); 

ثم اكتب التصميم والأنماط الخاصة بالباركود الخاص بنا:

 //   ,    «» . //  ,      DOM API   innerHTML,     . //     ,      ,      «». //         —   ,        . const contentElId = 'content-' + Math.random(); element.style.display = 'flex'; element.innerHTML = ` <style> .barcode { border: 3px solid black; box-sizing: border-box; } .content { margin-top: 3px; margin-left: 3px; width: 136px; height: 144px; display: flex; flex-wrap: wrap; } .content__bit { width: 8px; height: 8px; } .content__bit_one { background: black; } </style> <div class="content" id="${contentElId}"></div> `; const contentDiv = document.getElementById(contentElId); element.className += ' barcode'; 

تقديم البيانات الثنائية في التخطيط:

 dataString .split('') .forEach((bit) => { const bitDiv = document.createElement('div'); bitDiv.className = 'content__bit content__bit_' + (bit === '0' ? 'zero' : 'one'); contentDiv.appendChild(bitDiv); }); 

يبقى لحساب وعرض الاختباري. هذا يمكن القيام به مثل هذا:

 for (let i = 0; i < 17; i++) { //   let sum = 0; for (let j = i; j < 17 ** 2; j += 17) { sum += parseInt(dataString[j], 2); } const check = 0; const bitDiv = document.createElement('div'); //       bitDiv.className = 'content__bit content__bit_' + (sum % 2 === 0 ? 'zero' : 'one'); contentDiv.appendChild(bitDiv); } 

D. أتمتة ذلك

المؤلفون: فلاديمير روسوف ، ديمتري كاناتنيكوف

في كل خيار من خيارات التأهيل ، كانت هناك مهمة تم فيها اقتراح صفحة HTML مع جدول أو قائمة كمدخلات. كان لمهام هذه السلسلة أسطورة مختلفة ، لكنهم جميعا توصلوا إلى حقيقة أنك بحاجة إلى نقل الصفحة إلى تنسيق مشابه لـ Markdown. سنقوم بتحليل الحل لأحد المشاكل.

حالة


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

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

تحتاج إلى كتابة وظيفة تأخذ جدول HTML كمدخلات وتحويلها إلى علامة تشبه Markdown.

كحل لهذه المهمة ، أرسل ملف .js الذي يتم فيه الإعلان عن وظيفة الحل:

 function solution(input) { // ... } 

تنسيق الإدخال / الإخراج والملاحظات

تنسيق الإدخال


يأتي جدول HTML كسلسلة:

 <table> <colgroup> <col align="right" /> <col /> <col align="center" /> </colgroup> <thead> <tr> <td>Command </td> <td>Description </td> <th>Is implemented </th> </tr> </thead> <tbody> <tr> <th>git status</th> <td>List all new or modified files</td> <th>Yes</th> </tr> <tr> <th>git diff</th> <td>Show file differences that haven't been staged</td> <td>No</td> </tr> </tbody> </table> 

قد يحتوي الجدول على علامات colgroup و thead و tbody بترتيب ثابت. كل هذه العلامات اختيارية ، ولكن على الأقل سيكون هناك دائمًا tead أو tbody.

- يحتوي colgroup على علامات col يمكن أن تحتوي على سمة المحاذاة الاختيارية مع إحدى القيم الثلاث (يسار | مركز | يمين)
- thead و tbody يحتوي على 1 أو أكثر tr
- tr ، بدوره ، يحتوي على كل من td و th
- سيكون الجدول دائما صف واحد على الأقل. - سيكون للصف دائمًا خلية واحدة على الأقل. - يوجد رمز غير أبيض على الأقل في الخلية.
- يتطابق عدد العناصر th / td في الخطوط دائمًا بين جميع الخطوط ومع عدد عناصر col في colgroup ، إذا كان هناك colgroup.
- يمكن أن تحدث المسافات وفواصل الأسطر في مصدر HTML في أي مكان لا ينتهك صلاحية HTML.

تنسيق الإخراج


يجب أن يكون الإخراج عبارة عن خط مع العلامات الترميزية:

| Command | Description | **Is implemented** |
| ---: | :--- | :---: |
| **git status** | List all new or modified files | **Yes** |
| **git diff** | Show file differences that haven't been staged | No |


- يجب أن يتحول الصف الأول الذي يتم مواجهته في جدول دائمًا إلى صف رأس في علامة تمييز.
- جميع الصفوف الأخرى تذهب إلى جسم الجدول.
- يتم عرض فاصل الرأس دائمًا.
- يتم إدراج محتويات td كما هي ، محتويات th كـ ** bold **.
- هناك دائمًا مسافة واحدة بين محتويات الخلية في علامة الترميز ومحددات الخلية (|).
- يجب إزالة المسافات على حواف محتويات علامات td و th.
- يجب حذف فواصل الأسطر في محتويات الخلية.
- يجب استبدال أكثر من مساحة واحدة على التوالي في محتويات الخلايا بمسافة واحدة.
- للمحاذاة في خلايا أعمدة جدول تخفيض السعر ، يكون تنسيق فاصل الرأس مسؤولاً:

| : --- | يعني اليسار المحاذاة
| : ---: | يعني محاذاة الوسط
| ---: | يعني المحاذاة الصحيحة

إذا لم يكن هناك سمة محاذاة محددة في علامة col ، فيجب تعيين المحاذاة إلى اليسار.

الملاحظات


- بالنسبة لخلاصة السطر ، يلزم استخدام الحرف \ n.
- سيتم اختبار الحل في بيئة مستعرض (Chrome 78) مع إمكانية الوصول إلى كائنات المستند والنافذة.
- يمكنك استخدام بناء الجملة حتى es2018 ضمناً.

قرار


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

بعد فحص الأمثلة ، يمكنك أن ترى أن التحويل بسيط للغاية. يوجد أدناه الكود الذي يمثل نص وظيفة الحل (الإدخال).

أولاً ، نحتاج إلى تحويل السلسلة من HTML إلى شجرة DOM:

 const div = document.createElement('div'); div.innerHTML = input; const table = div.firstChild; 

بعد تلقي شجرة DOM ، يمكننا فقط المرور عبرها ومعالجة البيانات من عقد DOM مختلفة. للقيام بذلك ، يكفي تجاوز تسلسل العناصر المختلفة لعناصر DOM:

 const processors = { 'colgroup': processColgroup, 'thead': processThead, 'tbody': processTbody, }; for (let child of table.children) { processors[child.tagName.toLowerCase()](child); } 

من علامات colgroup و col ، نحن مهتمون بمعرفة محاذاة أعمدة الجدول:

 const alignments = []; const defaultAlign = 'left'; const processColgroup = (colgroup) => { alignments.push(...Array(...colgroup.children).map(col => { return col.align || defaultAlign; })); }; 

في العلامات ، tbody و tr ، نحن مهتمون فقط بالأطفال:

 const rows = []; const processThead = (thead) => { rows.push(...Array(...thead.children).map(processTr)); }; const processTbody = (tbody) => { rows.push(...Array(...tbody.children).map(processTr)); }; const processTr = (tr) => { return Array(...tr.children).map(processCell); }; 

من المهم ألا ننسى أنه ، وفقًا للاتفاقية ، يتم تنسيق td و th بشكل مختلف:

 const processCell = (cell) => { const tag = cell.tagName.toLowerCase(); const content = clearString(cell.innerHTML); return { 'td': content, 'th': `**${content}**`, }[tag]; }; 

للعمل مع محتوى اختبار DOM ، يجب عليك تلبية المتطلبات الموضحة في الشرط:

 const clearLineBreaks = (str) => str.replace(/\r?\n|\r/g, ''); const clearSpaces = (str) => str.replace(/\s+/g, ' '); const clearString = (str) => clearSpaces(clearLineBreaks(str)).trim(); 

بعد أن تجولنا حول شجرة DOM ، تم كتابة الجزء الأكبر من طاولتنا إلى مجموعة الصفوف:

[
["Command","Description","**Is implemented**"],
["**git status**","List all new or modified files","**Yes**"],
["**git diff**","Show file differences that haven't been staged","No"]
]


كانت معلومات محاذاة العمود في صفيف المحاذاة:

["right","left","center"]

من المهم أن تتذكر أن معلومات محاذاة العمود قد لا تكون في الإدخال:

 const updateAlignments = () => { if (alignments.length > 0) return; alignments.push(...rows[0].map(x => defaultAlign)); }; updateAlignments(); 

تحويل المحاذاة إلى النموذج النهائي:

 const alignmentsContents = alignments.map(align => { return { 'left': ' :--- ', 'center': ' :---: ', 'right': ' ---: ' }[align]; }); const delimiter = `|${alignmentsContents.join('|')}|`; 

مثال على قيمة المحدد:

"| ---: | :--- | :---: |"

ستكون الخطوة الأخيرة هي تشكيل خط تخفيض السعر الذي يحتوي على جميع البيانات المقروءة من HTML:

 const lineEnd = '\n'; rows.forEach((row, i) => { if (i > 0) markdown += lineEnd; const mdRow = `| ${row.join(' | ')} |`; markdown += mdRow; if (i === 0) { markdown += lineEnd; markdown += delimiter; } }); return markdown; 

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

E. وباء الفيروس

المؤلفون: أندريه موكروسوف ، إيفان بيتوخوف

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

يذكر التقرير أن الفيروس قد تم تنشيطه من خلال النظر في استخدام الوسيطة الأولى من الدالة التي تم تمريرها بواسطة الوسيطة إلى استدعاء دالة Zyn ، أي أن الشخص المصاب لا يمكنه إظهار تعبير مثل Zyn (دالة (a ، b ، c) {console.log (a)}).

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

حول رمز AST & Co ، نعلم أن:

- هو مكتوب في ES3 ،
- الوصول إلى خصائص كائن ممكن من خلال نقطة وعبر الأقواس (ab و ['b']) ،
- يمكن تخزين جزء من التعبير في متغير ، لكن لا يتم تمريره إلى الدالة بواسطة المعلمة (a (x) - محظور) ،
- لا توجد وظائف ترجع جزءًا من التعبير المطلوب ،
— , ,
— (a[x], x — ),
— , . . var a = x; a = y; var a = b = 1.


CommonJS-, , (ast) .

ast-, callback-, Zyn , .

 module.exports = function (ast) { ... return [...]; } 

الملاحظات


.

 /** *   .     , *   callback- onNodeEnter (  ) *  onNodeLeave (  )    *     (  Scope ). * * @param {object} ast  ast. * @param {Function} [onNodeEnter=(node, scope)=>{}]       . * @param {Function} [onNodeLeave=(node, scope)=>{}]       . */ function traverse( ast, onNodeEnter = (node, scope) => {}, onNodeLeave = (node, scope) => {} ) { const rootScope = new Scope(ast); _inner(ast, rootScope); /** *    . *     scope,   . * * @param {object} astNode ast-. * @param {Scope} currentScope   . * @return {Scope}      astNode. */ function resolveScope(astNode, currentScope) { let isFunctionExpression = ast.type === 'FunctionExpression', isFunctionDeclaration = ast.type === 'FunctionDeclaration'; if (!isFunctionExpression && !isFunctionDeclaration) { //      . return currentScope; } //      . const newScope = new Scope(ast, currentScope); ast.params.forEach(param => { //     . newScope.add(param.name); }); if (isFunctionDeclaration) { //       . currentScope.add(ast.id.name); } else { //  -    . newScope.add(ast.id.name); } return newScope; } /** *    ast. * * @param {object} astNode  ast-. * @param {Scope} scope     ast-. */ function _inner(astNode, scope) { if (Array.isArray(astNode)) { astNode.forEach(node => { /*    . *  , ,  . */ _inner(node, scope); }); } else if (astNode && typeof astNode === 'object') { onNodeEnter(astNode, scope); const innerScope = resolveScope(astNode, scope), keys = Object.keys(astNode).filter(key => { // loc -  ,   ast-. return key !== 'loc' && astNode[key] && typeof astNode[key] === 'object'; }); keys.forEach(key => { //   . _inner(astNode[key], innerScope); }); onNodeLeave(astNode, scope); } } } /** *   . * * @class Scope (name) * @param {object} astNode ast-,    . * @param {object} parentScope   . */ function Scope(astNode, parentScope) { this._node = astNode; this._parent = parentScope; this._vars = new Set(); } Scope.prototype = { /** *      . * * @param {string} name  . */ add(name) { this._vars.add(name); }, /** *       . * * @param {string} name  . * @return {boolean}          . */ isDefined(name) { return this._vars.has(name) || (this._parent && this._parent.isDefined(name)); } }; 

قرار


.

— ES3
, . , .

— , (ab a['b'])
Zyn, Z['y'].n, Zy['n'] Z['y']['n'].

, (a(x) — )
, . , : var x = Zy; xn(...).

— , ,
— , ,
— , .. var a = x; a = y; var a = b = 1.
( ) , - .

— , (a[x], x — )
, : var x = 'y'; Z[x].n(...).

C :
1. , , .
2. , .

, , — . 2.



: Zyn(function(a, b, c){...}), — .

FunctionExpression — CallExpression, callee — MemberExpression. property — n, object ( MemberExpression object property y) — Z.

, — — . — Identifier , MemberExpression ObjectLiteral (xa var x = {a: ...} ).

 +++ b/traverse.js @@ -120,3 +120,59 @@ Scope.prototype = { return this._vars.has(name) || (this._parent && this._parent.isDefined(name)); } }; + +module.exports = function (ast) { + var result = []; + + traverse(ast, (node, scope) => { + if (node.type !== 'CallExpression') { + return; + } + let args = node.arguments; + if (args.length !== 1 || + args[0].type !== 'FunctionExpression') { + return; + } + let callee = node.callee; + if (callee.type !== 'MemberExpression') { + return; + } + let property = callee.property, + object = callee.object; + if (property.name !== 'n') { + return; + } + if (object.type !== 'MemberExpression') { + return; + } + property = object.property; + object = object.object; + if (property.name !== 'y') { + return; + } + if (object.type !== 'Identifier' || + object.name !== 'Z') { + return; + } + + checkFunction(args[0]); + }); + + function checkFunction(ast) { + let firstArg = ast.params[0]; + if (!firstArg) { + return; + } + + traverse(ast.body, (node, scope) => { + if (node.type !== 'Identifier') { + return; + } + if (node.name === firstArg.name) { + result.push(node); + } + }); + } + + return result; +}; 

traverse , , MemberExpression ObjectProperty. :

 --- a/traverse.js +++ b/traverse.js @@ -60,16 +60,16 @@ function traverse( * @param {object} astNode  ast- * @param {Scope} scope     ast- */ - function _inner(astNode, scope) { + function _inner(astNode, scope, parent) { if (Array.isArray(astNode)) { astNode.forEach(node => { /*    . *  , ,   */ - _inner(node, scope); + _inner(node, scope, parent); }); } else if (astNode && typeof astNode === 'object') { - onNodeEnter(astNode, scope); + onNodeEnter(astNode, scope, parent); const innerScope = resolveScope(astNode, scope), keys = Object.keys(astNode).filter(key => { @@ -80,10 +80,10 @@ function traverse( keys.forEach(key => { //    - _inner(astNode[key], innerScope); + _inner(astNode[key], innerScope, astNode); }); - onNodeLeave(astNode, scope); + onNodeLeave(astNode, scope, parent); } } } @@ -164,10 +164,22 @@ module.exports = function (ast) { return; } - traverse(ast.body, (node, scope) => { + traverse(ast.body, (node, scope, parent) => { if (node.type !== 'Identifier') { return; } + if (!parent) { + return; + } + if (parent.type === 'MemberExpression' && + parent.computed === false && + parent.property === node) { + return; + } + if (parent.type === 'ObjectProperty' && + parent.key === node) { + return; + } if (node.name === firstArg.name) { result.push(node); } 

. getPropName:

 --- a/traverse.js +++ b/traverse.js @@ -121,6 +121,18 @@ Scope.prototype = { } }; +function getPropName(node) { + let prop = node.property; + + if (!node.computed) { + return prop.name; + } + + if (prop.type === 'StringLiteral') { + return prop.value; + } +} + module.exports = function (ast) { var result = []; @@ -137,17 +149,17 @@ module.exports = function (ast) { if (callee.type !== 'MemberExpression') { return; } - let property = callee.property, + let property = getPropName(callee), object = callee.object; - if (property.name !== 'n') { + if (property !== 'n') { return; } if (object.type !== 'MemberExpression') { return; } - property = object.property; + property = getPropName(object); object = object.object; - if (property.name !== 'y') { + if (property !== 'y') { return; } if (object.type !== 'Identifier' || 

: . . 1.

Scope

Scope . , , traverse:

 --- a/traverse.js +++ b/traverse.js @@ -1,3 +1,12 @@ +const scopeStorage = new Map(); + +function getScopeFor(ast, outerScope) { + if (!scopeStorage.has(ast)) { + scopeStorage.set(ast, new Scope(ast, outerScope)); + } + + return scopeStorage.get(ast); +} /** *   .     , *   callback- onNodeEnter (  ). @@ -13,7 +22,7 @@ function traverse( onNodeEnter = (node, scope) => {}, onNodeLeave = (node, scope) => {} ) { - const rootScope = new Scope(ast); + const rootScope = getScopeFor(ast); _inner(ast, rootScope); @@ -36,19 +45,19 @@ function traverse( } //      . - const newScope = new Scope(ast, currentScope); + const newScope = getScopeFor(ast, currentScope); ast.params.forEach(param => { //     . - newScope.add(param.name); + newScope.add(param.name, param); }); if (isFunctionDeclaration) { //       . - currentScope.add(ast.id.name); + currentScope.add(ast.id.name, ast); } else if (ast.id) { //  -    . - newScope.add(ast.id.name); + newScope.add(ast.id.name, ast); } return newScope; @@ -98,7 +107,7 @@ function traverse( function Scope(astNode, parentScope) { this._node = astNode; this._parent = parentScope; - this._vars = new Set(); + this._vars = new Map(); } Scope.prototype = { @@ -107,8 +116,24 @@ Scope.prototype = { * * @param {string} name   */ - add(name) { - this._vars.add(name); + add(name, value) { + this._vars.set(name, { + value: value, + scope: this + }); + }, + resolve(node) { + if (!node) { + return node; + } + if (node.type === 'Identifier') { + let value = this._vars.get(node.name); + if (value) { + return value; + } + value = (this._parent && this._parent.resolve(node)); + return value; + } }, /** *       . @@ -136,6 +161,12 @@ function getPropName(node) { module.exports = function (ast) { var result = []; + traverse(ast, (node, scope) => { + if (node.type === 'VariableDeclarator') { + scope.add(node.id.name, node.init); + } + }); + traverse(ast, (node, scope) => { if (node.type !== 'CallExpression') { return; 

Scope

. , Scope . , Scope , :

 --- a/traverse.js +++ b/traverse.js @@ -146,13 +146,17 @@ Scope.prototype = { } }; -function getPropName(node) { +function getPropName(node, scope) { let prop = node.property; if (!node.computed) { return prop.name; } + let resolved = scope.resolve(prop); + if (resolved) { + prop = resolved.value; + } if (prop.type === 'StringLiteral') { return prop.value; } @@ -177,22 +181,43 @@ module.exports = function (ast) { return; } let callee = node.callee; + + let resolved = scope.resolve(callee); + if (resolved) { + callee = resolved.value; + scope = resolved.scope; + } + if (callee.type !== 'MemberExpression') { return; } - let property = getPropName(callee), + let property = getPropName(callee, scope), object = callee.object; if (property !== 'n') { return; } + + resolved = scope.resolve(object); + if (resolved) { + object = resolved.value; + scope = resolved.scope; + } + if (object.type !== 'MemberExpression') { return; } - property = getPropName(object); + property = getPropName(object, scope); object = object.object; if (property !== 'y') { return; } + + resolved = scope.resolve(object); + if (resolved) { + object = resolved.value; + scope = resolved.scope; + } + if (object.type !== 'Identifier' || object.name !== 'Z') { return; 



: . :

— , Z — , - .
— , , .
— , var a = 'x', b = a.

, .

 --- a/traverse.js +++ b/traverse.js @@ -128,10 +128,23 @@ Scope.prototype = { } if (node.type === 'Identifier') { let value = this._vars.get(node.name); - if (value) { - return value; + if (!value) { + if (this._parent) { + value = this._parent.resolve(node); + } else { + //   scope,  node — + //   . + this.add(node.name, node); + return this.resolve(node); + } + } + if (!value) { + return; + } + if (value.value.type === 'Identifier' && + value.value !== node) { + return value.scope.resolve(value.value) || value; } - value = (this._parent && this._parent.resolve(node)); return value; } }, @@ -165,12 +178,15 @@ function getPropName(node, scope) { module.exports = function (ast) { var result = []; + traverse(ast, (node, scope) => { if (node.type === 'VariableDeclarator') { scope.add(node.id.name, node.init); } }); + let rootScope = getScopeFor(ast); + traverse(ast, (node, scope) => { if (node.type !== 'CallExpression') { return; @@ -213,9 +229,10 @@ module.exports = function (ast) { } resolved = scope.resolve(object); + let zScope; if (resolved) { object = resolved.value; - scope = resolved.scope; + zScope = resolved.scope; } if (object.type !== 'Identifier' || @@ -223,6 +240,10 @@ module.exports = function (ast) { return; } + if (zScope && zScope !== rootScope) { + return; + } + checkFunction(args[0]); }); @@ -232,7 +253,10 @@ module.exports = function (ast) { return; } - traverse(ast.body, (node, scope, parent) => { + traverse(ast, (node, scope, parent) => { + if (parent === ast) { + return; + } if (node.type !== 'Identifier') { return; } @@ -248,7 +272,9 @@ module.exports = function (ast) { parent.key === node) { return; } - if (node.name === firstArg.name) { + + let resolved = scope.resolve(node); + if (resolved && resolved.value === firstArg) { result.push(node); } }); 

:

 const scopeStorage = new Map(); function getScopeFor(ast, outerScope) { if (!scopeStorage.has(ast)) { scopeStorage.set(ast, new Scope(ast, outerScope)); } return scopeStorage.get(ast); } /** *   .     , *  callback- onNodeEnter (  ) *  onNodeLeave (  )    *     (  Scope ) * * @param {object} ast  ast * @param {Function} [onNodeEnter=(node, scope)=>{}]        * @param {Function} [onNodeLeave=(node, scope)=>{}]        */ function traverse( ast, onNodeEnter = (node, scope) => {}, onNodeLeave = (node, scope) => {} ) { const rootScope = getScopeFor(ast); _inner(ast, rootScope); /** *    . *     scope,    * * @param {object} ast ast- * @param {Scope} currentScope    * @return {Scope}      astNode */ function resolveScope(ast, currentScope) { let isFunctionExpression = ast.type === 'FunctionExpression', isFunctionDeclaration = ast.type === 'FunctionDeclaration'; if (!isFunctionExpression && !isFunctionDeclaration) { //       return currentScope; } //       const newScope = getScopeFor(ast, currentScope); ast.params.forEach(param => { //      newScope.add(param.name, param); }); if (isFunctionDeclaration) { //        currentScope.add(ast.id.name, ast); } else if (ast.id) { //  -     newScope.add(ast.id.name, ast); } return newScope; } /** *    ast * * @param {object} astNode  ast- * @param {Scope} scope     ast- */ function _inner(astNode, scope, parent) { if (Array.isArray(astNode)) { astNode.forEach(node => { /*    . *  , ,   */ _inner(node, scope, parent); }); } else if (astNode && typeof astNode === 'object') { onNodeEnter(astNode, scope, parent); const innerScope = resolveScope(astNode, scope), keys = Object.keys(astNode).filter(key => { // loc -  ,   ast- return key !== 'loc' && astNode[key] && typeof astNode[key] === 'object'; }); keys.forEach(key => { //    _inner(astNode[key], innerScope, astNode); }); onNodeLeave(astNode, scope, parent); } } } /** *    * * @class Scope (name) * @param {object} astNode ast-,     * @param {object} parentScope    */ function Scope(astNode, parentScope) { this._node = astNode; this._parent = parentScope; this._vars = new Map(); } Scope.prototype = { /** *       * * @param {string} name   */ add(name, value) { this._vars.set(name, { value: value, scope: this }); }, resolve(node) { if (!node) { return node; } if (node.type === 'Identifier') { let value = this._vars.get(node.name); if (!value) { if (this._parent) { value = this._parent.resolve(node); } else { //   scope,  node - //    this.add(node.name, node); return this.resolve(node); } } if (!value) { return; } if (value.value.type === 'Identifier' && value.value !== node) { return value.scope.resolve(value.value) || value; } return value; } }, /** *       . * * @param {string} name   * @return {boolean}           */ isDefined(name) { return this._vars.has(name) || (this._parent && this._parent.isDefined(name)); } }; function getPropName(node, scope) { let prop = node.property; if (!node.computed) { return prop.name; } let resolved = scope.resolve(prop); if (resolved) { prop = resolved.value; } if (prop.type === 'StringLiteral') { return prop.value; } } module.exports = function (ast) { var result = []; traverse(ast, (node, scope) => { if (node.type === 'VariableDeclarator') { scope.add(node.id.name, node.init); } }); let rootScope = getScopeFor(ast); traverse(ast, (node, scope) => { if (node.type !== 'CallExpression') { return; } let args = node.arguments; if (args.length !== 1 || args[0].type !== 'FunctionExpression') { return; } let callee = node.callee; let resolved = scope.resolve(callee); if (resolved) { callee = resolved.value; scope = resolved.scope; } if (callee.type !== 'MemberExpression') { return; } let property = getPropName(callee, scope), object = callee.object; if (property !== 'n') { return; } resolved = scope.resolve(object); if (resolved) { object = resolved.value; scope = resolved.scope; } if (object.type !== 'MemberExpression') { return; } property = getPropName(object, scope); object = object.object; if (property !== 'y') { return; } resolved = scope.resolve(object); let zScope; if (resolved) { object = resolved.value; zScope = resolved.scope; } if (object.type !== 'Identifier' || object.name !== 'Z') { return; } if (zScope && zScope !== rootScope) { return; } checkFunction(args[0]); }); function checkFunction(ast) { let firstArg = ast.params[0]; if (!firstArg) { return; } traverse(ast, (node, scope, parent) => { if (parent === ast) { return; } if (node.type !== 'Identifier') { return; } if (!parent) { return; } if (parent.type === 'MemberExpression' && parent.computed === false && parent.property === node) { return; } if (parent.type === 'ObjectProperty' && parent.key === node) { return; } let resolved = scope.resolve(node); if (resolved && resolved.value === firstArg) { result.push(node); } }); } return result; }; 

F. Framework-

: , collapsus

API. — , . .


— . . , . !

. , . , , , ( ). , , 0 (0 , 0 , 0 ).

, , . JavaScript JS- Framework.

: , . ( , ). () . ( ).

0. , ( time) .



 const ONE_SECOND_DEGREES = 6; const ONE_SECOND_FACTOR = 1 / Framework.SPEED * ONE_SECOND_DEGREES; class MyClock extends Framework.Clock { constructor() { super(); this.arrows.push(new Framework.Arrow("seconds", { color: "red" })); this.arrows.push(new Framework.Arrow("minutes", { weight: 3, length: 80 })); this.arrows.push(new Framework.Arrow("hours", { weight: 3, length: 60 })); this.buttons.push(new Framework.Button("A", () => { alert("A"); })); this.tick = 0; } onBeforeTick() { const [arrow] = this.arrows; this.tick++; arrow.rotateFactor = this.tick % 10 ? 0 : ONE_SECOND_FACTOR; console.log("before: " + arrow.pos); } onAfterTick() { const [arrow] = this.arrows; console.log("after: " + arrow.pos); } } 

:
— — , ,
— ,
— , ; (100 ) ; , .

قرار


, -, « », . , , , .

: , . . , , .

:

 const TPS = 1000 / Framework.INTERVAL; //    

// .

 function getTarget(ticks, planet) { const { h, m, s } = planet; //    const ts = Math.floor(ticks / TPS); //   const ss = ts % s * 360 / s; const mm = Math.floor(ts / s) % m * 360 / m; const hh = Math.floor(ts / (s * m)) % h * 360 / h; return { hh, mm, ss }; } 

, — rotateFactor. getRotateFactor, , , . :
1. ,
2. .

. .

 function getRotateFactor(pos, target, forward = true) { let angle = target - pos; //        if (forward) { //      angle < 0 && (angle += 360); //        0  360 ( 360   0),    } else { //         Math.abs(angle) > 180 && (angle -= Math.sign(angle) * 360) } return angle / Framework.SPEED; } 

, MAX_SPEED . getRotateFactor.

 const MAX_FACTOR = Framework.MAX_SPEED / Framework.SPEED; function getRotateFactor(pos, target, forward = true) { let angle = target - pos; if (forward) { angle < 0 && (angle += 360); } else { Math.abs(angle) > 180 && (angle -= Math.sign(angle) * 360) } const factor = angle / Framework.SPEED; //      ,    return Math.abs(factor) > MAX_FACTOR ? Math.sign(factor) * MAX_FACTOR : factor; } 

:

 buttonAHandler() { //     this.pos = (this.pos + 1) % this.planets.length; //      this.forward = false; } 

, :

 onBeforeTick() { const [sec, min, hour] = this.arrows; const time = ++this.ticks; const planet = this.planets[this.pos]; //        const target = getTarget(time, planet); //      sec.rotateFactor = getRotateFactor(sec.pos, target.ss, this.forward); min.rotateFactor = getRotateFactor(min.pos, target.mm, this.forward); hour.rotateFactor = getRotateFactor(hour.pos, target.hh, this.forward); //       ,       !sec.rotateFactor && !min.rotateFactor && !hour.rotateFactor && (this.forward = true); } 

:

 const TPS = 1000 / Framework.INTERVAL; const MAX_FACTOR = Framework.MAX_SPEED / Framework.SPEED; function getTarget(ticks, planet) { const { h, m, s } = planet; const ts = Math.floor(ticks / TPS); // total seconds const ss = ts % s * 360 / s; const mm = Math.floor(ts / s) % m * 360 / m; const hh = Math.floor(ts / (s * m)) % h * 360 / h; return { hh, mm, ss }; } function getRotateFactor(pos, target, forward = true) { let angle = target - pos; if (forward) { angle < 0 && (angle += 360); } else { Math.abs(angle) > 180 && (angle -= Math.sign(angle) * 360) } const factor = angle / Framework.SPEED; return Math.abs(factor) > MAX_FACTOR ? Math.sign(factor) * MAX_FACTOR : factor; } class MyClock extends Clock { // planets -   // [ { h: 4, m: 20, s: 10 }, ... ] constructor({ planets, time }) { super(); this.arrows.push(new Arrow('seconds', { color: 'red' })); this.arrows.push(new Arrow('minutes', { weight: 3, length: 80 })); this.arrows.push(new Arrow('hours', { weight: 3, length: 60 })); this.buttons.push(new Button('Switch', this.buttonAHandler.bind(this))); this.planets = planets; this.ticks = time * TPS; this.pos = 0; this.forward = false; } onBeforeTick() { const [sec, min, hour] = this.arrows; const time = ++this.ticks; const planet = this.planets[this.pos]; const target = getTarget(time, planet); sec.rotateFactor = getRotateFactor(sec.pos, target.ss, this.forward); min.rotateFactor = getRotateFactor(min.pos, target.mm, this.forward); hour.rotateFactor = getRotateFactor(hour.pos, target.hh, this.forward); !sec.rotateFactor && !min.rotateFactor && !hour.rotateFactor && (this.forward = true); } buttonAHandler() { this.pos = (this.pos + 1) % this.planets.length; this.forward = false; } } 



. . , , , .

: , , , , (, , ).

استنتاج

. . — , . .

, . , ( ) 18 .



المراجع:

ML-
-
-

Source: https://habr.com/ru/post/ar478550/


All Articles