Skip to content

Instantly share code, notes, and snippets.

@nickolay
Last active June 14, 2018 11:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nickolay/24507e7cda7cce0da9d8a18e5d2e459b to your computer and use it in GitHub Desktop.
Save nickolay/24507e7cda7cce0da9d8a18e5d2e459b to your computer and use it in GitHub Desktop.
"маски"
<!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