Wie verwalte ich eine Uhr? Analyse der Front-End-Strecke der zweiten Programmiermeisterschaft

Ein neuer Habrapost in einer Reihe von Analysen der jüngsten Meisterschaft. Qualifikationsteilnehmer, die sich für den Frontend-Bereich entschieden hatten, mussten mehrere Aufgaben unterschiedlichster Komplexität lösen: Die erste (nach unseren Erwartungen) dauerte 20 Minuten, die letzte etwa eine Stunde. Wir haben eine breite Palette von Entwicklerfähigkeiten für Benutzeroberflächen getestet, einschließlich der Fähigkeit, ein ungewöhnliches Fachgebiet zu verstehen.

A. Vernichte es

Autoren: Maxim Sysoev, Konstantin Petryaev

Die erste Aufgabe ist das Aufwärmen. Jeder Teilnehmer hat eine von vier Optionen für die Aufgabe, die einander ähnlich sind. Wir schlugen nicht nur eine Textbedingung vor, sondern auch eine „schlechte“ rekursive Lösung. Es war notwendig, den Code zu wiederholen (einen gierigen Algorithmus zu schreiben, der die schnellste Lösung hervorbrachte), Rekursion und verschiedene Unsinn wie unnötige Operationen und Berechnungen zu entfernen.

Zustand


Sie haben einen Job in einem Labor für die Untersuchung von Antimaterie, wo sie verschiedene Experimente durchführen. Ihre Abteilung untersucht die Prozesse, die beim Kombinieren von Materie und Antimaterie auftreten. Sie müssen eine Reihe von Experimenten mit einer bestimmten Anzahl von Molekülen durchführen.

Die benachbarte Abteilung hat einen Apparat entwickelt, der Materie für kurze Zeit in Antimaterie verwandelt. Es wird Ihnen bei der Durchführung von Experimenten nützlich sein, bei denen der folgende Algorithmus verwendet wird:

- Wir finden 2 der schwersten Moleküle.
- Wir verwandeln einen in Antimaterie.
- Kombiniere sie. Darüber hinaus werden sie vernichtet, wenn das Gewicht gleich ist. Wenn das Gewicht unterschiedlich ist, erhalten wir ein neues Molekül, dessen Gewicht gleich dem Gewichtsunterschied der beiden vorherigen ist. Das resultierende Molekül selbst ist Materie.
- Wenn noch ein Molekül übrig ist, müssen Sie dessen Gewicht herausfinden. Wenn es viele Moleküle gibt, kehren wir zu Schritt 1 zurück.

Sie müssen das Molekül herausfinden, dessen Gewicht am Ende des Experiments verbleibt. Dieses Wissen wird von Wissenschaftlern einer anderen Abteilung benötigt.

Der vorherige Entwickler skizzierte den Code, der an diesen Berechnungen beteiligt war, aber der Code kann die Berechnungen nicht beenden, wenn das Experiment mit einer großen Anzahl von Molekülen durchgeführt wird. Sie müssen den Code verfeinern, damit er in angemessener Zeit funktioniert.

Code, der an Sie vererbt wurde

Als Eingabe erhalten Sie ein Array mit Molekulargewichten. Als Ausgabe müssen Sie eine Zahl zurückgeben, die das Gewicht des letzten Moleküls angibt. Wenn keine Moleküle mehr vorhanden sind, muss 0 zurückgegeben werden.

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); } 

Beispiel und Notizen

Beispiel


Eintritt: [2,7,4,1,8,1]
Ausgang: 1

Wir nehmen Moleküle mit einem Gewicht von 7 und 8, verwandeln 7 in ein Antimolekül und kollidieren mit einem Molekül mit Gewicht 8. Es bleibt ein Molekül mit Gewicht 1. Das Gewicht der verbleibenden Stahlmoleküle [2,4,1,1,1]. Wir nehmen Moleküle mit einem Gewicht von 2 und 4, verwandeln 2 in ein Antimolekül und kollidieren mit einem Molekül mit Gewicht 4. Es bleibt ein Molekül mit Gewicht 2. Das Gewicht der verbleibenden Stahlmoleküle [2,1,1,1]. Wir nehmen Moleküle mit einem Gewicht von 2 und 1, verwandeln 1 in ein Antimolekül und kollidieren mit einem Molekül mit Gewicht 2. Es bleibt ein Molekül mit Gewicht 1. Das Gewicht der verbleibenden Stahlmoleküle [1,1,1]. Wir nehmen Moleküle mit einem Gewicht von 1 und 1, machen aus einem ein Antimolekül und kollidieren mit dem zweiten. Sie werden vernichtet. Das Gewicht der verbleibenden Moleküle [1]. Ein Molekül übrig. Das Ergebnis ist 1.

Hinweise


Stellen Sie als Lösung eine Datei bereit, die die korrigierte Version der Funktion findLatestWeight exportiert:

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

Die Lösung wird in Node.js 12 ausgeführt.

Lösung


Die bereitgestellte "schlechte" Lösung hat mehrere Probleme gleichzeitig. Der erste ist die Rekursion. Wie in der Bedingung angegeben, verarbeiten wir große Anordnungen von Zahlen, wodurch eine rekursive Lösung sofort beseitigt wird.

 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); } 

Die Erweiterung der Rekursion ist hier recht einfach, es tritt jedoch ein weiteres Problem auf: Es wird ständig neu sortiert (von klein nach groß) und mit dem Ende des Arrays gearbeitet. Als Ergebnis erhalten wir eine Abnahme des vorletzten Elements im Array. Aber danach schneiden wir das Array nicht zu und wenn ein Array von einer Million Elementen an die Funktion übergeben wurde, werden wir es bis zum Ende neu sortieren.

Eine Möglichkeit, dieses Problem zu lösen, besteht darin, das Array ständig zu trimmen.

 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); } 

Nicht schlecht, aber wir müssen auch das Sortieren loswerden, was an sich eine teure Operation ist. Im Großen und Ganzen werden wir uns zu jeder Zeit für die 2 größten Mitglieder des Arrays interessieren. Das heißt, es ist eine Suche nach zwei Höhen, die ganz einfach in einem Durchgang durchgeführt wird. Der Einfachheit halber führen wir eine solche Suche in einer separaten Funktion durch.

 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]; }; 

Und wir ändern die Suchfunktion wie folgt:

 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); }; 

Daher werden wir immer das kleinere der beiden Elemente auf Null setzen und das größere in den Unterschied zwischen ihnen umwandeln. Wir haben die Sortierung aufgegeben und stattdessen einen linearen Durchgang erhalten.

Von den bekannten Fehlern nahmen die Teilnehmer das maximale Element, multiplizierten es mit –1 und addierten es zum zweitgrößten Stein. Das Ergebnis ist eine negative Zahl, die dann in der Ist-Berechnung verwendet wurde. Darüber hinaus hat die Aufgabe eine mentale Falle, die damit verbunden ist, dass Sie versuchen können, Steine ​​mit einem einzigartigen Gewicht zu belassen und die Differenz daraus zu berechnen. Dieser Ansatz liefert jedoch nicht das richtige Ergebnis.

B. BEM

Autoren: Eugene Mishchenko, Vladimir Grinenko tadatuta

Zustand


Layout Alexander ist an vielen Projekten mit der BEM-Methodik beteiligt. Er hat sogar ein praktisches Plugin für seine Lieblings-IDE erstellt, mit dem er Klassennamen in einer Kurznotation schreiben und vollständig bereitstellen kann. Das Problem ist jedoch, dass für jedes Projekt unterschiedliche Trennzeichen zwischen Block, Element und Modifikator (block__mod__val - elem, block - mod - val ___ elem) festgelegt werden und jedes Mal, wenn er dies manuell in seinem Plugin bearbeiten muss. Helfen Sie Alexander, ein Modul zu schreiben, das das Trennzeichen für Entitäten basierend auf der Klasse bestimmt. Die Regel für Trennzeichen ist eine beliebige Anzahl von Zeichen (keine Buchstaben). Beispiele für mögliche Notationen (Modifikatoren für einen Block in den Eingabedaten können ohne Wert sein):

 block_mod__elem // ,     block_mod_mod__elem block__elem_mod_mod 

Erläuterungen:
- Klassen in Projekten werden nur in Kleinbuchstaben geschrieben.
- Ein String mit einer gültigen CSS-Klasse wird dem Eingang des Moduls zugeführt.

Das Modul sollte eine Antwort des Formulars zurückgeben:

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

Das Modul muss als commonJS-Modul ausgegeben werden:

 module.exports = function(str) { } 

Lösung


Die zweite Aufgabe dauerte ungefähr 20 Minuten. Mit ihrer Hilfe wollten wir das Wissen über reguläre Ausdrücke unter den Teilnehmern testen.

Aus der Bedingung erfahren wir, dass eine Zeichenfolge, die eine gültige CSS-Klasse mit zusätzlichen Einschränkungen enthält, zur Eingabe in die Funktion kommt, in der Buchstabenfolgen durch beliebige Folgen von Nichtbuchstaben getrennt sind. Unsere Aufgabe ist es, Separatoren zu finden und deren Semantik zu verstehen.

Der erste Teil des Klassennamens ist immer der Name des Blocks. Dies ist eine Folge von einem oder mehreren Buchstaben. Wir schreiben den entsprechenden regulären Ausdruck: [az] +.

Für die Suche nach den übrigen Teilen werden ähnliche Ausdrücke benötigt: der Name des Modifikators und sein Wert oder der Name des Elements mit dem entsprechenden Modifikator und Wert.

Um nach Trennzeichen zu suchen, benötigen wir Nicht-Buchstabenfolgen, der Ausdruck: [^ az] + ist geeignet.

Stellen Sie es zusammen und definieren Sie die Gruppen, deren Werte wir verwenden werden:

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

Jetzt müssen Sie sicherstellen, dass wir die Semantik der gefundenen Gruppen korrekt definiert haben. Sie können die Tatsache nutzen, dass sich nur ein Modifikator zweimal treffen kann.

Wir werden eine Funktion schreiben, die die ursprüngliche Zeichenfolge und das gefundene Trennzeichen verwendet, um die Anzahl der Vorkommen zu berechnen:

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

Wenn sich herausstellt, dass das Trennzeichenelement zweimal vorkommt und einmal modifiziert, ist das Gegenteil der Fall. Die endgültige Entscheidung:

 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 }; } 

C. Klonfabrik

Autoren: Dmitry Andriyanov dima117 , Alexey Gusev

Zustand


Außerhalb des Fensters ist 2319. Unternehmen klonen erfolgreiche Mitarbeiter, um komplexe Aufgaben zu erledigen.

Bei der Herstellung von Klonen entschieden sie sich, neue „Produkte“ mit einem Barcode-Tattoo auf der Schulter zu kennzeichnen, um die Klone voneinander zu unterscheiden.

Helfen Sie dem Fabrikpersonal, eine Funktion zu schreiben, mit der ein Barcode mit Informationen zum Klon erstellt wird.

Clone-Informationsformat

Informationen zum Klon werden wie folgt gespeichert:

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

Barcode-Rendering-Algorithmus

Die in der Klonfabrik verwendeten Barcodes sehen folgendermaßen aus:



Der Barcode hat eine feste Größe - 148 x 156 Pixel. Rund um den Umfang des Barcodes befinden sich schwarze und weiße Rahmen mit jeweils 3 Pixeln. Innerhalb der Rahmen befindet sich der Barcode-Inhalt, bestehend aus 18 Zeilen mit 17 schwarzen oder weißen Quadraten pro Zeile. Die Größe jedes Quadrats beträgt 8 x 8 Pixel.

Weiße Quadrate im Inhalt codieren 0, schwarz - 1.

Algorithmus zur Erzeugung von Barcode-Inhalten

Am Schnittpunkt der ersten Zeile und der ersten Inhaltsspalte wird ein Quadrat gezeichnet, das das Geschlecht des Klons codiert. Der Wert von weiblich wird durch null (weiß) und männlich durch eins (schwarz) codiert.

Ferner wird aus den Feldern id und name eine Zeile der Form <id> <name> gebildet. Das Namensfeld wird mit Leerzeichen am Ende von bis zu 26 Zeichen aufgefüllt.

Die resultierende Zeichenfolge wird in ein Byte-Array konvertiert. Jedem Zeichen der Zeichenfolge wird der entsprechende ASCII-Code zugewiesen (eine Zahl von 0 bis 255).

Dann wird jedes Element des resultierenden Arrays in eine Binärnotation (acht Zeichen 0 oder 1) übersetzt und durch eine Folge von acht Quadraten (0 - weißes Viertel, 1 - schwarzes Quadrat) codiert. Quadrate werden nacheinander und zeilenweise in den Barcode-Inhalt gezeichnet.

Die letzte Inhaltszeile enthält Steuerinformationen.

Zählalgorithmus für Steuerinformationen

Jedes Quadrat in der Kontrollinformationszeile bestimmt die Parität der Summe der Inhaltswerte in der entsprechenden Spalte. Wenn die Summe aus Nullen und Einsen in der Spalte gerade ist, wird in den Steuerinformationen ein weißes Quadrat gezeichnet, andernfalls ein schwarzes Quadrat.

Lösungsformat und Beispiele
Lösungsformat

Die geladene Lösung sollte die renderBarcode-Funktion enthalten:

 /** *       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" } 

Barcode:



Beispiel 2


Informationen zum Klonen:

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

Barcode:


Lösung


Es war notwendig, die Binärdarstellung der Daten korrekt zu bilden, die Prüfsumme dafür zu berechnen und diese Daten in das Layout zu zeichnen. Versuchen wir dies so einfach und Stirn wie möglich zu machen - ohne Code-Optimierungen.

Beginnen wir mit der binären Darstellung. Deklarieren Sie zunächst die Hilfsfunktionen:

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

Wir bilden aus den Quelldaten einen String bestehend aus Nullen und Einsen:

 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(''); 

Dann schreiben Sie das Layout und die Stile für unseren Barcode:

 //   ,    «» . //  ,      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'; 

Rendern von Binärdaten im Layout:

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

Es bleibt die Prüfsumme zu berechnen und anzuzeigen. Das kann so gemacht werden:

 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. Automatisieren Sie es

Autoren: Vladimir Rusov, Dmitry Kanatnikov

In jeder der Qualifizierungsoptionen gab es eine Aufgabe, bei der eine HTML-Seite mit einer Tabelle oder Liste als Eingabe vorgeschlagen wurde. Die Aufgaben dieser Reihe hatten eine andere Legende, aber alle beruhten darauf, dass Sie die Seite in ein Format bringen müssen, das Markdown ähnelt. Wir werden die Lösung für eines der Probleme analysieren.

Zustand


Auf dem Landesportal für die Erbringung von Dienstleistungen haben sie es ermöglicht, einen Antrag auf Unterlagen vollautomatisch einzureichen, dazu müssen Sie lediglich eine Tabelle mit personenbezogenen Daten ausfüllen.

Diese Daten werden dann zur Überprüfung an mehrere Behörden weitergeleitet, darunter das Innenministerium. Nach dem Teststart stellte sich heraus, dass das Innenministerium Daten im Markdown-Format akzeptiert und die staatlichen Dienste das HTML-Format verwenden. Hilf mir, ein Skript für die Migration eines Formats in ein anderes zu schreiben, damit die Jungs so schnell wie möglich anfangen.

Sie müssen eine Funktion schreiben, die eine HTML-Tabelle als Eingabe verwendet und in ein Markdown-ähnliches Markup konvertiert.

Senden Sie als Lösung für diese Aufgabe die JS-Datei, in der die Lösungsfunktion deklariert ist:

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

Eingabe- / Ausgabeformat und Notizen

Eingabeformat


Die HTML-Tabelle wird als String geliefert:

 <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> 

Die Tabelle kann colgroup-, thead- und tbody-Tags in einer festen Reihenfolge enthalten. Alle diese Tags sind optional, aber mindestens Thead oder Tbody wird immer vorhanden sein.

- colgroup enthält col-Tags, die das optionale align-Attribut mit einem von drei Werten haben können (left | center | right)
- Thead und Tbody enthalten 1 oder mehr tr
- tr enthält wiederum sowohl td als auch th
- Die Tabelle enthält immer mindestens eine Zeile. - Die Zeile enthält immer mindestens eine Zelle. - In der Zelle ist immer mindestens ein Nicht-Leerzeichensymbol vorhanden.
- Die Anzahl der th / td-Elemente in Zeilen stimmt immer mit der Anzahl der col-Elemente in colgroup überein, wenn colgroup vorhanden ist.
- Leerzeichen und Zeilenumbrüche im Quell-HTML können überall dort vorkommen, wo die Gültigkeit von HTML nicht verletzt wird.

Ausgabeformat


Die Ausgabe sollte eine Zeile mit Markdown-Markup sein:

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


- Die erste Zeile in einer Tabelle sollte im Markdown-Markup immer in eine Kopfzeile umgewandelt werden.
- Alle anderen Zeilen gehen zum Hauptteil der Tabelle.
- Das Header-Trennzeichen wird immer angezeigt.
- Der Inhalt von td wird unverändert eingefügt, der Inhalt in ** Fettdruck **.
- Zwischen dem Inhalt der Zelle im Markdown-Markup und den Zellentrennzeichen (|) befindet sich immer ein Leerzeichen.
- Leerzeichen an den Rändern des Inhalts der Tags td und th sollten entfernt werden.
- Zeilenumbrüche im Zelleninhalt müssen gelöscht werden.
- Es muss mehr als ein Leerzeichen hintereinander im Inhalt der Zellen durch ein Leerzeichen ersetzt werden.
- Für die Ausrichtung in den Zellen der Spalten der Markdown-Tabelle ist die Formatierung des Header-Trennzeichens verantwortlich:

| : --- | bedeutet linksbündig
| : ---: | bedeutet Mittenausrichtung
| ---: | bedeutet richtige Ausrichtung

Wenn im col-Tag kein Ausrichtungsattribut angegeben ist, sollte die Ausrichtung auf der linken Seite festgelegt werden.

Hinweise


- Für den Zeilenumbruch müssen Sie das Zeichen \ n verwenden.
- Die Lösung wird in einer Browserumgebung (Chrome 78) mit Zugriff auf Dokument- und Fensterobjekte getestet.
- Sie können Syntax bis einschließlich es2018 verwenden .

Lösung


Das Problem wird durch einfaches Durchlaufen des DOM-Baums der Tabelle gelöst. Die Unterstützung für den DOM-Baum wird auf Browserebene implementiert und ist ein wesentlicher Bestandteil davon, sodass es keine Probleme gibt. Um das Problem zu lösen, reicht es aus, den DOM-Baum von HTML in Markdown-Markup zu übersetzen.

Wenn Sie sich die Beispiele angesehen haben, können Sie feststellen, dass die Konvertierung recht einfach ist. Nachfolgend finden Sie den Code, der den Hauptteil der Lösungsfunktion (Eingabe) darstellt.

Zuerst müssen wir den String von HTML in den DOM-Baum konvertieren:

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

Nachdem wir einen DOM-Baum erhalten haben, können wir ihn einfach durchgehen und Daten von verschiedenen DOM-Knoten verarbeiten. Dazu reicht es aus, die Reihenfolge der untergeordneten Elemente verschiedener DOM-Elemente rekursiv zu umgehen:

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

Anhand der Tags colgroup und col möchten wir die Ausrichtung der Tabellenspalten kennen:

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

In den Tags Thead, Tbody und TR interessieren wir uns nur für Kinder:

 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); }; 

Es ist wichtig, nicht zu vergessen, dass td und th laut Konvention unterschiedlich formatiert sind:

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

Um mit dem Testinhalt des DOM zu arbeiten, müssen Sie die in der Bedingung beschriebenen Anforderungen erfüllen:

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

Nachdem wir den DOM-Baum umrundet hatten, wurde der Großteil unserer Tabelle in das Zeilenarray geschrieben:

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


Die Informationen zur Spaltenausrichtung befanden sich im Alignments-Array:

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

Beachten Sie, dass die Informationen zur Spaltenausrichtung möglicherweise nicht in der Eingabe enthalten sind:

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

Ausrichtungen in endgültige Form umwandeln:

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

Beispiel für einen Begrenzerwert:

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

Der letzte Schritt wird die Bildung einer Markdown-Zeile sein, die alle aus dem HTML gelesenen Daten enthält:

 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; 

Das Rückgabekonstrukt bedeutet, dass der gesamte obige Code der Hauptteil der Lösungsfunktion (Eingabe) war. Als Ergebnis dieser Funktion erhalten wir den gewünschten Markdown-Tabellencode, der in der Beispielausgabe der Taskbedingung gezeigt wird.

E. Pandemievirus

Autoren: Andrey Mokrousov, Ivan Petukhov

Die Weltgesundheitsorganisation hat einen Bericht über Anzeichen einer bevorstehenden Pandemie eines neuen Virus veröffentlicht, das Front-End-Entwickler bedroht. Es ist bekannt, dass sich der Virus erst manifestiert, wenn der Host den JS-Code sieht, der einen Ausdruck enthält. Sobald die infizierte Person diesen Ausdruck gesehen hat, verliert sie ihre Fähigkeit, Code in JS zu schreiben, und beginnt, spontan Code in Fortran zu schreiben.

In dem Bericht wird erwähnt, dass der Virus aktiviert wird, indem das erste Argument der Funktion verwendet wird, das vom Argument an den Zyn-Funktionsaufruf übergeben wird. Eine infizierte Person kann also keinen Ausdruck wie Zyn (function (a, b, c) {console.log (a)}) anzeigen.

Um nicht versehentlich das gesamte Front-End zu verlieren, hat AST & Co entschieden, zu überprüfen, ob der Code den obigen Ausdruck enthält. Helfen Sie den Ingenieuren des Unternehmens, einen solchen Scheck auszustellen.

Über den Code von AST & Co wissen wir, dass:

- es ist in ES3 geschrieben,
- Der Zugriff auf die Eigenschaften eines Objekts ist sowohl über einen Punkt als auch über eckige Klammern (ab und a ['b']) möglich.
- Ein Teil des Ausdrucks kann in einer Variablen gespeichert werden, er wird jedoch niemals vom Parameter (a (x) - verboten) an die Funktion übergeben.
- Es gibt keine Funktionen, die einen Teil des gewünschten Ausdrucks zurückgeben.
— , ,
— (a[x], x — ),
— , . . var a = x; a = y; var a = b = 1.


CommonJS-, , (ast) .

ast-, callback-, Zyn , .

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

Hinweise


.

 /** *   .     , *   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)); } }; 

Lösung


.

— 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. — , . .

Zustand


— . . , . !

. , . , , , ( ). , , 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 ) ; , .

Lösung


, -, « », . , , , .

: , . . , , .

:

 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); } 

:

Öffnen
 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; } } 



. . , , , .

: , , , , (, , ).

Fazit

. . — , . .

, . , ( ) 18 .



:

ML-
-
-

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


All Articles