Last active
June 14, 2018 11:40
-
-
Save nickolay/24507e7cda7cce0da9d8a18e5d2e459b to your computer and use it in GitHub Desktop.
"маски"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<title>"Маски" категорий</title> | |
<script> | |
/** Вспомогательный: "похожи" ли значимые части двух названий (true/false). */ | |
function namesAreSimilar(a, b) { | |
var debug = false ? console.log : function() {}; | |
// https://github.com/gustf/js-levenshtein/blob/ce4e5d3a9a3155cf64ac55757d0a61d00d3ee98d/index.js | |
// the minimum number of single-character edits (insertions, deletions or substitutions) required to change one word into the other | |
var levenshtein = function(){function r(r,t,e,o,h){return r<t||e<t?r>e?e+1:r+1:o===h?t:t+1}return function(t,e){if(t===e)return 0;if(t.length>e.length){var o=t;t=e,e=o}for(var h=t.length,n=e.length;h>0&&t.charCodeAt(h-1)===e.charCodeAt(n-1);)h--,n--;for(var a=0;a<h&&t.charCodeAt(a)===e.charCodeAt(a);)a++;if(n-=a,0===(h-=a)||n<3)return n;var c,f,u,d,A,C,i,l,g,s,v,p,m=0,x=[];for(c=0;c<h;c++)x.push(c+1),x.push(t.charCodeAt(a+c));for(;m+3<n;)for(g=e.charCodeAt(a+(f=m)),s=e.charCodeAt(a+(u=m+1)),v=e.charCodeAt(a+(d=m+2)),p=e.charCodeAt(a+(A=m+3)),C=m+=4,c=0;c<x.length;c+=2)C=r(d=r(u=r(f=r(i=x[c],f,u,g,l=x[c+1]),u,d,s,l),d,A,v,l),A,C,p,l),x[c]=C,A=d,d=u,u=f,f=i;for(;m<n;)for(g=e.charCodeAt(a+(f=m)),C=++m,c=0;c<x.length;c+=2)i=x[c],x[c]=C=i<f||C<f?i>C?C+1:i+1:g===x[c+1]?f:f+1,f=i;return C}}(); | |
/* альтернативной идеей было измерить кол-во "опечаток", но высок риск посчитать "похожими" разные названия (напр. 50МБ vs 500МБ) | |
var distance = levenshtein(newSig, existingSig); | |
console.log(`distance(${newSig}, ${existingSig}) = ${distance}`) | |
return ( (distance <= 1) || (distance <= 0.20 * existingSig.length) ); | |
*/ | |
// - различия в одной букве в начале или конце слова и/или совпадение на более чем <del>98%</del> 60% по буквам | |
// - добавление цифры в начале, конце, середине уже существующего названия | |
var L = levenshtein(a,b); | |
window.compareDetails = {L:L, maxLength: Math.max(a.length, b.length)}; | |
debug("L(" + a + "," + b + ")=" + L + "; lengths=" + [a.length, b.length]); | |
if ((L <= parseInt(document.getElementById("abs").value, 10)) || // отличие в 1 букве | |
(L < (1-parseInt(document.getElementById("rel").value, 10)/100.0) * | |
Math.max(a.length, b.length))) // "совпадение на более чем 60% по буквам" | |
return true; | |
return false; | |
} | |
/** Основная ф-ия проверки названия против массива существующих названий. | |
* Возвращает объект с одним ключом ({<result>: <data>}), где <result> отражает рез-т проверки, а <data> доп.инфу для формирования текста сообщения. | |
*/ | |
function checkNewName(name, existingNames) { | |
function regexpFromChars(chars, reArgs, invert) { // формируем regexp, которые матчит любой из символов переданного списка (а если invert, то НЕ из списка) | |
return new RegExp("[" + // любой из символов | |
(invert ? "^" : "") + // или НЕ из символов | |
chars.replace(/[-\]^]/g, '\\$&') + // перечисленных в `chars` (экскейпим, приписывая `\` к тем, которые имеют спец.смысл в контекте 'character sets' в regexp) | |
"]", reArgs); | |
} | |
function stripChars(name, chars) { return name.replace(regexpFromChars(chars, "g"), ""); } | |
function keepChars(name, chars) { return name.replace(regexpFromChars(chars, "g", "invert"), ""); } | |
var ALLOWED_SIGNIFICANT = "abcdefghijklmnopqrstuvwxyzабвгдеёжзиклмнопрстуфхцчшщъыьэюя"; | |
var ALLOWED_OTHER = "'\"`\\ ~-=_|+.,;!?@#$%^&*/<>[]()0123456789"; | |
var invalid = stripChars(name.toLowerCase(), ALLOWED_SIGNIFICANT + ALLOWED_OTHER); | |
if (invalid !== "") return {invalid: invalid}; | |
var newSig = keepChars(name.toLowerCase(), ALLOWED_SIGNIFICANT); | |
if (newSig === "") return {noSignificantChars: true}; | |
for (var i = 0; i < existingNames.length; i++) { | |
var existingSig = keepChars(existingNames[i].toLowerCase(), ALLOWED_SIGNIFICANT); | |
if (namesAreSimilar(newSig, existingSig)) return {duplicate: existingNames[i]}; | |
} | |
return { OK: name.trim() }; | |
} | |
function test(name, existingNames, expect) { | |
var rv = checkNewName(name, existingNames); | |
if (JSON.stringify(rv) !== JSON.stringify(expect)) { | |
console.error("'" + name + "' vs " + existingNames + " = " + JSON.stringify(rv) + ", expected " + JSON.stringify(expect)); | |
} else { | |
console.log("OK! '" + name + "' vs " + existingNames + " = " + JSON.stringify(rv)); | |
} | |
} | |
function handleInput() { | |
var newName = document.getElementById("new").value; | |
var rv = checkNewName(newName, document.getElementById("cats").value.split("\n").map(function(name) {return name.trim()})); | |
var msg = document.getElementById("message"); | |
if (newName === "") msg.innerText = ""; | |
else if (rv.invalid) msg.innerText = "В названии используются запрещенные символы: " + rv.invalid + "\n\nИспользуйте только буквы, цифры и знаки пунктуации."; | |
else if (rv.duplicate) msg.innerText = "Похожее название уже существует: " + rv.duplicate + " (отличается на " + compareDetails.L + | |
" букв, совпадает по буквам на " + (100 - 100 * compareDetails.L / compareDetails.maxLength).toFixed(0) + "%)"; | |
else if (rv.noSignificantChars) msg.innerText = "В названии должна присутствовать хотя бы одна буква."; | |
else msg.innerText = "Название принято!" | |
} | |
window.onload = function() { | |
var cats = document.getElementById("cats"); | |
if (document.location.hash !== "") { | |
cats.value = decodeURIComponent(document.location.hash.substring(1)); | |
} | |
handleInput(); | |
cats.onchange = function(ev) { | |
document.location.hash = encodeURIComponent(cats.value); | |
handleInput(); | |
} | |
} | |
</script> | |
</head> | |
<body> | |
<label for="abs" style="padding-right:1em">Не разрешать отличия на >=N букв:</label><input type="number" id="abs" value="1" oninput="handleInput()"><br> | |
<label for="rel">Также не разрешать совпадение >X%:</label> <input type="number" id="rel" value="70" oninput="handleInput()"><br><br> | |
<label for="new">Новая категория</label><br> | |
<input type="text" id="new" oninput="handleInput()" size="100" autofocus> | |
<div id="message" style="height: 100px"></div> | |
<label for="cats">Существующие категории:</label><br> | |
<textarea id="cats" rows="30" cols="80"></textarea> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment