Bagaimana cara mengatur jam tangan? Analisis trek front-end kejuaraan pemrograman kedua

Habrapost baru dalam serangkaian analisis kejuaraan baru-baru ini. Peserta kualifikasi yang memilih bagian frontend harus menyelesaikan beberapa tugas dengan kompleksitas yang sangat berbeda: yang pertama (sesuai dengan harapan kami) memakan waktu 20 menit, yang terakhir - sekitar satu jam. Kami menguji berbagai keterampilan pengembang antarmuka, termasuk kemampuan untuk memahami bidang subjek yang tidak biasa.

A. Musnahkan itu

Penulis: Maxim Sysoev, Konstantin Petryaev

Tugas pertama adalah pemanasan. Setiap peserta mendapat satu dari empat opsi untuk tugas tersebut, mirip satu sama lain. Kami mengusulkan tidak hanya kondisi tekstual, tetapi juga solusi rekursif "buruk". Itu perlu untuk mengulang kode (menulis algoritma serakah yang menghasilkan solusi tercepat), menghilangkan rekursi dan berbagai omong kosong seperti operasi dan perhitungan yang tidak perlu.

Ketentuan


Anda mendapat pekerjaan di laboratorium untuk mempelajari antimateri, di mana mereka melakukan berbagai percobaan. Departemen Anda mempelajari proses yang terjadi saat menggabungkan materi dan antimateri. Anda perlu melakukan serangkaian percobaan pada sejumlah molekul tertentu.

Departemen tetangga telah mengembangkan alat yang mengubah materi menjadi antimateri untuk waktu yang singkat. Ini akan bermanfaat bagi Anda dalam melakukan percobaan di mana algoritma berikut digunakan:

- Kami menemukan 2 molekul terberat.
- Kami mengubah salah satunya menjadi antimateri.
- Gabungkan mereka. Apalagi jika beratnya sama, mereka dimusnahkan. Jika beratnya berbeda, maka kita mendapatkan molekul baru, yang beratnya sama dengan perbedaan bobot dari dua sebelumnya. Molekul yang dihasilkan itu sendiri adalah materi.
- Jika ada satu molekul yang tersisa, Anda perlu mencari tahu beratnya. Jika ada banyak molekul, kita kembali ke langkah 1.

Anda perlu mengetahui molekul berapa berat yang akan tersisa di akhir percobaan, pengetahuan ini dibutuhkan oleh para ilmuwan dari departemen lain.

Pengembang sebelumnya membuat sketsa kode yang terlibat dalam perhitungan ini, tetapi kode tersebut tidak dapat menyelesaikan perhitungan saat percobaan dilakukan pada sejumlah besar molekul. Anda perlu memperbaiki kode agar berfungsi dalam jumlah waktu yang wajar.

Kode diwarisi untuk Anda

Sebagai input, Anda akan memiliki array dengan bobot molekul. Sebagai hasil, Anda harus mengembalikan angka yang menunjukkan berat molekul terakhir. Jika tidak ada molekul yang tersisa, maka perlu untuk mengembalikan 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); } 

Contoh dan catatan

Contoh


Pintu Masuk: [2,7,4,1,8,1]
Output: 1

Kami mengambil molekul dengan berat 7 dan 8, mengubah 7 menjadi antimolekul dan menabraknya dengan molekul berat 8. Masih ada molekul berbobot 1. Berat molekul sisa baja [2,4,1,1,1]. Kami mengambil molekul dengan berat 2 dan 4, mengubah 2 menjadi antimolekul dan menabraknya dengan molekul berat 4. Masih ada molekul berat 2. Berat molekul sisa baja [2,1,1,1]. Kami mengambil molekul dengan berat 2 dan 1, mengubah 1 menjadi antimolekul dan menabraknya dengan molekul berat 2. Masih ada molekul berbobot 1. Bobot sisa molekul baja [1,1,1]. Kami mengambil molekul dengan berat 1 dan 1, mengubah salah satunya menjadi antimolekul dan berbenturan dengan yang kedua. Mereka dimusnahkan. Berat molekul yang tersisa [1]. Satu molekul tersisa. Hasilnya adalah 1.

Catatan


Sebagai solusi, berikan file yang mengekspor versi terkoreksi dari fungsi findLatestWeight:

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

Solusinya akan berjalan di Node.js 12.

Solusi


Solusi "buruk" yang diberikan memiliki beberapa masalah sekaligus. Yang pertama adalah rekursi. Seperti yang dinyatakan dalam kondisi tersebut, kami akan memproses sejumlah besar array, yang segera menghilangkan solusi rekursif.

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

Memperluas rekursi di sini cukup sederhana, namun, masalah lain muncul - ada penyortiran konstan (dari kecil ke besar) dan bekerja dengan akhir array. Akibatnya, kami mendapatkan penurunan elemen kedua dari belakang dalam array. Tetapi setelah itu kita tidak memangkas array, dan jika array sejuta elemen dilewatkan ke fungsi, maka kita akan mengurutkannya kembali sampai akhir.

Pilihan untuk mengatasi masalah ini adalah mencoba untuk terus memangkas 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); } 

Tidak buruk, tetapi kita juga harus menyingkirkan penyortiran, yang dengan sendirinya merupakan operasi yang mahal. Pada umumnya, pada waktu tertentu, kami akan tertarik dengan 2 anggota array terbesar. Artinya, itu adalah pencarian untuk dua tertinggi, yang dilakukan dalam satu pass cukup sederhana. Untuk kenyamanan, kami melakukan pencarian seperti itu di fungsi yang terpisah.

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

Dan kami mengubah fungsi pencarian sebagai berikut:

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

Dengan demikian, kita akan selalu nol yang lebih kecil dari dua elemen, dan mengubah yang lebih besar menjadi perbedaan di antara mereka. Kami menyingkirkan penyortiran dan mendapatkan satu linear pass sebagai gantinya.

Dari kesalahan umum yang kami perhatikan, para peserta mengambil elemen maksimum, mengalikannya dengan โ€“1 dan menambahkannya ke batu terbesar kedua. Hasilnya adalah angka negatif, yang kemudian digunakan dalam perhitungan "sebagaimana adanya". Selain itu, tugas tersebut memiliki jebakan mental yang terkait dengan fakta bahwa Anda dapat mencoba untuk meninggalkan batu yang unik dalam berat dan menghitung perbedaannya. Namun, pendekatan ini tidak memberikan hasil yang benar.

B. BEM

Penulis: Eugene Mishchenko, Vladimir Grinenko tadatuta

Ketentuan


Layout Alexander terlibat dalam banyak proyek menggunakan metodologi BEM. Dia bahkan membuat plugin yang berguna untuk IDE favoritnya, yang memungkinkannya untuk menulis nama kelas dalam notasi singkat dan menyebarkannya secara penuh. Tetapi masalahnya adalah bahwa untuk setiap proyek, orang-orang mengatur pembatas yang berbeda antara blok, elemen dan pengubah (block__mod__val-elem, blok-mod-val ___ elem), dan setiap kali ia harus mengedit ini secara manual dalam plugin-nya. Bantu Alexander menulis modul yang akan menentukan pemisah untuk entitas berdasarkan kelas. Aturan untuk pembatas adalah jumlah karakter yang berubah-ubah (bukan huruf). Contoh kemungkinan notasi (pengubah untuk blok dalam data input mungkin tanpa nilai):

 block_mod__elem // ,     block_mod_mod__elem block__elem_mod_mod 

Klarifikasi:
- Kelas dalam proyek hanya ditulis dalam huruf kecil.
- Sebuah string dengan kelas CSS yang valid diumpankan ke input modul.

Modul harus mengembalikan respons formulir:

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

Modul harus dikeluarkan sebagai modul commonJS:

 module.exports = function(str) { } 

Solusi


Tugas kedua memakan waktu sekitar 20 menit. Dengan bantuannya, kami ingin menguji pengetahuan tentang ekspresi reguler di antara para peserta.

Dari kondisi tersebut, kita belajar bahwa input ke fungsi akan berupa string yang berisi kelas CSS yang valid dengan batasan tambahan, di mana urutan huruf dipisahkan oleh urutan sewenang-wenang dari karakter non-huruf. Tugas kita adalah menemukan pemisah dan memahami semantiknya.

Bagian pertama dari nama kelas akan selalu menjadi nama blok. Ini adalah urutan satu huruf atau lebih. Kami menulis persamaan reguler yang sesuai: [az] +.

Kita akan memerlukan ekspresi yang sama untuk mencari bagian yang tersisa: nama pengubah dan nilainya, atau nama elemen dengan pengubah dan nilai yang sesuai.

Untuk mencari pembatas, kita perlu urutan non-huruf, ungkapan: [^ az] + cocok.

Gabungkan dan tentukan grup yang nilainya akan kita gunakan:

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

Sekarang Anda perlu memastikan bahwa kami mendefinisikan semantik dari grup yang ditemukan dengan benar. Anda dapat memanfaatkan fakta bahwa hanya pengubah yang dapat bertemu dua kali.

Kami akan menulis fungsi yang akan mengambil string asli dan pemisah ditemukan untuk menghitung jumlah kemunculan:

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

Jika ternyata pembatas elem terjadi dua kali, dan mod - sekali, maka sebenarnya yang terjadi adalah sebaliknya. Keputusan akhir:

 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. Pabrik Klon

Penulis: Dmitry Andriyanov dima117 , Alexey Gusev

Ketentuan


Di luar jendela adalah 2319. Perusahaan mengkloning karyawan yang sukses untuk melakukan tugas yang kompleks.

Dalam produksi klon, mereka memutuskan untuk memberi label "produk" baru dengan tato barcode di bahu mereka - untuk membedakan klon satu sama lain.

Bantu staf pabrik menulis fungsi yang akan menggambar barcode dengan informasi tentang klon.

Format Informasi Klon

Informasi tentang klon disimpan sebagai berikut:

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

Algoritma Rendering Barcode

Barcode yang digunakan di pabrik klon terlihat seperti ini:



Barcode memiliki ukuran tetap - 148 x 156 piksel. Di sekeliling barcode terdapat bingkai hitam putih masing-masing 3 piksel. Di dalam bingkai adalah konten barcode, terdiri dari 18 baris 17 kotak hitam atau putih per baris. Ukuran setiap kotak adalah 8 kali 8 piksel.

Kotak putih dalam konten menyandikan 0, hitam - 1.

Algoritma Pembuatan Konten Kode Batang

Di persimpangan baris pertama dan kolom konten pertama, sebuah kotak diambil yang mengkode jenis kelamin klon. Nilai perempuan dikodekan oleh nol (putih), laki-laki oleh satu (hitam).

Selanjutnya, garis formulir <id> <name> dibentuk dari kolom isian dan nama. Bidang nama diisi dengan spasi di ujung hingga 26 karakter.

String yang dihasilkan dikonversi menjadi array byte - setiap karakter string diberi kode ASCII yang sesuai (angka dari 0 hingga 255).

Kemudian setiap elemen dari array yang dihasilkan diterjemahkan ke dalam notasi biner (delapan karakter 0 atau 1) dan dikodekan dengan urutan delapan kotak (0 - kuartrat putih, 1 - kotak hitam). Kotak diambil dalam konten barcode secara berurutan dan baris demi baris.

Baris terakhir konten berisi informasi kontrol.

Mengontrol Algoritma Penghitungan Informasi

Setiap kotak di garis informasi kontrol menentukan paritas jumlah nilai konten di kolom yang sesuai. Jika jumlah nol dan yang ada di kolom adalah genap, maka kotak putih digambar dalam informasi kontrol, jika tidak, kotak hitam.

Format dan contoh solusi
Format Solusi

Solusi yang Anda muat harus berisi fungsi 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" } 

Kode batang:



Contoh 2


Informasi Klon:

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

Kode batang:


Solusi


Itu perlu untuk benar membentuk representasi biner dari data, menghitung checksum untuk itu dan menggambar data ini di tata letak. Mari kita coba melakukan ini sesederhana dan mungkin dahi - tanpa optimasi kode.

Mari kita mulai dengan representasi biner. Pertama, deklarasikan fungsi pembantu:

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

Kami membentuk dari sumber data string yang terdiri dari nol dan yang:

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

Kemudian tulis tata letak dan gaya untuk barcode kami:

 //   ,    ยซยป . //  ,      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'; 

Jadikan data biner dalam tata letak:

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

Tetap menghitung dan menampilkan checksum. Ini bisa dilakukan seperti ini:

 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. Otomatiskan itu

Penulis: Vladimir Rusov, Dmitry Kanatnikov

Di setiap opsi kualifikasi, ada tugas di mana halaman HTML dengan tabel atau daftar diusulkan sebagai input. Tugas-tugas dari seri ini memiliki legenda yang berbeda, tetapi semuanya bermuara pada kenyataan bahwa Anda perlu membawa halaman ke format yang mirip dengan Markdown. Kami akan menganalisis solusi untuk salah satu masalah.

Ketentuan


Di portal negara bagian untuk penyediaan layanan mereka memungkinkan untuk mengajukan dokumen sepenuhnya secara otomatis, untuk ini Anda hanya perlu mengisi tabel dengan data pribadi.

Data ini kemudian ditransfer untuk verifikasi ke beberapa otoritas, termasuk Kementerian Dalam Negeri. Setelah dimulainya pengujian, ternyata Kementerian Dalam Negeri menerima data dalam format Penurunan harga, dan Layanan Negara menggunakan format HTML. Bantu saya menulis skrip untuk memigrasikan satu format ke format yang lain sehingga orang-orang segera memulai

Anda perlu menulis fungsi yang mengambil tabel HTML sebagai input dan mengubahnya menjadi markup seperti penurunan harga.

Sebagai solusi untuk tugas ini, kirim file .js di mana fungsi solusi dinyatakan:

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

Input / Output Format dan Catatan

Format input


Tabel HTML hadir sebagai 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> 

Tabel dapat berisi tag grup, thead, dan tbody dalam urutan yang tetap. Semua tag ini bersifat opsional, tetapi setidaknya tag atau kartu akan selalu ada.

- colgroup berisi tag col yang dapat memiliki atribut penyelarasan opsional dengan salah satu dari tiga nilai (kiri | tengah | kanan)
- thead dan tbody berisi 1 atau lebih tr
- tr, pada gilirannya, mengandung td dan th
- Tabel akan selalu memiliki setidaknya satu baris. - Baris akan selalu memiliki setidaknya satu sel. - Setidaknya satu simbol non-spasi putih selalu ada dalam sel.
- Jumlah elemen th / td dalam garis selalu bertepatan antara semua baris dan dengan jumlah elemen col dalam grup, jika ada grup.
- Spasi dan jeda baris dalam HTML sumber dapat terjadi di mana saja yang tidak melanggar validitas HTML.

Format output


Output harus berupa garis dengan markdown 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 |


- Baris pertama yang ditemui dalam sebuah tabel harus selalu berubah menjadi baris tajuk dalam markdown markdown.
- Semua baris lainnya masuk ke badan tabel.
- Pemisah header selalu ditampilkan.
- Isi td dimasukkan apa adanya, isi th sebagai ** tebal **.
- Selalu ada satu ruang antara isi sel di markup markdown dan pembatas sel (|).
- Spasi di tepi isi tag td dan th harus dihapus.
- Hentian baris dalam konten sel harus dihapus.
- Lebih dari satu spasi berturut-turut dalam isi sel harus diganti dengan satu spasi.
- Untuk penyelarasan dalam sel kolom dari tabel penurunan harga, format pemisah header bertanggung jawab:

| : --- | berarti perataan kiri
| : ---: | berarti perataan tengah
| ---: | berarti perataan kanan

Jika tidak ada atribut align yang ditentukan dalam tag col, alignment harus diatur ke kiri.

Catatan


- Untuk umpan baris Anda harus menggunakan karakter \ n.
- Solusinya akan diuji di lingkungan browser (Chrome 78) dengan akses ke dokumen dan objek jendela.
- Anda dapat menggunakan sintaks hingga es2018 inklusif.

Solusi


Masalahnya diselesaikan dengan hanya melintasi pohon DOM dari tabel. Dukungan untuk pohon DOM diimplementasikan pada tingkat browser, ini merupakan bagian integral darinya, sehingga tidak akan ada masalah. Untuk mengatasi masalah, cukup menerjemahkan pohon DOM dari HTML ke markdown Markup.

Setelah memeriksa contoh-contohnya, Anda dapat melihat bahwa konversi tersebut cukup sederhana. Di bawah ini adalah kode yang merupakan badan fungsi solusi (input).

Pertama, kita perlu mengonversi string dari HTML ke struktur DOM:

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

Setelah menerima pohon DOM, kita bisa melewatinya dan memproses data dari berbagai node DOM. Untuk melakukan ini, cukup secara rekursif memotong urutan anak-anak dari berbagai elemen DOM:

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

Dari colgroup dan tag col, kami tertarik untuk mengetahui perataan kolom tabel:

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

Dalam tag thead, tbody dan tr, kami hanya tertarik pada anak-anak:

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

Penting untuk tidak lupa bahwa, dengan konvensi, td dan th diformat secara berbeda:

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

Untuk bekerja dengan konten uji DOM, Anda harus memenuhi persyaratan yang dijelaskan dalam ketentuan:

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

Setelah kami berjalan di sekitar pohon DOM, sebagian besar tabel kami ditulis ke deretan baris:

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


Informasi perataan kolom ada di larik perataan:

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

Penting untuk diingat bahwa informasi perataan kolom mungkin tidak ada dalam input:

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

Konversi keberpihakan ke bentuk akhir:

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

Nilai pembatas contoh:

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

Langkah terakhir adalah pembentukan garis Markdown yang berisi semua data yang dibaca dari 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; 

Membangun kembali berarti bahwa semua kode di atas adalah badan fungsi solusi (input). Sebagai hasil dari fungsi ini, kita mendapatkan kode tabel penurunan harga yang diinginkan ditunjukkan pada contoh output dari kondisi tugas.

E. Virus pandemi

Penulis: Andrey Mokrousov, Ivan Petukhov

Organisasi Kesehatan Dunia telah menerbitkan laporan tentang tanda-tanda pandemi virus baru yang akan datang yang mengancam pengembang front-end. Diketahui bahwa virus tidak memanifestasikan dirinya sampai tuan rumah melihat kode JS yang mengandung beberapa ekspresi. Segera setelah orang yang terinfeksi melihat ungkapan ini, ia kehilangan kemampuannya untuk menulis kode dalam JS dan mulai secara spontan menulis kode dalam Fortran.

Laporan tersebut menyebutkan bahwa virus diaktifkan dengan melihat penggunaan argumen pertama dari fungsi yang dilewatkan oleh argumen ke panggilan fungsi Zyn, yaitu, orang yang terinfeksi tidak dapat menunjukkan ekspresi seperti Zyn (fungsi (a, b, c) {console.log (a)}).

Agar tidak kehilangan semua front-end mereka secara tidak sengaja, AST & Co memutuskan untuk memeriksa apakah kode mereka mengandung ekspresi di atas. Bantu insinyur perusahaan menulis cek semacam itu.

Tentang kode AST & Co, kita tahu bahwa:

- ada tertulis dalam ES3,
- akses ke properti dari suatu objek dimungkinkan baik melalui titik dan melalui tanda kurung (ab dan a ['b']),
- bagian dari ekspresi dapat disimpan dalam variabel, tetapi tidak pernah diteruskan ke fungsi oleh parameter (a (x) - dilarang),
- tidak ada fungsi yang mengembalikan bagian dari ekspresi yang diinginkan,
โ€” , ,
โ€” (a[x], x โ€” ),
โ€” , . . var a = x; a = y; var a = b = 1.

Format Solusi


CommonJS-, , (ast) .

ast-, callback-, Zyn , .

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

Catatan


.

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

Solusi


.

โ€” 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 ) ; , .

Solusi


, -, ยซ ยป, . , , , .

: , . . , , .

:

 const TPS = 1000 / Framework.INTERVAL; //    

// .

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

, โ€” rotateFactor. getRotateFactor, , , . :
1. ,
2. .

. .

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

, MAX_SPEED . getRotateFactor.

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

:

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

, :

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

:

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



. . , , , .

: , , , , (, , ).



. . โ€” , . .

, . , ( ) 18 .



:

โ€” ML-
โ€” -
โ€” -

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


All Articles