¿Cómo gestionar un reloj? Análisis de la pista front-end del segundo campeonato de programación.

Un nuevo habrapost en una serie de análisis del reciente campeonato. Los participantes de calificación que eligieron la sección de interfaz tuvieron que resolver varias tareas de complejidad muy diferente: la primera (de acuerdo con nuestras expectativas) tomó 20 minutos, la última, aproximadamente una hora. Pusimos a prueba una amplia gama de habilidades para desarrolladores de interfaces, incluida la capacidad de comprender un área temática inusual.

A. Aniquilarlo

Autores: Maxim Sysoev, Konstantin Petryaev

La primera tarea es un calentamiento. Cada participante tiene una de las cuatro opciones para la tarea, similar entre sí. Propusimos no solo una condición textual, sino también una solución recursiva “mala”. Era necesario rehacer el código (escribir un algoritmo codicioso que produjera la solución más rápida), eliminando la recursión y varias tonterías como operaciones y cálculos innecesarios.

Condición


Conseguiste un trabajo en un laboratorio para el estudio de la antimateria, donde realizan varios experimentos. Su departamento está estudiando los procesos que ocurren al combinar materia y antimateria. Debe realizar una serie de experimentos con un cierto número de moléculas.

El departamento vecino ha desarrollado un aparato que convierte la materia en antimateria por un corto tiempo. Será útil para realizar experimentos en los que se utiliza el siguiente algoritmo:

- Encontramos 2 de las moléculas más pesadas.
- Convertimos uno de ellos en antimateria.
- Combínalos. Además, si el peso es el mismo, se aniquilan. Si el peso es diferente, entonces obtenemos una nueva molécula, cuyo peso es igual a la diferencia en los pesos de los dos anteriores. La molécula resultante en sí misma es la materia.
- Si queda una molécula, debe averiguar su peso. Si hay muchas moléculas, volvemos al paso 1.

Debe averiguar la molécula de qué peso permanecerá al final del experimento, los científicos de otro departamento necesitan este conocimiento.

El desarrollador anterior bosquejó el código que estaba involucrado en estos cálculos, pero el código no puede finalizar los cálculos cuando el experimento se lleva a cabo en una gran cantidad de moléculas. Debe refinar el código para que funcione en un período de tiempo razonable.

Código heredado para ti

Como entrada, tendrá una matriz con pesos moleculares. Como salida, debe devolver un número que indique el peso de la última molécula. Si no quedan moléculas, entonces es necesario devolver 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); } 

Ejemplo y notas

Ejemplo


Entrada: [2,7,4,1,8,1]
Salida: 1

Tomamos moléculas con un peso de 7 y 8, convertimos 7 en un antimolécula y lo chocamos con una molécula de peso 8. Queda una molécula de peso 1. Los pesos de las moléculas restantes de acero [2,4,1,1,1]. Tomamos moléculas con un peso de 2 y 4, convertimos 2 en una antimolécula y la colisionamos con una molécula de peso 4. Queda una molécula de peso 2. Los pesos de las moléculas restantes de acero [2,1,1,1]. Tomamos moléculas con un peso de 2 y 1, convertimos 1 en una antimolécula y colisionamos con una molécula de peso 2. Queda una molécula de peso 1. Los pesos de las moléculas restantes de acero [1,1,1]. Tomamos moléculas con un peso de 1 y 1, convertimos una de ellas en una antimolécula y la chocamos con la segunda. Ellos son aniquilados. Los pesos de las moléculas restantes [1]. Queda una molécula. El resultado es 1.

Notas


Como solución, proporcione un archivo que exporte la versión corregida de la función findLatestWeight:

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

La solución se ejecutará en Node.js 12.

Solución


La solución "mala" proporcionada tiene varios problemas a la vez. El primero es la recursividad. Como se indica en la condición, procesaremos grandes matrices de números, lo que elimina inmediatamente una solución recursiva.

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

Expandir la recursividad aquí es bastante simple, pero surge otro problema: hay una reordenación constante (de menor a mayor) y funciona con el final de la matriz. Como resultado, obtenemos una disminución en el penúltimo elemento en la matriz. Pero después de eso no recortamos la matriz, y si se pasó una matriz de un millón de elementos a la función, la volveremos a ordenar hasta el final.

Una opción para resolver este problema es intentar recortar constantemente la matriz.

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

No está mal, pero también tenemos que deshacernos de la clasificación, que en sí misma es una operación costosa. En general, en cualquier momento dado, estaremos interesados ​​en los 2 miembros más grandes de la matriz. Es decir, es una búsqueda de dos máximos, lo que se hace en una sola pasada es bastante simple. Por conveniencia, llevamos a cabo dicha búsqueda en una función separada.

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

Y cambiamos la función de búsqueda de la siguiente manera:

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

Por lo tanto, siempre pondremos a cero el menor de los dos elementos y convertiremos el mayor en la diferencia entre ellos. Nos deshicimos de la clasificación y obtuvimos una pasada lineal.

De los errores comunes que notamos, los participantes tomaron el elemento máximo, lo multiplicaron por -1 y lo agregaron a la segunda piedra más grande. El resultado es un número negativo, que luego se utilizó en el cálculo "tal cual". Además, la tarea tiene una trampa mental asociada con el hecho de que puedes tratar de dejar piedras de peso único y calcular la diferencia a partir de ellas. Sin embargo, este enfoque no da el resultado correcto.

B. BEM

Autores: Eugene Mishchenko, Vladimir Grinenko tadatuta

Condición


Diseño Alexander participa en muchos proyectos utilizando la metodología BEM. Incluso creó un práctico complemento para su IDE favorito, que le permite escribir nombres de clase en una notación abreviada y desplegarlos al máximo. Pero el problema es que para cada proyecto, las personas establecen diferentes delimitadores entre el bloque, el elemento y el modificador (block__mod__val - elem, block - mod - val ___ elem), y cada vez que tiene que editar esto manualmente en su complemento. Ayuda a Alexander a escribir un módulo que determinará el separador para las entidades en función de la clase. La regla para los delimitadores es un número arbitrario de caracteres (no letras). Ejemplos de notaciones posibles (los modificadores para un bloque en los datos de entrada pueden no tener valor):

 block_mod__elem // ,     block_mod_mod__elem block__elem_mod_mod 

Aclaraciones:
- Las clases en proyectos se escriben solo en letras minúsculas.
- Una cadena con una clase CSS válida se alimenta a la entrada del módulo.

El módulo debe devolver una respuesta del formulario:

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

El módulo debe emitirse como un módulo commonJS:

 module.exports = function(str) { } 

Solución


La segunda tarea tomó unos 20 minutos. Con su ayuda, queríamos probar el conocimiento de las expresiones regulares entre los participantes.

A partir de la condición, aprendemos que una cadena que contiene una clase CSS válida con restricciones adicionales llegará a la entrada de la función, en la que las secuencias de letras están separadas por secuencias arbitrarias de caracteres que no son letras. Nuestra tarea es encontrar separadores y comprender su semántica.

La primera parte del nombre de la clase siempre será el nombre del bloque. Esta es una secuencia de una o más letras. Escribimos la expresión regular correspondiente: [az] +.

Necesitaremos expresiones similares para buscar las partes restantes: el nombre del modificador y su valor, o el nombre del elemento con el correspondiente modificador y valor.

Para buscar delimitadores, necesitamos secuencias que no sean letras, la expresión: [^ az] + es adecuada.

Póngalo junto y defina los grupos cuyos valores usaremos:

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

Ahora debe asegurarse de que definimos correctamente la semántica de los grupos encontrados. Puede aprovechar el hecho de que solo un modificador puede reunirse dos veces.

Escribiremos una función que tomará la cadena original y el separador encontrado para calcular el número de ocurrencias:

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

Si resulta que el elemento delimitador se produce dos veces, y mod - una vez, de hecho, lo contrario es cierto. La decisión final:

 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. Fábrica de clones

Autores: Dmitry Andriyanov dima117 , Alexey Gusev

Condición


Fuera de la ventana es 2319. Las corporaciones clonan a empleados exitosos para realizar tareas complejas.

En la producción de clones, decidieron etiquetar nuevos "productos" con un tatuaje de código de barras en el hombro, para distinguir los clones entre sí.

Ayude al personal de la fábrica a escribir una función que dibuje un código de barras con información sobre el clon.

Formato de información de clonación

La información sobre el clon se almacena de la siguiente manera:

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

Algoritmo de representación de código de barras

Los códigos de barras que se usan en la fábrica de clones se ven así:



El código de barras tiene un tamaño fijo: 148 por 156 píxeles. Alrededor del perímetro del código de barras hay marcos en blanco y negro de 3 píxeles cada uno. Dentro de los marcos está el contenido del código de barras, que consta de 18 líneas de 17 cuadrados negros o blancos por línea. El tamaño de cada cuadrado es de 8 por 8 píxeles.

Los cuadrados blancos en el contenido codifican 0, negro - 1.

Algoritmo de generación de contenido de código de barras

En la intersección de la primera fila y la primera columna de contenido, se dibuja un cuadrado que codifica el género del clon. El valor de hembra está codificado por cero (blanco), macho por uno (negro).

Además, se forma una línea de la forma <id> <nombre> a partir de los campos id y name. El campo de nombre se rellena con espacios al final de hasta 26 caracteres.

La cadena resultante se convierte en una matriz de bytes: a cada carácter de la cadena se le asigna el código ASCII correspondiente (un número de 0 a 255).

Luego, cada elemento de la matriz resultante se traduce a notación binaria (ocho caracteres 0 o 1) y se codifica mediante una secuencia de ocho cuadrados (0 - cuarto blanco, 1 - cuadrado negro). Los cuadrados se dibujan en el contenido del código de barras secuencialmente y línea por línea.

La última línea de contenido contiene información de control.

Algoritmo de recuento de información de control

Cada cuadrado en la línea de información de control determina la paridad de la suma de los valores de contenido en la columna correspondiente. Si la suma de ceros y unos en la columna es par, entonces se dibuja un cuadrado blanco en la información de control, de lo contrario, un cuadrado negro.

Formato de solución y ejemplos
Formato de solución

La solución que cargue debe contener la función 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" } 

Código de barras:



Ejemplo 2


Información de clonación:

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

Código de barras:


Solución


Era necesario formar correctamente la representación binaria de los datos, calcular la suma de comprobación y dibujar estos datos en el diseño. Intentemos hacer esto de la manera más simple y directa posible, sin optimizaciones de código.

Comencemos con la representación binaria. Primero, declare las funciones de ayuda:

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

A partir de los datos de origen, formamos una cadena que consta de ceros y unos:

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

Luego escriba el diseño y los estilos para nuestro código de barras:

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

Representar datos binarios en el diseño:

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

Queda por calcular y mostrar la suma de verificación. Esto se puede hacer así:

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

Autores: Vladimir Rusov, Dmitry Kanatnikov

En cada una de las opciones de calificación, hubo una tarea en la que se propuso una página HTML con una tabla o lista como entrada. Las tareas de esta serie tenían una leyenda diferente, pero todas se reducían al hecho de que necesita llevar la página a un formato similar a Markdown. Analizaremos la solución a uno de los problemas.

Condición


En el portal estatal para la provisión de servicios, hicieron posible enviar una solicitud de documentos de forma completamente automática, para esto solo necesita completar una tabla con datos personales.

Luego, estos datos se transfieren para su verificación a varias autoridades, incluido el Ministerio del Interior. Después del inicio de las pruebas, resultó que el Ministerio del Interior acepta datos en formato Markdown y los Servicios del Estado usan el formato HTML. Ayúdame a escribir un script para migrar un formato a otro para que los chicos comiencen lo antes posible.

Debe escribir una función que tome una tabla HTML como entrada y la convierta en marcado similar a Markdown.

Como solución a esta tarea, envíe el archivo .js en el que se declara la función de solución:

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

Formato de entrada / salida y notas

Formato de entrada


La tabla HTML viene como una cadena:

 <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 tabla puede contener etiquetas colgroup, thead y tbody en un orden fijo. Todas estas etiquetas son opcionales, pero al menos siempre estarán presentes thead o tbody.

- colgroup contiene etiquetas col que pueden tener el atributo opcional de alineación con uno de los tres valores (izquierda | centro | derecha)
- thead y tbody contienen 1 o más tr
- tr, a su vez, contiene td y th
- La tabla siempre tendrá al menos una fila. - La fila siempre tendrá al menos una celda. - Al menos un símbolo que no sea un espacio en blanco siempre está presente en la celda.
- El número de elementos th / td en líneas siempre coincide entre todas las líneas y con el número de elementos col en colgroup, si hay colgroup.
- Los espacios y los saltos de línea en el código fuente HTML pueden ocurrir en cualquier lugar que no viole la validez del código HTML.

Formato de salida


La salida debe ser una línea con marcado 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 primera fila encontrada en una tabla siempre debe convertirse en una fila de encabezado en el marcado Markdown.
- Todas las demás filas van al cuerpo de la mesa.
- El separador de encabezado siempre se muestra.
- Los contenidos de td se insertan tal cual, los contenidos de th como ** negrita **.
- Siempre hay un espacio entre el contenido de la celda en el marcado de reducción y los delimitadores de celda (|).
- Deben eliminarse los espacios en los bordes del contenido de las etiquetas td y th.
- Se deben eliminar los saltos de línea en el contenido de la celda.
- Más de un espacio en una fila en el contenido de las celdas debe reemplazarse por un espacio.
- Para la alineación en las celdas de las columnas de la tabla Markdown, el formato del separador de encabezado es responsable:

El | : --- | significa alineación izquierda
El | : ---: | significa alineación central
El | ---: | significa alineación correcta

Si no hay un atributo de alineación especificado en la etiqueta col, la alineación debe establecerse a la izquierda.

Notas


- Para el avance de línea necesita usar el carácter \ n.
- La solución se probará en un entorno de navegador (Chrome 78) con acceso a documentos y ventanas.
- Puede usar la sintaxis hasta es2018 inclusive.

Solución


El problema se resuelve simplemente atravesando el árbol DOM de la tabla. El soporte para el árbol DOM se implementa a nivel del navegador, es una parte integral del mismo, por lo que no habrá problemas. Para resolver el problema, es suficiente traducir el árbol DOM de HTML a marcado Markdown.

Después de examinar los ejemplos, puede ver que la conversión es bastante simple. A continuación se muestra el código que es el cuerpo de la función de solución (entrada).

Primero, necesitamos convertir la cadena de HTML al árbol DOM:

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

Habiendo recibido un árbol DOM, podemos revisarlo y procesar datos de diferentes nodos DOM. Para hacer esto, es suficiente omitir recursivamente la secuencia de elementos secundarios de varios elementos DOM:

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

A partir de las etiquetas colgroup y col, nos interesa conocer la alineación de las columnas de la tabla:

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

En las etiquetas thead, tbody y tr, solo nos interesan los niños:

 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 importante no olvidar que, por convención, td y th tienen un formato diferente:

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

Para trabajar con el contenido de prueba del DOM, debe cumplir con los requisitos descritos en la condición:

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

Después de caminar alrededor del árbol DOM, la mayor parte de nuestra tabla se escribió en la matriz de filas:

[
["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 información de alineación de la columna estaba en la matriz de alineaciones:

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

Es importante recordar que la información de alineación de columnas puede no estar en la entrada:

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

Convierta las alineaciones a la forma final:

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

Valor delimitador de ejemplo:

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

El paso final será la formación de una línea Markdown que contenga todos los datos leídos del 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 construcción de retorno significa que todo el código anterior era el cuerpo de la función de solución (entrada). Como resultado de esta función, obtenemos el código de la tabla Markdown deseado que se muestra en el resultado de ejemplo de la condición de la tarea.

E. Virus pandémico

Autores: Andrey Mokrousov, Ivan Petukhov

La Organización Mundial de la Salud ha publicado un informe sobre signos de una pandemia inminente de un nuevo virus que amenaza a los desarrolladores front-end. Se sabe que el virus no se manifiesta hasta que el host ve el código JS que contiene alguna expresión. Tan pronto como la persona infectada vio esta expresión, pierde su capacidad de escribir código en JS y comienza a escribir código espontáneamente en Fortran.

El informe menciona que el virus se activa al observar el uso del primer argumento de la función pasada por el argumento a la llamada a la función Zyn, es decir, una persona infectada no puede mostrar una expresión como Zyn (function (a, b, c) {console.log (a)}).

Para no perder accidentalmente todo su front-end, AST & Co decidió verificar si su código contiene la expresión anterior. Ayude a los ingenieros de la compañía a escribir tal cheque.

Sobre el código de AST & Co, sabemos que:

- está escrito en ES3,
- el acceso a las propiedades de un objeto es posible a través de un punto y entre paréntesis (ab y a ['b']),
- parte de la expresión se puede almacenar en una variable, pero nunca se pasa a la función por el parámetro (a (x) - prohibido),
— , ,
— , ,
— (a[x], x — ),
— , . . var a = x; a = y; var a = b = 1.

Formato de solución


CommonJS-, , (ast) .

ast-, callback-, Zyn , .

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


.

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

Solución


.

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

Solución


, -, « », . , , , .

: , . . , , .

:

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



. . , , , .

: , , , , (, , ).

Conclusión

. . — , . .

, . , ( ) 18 .



:

ML-
-
-

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


All Articles