Javascript-форум (https://javascript.ru/forum/)
-   Оффтопик (https://javascript.ru/forum/offtopic/)
-   -   Похожесть двух строк (https://javascript.ru/forum/offtopic/76115-pokhozhest-dvukh-strok.html)

Malleys 16.03.2019 00:06

ДЖАВАСКРИПТИЗЕР,
Цитата:

Сообщение от ДЖАВАСКРИПТИЗЕР
Как снять двушку?
...
Слово "двушку" смело меняем на "девушку" ибо никто в здравом уме двушки в 11 вечера не снимает...

Раз так, то... ответ с 0:25
<iframe width="560" height="315" src="https://www.youtube.com/embed/tWEBbYoDaU8?start=25" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen style="margin: auto;"></iframe>
На все ваши возражения смотрите 1:03 там же!

Nexus, Пусть имеются строки a и b, задача заключается в том, чтобы найти такую функцию F(x), чтобы для похожих в некотором смысле строк выполнялось F(a) === F(b)

Первым приближением F(x) может быть перевод в нижний регистр. Действительно, F("Есть ли") === F("есть ли")

Вторым приближением может быть добавленная транслитерация, F("Есть ли") === F("еst li") или F("Ест ли") === F("есть ли")

Третим приближением может быть добавленный алгоритм сравнения двух строк по их звучанию (например, SOUNDEX (произносится как /ˈsaʊndɛks/ или /ˈsaʊɾ̃ɛks/), но для русского языка лучше подходит «Полифон»[1]), F("Ест ли") === F("есть ли") или F("Если ли") === F("есть ли")

Конечно, простое регулярное выражение не подойдёт для определения, подходит ли строка под РегВыр, для этого его нужно трансформировать по определённым правилам, но можно обойтись обёрткой над простыми РегВырами. (я назвал её Rx)

Алгоритм ставит определённому сочетанию букв одинаковый код, если они звучат одинаково.

Рассмотрите пример. Слово «сердце» произносится как /ˈs̪ʲɛ̝rt̻͡s̪ɨ̞/, из-за этого оно может писаться с ошибкой, писаться так, как оно звучит — «серце», или «сертце». Рассматривая /rt̻͡s̪/, становится понятно, что этой группе согласных может соответствовать «рдц», «рц» или «ртц». Согласные заменяются на числа, причём похожим по звучанию буквам/группам букв соответствуют одинаковые числа.

Проверить подходить ли предложение под Rx регулярное выражение можно так
const ok = Rx`Есть ли ${/.+/} в наличии?`.test("Если ли лапата в налеции?");


Если же нужно именно слово, которое совпало в захватывающей группе, то его напрямую получить нельзя. (И действительно, если вы получите из этого предложения «лапата», то что с этим делать дальше? Вместо этого, предлагается код этого слова 873. У вас в базе данных может храниться слово «лопата», вычислив Soundex("лопата") получите 873, сравнив узнаете, что «лапата» оказывается — «лопата»)
const match = Rx`Есть ли ${/.+/} в наличии?`.match("Если ли лапата в налеции?");


В предложенном варианте Soundex это не SOUNDEX, а приспособленное к русскому языку поделие, основанное на алгоритмах SOUNDEX и «Полифон», нуждается в некоторой доработке в плане данных объекта Soundex.prototype._codes

Итоговый пример, в котором вы можете ввести предложение, и программа проверит под какое Rx регулярное выражение подходит предложение (походящее выделится зелёным цветом!)

Продолжение следует...

Malleys 16.03.2019 00:06

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<style>

#inputField {
	font-size: 200%;
	width: 100%;
	box-sizing: border-box;
}
		
#patternsField > * {
	border: 1px solid #eee;
	border-radius: .4em;
	margin: 1em 0;
	padding: 1em;
	font: bold 1em sans-serif;
}

#patternsField > .matches {
	background-color: #4CAF50;
	color: white;
}

	</style>
</head>
<body>
	<input id="inputField" value="Если ли в нолечие лапата?">
	<div id="patternsField"></div>
	<script>

function Soundex(expression) {
	if(this instanceof Soundex === false)
		return new Soundex(expression);

	this.expression = expression;
	this.calculate(this.expression);
}

Soundex.prototype = {
	__proto__: Array.prototype,
	_codes: {
		"a":{
			"0":[0,-1,-1],
			"i":[[0,1,-1]],
			"j":[[0,1,-1]],
			"y":[[0,1,-1]],
			"u":[[0,7,-1]]
		},
		"b":[[7,7,7]],
		"c":{
			"0":[5,5,5],
			"1":[4,4,4],
			"z":{"0":[4,4,4],"s":[[4,4,4]]},
			"s":{"0":[4,4,4],"z":[[4,4,4]]},
			"k":[[5,5,5],[45,45,45]],
			"h":{"0":[5,5,5],"1":[4,4,4],"s":[[5,54,54]]}
		},
		"d":{
			"0":[3,3,3],
			"t":[[3,3,3]],
			"z":{"0":[4,4,4],"h":[[4,4,4]],"s":[[4,4,4]]},
			"s":{"0":[4,4,4],"h":[[4,4,4]],"z":[[4,4,4]]},
			"r":{"s":[[4,4,4]],"z":[[4,4,4]]}
		},
		"e":{
			"0":[0,-1,-1],
			"i":[[0,1,-1]],
			"j":[[0,1,-1]],
			"y":[[0,1,-1]],
			"u":[[1,1,-1]],
			"w":[[1,1,-1]]
		},
		"f":{"0":[7,7,7],"b":[[7,7,7]]},
		"g":[[5,5,5]],
		"h":[[5,5,-1]],
		"i":{
			"0":[0,-1,-1],
			"a":[[1,-1,-1]],
			"e":[[1,-1,-1]],
			"o":[[1,-1,-1]],
			"u":[[1,-1,-1]]
		},
		"j":[[4,4,4]],
		"k":{"0":[5,5,5],"h":[[5,5,5]],"s":[[5,54,54]]},
		"l":[[8,8,8]],
		"m":{"0":[6,6,6],"n":[[66,66,66]]},
		"n":{"0":[6,6,6],"m":[[66,66,66]]},
		"o":{"0":[0,-1,-1],"i":[[0,1,-1]],"j":[[0,1,-1]],"y":[[0,1,-1]]},
		"p":{"0":[7,7,7],"f":[[7,7,7]],"h":[[7,7,7]]},
		"q":[[5,5,5]],
		"r":{
			"0":[9,9,9],
			"d": {"0":[93,93,93],"c":[[95,95,95]]},
			"t": {"0":[93,93,93],"c":[[95,95,95]]},
			"z":[[94,94,94],[94,94,94]],
			"s":[[94,94,94],[94,94,94]]
		},
		"s":{
			"0":[4,4,4],
			"l":{"0":[2,43,43]},
			"z":{
				"0":[4,4,4],
				"t":[[2,43,43]],
				"c":{"z":[[2,4,4]],"s":[[2,4,4]]},
				"d":[[2,43,43]]
			},
			"d":[[2,43,43]],
			"t":{
				"0":[2,43,43],
				"r":{"z":[[2,4,4]],"s":[[2,4,4]]},
				"n":[[26,46,46]],
				"c":{"h":[[2,4,4]]},
				"s":{"h":[[2,4,4]],"c":{"h":[[2,4,4]]}}
			},
			"c":{
				"0":[2,4,4],
				"h":{
					"0":[4,4,4],
					"t":{
						"0":[2,43,43],
						"s":{"c":{"h":[[2,4,4]]},"h":[[2,4,4]]},
						"c":{"h":[[2,4,4]]}
					},
					"d":[[2,43,43]]
				}
			},
			"h":{
				"0":[4,4,4],
				"t":{"0":[2,43,43],"c":{"h":[[2,4,4]]},"s":{"h":[[2,4,4]]}},
				"c":{"h":[[2,4,4]]},
				"d":[[2,43,43]]
			}
		},
		"t":{
			"0":[3,3,3],
			"c":{"0":[4,4,4],"h":[[4,4,4]]},
			"z":{"0":[4,4,4],"s":[[4,4,4]]},
			"s":{"0":[4,4,4],"z":[[4,4,4]],"h":[[4,4,4]],"c":{"h":[[4,4,4]]}},
			"t":{
				"s":{"0":[4,4,4],"z":[[4,4,4]],"c":{"h":[[4,4,4]]}},
				"c":{"h":[[4,4,4]]},
				"z":[[4,4,4]]
			},
			"h":[[3,3,3]],
			"r":{"z":[[4,4,4]],"s":[[4,4,4]]}
		},
		"u":{
			"0":[0,-1,-1],
			"e":[[0,-1,-1]],
			"i":[[0,1,-1]],
			"j":[[0,1,-1]],
			"y":[[0,1,-1]]
		},
		"v":[[7,7,7]],
		"w":[[7,7,7]],
		"x":[[5,54,54]],
		"y":[[1,-1,-1]],
		"z":{
			"0":[4,4,4],
			"d":{"0":[2,43,43],"z":{"0":[2,4,4],"h":[[2,4,4]]}},
			"h":{"0":[4,4,4],"d":{"0":[2,43,43],"z":{"h":[[2,4,4]]}}},
			"s":{"0":[4,4,4],"h":[[4,4,4]],"c":{"h":[[4,4,4]]}}
		}
	},
	_map: {
		а: "a", б: "b", в: "v", г: "g", д: "d", е: "e", ё: "e", ж: "zh", з: "z",
		и: "i", й: "i", к: "k", л: "l", м: "m", н: "n", о: "o", п: "p", р: "r",
		с: "s", т: "t", у: "u", ф: "f", х: "h", ц: "c", ч: "ch", ш: "sh", щ: "sch",
		ь: "'", ы: "y", ъ: "'", э: "e", ю: "yu", я: "ya"
	},
	_accented: {
		a: /[AaªÀ-Åà-åĀ-ąǍǎȀ-ȃȦȧᴬᵃḀḁẚẠ-ảₐ⒜ⒶⓐAa]/ig,
		b: /[BbᴮᵇḂ-ḇℬ⒝ⒷⓑBb]/ig,
		c: /[CcÇçĆ-čᶜ℀ℂ℃ℭⅭⅽ⒞Ⓒⓒ㏄-㏇Cc]/ig,
		d: /[DdĎďDŽ-džDZ-dzᴰᵈḊ-ḓⅅⅆⅮⅾ⒟ⒹⓓDd]/ig,
		e: /[EeÈ-Ëè-ëĒ-ěȄ-ȇȨȩᴱᵉḘ-ḛẸ-ẽₑℯℰⅇ⒠ⒺⓔEe]/ig,
		f: /[FfᶠḞḟ℉ℱ℻⒡Ⓕⓕff-fflFf]/ig,
		g: /[GgĜ-ģǦǧǴǵᴳᵍḠḡℊ⒢ⒼⓖGg]/ig,
		h: /[HhĤĥȞȟʰᴴḢ-ḫẖℋ-ℎ⒣ⒽⓗHh]/ig,
		i: /[IiÌ-Ïì-ïĨ-İIJijǏǐȈ-ȋᴵᵢḬḭỈ-ịⁱℐℑℹⅈ⒤ⒾⓘIi]/ig,
		j: /[JjIJ-ĵLJ-njǰʲᴶⅉ⒥ⒿⓙⱼJj]/ig,
		k: /[KkĶķǨǩᴷᵏḰ-ḵK⒦ⓀⓚKk]/ig,
		l: /[LlĹ-ŀLJ-ljˡᴸḶḷḺ-ḽℒℓⅬⅼ⒧ⓁⓛLl]/ig,
		m: /[MmᴹᵐḾ-ṃ℠™ℳⅯⅿ⒨ⓂⓜMm]/ig,
		n: /[NnÑñŃ-ʼnNJ-njǸǹᴺṄ-ṋⁿℕ№⒩ⓃⓝNn]/ig,
		o: /[OoºÒ-Öò-öŌ-őƠơǑǒǪǫȌ-ȏȮȯᴼᵒỌ-ỏₒℴ⒪ⓄⓞOo]/ig,
		p: /[PpᴾᵖṔ-ṗℙ⒫Ⓟⓟ㎀㎊㎩Pp]/ig,
		q: /[Qqℚ⒬Ⓠⓠ㏃Qq]/ig,
		r: /[RrŔ-řȐ-ȓʳᴿᵣṘ-ṛṞṟ₨ℛ-ℝ⒭ⓇⓡRr]/ig,
		s: /[SsŚ-šſȘșˢṠ-ṣ₨℁℠⒮ⓈⓢstSs]/ig,
		t: /[TtŢ-ťȚțᵀᵗṪ-ṱẗ℡™⒯Ⓣⓣ㏏ſtstTt]/ig,
		u: /[UuÙ-Üù-üŨ-ųƯưǓǔȔ-ȗᵁᵘᵤṲ-ṷỤ-ủ℆⒰Ⓤⓤ㍳㍺Uu]/ig,
		v: /[VvᵛᵥṼ-ṿ⒱ⓋⓥⱽVv]/ig,
		w: /[WwŴŵʷᵂẀ-ẉẘ⒲ⓌⓦWw]/ig,
		x: /[XxˣẊ-ẍₓ⒳Ⓧⓧ㏓Xx]/ig,
		y: /[YyÝýÿŶ-ŸȲȳʸẎẏẙỲ-ỹ⒴ⓎⓨYy]/ig,
		z: /[ZzŹ-žDZ-dzᶻẐ-ẕℤℨ⒵ⓏⓩZz]/ig
	},
	_cache: {},
	word(str, isCyrillic = true) {
		var length = str.length,
			result = "",
			i = 0,
			previousCode = -1,
			maxLength = 6,
			code;

		while(i < length) {
			var current = last = this._codes[str[i]];

			for(var j = k = 1; k <= maxLength; k++) {
				if(!str[i + k] || !current[str[i + k]]) break;

				current = current[str[i + k]];

				if(current[0]) {
					last = current;
					j = k + 1;
				}
			}

			if(i === 0)
				code = last[0][0];
			else
				code = (isCyrillic || "1" in last === false ? last[0] : last[1])[
					str[i + j] && this._codes[str[i + j]][0][0] === 0 ? 1 : 2
				];

			if(code !== -1 && code !== previousCode) result += code;

			previousCode = code;
			i += j;
		}

		while(result && result.length < 3)
			result += "0";
	 
		return result;
	},

	calculate(str) {
		str = str.trim().toLowerCase();
		var cyrillicMatch = str.match(/[ёа-я]/g);

		if(cyrillicMatch)
			str = this.transliterate(str, cyrillicMatch);

		for(var letter in this._accented)
			str = str.replace(this._accented[letter], letter);

		str = str
			.replace(/[^\sa-z]/g, "")
			.replace(/\s{2,}/g, " ");

		if(!str) return [];

		str = str.split(" ").map(word => {
			if(word in this._cache === false)
				this._cache[word] = this.word(word, cyrillicMatch !== null);

			return this._cache[word];
		});

		Object.assign(this, str);
		this.length = str.length;
	},

	transliterate(str, matches) {
		for(const match of matches)
			str = str.replace(match, this._map[match]);

		return str;
	},

	toString() {
		return this.join(" ");
	}
};

function Rx(template, ...substitutions) {
	if(this instanceof Rx === false) return new Rx(template, ...substitutions);

	this.raw = template[0] + substitutions.map((v, i) => `(${v.source})${template[i + 1]}`).join("");
	
	this.pattern = (template.slice(1).map((part, index) => `(${substitutions[index].source}) ${Soundex(part)}`));
	this.pattern.unshift(Soundex(template[0]));
	this.pattern = new RegExp(this.pattern.join(" ").trim());
}

Rx.prototype = {
	test(expression) {
		return this.pattern.test(Soundex(expression));
	},

	match(expression) {
		return (this.pattern.exec(Soundex(expression)) || []).slice(1);
	}
};

// пример
const patterns = [
	Rx`Есть ли в наличии ${/[\s\S]+/}?`,
	Rx`Есть ли ${/[\s\S]+/} в наличии?`,
];

inputField.addEventListener("input", () => {
	patternsField.innerHTML = patterns.map(pattern => {
		const ok = pattern.test(inputField.value);
		return `<div class="${ok ? "matches" : ""}">${pattern.raw}</div>`;
	}).join("");
});

inputField.dispatchEvent(new Event("input"));
	
	</script>
</body>
</html>


[1]: Paramonov V., Shigarov A., Ruzhnikov G., Belykh P. Polyphon: An Algorithm for Phonetic String Matching in Russian Language // Communications in Computer and Information Science. Springer. 2016. Vol. 639, pp. 568-579.

ДЖАВАСКРИПТИЗЕР 16.03.2019 01:24

давно такого дерьма не смотрел, как уснуть то теперь

Nexus 16.03.2019 11:05

Malleys, Это гениально.
Большое спасибо за столь развернутый ответ, отдельное спасибо за пример.

К сожалению из-за ограничений сайта не могу вам карму увеличить.


Часовой пояс GMT +3, время: 13:28.