Comment gérer une montre? Analyse de la piste front-end du deuxième championnat de programmation

Un nouvel habrapost dans une série d'analyses du récent championnat. Les participants à la qualification qui ont choisi la section frontend ont dû résoudre plusieurs tâches de complexité très différente: la première (selon nos attentes) a pris 20 minutes, la dernière - environ une heure. Nous avons testé un large éventail de compétences de développeur d'interface, y compris la capacité de comprendre un sujet inhabituel.

A. l'anéantir

Auteurs: Maxim Sysoev, Konstantin Petryaev

La première tâche est un échauffement. Chaque participant a obtenu l'une des quatre options pour la tâche, similaires les unes aux autres. Nous avons proposé non seulement une condition textuelle, mais aussi une «mauvaise» solution récursive. Il était nécessaire de refaire le code (écrire un algorithme gourmand qui a produit la solution la plus rapide), en supprimant la récursivité et diverses absurdités comme les opérations et les calculs inutiles.

Condition


Vous avez obtenu un emploi dans un laboratoire pour l'étude de l'antimatière, où ils mènent diverses expériences. Votre service étudie les processus qui se produisent lors de la combinaison de la matière et de l'antimatière. Vous devez mener une série d'expériences sur un certain nombre de molécules.

Le département voisin a développé un appareil qui transforme la matière en antimatière pendant une courte période. Il vous sera utile pour mener des expériences dans lesquelles l'algorithme suivant est utilisé:

- On retrouve 2 des molécules les plus lourdes.
- Nous transformons l'un d'eux en antimatière.
- Combinez-les. De plus, si le poids est le même, ils sont anéantis. Si le poids est différent, alors nous obtenons une nouvelle molécule, dont le poids est égal à la différence de poids des deux précédents. La molécule résultante elle-même est de la matière.
- S'il reste une molécule, vous devez connaître son poids. S'il y a beaucoup de molécules, nous revenons à l'étape 1.

Vous devez connaître la molécule de ce poids qui restera à la fin de l'expérience, cette connaissance est nécessaire aux scientifiques d'un autre département.

Le développeur précédent a esquissé le code qui était impliqué dans ces calculs, mais le code ne peut pas terminer les calculs lorsque l'expérience est effectuée sur un grand nombre de molécules. Vous devez affiner le code afin qu'il fonctionne dans un délai raisonnable.

Code hérité de vous

En entrée, vous aurez un tableau avec des poids moléculaires. En sortie, vous devez renvoyer un nombre qui indique le poids de la dernière molécule. S'il ne reste plus de molécules, il est nécessaire de retourner 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); } 

Exemple et notes

Exemple


Entrée: [2,7,4,1,8,1]
Sortie: 1

Nous prenons des molécules d'un poids de 7 et 8, transformons le 7 en antimolécule et le heurtons avec une molécule de poids 8. Il reste une molécule de poids 1. Les poids des molécules d'acier restantes [2,4,1,1,1]. Nous prenons des molécules d'un poids de 2 et 4, transformons 2 en antimolécule et la heurtons avec une molécule de poids 4. Il reste une molécule de poids 2. Les poids des molécules d'acier restantes [2,1,1,1]. Nous prenons des molécules d'un poids de 2 et 1, transformons 1 en antimolécule et le heurtons avec une molécule de poids 2. Il reste une molécule de poids 1. Les poids des molécules d'acier restantes [1,1,1]. Nous prenons des molécules d'un poids de 1 et 1, transformons l'une d'elles en antimolécule et la heurtons avec la seconde. Ils sont anéantis. Les poids des molécules restantes [1]. Il reste une molécule. Le résultat est 1.

Remarques


Comme solution, fournissez un fichier qui exporte la version corrigée de la fonction findLatestWeight:

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

La solution s'exécutera dans Node.js 12.

Solution


La «mauvaise» solution proposée présente plusieurs problèmes à la fois. Le premier est la récursivité. Comme indiqué dans la condition, nous traiterons de grands tableaux de nombres, ce qui élimine immédiatement une solution récursive.

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

Développer la récursion ici est assez simple, mais un autre problème se pose - il y a un nouveau tri constant (du plus petit au plus grand) et fonctionne avec la fin du tableau. En conséquence, nous obtenons une diminution de l'avant-dernier élément du tableau. Mais après cela, nous ne coupons pas le tableau, et si un tableau d'un million d'éléments a été passé à la fonction, nous allons le trier à nouveau jusqu'à la fin.

Une option pour résoudre ce problème consiste à essayer de découper constamment le tableau.

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

Pas mal, mais il faut aussi se débarrasser du tri qui est en soi une opération coûteuse. Dans l'ensemble, à tout moment, nous serons intéressés par les 2 plus grands membres du tableau. Autrement dit, c'est une recherche de deux sommets, qui se fait en un seul passage est assez simple. Pour plus de commodité, nous effectuons une telle recherche dans une fonction distincte.

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

Et nous changeons la fonction de recherche comme suit:

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

Ainsi, nous allons toujours mettre à zéro le plus petit des deux éléments et transformer le plus grand en différence entre eux. Nous nous sommes débarrassés du tri et avons obtenu un passage linéaire à la place.

Parmi les erreurs courantes que nous avons remarquées, les participants ont pris l'élément maximal, l'ont multiplié par –1 et l'ont ajouté à la deuxième plus grande pierre. Le résultat est un nombre négatif, qui a ensuite été utilisé dans le calcul «en l'état». De plus, la tâche a un piège mental associé au fait que vous pouvez essayer de laisser des pierres de poids unique et en calculer la différence. Cependant, cette approche ne donne pas le résultat correct.

B. BEM

Auteurs: Eugene Mishchenko, Vladimir Grinenko tadatuta

Condition


Layout Alexander est impliqué dans de nombreux projets utilisant la méthodologie BEM. Il a même créé un plugin pratique pour son IDE préféré, qui lui permet d'écrire les noms de classe dans une notation abrégée et de les déployer au maximum. Mais le problème est que pour chaque projet, les gens définissent différents délimiteurs entre le bloc, l'élément et le modificateur (block__mod__val - elem, block - mod - val ___ elem), et chaque fois qu'il doit le modifier manuellement dans son plugin. Aidez Alexander à écrire un module qui déterminera le séparateur des entités en fonction de la classe. La règle pour les délimiteurs est un nombre arbitraire de caractères (pas de lettres). Exemples de notations possibles (les modificateurs d'un bloc dans les données d'entrée peuvent être sans valeur):

 block_mod__elem // ,     block_mod_mod__elem block__elem_mod_mod 

Clarifications:
- Les cours dans les projets sont Ă©crits uniquement en petites lettres.
- Une chaîne avec une classe CSS valide est envoyée à l'entrée du module.

Le module doit renvoyer une réponse du formulaire:

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

Le module doit ĂŞtre Ă©mis en tant que module JS commun:

 module.exports = function(str) { } 

Solution


La deuxième tâche a pris environ 20 minutes. Avec son aide, nous avons voulu tester la connaissance des expressions régulières parmi les participants.

De la condition, nous apprenons qu'une chaîne contenant une classe CSS valide avec des restrictions supplémentaires viendra à l'entrée de la fonction, dans laquelle les séquences de lettres sont séparées par des séquences arbitraires de caractères non-lettres. Notre tâche est de trouver des séparateurs et de comprendre leur sémantique.

La première partie du nom de classe sera toujours le nom du bloc. Il s'agit d'une séquence d'une ou plusieurs lettres. Nous écrivons l'expression régulière correspondante: [az] +.

Nous aurons besoin d'expressions similaires pour rechercher les parties restantes: le nom du modificateur et sa valeur, ou le nom de l'élément avec le modificateur et la valeur correspondants.

Pour rechercher des délimiteurs, nous avons besoin de séquences non-lettres, l'expression: [^ az] + convient.

Mettez-le ensemble et définissez les groupes dont nous utiliserons les valeurs:

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

Vous devez maintenant vous assurer que nous avons correctement défini la sémantique des groupes trouvés. Vous pouvez profiter du fait que seul un modificateur peut se rencontrer deux fois.

Nous allons écrire une fonction qui prendra la chaîne d'origine et le séparateur trouvé pour calculer le nombre d'occurrences:

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

S'il s'avère que le délimiteur se produit deux fois, et mod - une fois, alors en fait, c'est le contraire. La décision finale:

 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. Clone Factory

Auteurs: Dmitry Andriyanov dima117 , Alexey Gusev

Condition


À l'extérieur de la fenêtre est 2319. Les entreprises clonent les employés qui réussissent pour effectuer des tâches complexes.

Dans la production de clones, ils ont décidé d'étiqueter de nouveaux «produits» avec un tatouage de code à barres sur leur épaule - pour distinguer les clones les uns des autres.

Aidez le personnel de l'usine Ă  Ă©crire une fonction qui dessinera un code-barres avec des informations sur le clone.

Format des informations de clonage

Les informations sur le clone sont stockées comme suit:

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

Algorithme de rendu de code-barres

Les codes-barres utilisés dans la fabrique de clones ressemblent à ceci:



Le code-barres a une taille fixe - 148 par 156 pixels. Autour du périmètre du code à barres se trouvent des cadres en noir et blanc de 3 pixels chacun. À l'intérieur des cadres se trouve le contenu du code-barres, composé de 18 lignes de 17 carrés noirs ou blancs par ligne. La taille de chaque carré est de 8 x 8 pixels.

Les carrés blancs dans le contenu codent 0, noir - 1.

Algorithme de génération de contenu de code-barres

À l'intersection de la première ligne et de la première colonne de contenu, un carré est dessiné qui code le sexe du clone. La valeur de femelle est codée par zéro (blanc), mâle par un (noir).

De plus, une ligne de la forme <id> <name> est formée à partir des champs id et name. Le champ de nom est rempli d'espaces à la fin de 26 caractères maximum.

La chaîne résultante est convertie en un tableau d'octets - chaque caractère de la chaîne se voit attribuer le code ASCII correspondant (un nombre compris entre 0 et 255).

Ensuite, chaque élément du tableau résultant est traduit en notation binaire (huit caractères 0 ou 1) et codé par une séquence de huit carrés (0 - carré blanc, 1 - carré noir). Les carrés sont dessinés dans le contenu du code-barres de manière séquentielle et ligne par ligne.

La dernière ligne de contenu contient des informations de contrôle.

Algorithme de comptage des informations de contrĂ´le

Chaque carré de la ligne d'informations de contrôle détermine la parité de la somme des valeurs de contenu dans la colonne correspondante. Si la somme des zéros et des uns dans la colonne est paire, un carré blanc est tracé dans les informations de contrôle, sinon, un carré noir.

Format de la solution et exemples
Format de la solution

La solution que vous chargez doit contenir la fonction 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" } 

Code Ă  barres:



Exemple 2


Informations sur le clone:

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

Code Ă  barres:


Solution


Il était nécessaire de former correctement la représentation binaire des données, de calculer la somme de contrôle correspondante et de dessiner ces données dans la présentation. Essayons de faire cela aussi simple et frontal que possible - sans optimisation de code.

Commençons par la représentation binaire. Tout d'abord, déclarez les fonctions d'assistance:

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

Nous formons à partir des données source une chaîne composée de zéros et de uns:

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

Ensuite, Ă©crivez la disposition et les styles de notre code-barres:

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

Rendre les données binaires dans la mise en page:

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

Il reste Ă  calculer et afficher la somme de contrĂ´le. Cela peut ĂŞtre fait comme ceci:

 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. Automatisez-le

Auteurs: Vladimir Rusov, Dmitry Kanatnikov

Dans chacune des options de qualification, il y avait une tâche où une page HTML avec un tableau ou une liste était proposée en entrée. Les tâches de cette série avaient une légende différente, mais elles se résumaient toutes au fait que vous devez amener la page dans un format similaire à Markdown. Nous analyserons la solution à l'un des problèmes.

Condition


Sur le portail national de prestation de services, ils ont permis de demander des documents de manière entièrement automatique, pour cela, il vous suffit de remplir un tableau avec des données personnelles.

Ces données sont ensuite transférées pour vérification à plusieurs autorités, dont le ministère de l'Intérieur. Après le début des tests, il s'est avéré que le ministère de l'Intérieur accepte les données au format Markdown et que les services d'État utilisent le format HTML. Aidez-moi à écrire un script pour migrer d'un format à un autre afin que les gars commencent le plus tôt possible.

Vous devez écrire une fonction qui prend un tableau HTML en entrée et le convertit en balisage de type Markdown.

Pour résoudre cette tâche, envoyez le fichier .js dans lequel la fonction solution est déclarée:

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

Format d'entrée / sortie et notes

Format d'entrée


Le tableau HTML se présente sous la forme d'une chaîne:

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

La table peut contenir des balises colgroup, thead et tbody dans un ordre fixe. Toutes ces balises sont facultatives, mais au moins la tête ou le corps seront toujours présents.

- colgroup contient des balises col qui peuvent avoir l'attribut align facultatif avec l'une des trois valeurs (gauche | centre | droite)
- la tĂŞte et le corps contiennent 1 ou plusieurs tr
- tr, Ă  son tour, contient Ă  la fois td et th
- La table aura toujours au moins une ligne. - La ligne aura toujours au moins une cellule. - Au moins un symbole non blanc est toujours présent dans la cellule.
- Le nombre d'éléments th / td dans les lignes coïncide toujours entre toutes les lignes et avec le nombre d'éléments col dans colgroup, s'il y a colgroup.
- Les espaces et les sauts de ligne dans le HTML source peuvent se produire n'importe où qui ne viole pas la validité du HTML.

Format de sortie


La sortie doit ĂŞtre une ligne avec un balisage Markdown:

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


- La première ligne rencontrée dans un tableau doit toujours se transformer en ligne d'en-tête dans le balisage Markdown.
- Toutes les autres lignes vont au corps du tableau.
- Le séparateur d'en-tête est toujours affiché.
- Le contenu de td est inséré tel quel, le contenu de th comme ** bold **.
- Il y a toujours un espace entre le contenu de la cellule dans le balisage de démarque et les délimiteurs de cellule (|).
- Les espaces sur les bords du contenu des balises td et th doivent être supprimés.
- Les sauts de ligne dans le contenu des cellules doivent être supprimés.
- Plus d'un espace d'affilée dans le contenu des cellules doit être remplacé par un espace.
- Pour l'alignement dans les cellules des colonnes du tableau Markdown, le formatage du séparateur d'en-tête est responsable:

| : --- | signifie alignement Ă  gauche
| : ---: | signifie alignement central
| ---: | signifie alignement Ă  droite

Si aucun attribut align n'est spécifié dans la balise col, l'alignement doit être défini à gauche.

Remarques


- Pour le saut de ligne, vous devez utiliser le caractère \ n.
- La solution sera testée dans un environnement de navigateur (Chrome 78) avec accès aux documents et objets fenêtre.
- Vous pouvez utiliser la syntaxe jusqu'Ă  es2018 inclus.

Solution


Le problème est résolu en traversant simplement l'arborescence DOM de la table. La prise en charge de l'arborescence DOM est implémentée au niveau du navigateur, elle en fait partie intégrante, il n'y aura donc aucun problème. Pour résoudre le problème, il suffit de traduire l'arborescence DOM du HTML au balisage Markdown.

Après avoir examiné les exemples, vous pouvez voir que la conversion est assez simple. Voici le code qui est le corps de la fonction solution (entrée).

Tout d'abord, nous devons convertir la chaîne de HTML en arborescence DOM:

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

Après avoir reçu un arbre DOM, nous pouvons simplement le parcourir et traiter les données de différents nœuds DOM. Pour ce faire, il suffit de contourner récursivement la séquence des enfants de divers éléments DOM:

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

À partir des balises colgroup et col, nous souhaitons connaître l'alignement des colonnes du tableau:

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

Dans les balises thead, tbody et tr, nous ne nous intéressons qu'aux enfants:

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

Il est important de ne pas oublier que, par convention, td et th sont formatés différemment:

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

Pour travailler avec le contenu de test du DOM, vous devez remplir les conditions décrites dans la condition:

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

Après avoir parcouru l'arborescence DOM, la majeure partie de notre table a été écrite dans le tableau de lignes:

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


Les informations d'alignement des colonnes se trouvaient dans le tableau d'alignements:

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

Il est important de se rappeler que les informations d'alignement des colonnes peuvent ne pas être dans l'entrée:

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

Convertissez les alignements en forme finale:

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

Exemple de valeur de délimiteur:

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

La dernière étape sera la formation d'une ligne Markdown contenant toutes les données lues à partir du 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; 

La construction de retour signifie que tout le code ci-dessus était le corps de la fonction solution (entrée). À la suite de cette fonction, nous obtenons le code de table Markdown souhaité affiché dans l'exemple de sortie de la condition de tâche.

E. Virus pandémique

Auteurs: Andrey Mokrousov, Ivan Petukhov

L'Organisation mondiale de la santé a publié un rapport sur les signes d'une pandémie imminente d'un nouveau virus qui menace les développeurs frontaux. Il est connu que le virus ne se manifeste pas tant que l'hôte n'a pas vu le code JS contenant une expression. Dès que la personne infectée a vu cette expression, elle perd sa capacité à écrire du code dans JS et commence à écrire spontanément du code dans Fortran.

Le rapport mentionne que le virus est activé en regardant en utilisant le premier argument de la fonction passée par l'argument à l'appel de fonction Zyn, c'est-à-dire qu'une personne infectée ne peut pas afficher une expression comme Zyn (fonction (a, b, c) {console.log (a)}).

Afin de ne pas perdre accidentellement tout leur front-end, AST & Co a décidé de vérifier si leur code contient l'expression ci-dessus. Aidez les ingénieurs de l'entreprise à rédiger un tel chèque.

Ă€ propos du code d'AST & Co, nous savons que:

- il est Ă©crit en ES3,
- l'accès aux propriétés d'un objet est possible à la fois par un point et par des crochets (ab et a ['b']),
- une partie de l'expression peut être stockée dans une variable, mais elle n'est jamais transmise à la fonction par le paramètre (a (x) - interdit),
- aucune fonction ne renvoie une partie de l'expression souhaitée,
— , ,
— (a[x], x — ),
— , . . var a = x; a = y; var a = b = 1.


CommonJS-, , (ast) .

ast-, callback-, Zyn , .

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

Remarques


.

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

Solution


.

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

Solution


, -, « », . , , , .

: , . . , , .

:

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



. . , , , .

: , , , , (, , ).

Conclusion

. . — , . .

, . , ( ) 18 .



Références:

— ML-
— -
— -

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


All Articles