How to manage a watch? Analysis of the frontend track of the second programming championship

A new habrapost in a series of analysis of the recent championship. Qualification participants who chose the frontend section had to solve several problems of very different complexity: the first (according to our expectations) took 20 minutes, the last - about an hour. We tested a wide range of interface developer skills, including the ability to understand an unusual subject area.



A. Annihilate it

Authors: Maxim Sysoev, Konstantin Petryaev



The first task is a warm-up. Each participant got one of four options for the task, similar to each other. We proposed not only a textual condition, but also a β€œbad” recursive solution. It was necessary to redo the code (write a greedy algorithm that produced the fastest solution), removing recursion and various nonsense like unnecessary operations and calculations.



Condition



You got a job in a laboratory for the study of antimatter, where they conduct various experiments. Your department is studying the processes that occur when combining matter and antimatter. You need to conduct a series of experiments on a certain number of molecules.



The neighboring department has developed an apparatus that turns matter into antimatter for a short time. It will be useful to you in conducting experiments in which the following algorithm is used:



- We find 2 of the heaviest molecules.

- We turn one of them into antimatter.

- Combine them. Moreover, if the weight is the same, they are annihilated. If the weight is different, then we get a new molecule, the weight of which is equal to the difference in the weights of the previous two. The resulting molecule itself is matter.

- If there is one molecule left, you need to find out its weight. If there are many molecules, we return to step 1.



You need to know the molecule of what weight will remain at the end of the experiment, this knowledge is needed by scientists of another department.



The previous developer sketched the code that was involved in these calculations, however, the code cannot finish the calculations when the experiment is carried out on a large number of molecules. You need to refine the code so that it works in a reasonable amount of time.



Code inherited to you



As input, you will have an array with molecular weights. As output, you need to return a number that indicates the weight of the last molecule. If there are no molecules left, then it is necessary to return 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); }
      
      





Example and notes

Example



Entrance: [2,7,4,1,8,1]

Output: 1



We take molecules with a weight of 7 and 8, turn 7 into an antimolecule and collide it with a molecule of weight 8. There remains a molecule of weight 1. The weights of the remaining molecules of steel [2,4,1,1,1]. We take molecules with a weight of 2 and 4, turn 2 into an antimolecule and collide it with a molecule of weight 4. There remains a molecule of weight 2. The weights of the remaining molecules of steel [2,1,1,1]. We take molecules with a weight of 2 and 1, turn 1 into an antimolecule and collide it with a molecule of weight 2. There remains a molecule of weight 1. The weights of the remaining molecules of steel [1,1,1]. We take molecules with a weight of 1 and 1, turn one of them into an antimolecule and collide it with the second. They are annihilated. The weights of the remaining molecules [1]. One molecule left. The result is 1.



Notes



As a solution, provide a file that exports the corrected version of the findLatestWeight function:



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





The solution will run in Node.js 12.



Decision



The provided β€œbad” solution has several problems at once. The first is the presence of recursion. As stated in the condition, we will process large arrays of numbers, which immediately eliminates a recursive solution.



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





Expanding recursion here is quite simple, however, another problem arises - there is a constant re-sorting (from smaller to large) and work with the end of the array. As a result, we get a decrease in the penultimate element in the array. But after that we do not trim the array, and if an array of a million elements was passed to the function, then we will re-sort it until the very end.



An option to solve this problem is to try to constantly trim the array.



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





Not bad, but we also need to get rid of sorting, which in itself is an expensive operation. By and large, at any given time, we will be interested in the 2 largest members of the array. That is, it is a search for two highs, which is done in one pass quite simply. For convenience, we carry out such a search in a separate function.



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





And the search function itself will be changed as follows:



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





Thus, we will always zero out the smaller of the two elements, and turn the larger into the difference between them. We got rid of sorting and got one linear pass instead.



Of the common mistakes we noticed, the participants took the maximum element, multiplied it by –1 and added it to the second largest stone. The result is a negative number, which was then used in the "as is" calculation. In addition, the task has a mental trap associated with the fact that you can try to leave stones of unique weight and calculate the difference from them. However, this approach does not give the correct result.



B. BEM

Authors: Eugene Mishchenko, Vladimir Grinenko tadatuta



Condition



Layout Alexander is involved in many projects using the BEM methodology. He even created a convenient plugin for his beloved IDE, which allows him to write class names in an abbreviated notation and deploy them to the full. But the problem is that for each project, people set different delimiters between the block, element and modifier (block__mod__val β€” elem, block – mod – val ___ elem), and each time he has to edit this manually in his plugin. Help Alexander write a module that will determine the separator for entities based on the class. The rule for delimiters is an arbitrary number of characters (not letters). Examples of possible notations (modifiers for a block in the input data may be without value):



 block_mod__elem // ,     block_mod_mod__elem block__elem_mod_mod
      
      





Clarifications:

- Classes in projects are written only in small letters.

- A string with a valid CSS class is fed to the input of the module.



The module should return a response of the form:



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





The module must be issued as a commonJS module:



 module.exports = function(str) { }
      
      





Decision



The second task took about 20 minutes. With its help, we wanted to test the knowledge of regular expressions among the participants.



From the condition, we learn that the input to the function will be a string containing a valid CSS class with additional restrictions, in which letter sequences are separated by arbitrary sequences of non-letter characters. Our task is to find separators and understand their semantics.



The first part of the class name will always be the name of the block. This is a sequence of one or more letters. We write the corresponding regular expression: [az] +.



We need similar expressions to search for the remaining parts: the name of the modifier and its value, or the name of the element with the corresponding modifier and value.



To search for delimiters, we need non-letter sequences, the expression: [^ az] + is suitable.



Put it together and define the groups whose values ​​we will use:



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





Now you need to make sure that we correctly defined the semantics of the found groups. You can take advantage of the fact that only a modifier can meet twice.



We will write a function that will take the original string and the separator found to calculate the number of occurrences:



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





If it turns out that the delimiter elem occurs twice, and mod - once, then in fact the opposite is true. The final decision:



 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

Authors: Dmitry Andriyanov dima117 , Alexey Gusev



Condition



Outside the window is 2319. Corporations clone successful employees to perform complex tasks.



In the production of clones, they decided to label new β€œproducts” with the help of a tattoo with a barcode on their shoulder - in order to distinguish the clones from each other.



Help the factory staff write a function that will draw a barcode with information about the clone.



Clone Information Format



Information about the clone is stored as follows:



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





Barcode Rendering Algorithm



The barcodes that are used in the clone factory look like this:







The barcode has a fixed size - 148 by 156 pixels. Along the perimeter of the barcode are black and white frames of 3 pixels each. Inside the frames is the barcode content, consisting of 18 lines of 17 black or white squares per line. The size of each square is 8 by 8 pixels.



White squares in the content encode 0, black - 1.



Barcode Content Generation Algorithm



At the intersection of the first row and the first column of content, a square is drawn that encodes the gender of the clone. The value of female is encoded by zero (white), male by one (black).



Further, a line of the form <id> <name> is formed from the fields id and name. The name field is padded with spaces at the end of up to 26 characters.



The resulting string is converted into a byte array - each character of the string is assigned the corresponding ASCII code (a number from 0 to 255).



Then, each element of the resulting array is translated into binary notation (eight characters 0 or 1) and encoded by a sequence of eight squares (0 - white quartrate, 1 - black square). Squares are drawn in the barcode content sequentially and line by line.



The last line of content contains control information.



Control Information Counting Algorithm



Each square in the line of control information determines the parity of the sum of the content values ​​in the corresponding column. If the sum of zeros and ones in the column is even, then a white square is drawn in the control information; otherwise, a black square is drawn.



Solution format and examples
Solution Format



The solution you download should contain the renderBarcode function:



 /** *       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:







Example 2



Clone Information:



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





Barcode:









Decision



It was necessary to correctly form the binary representation of the data, calculate the checksum for it and draw this data in the layout. Let's try to do this as simple and forehead as possible - without code optimizations.



Let's start with the binary representation. First, declare helper functions:



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





We form from the source data a string consisting of zeros and ones:



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





Then write the layout and styles for our 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';
      
      





Render binary data in the layout:



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





It remains to calculate and display the checksum. This can be done like this:



 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. Automate it

Authors: Vladimir Rusov, Dmitry Kanatnikov



In each of the qualification options, there was a task where an HTML page with a table or list was proposed as input. The tasks of this series had a different legend, but they all boiled down to the fact that you need to bring the page to a format similar to Markdown. We will analyze the solution to one of the problems.



Condition



On the state portal for the provision of services, they made it possible to apply for documents completely automatically, for this you only need to fill out a table with personal data.



This data is then transmitted for verification to several authorities, including the Ministry of Internal Affairs. After the start of testing, it turned out that the Ministry of Internal Affairs accepts data in the Markdown format, and the State Services use the HTML format. Help me write a script for migrating one format to another so that the guys start up as soon as possible.



You need to write a function that takes an HTML table as input and converts it to Markdown-like markup.



As a solution to this task, send the .js file in which the solution function is declared:



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





Input / Output Format and Notes

Input format



The HTML table comes as a string:



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





The table may contain colgroup, thead, and tbody tags in a fixed order. All these tags are optional, but at least thead or tbody will always be present.



- colgroup contains col tags that can have the optional align attribute with one of three values ​​(left | center | right)

- thead and tbody contain 1 or more tr

- tr, in turn, contain both td and th

- The table will always have at least one row. - The row will always have at least one cell. - At least one non-whitespace symbol is always present in the cell.

- The number of th / td elements in lines always coincides between all lines and with the number of col elements in colgroup, if there is colgroup.

- Spaces and line breaks in the source HTML can occur anywhere that does not violate the validity of HTML.



Output format



The output should be a line with Markdown markup:



| Command | Description | **Is implemented** |

| ---: | :--- | :---: |

| **git status** | List all new or modified files | **Yes** |

| **git diff** | Show file differences that haven't been staged | No |








- The first row encountered in the table should always turn into a header row in the Markdown markup.

- All other rows go to the body of the table.

- The header separator is always displayed.

- The contents of td are inserted as is, the contents of th as ** bold **.

- There is always one space between the contents of the cell in the markdown markup and the cell delimiters (|).

- Spaces around the edges of the contents of the td and th tags should be removed.

- Line breaks in cell contents must be deleted.

- More than one space in a row in the contents of the cells must be replaced by one space.

- For alignment in the cells of the columns of the Markdown table, the formatting of the header separator is responsible:



| : --- | means left alignment

| : ---: | means center alignment

| ---: | means right alignment



If there is no align attribute specified in the col tag, alignment must be set to the left.



Notes



- For the line feed you need to use the \ n character.

- The solution will be tested in a browser environment (Chrome 78) with access to document and window objects.

- You can use syntax up to es2018 inclusive.


Decision



The problem is solved by simply traversing the DOM tree of the table. Support for the DOM tree is implemented at the browser level, it is an integral part of it, so there will be no problems. To solve the problem, it is enough to translate the DOM tree from HTML to Markdown markup.



Having examined the examples, you can see that the conversion is quite simple. Below is the code that is the body of the solution (input) function.



First, we need to convert the string from HTML to the DOM tree:



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





Having received the DOM tree, we can just go through it and process the data from different DOM nodes. To do this, it is enough to recursively bypass the sequence of children of various DOM elements:



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





From the colgroup and col tags, we are interested in knowing the table column alignment:



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





In the thead, tbody, and tr tags, we are only interested in children:



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





It is important not to forget that, by convention, td and th are formatted differently:



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





To work with the test content of the DOM, you must fulfill the requirements described in the condition:



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





After we walked around the DOM tree, the main part of our table was written to the rows array:



[

["Command","Description","**Is implemented**"],

["**git status**","List all new or modified files","**Yes**"],

["**git diff**","Show file differences that haven't been staged","No"]

]








The column alignment information was in the alignments array:



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







It is important to remember that column alignment information may not be in the input:



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





Convert alignments to final form:



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





Example delimiter value:



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







The final step will be the formation of a Markdown line containing all the data read from the 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;
      
      





The return construct means that all of the above code was the body of the solution (input) function. As a result of this function, we get the desired Markdown table code shown in the example output from the task condition.



E. Pandemic virus

Authors: Andrey Mokrousov, Ivan Petukhov



The World Health Organization has published a report on signs of an impending pandemic of a new virus that threatens front-end developers. It is known that the virus does not manifest itself until the host sees the JS code containing some expression. As soon as the infected person saw this expression, it loses its ability to write code in JS and begins to spontaneously write code in Fortran.



The report mentions that the virus is activated by looking at the use of the first argument of the function passed by the argument to the Zyn function call, that is, an infected person cannot show an expression like Zyn (function (a, b, c) {console.log (a)}).



In order not to accidentally lose all of their front-end, AST & Co decided to check whether their code contains the above expression. Help the company engineers write such a check.



About AST & Co's code, we know that:



- it is written in ES3,

- access to the properties of an object is possible both through a point and through brackets (ab and a ['b']),

- part of the expression can be stored in a variable, but never passed to the function by the parameter (a (x) - forbidden),

- there are no functions that return part of the desired expression,

- there are no properties of objects or elements of arrays that contain part of the expression,

- when accessing a property of an object, the name of the property can be taken from a variable (a [x], x is a variable),

- the variables get their values ​​during declaration and are not overwritten, etc. e. the code will not be like var a = x; a = y; and var a = b = 1.



Solution Format and Notes

Solution Format



The check should be issued in the form of a CommonJS-module, which exports a function that receives the abstract syntax tree (ast) of the code being checked.



The function should return an array of ast-nodes that correspond to the places of use of the first parameter of the callback function passed by the argument to Zyn. The order of elements in the array is not important, duplicates are not allowed.



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





Notes



The following code can be used as a basis for traversing a tree.



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





Decision



First, let's deal with the limitations.



- it is written in ES3
So, when traversing a tree, only the most basic language constructs can be found. For example, only loops are available for manipulating arrays.



- access to the properties of an object is possible both through a point and through brackets (ab and a ['b'])
That is, you need to look not only for Zyn, but also for Z ['y']. N, Zy ['n'] and 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 ) ; , .



Decision



, -, Β« Β», . , , , .



: , . . , , .



:



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





Testing



. . , , , .



: , , , , (, , ).



Output



. . β€” , . .



, . , ( ) 18 .






References:



β€” ML-

β€” -

β€” -



All Articles