From 5aba23e92c41cd6b325019b46bac12a08510f499 Mon Sep 17 00:00:00 2001 From: Andrew Burke Date: Mon, 12 Sep 2022 13:40:47 -0400 Subject: [PATCH] First release --- .gitignore | 71 + Editor.meta | 8 + Editor/ComponentField.cs | 96 ++ Editor/ComponentField.cs.meta | 3 + Editor/ComponentGUI.cs | 53 + Editor/ComponentGUI.cs.meta | 3 + Editor/DuoVia.FuzzyStrings.meta | 3 + .../DiceCoefficientExtensions.cs | 70 + .../DiceCoefficientExtensions.cs.meta | 3 + .../DoubleMetaphoneExtensions.cs | 1234 +++++++++++++++++ .../DoubleMetaphoneExtensions.cs.meta | 3 + .../LevenshteinDistanceExtensions.cs | 72 + .../LevenshteinDistanceExtensions.cs.meta | 3 + .../LongestCommonSubsequenceExtensions.cs | 128 ++ ...LongestCommonSubsequenceExtensions.cs.meta | 3 + .../DuoVia.FuzzyStrings/StringExtensions.cs | 79 ++ .../StringExtensions.cs.meta | 3 + Editor/EcsLiteHierarchyWindow.cs | 226 +++ Editor/EcsLiteHierarchyWindow.cs.meta | 11 + Editor/EcsLiteInspectorWindow.cs | 186 +++ Editor/EcsLiteInspectorWindow.cs.meta | 11 + Editor/Fields.meta | 3 + Editor/Fields/ArrayGui.cs | 23 + Editor/Fields/ArrayGui.cs.meta | 3 + Editor/Fields/BoolGui.cs | 18 + Editor/Fields/BoolGui.cs.meta | 3 + Editor/Fields/BoundsGui.cs | 20 + Editor/Fields/BoundsGui.cs.meta | 3 + Editor/Fields/BoundsIntGui.cs | 20 + Editor/Fields/BoundsIntGui.cs.meta | 3 + Editor/Fields/Color32Gui.cs | 20 + Editor/Fields/Color32Gui.cs.meta | 3 + Editor/Fields/ColorGui.cs | 20 + Editor/Fields/ColorGui.cs.meta | 3 + Editor/Fields/CurveGui.cs | 20 + Editor/Fields/CurveGui.cs.meta | 3 + Editor/Fields/DoubleGui.cs | 19 + Editor/Fields/DoubleGui.cs.meta | 3 + Editor/Fields/EnumGui.cs | 34 + Editor/Fields/EnumGui.cs.meta | 3 + Editor/Fields/FieldGui.cs | 54 + Editor/Fields/FieldGui.cs.meta | 3 + Editor/Fields/FloatGui.cs | 19 + Editor/Fields/FloatGui.cs.meta | 3 + Editor/Fields/GradientGui.cs | 20 + Editor/Fields/GradientGui.cs.meta | 3 + Editor/Fields/IntegerGui.cs | 19 + Editor/Fields/IntegerGui.cs.meta | 3 + Editor/Fields/LayerMaskGui.cs | 20 + Editor/Fields/LayerMaskGui.cs.meta | 3 + Editor/Fields/LongField.cs | 19 + Editor/Fields/LongField.cs.meta | 3 + Editor/Fields/ObjectGui.cs | 28 + Editor/Fields/ObjectGui.cs.meta | 3 + Editor/Fields/QuaternionGui.cs | 24 + Editor/Fields/QuaternionGui.cs.meta | 3 + Editor/Fields/RectGui.cs | 20 + Editor/Fields/RectGui.cs.meta | 3 + Editor/Fields/RectIntGui.cs | 20 + Editor/Fields/RectIntGui.cs.meta | 3 + Editor/Fields/StringGui.cs | 18 + Editor/Fields/StringGui.cs.meta | 3 + Editor/Fields/Vector2Gui.cs | 20 + Editor/Fields/Vector2Gui.cs.meta | 3 + Editor/Fields/Vector2IntGui.cs | 20 + Editor/Fields/Vector2IntGui.cs.meta | 3 + Editor/Fields/Vector3Gui.cs | 20 + Editor/Fields/Vector3Gui.cs.meta | 3 + Editor/Fields/Vector3IntGui.cs | 20 + Editor/Fields/Vector3IntGui.cs.meta | 3 + Editor/Fields/Vector4Gui.cs | 20 + Editor/Fields/Vector4Gui.cs.meta | 3 + Editor/Resources.meta | 3 + Editor/Resources/ecsLite.meta | 3 + Editor/Resources/ecsLite/BaseComponent.uxml | 5 + .../Resources/ecsLite/BaseComponent.uxml.meta | 10 + Editor/Resources/ecsLite/HierarchyWindow.uxml | 11 + .../ecsLite/HierarchyWindow.uxml.meta | 10 + Editor/Resources/ecsLite/InspectorWindow.uxml | 15 + .../ecsLite/InspectorWindow.uxml.meta | 10 + Editor/UIElementsExtensions.cs | 26 + Editor/UIElementsExtensions.cs.meta | 3 + .../com.nomnom.ecslite-debugger.Editor.asmdef | 19 + ...nomnom.ecslite-debugger.Editor.asmdef.meta | 7 + LICENSE.md | 0 LICENSE.md.meta | 7 + README.md | 61 + README.md.meta | 7 + Runtime.meta | 8 + Runtime/WorldDebugSystem.cs | 80 ++ Runtime/WorldDebugSystem.cs.meta | 3 + Runtime/WorldDebugView.cs | 137 ++ Runtime/WorldDebugView.cs.meta | 3 + ...com.nomnom.ecslite-debugger.Runtime.asmdef | 16 + ...omnom.ecslite-debugger.Runtime.asmdef.meta | 7 + package.json | 24 + package.json.meta | 7 + ~Images.meta | 3 + ~Images/preview.png | Bin 0 -> 84587 bytes ~Images/preview.png.meta | 98 ++ 100 files changed, 3485 insertions(+) create mode 100644 .gitignore create mode 100644 Editor.meta create mode 100644 Editor/ComponentField.cs create mode 100644 Editor/ComponentField.cs.meta create mode 100644 Editor/ComponentGUI.cs create mode 100644 Editor/ComponentGUI.cs.meta create mode 100644 Editor/DuoVia.FuzzyStrings.meta create mode 100644 Editor/DuoVia.FuzzyStrings/DiceCoefficientExtensions.cs create mode 100644 Editor/DuoVia.FuzzyStrings/DiceCoefficientExtensions.cs.meta create mode 100644 Editor/DuoVia.FuzzyStrings/DoubleMetaphoneExtensions.cs create mode 100644 Editor/DuoVia.FuzzyStrings/DoubleMetaphoneExtensions.cs.meta create mode 100644 Editor/DuoVia.FuzzyStrings/LevenshteinDistanceExtensions.cs create mode 100644 Editor/DuoVia.FuzzyStrings/LevenshteinDistanceExtensions.cs.meta create mode 100644 Editor/DuoVia.FuzzyStrings/LongestCommonSubsequenceExtensions.cs create mode 100644 Editor/DuoVia.FuzzyStrings/LongestCommonSubsequenceExtensions.cs.meta create mode 100644 Editor/DuoVia.FuzzyStrings/StringExtensions.cs create mode 100644 Editor/DuoVia.FuzzyStrings/StringExtensions.cs.meta create mode 100644 Editor/EcsLiteHierarchyWindow.cs create mode 100644 Editor/EcsLiteHierarchyWindow.cs.meta create mode 100644 Editor/EcsLiteInspectorWindow.cs create mode 100644 Editor/EcsLiteInspectorWindow.cs.meta create mode 100644 Editor/Fields.meta create mode 100644 Editor/Fields/ArrayGui.cs create mode 100644 Editor/Fields/ArrayGui.cs.meta create mode 100644 Editor/Fields/BoolGui.cs create mode 100644 Editor/Fields/BoolGui.cs.meta create mode 100644 Editor/Fields/BoundsGui.cs create mode 100644 Editor/Fields/BoundsGui.cs.meta create mode 100644 Editor/Fields/BoundsIntGui.cs create mode 100644 Editor/Fields/BoundsIntGui.cs.meta create mode 100644 Editor/Fields/Color32Gui.cs create mode 100644 Editor/Fields/Color32Gui.cs.meta create mode 100644 Editor/Fields/ColorGui.cs create mode 100644 Editor/Fields/ColorGui.cs.meta create mode 100644 Editor/Fields/CurveGui.cs create mode 100644 Editor/Fields/CurveGui.cs.meta create mode 100644 Editor/Fields/DoubleGui.cs create mode 100644 Editor/Fields/DoubleGui.cs.meta create mode 100644 Editor/Fields/EnumGui.cs create mode 100644 Editor/Fields/EnumGui.cs.meta create mode 100644 Editor/Fields/FieldGui.cs create mode 100644 Editor/Fields/FieldGui.cs.meta create mode 100644 Editor/Fields/FloatGui.cs create mode 100644 Editor/Fields/FloatGui.cs.meta create mode 100644 Editor/Fields/GradientGui.cs create mode 100644 Editor/Fields/GradientGui.cs.meta create mode 100644 Editor/Fields/IntegerGui.cs create mode 100644 Editor/Fields/IntegerGui.cs.meta create mode 100644 Editor/Fields/LayerMaskGui.cs create mode 100644 Editor/Fields/LayerMaskGui.cs.meta create mode 100644 Editor/Fields/LongField.cs create mode 100644 Editor/Fields/LongField.cs.meta create mode 100644 Editor/Fields/ObjectGui.cs create mode 100644 Editor/Fields/ObjectGui.cs.meta create mode 100644 Editor/Fields/QuaternionGui.cs create mode 100644 Editor/Fields/QuaternionGui.cs.meta create mode 100644 Editor/Fields/RectGui.cs create mode 100644 Editor/Fields/RectGui.cs.meta create mode 100644 Editor/Fields/RectIntGui.cs create mode 100644 Editor/Fields/RectIntGui.cs.meta create mode 100644 Editor/Fields/StringGui.cs create mode 100644 Editor/Fields/StringGui.cs.meta create mode 100644 Editor/Fields/Vector2Gui.cs create mode 100644 Editor/Fields/Vector2Gui.cs.meta create mode 100644 Editor/Fields/Vector2IntGui.cs create mode 100644 Editor/Fields/Vector2IntGui.cs.meta create mode 100644 Editor/Fields/Vector3Gui.cs create mode 100644 Editor/Fields/Vector3Gui.cs.meta create mode 100644 Editor/Fields/Vector3IntGui.cs create mode 100644 Editor/Fields/Vector3IntGui.cs.meta create mode 100644 Editor/Fields/Vector4Gui.cs create mode 100644 Editor/Fields/Vector4Gui.cs.meta create mode 100644 Editor/Resources.meta create mode 100644 Editor/Resources/ecsLite.meta create mode 100644 Editor/Resources/ecsLite/BaseComponent.uxml create mode 100644 Editor/Resources/ecsLite/BaseComponent.uxml.meta create mode 100644 Editor/Resources/ecsLite/HierarchyWindow.uxml create mode 100644 Editor/Resources/ecsLite/HierarchyWindow.uxml.meta create mode 100644 Editor/Resources/ecsLite/InspectorWindow.uxml create mode 100644 Editor/Resources/ecsLite/InspectorWindow.uxml.meta create mode 100644 Editor/UIElementsExtensions.cs create mode 100644 Editor/UIElementsExtensions.cs.meta create mode 100644 Editor/com.nomnom.ecslite-debugger.Editor.asmdef create mode 100644 Editor/com.nomnom.ecslite-debugger.Editor.asmdef.meta create mode 100644 LICENSE.md create mode 100644 LICENSE.md.meta create mode 100644 README.md create mode 100644 README.md.meta create mode 100644 Runtime.meta create mode 100644 Runtime/WorldDebugSystem.cs create mode 100644 Runtime/WorldDebugSystem.cs.meta create mode 100644 Runtime/WorldDebugView.cs create mode 100644 Runtime/WorldDebugView.cs.meta create mode 100644 Runtime/com.nomnom.ecslite-debugger.Runtime.asmdef create mode 100644 Runtime/com.nomnom.ecslite-debugger.Runtime.asmdef.meta create mode 100644 package.json create mode 100644 package.json.meta create mode 100644 ~Images.meta create mode 100644 ~Images/preview.png create mode 100644 ~Images/preview.png.meta diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07da593 --- /dev/null +++ b/.gitignore @@ -0,0 +1,71 @@ +# This .gitignore file should be placed at the root of your Unity project directory +# +# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore +# +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/[Ll]ogs/ +/[Uu]ser[Ss]ettings/ + +# MemoryCaptures can get excessive in size. +# They also could contain extremely sensitive data +/[Mm]emoryCaptures/ + +# Asset meta data should only be ignored when the corresponding asset is also ignored +!/[Aa]ssets/**/*.meta + +# Uncomment this line if you wish to ignore the asset store tools plugin +# /[Aa]ssets/AssetStoreTools* + +# Autogenerated Jetbrains Rider plugin +/[Aa]ssets/Plugins/Editor/JetBrains* + +# Visual Studio cache directory +.vs/ + +# Gradle cache directory +.gradle/ + +# Autogenerated VS/MD/Consulo solution and project files +ExportedObj/ +.consulo/ +*.csproj +*.unityproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd +*.pdb +*.mdb +*.opendb +*.VC.db + +# Unity3D generated meta files +*.pidb.meta +*.pdb.meta +*.mdb.meta + +# Unity3D generated file on crash reports +sysinfo.txt + +# Builds +*.apk +*.aab +*.unitypackage + +# Crashlytics generated file +crashlytics-build.properties + +# Packed Addressables +/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* + +# Temporary auto-generated Android Assets +/[Aa]ssets/[Ss]treamingAssets/aa.meta +/[Aa]ssets/[Ss]treamingAssets/aa/* \ No newline at end of file diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..2c863be --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c449bc69e42472648865b4f298e82e7f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ComponentField.cs b/Editor/ComponentField.cs new file mode 100644 index 0000000..22454fe --- /dev/null +++ b/Editor/ComponentField.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Leopotam.EcsLite; +using Nomnom.EcsLiteDebugger.Editor.Fields; +using UnityEditor; +using UnityEngine.UIElements; + +namespace Nomnom.EcsLiteDebugger.Editor { + public class ComponentField: VisualElement { + private static Dictionary _lookup; + + private readonly IEcsPool _pool; + private readonly int _entity; + private readonly FieldInfo _fieldInfo; + + private VisualElement _field; + private IFieldGui _guiInstance; + + static ComponentField() { + _lookup = new Dictionary(); + + TypeCache.TypeCollection types = TypeCache.GetTypesDerivedFrom(typeof(FieldGui<>)); + + foreach (Type type in types) { + Type[] args = type.BaseType.GenericTypeArguments; + + _lookup[args[0]] = type; + } + } + + public ComponentField(IEcsPool pool, int entity, FieldInfo fieldInfo) { + _pool = pool; + _entity = entity; + _fieldInfo = fieldInfo; + } + + public void Refresh() { + if (_field == null) { + _field = GatherValue(GetReference(), _fieldInfo); + Add(_field); + return; + } + + UpdateValue(); + } + + private object GetReference() { + return _pool.GetRaw(_entity); + } + + private VisualElement GatherValue(object item, FieldInfo fieldInfo) { + Type type = fieldInfo.FieldType; + VisualElement element; + bool hasLookup = _lookup.TryGetValue(type, out Type guiType); + + if (hasLookup || TryFindBaseType(type, out guiType)) { + _guiInstance = (IFieldGui)Activator.CreateInstance(guiType, this); + element = _guiInstance.Create(item, fieldInfo); + } else { + element = new Label("Unsupported type"); + } + + return element; + } + + private bool TryFindBaseType(Type type, out Type gui) { + foreach (Type lookupKey in _lookup.Keys) { + if (!lookupKey.IsAssignableFrom(type)) { + continue; + } + + gui = _lookup[lookupKey]; + return true; + } + + gui = default; + return false; + } + + public void UpdateReference(FieldInfo info, object value) { + object reference = GetReference(); + info.SetValue(reference, value); + _pool.SetRaw(_entity, reference); + } + + private void UpdateValue() { + if (_guiInstance == null || _guiInstance.IsEditing) { + return; + } + + object value = _fieldInfo.GetValue(GetReference()); + _guiInstance.UpdateValue(value); + } + } +} \ No newline at end of file diff --git a/Editor/ComponentField.cs.meta b/Editor/ComponentField.cs.meta new file mode 100644 index 0000000..27706fb --- /dev/null +++ b/Editor/ComponentField.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e69ffb782c3d4b5d971deff551644143 +timeCreated: 1662923411 \ No newline at end of file diff --git a/Editor/ComponentGUI.cs b/Editor/ComponentGUI.cs new file mode 100644 index 0000000..8d98e23 --- /dev/null +++ b/Editor/ComponentGUI.cs @@ -0,0 +1,53 @@ +using System; +using System.Reflection; +using Leopotam.EcsLite; +using UnityEngine.UIElements; + +namespace Nomnom.EcsLiteDebugger.Editor { + internal class ComponentGUI: VisualElement { + private Type _type; + private FieldInfo[] _fields; + private EcsWorld _world; + private int _entity; + private bool _empty; + + public ComponentGUI(Type type, int entity, EcsWorld world) { + _type = type; + _fields = _type.GetFields(); + _world = world; + _entity = entity; + _empty = _fields.Length == 0; + + if (_empty) { + Add(new Label("No fields to show")); + } + } + + public void Refresh() { + if (_empty) { + return; + } + + if (_fields.Length != childCount) { + Clear(); + + IEcsPool pool = _world.GetPoolByType(_type); + + foreach (FieldInfo fieldInfo in _fields) { + ComponentField field = new ComponentField(pool, _entity, fieldInfo); + field.Refresh(); + Add(field); + } + } + + if (parent is Toggle {value: false}) { + return; + } + + foreach (VisualElement visualElement in Children()) { + ComponentField field = visualElement as ComponentField; + field.Refresh(); + } + } + } +} \ No newline at end of file diff --git a/Editor/ComponentGUI.cs.meta b/Editor/ComponentGUI.cs.meta new file mode 100644 index 0000000..1279989 --- /dev/null +++ b/Editor/ComponentGUI.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 40358bdd467141949cfeaf8155b3fe88 +timeCreated: 1662921773 \ No newline at end of file diff --git a/Editor/DuoVia.FuzzyStrings.meta b/Editor/DuoVia.FuzzyStrings.meta new file mode 100644 index 0000000..d250b44 --- /dev/null +++ b/Editor/DuoVia.FuzzyStrings.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f5e4e596318c4343b3d9988b20c1949d +timeCreated: 1662837350 \ No newline at end of file diff --git a/Editor/DuoVia.FuzzyStrings/DiceCoefficientExtensions.cs b/Editor/DuoVia.FuzzyStrings/DiceCoefficientExtensions.cs new file mode 100644 index 0000000..d765d16 --- /dev/null +++ b/Editor/DuoVia.FuzzyStrings/DiceCoefficientExtensions.cs @@ -0,0 +1,70 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Derived from http://www.codeguru.com/vb/gen/vb_misc/algorithms/article.php/c13137__1/Fuzzy-Matching-Demo-in-Access.htm + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +using System.Linq; + +namespace DuoVia.FuzzyStrings +{ + public static class DiceCoefficientExtensions + { + /// + /// Dice Coefficient based on bigrams.
+ /// A good value would be 0.33 or above, a value under 0.2 is not a good match, from 0.2 to 0.33 is iffy. + ///
+ /// + /// + /// + public static double DiceCoefficient(this string input, string comparedTo) + { + var ngrams = input.ToBiGrams(); + var compareToNgrams = comparedTo.ToBiGrams(); + return ngrams.DiceCoefficient(compareToNgrams); + } + + /// + /// Dice Coefficient used to compare nGrams arrays produced in advance. + /// + /// + /// + /// + public static double DiceCoefficient(this string[] nGrams, string[] compareToNGrams) + { + int matches = nGrams.Intersect(compareToNGrams).Count(); + if (matches == 0) return 0.0d; + double totalBigrams = nGrams.Length + compareToNGrams.Length; + return (2 * matches) / totalBigrams; + } + + public static string[] ToBiGrams(this string input) + { + // nLength == 2 + // from Jackson, return %j ja ac ck ks so on n# + // from Main, return #m ma ai in n# + input = SinglePercent + input + SinglePound; + return ToNGrams(input, 2); + } + + public static string[] ToTriGrams(this string input) + { + // nLength == 3 + // from Jackson, return %%j %ja jac ack cks kso son on# n## + // from Main, return ##m #ma mai ain in# n## + input = DoublePercent + input + DoublePount; + return ToNGrams(input, 3); + } + + private static string[] ToNGrams(string input, int nLength) + { + int itemsCount = input.Length - 1; + string[] ngrams = new string[input.Length - 1]; + for (int i = 0; i < itemsCount; i++) ngrams[i] = input.Substring(i, nLength); + return ngrams; + } + + private const string SinglePercent = "%"; + private const string SinglePound = "#"; + private const string DoublePercent = "&&"; + private const string DoublePount = "##"; + } +} diff --git a/Editor/DuoVia.FuzzyStrings/DiceCoefficientExtensions.cs.meta b/Editor/DuoVia.FuzzyStrings/DiceCoefficientExtensions.cs.meta new file mode 100644 index 0000000..c60aa19 --- /dev/null +++ b/Editor/DuoVia.FuzzyStrings/DiceCoefficientExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 98c1bc3445314715b3fd92f11200e2b2 +timeCreated: 1662837350 \ No newline at end of file diff --git a/Editor/DuoVia.FuzzyStrings/DoubleMetaphoneExtensions.cs b/Editor/DuoVia.FuzzyStrings/DoubleMetaphoneExtensions.cs new file mode 100644 index 0000000..ee42b5e --- /dev/null +++ b/Editor/DuoVia.FuzzyStrings/DoubleMetaphoneExtensions.cs @@ -0,0 +1,1234 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Derived from http://doublemetaphone.googlecode.com/svn/tags/1/DoubleMetaphone.cs + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* Origianal Work Copyright Notice included here as required: + + Copyright (c) 2008 Anthony Tong Lee + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Text; + +namespace DuoVia.FuzzyStrings +{ + /// + /// DoubleMetaphone string extension + /// + /// + /// Original C++ implementation: + /// "Double Metaphone (c) 1998, 1999 by Lawrence Philips" + /// http://www.ddj.com/cpp/184401251?pgno=1 + /// + public static class DoubleMetaphoneExtensions + { + public static string ToDoubleMetaphone(this string input) + { + MetaphoneData metaphoneData = new MetaphoneData(); + int current = 0; + + if (input.Length < 1) + { + return input; + } + int last = input.Length - 1; //zero based index + + string workingString = input.ToUpperInvariant() + " "; + + bool isSlavoGermanic = (input.IndexOf(charW) > -1) + || (input.IndexOf(charK) > -1) + || (input.IndexOf(strCZ, StringComparison.OrdinalIgnoreCase) > -1) + || (input.IndexOf(strWITZ, StringComparison.OrdinalIgnoreCase) > -1); + + //skip these when at start of word + if (workingString.StartsWith(StringComparison.OrdinalIgnoreCase, strGN, strKN, strPN, strWR, strPS)) + { + current += 1; + } + + //Initial 'X' is pronounced 'Z' e.g. 'Xavier' + if (workingString[0] == charX) + { + metaphoneData.Add(strS); //'Z' maps to 'S' + current += 1; + } + + while ((metaphoneData.PrimaryLength < 4) || (metaphoneData.SecondaryLength < 4)) + { + if (current >= input.Length) + { + break; + } + + switch (workingString[current]) + { + case charA: + case charE: + case charI: + case charO: + case charU: + case charY: + if (current == 0) + { + //all init vowels now map to 'A' + metaphoneData.Add("A"); + } + current += 1; + break; + + case charB: + //"-mb", e.g", "dumb", already skipped over... + metaphoneData.Add("P"); + + if (workingString[current + 1] == charB) + { + current += 2; + } + else + { + current += 1; + } + break; + + case charAdash: + metaphoneData.Add(strS); + current += 1; + break; + + case charC: + //various germanic + if ((current > 1) + && !IsVowel(workingString[current - 2]) + && StringAt(workingString, (current - 1), strACH) + && ((workingString[current + 2] != charI) + && ((workingString[current + 2] != charE) + || StringAt(workingString, (current - 2), strBACHER, strMACHER)))) + { + metaphoneData.Add(strK); + current += 2; + break; + } + + //special case 'caesar' + if ((current == 0) && StringAt(workingString, current, strCAESAR)) + { + metaphoneData.Add(strS); + current += 2; + break; + } + + //italian 'chianti' + if (StringAt(workingString, current, strCHIA)) + { + metaphoneData.Add(strK); + current += 2; + break; + } + + if (StringAt(workingString, current, strCH)) + { + //find 'michael' + if ((current > 0) && StringAt(workingString, current, strCHAE)) + { + metaphoneData.Add(strK, strX); + current += 2; + break; + } + + //greek roots e.g. 'chemistry', 'chorus' + if ((current == 0) + && (StringAt(workingString, (current + 1), strHARAC, strHARIS) + || StringAt(workingString, (current + 1), strHOR, strHYM, strHIA, strHEM)) + && !StringAt(workingString, 0, strCHORE)) + { + metaphoneData.Add(strK); + current += 2; + break; + } + + //germanic, greek, or otherwise 'ch' for 'kh' sound + if ((StringAt(workingString, 0, strVANsp, strVONsp) + || StringAt(workingString, 0, strSCH)) // 'architect but not 'arch', 'orchestra', 'orchid' + || StringAt(workingString, (current - 2), strORCHES, strARCHIT, strORCHID) + || StringAt(workingString, (current + 2), strT, strS) + || ((StringAt(workingString, (current - 1), strA, strO, strU, strE) + || (current == 0)) //e.g., 'wachtler', 'wechsler', but not 'tichner' + && StringAt(workingString, (current + 2), strL, strR, strN, strM, strB, strH, strF, strV, strW, sp))) + { + metaphoneData.Add(strK); + } + else + { + if (current > 0) + { + if (StringAt(workingString, 0, strMC)) + { + //e.g., "McHugh" + metaphoneData.Add(strK); + } + else + { + metaphoneData.Add(strX, strK); + } + } + else + { + metaphoneData.Add(strX); + } + } + current += 2; + break; + } + //e.g, 'czerny' + if (StringAt(workingString, current, strCZ) && !StringAt(workingString, (current - 2), strWICZ)) + { + metaphoneData.Add(strS, strX); + current += 2; + break; + } + + //e.g., 'focaccia' + if (StringAt(workingString, (current + 1), strCIA)) + { + metaphoneData.Add(strX); + current += 3; + break; + } + + //double 'C', but not if e.g. 'McClellan' + if (StringAt(workingString, current, strCC) && !((current == 1) && (workingString[0] == charM))) + { + //'bellocchio' but not 'bacchus' + if (StringAt(workingString, (current + 2), strI, strE, strH) + && !StringAt(workingString, (current + 2), strHU)) + { + //'accident', 'accede' 'succeed' + if (((current == 1) && (workingString[current - 1] == charA)) + || StringAt(workingString, (current - 1), strUCCEE, strUCCES)) + { + metaphoneData.Add(strKS); + } + //'bacci', 'bertucci', other italian + else + { + metaphoneData.Add(strX); + } + current += 3; + break; + } + else + { + //Pierce's rule + metaphoneData.Add(strK); + current += 2; + break; + } + } + + if (StringAt(workingString, current, strCK, strCG, strCQ)) + { + metaphoneData.Add(strK); + current += 2; + break; + } + + if (StringAt(workingString, current, strCI, strCE, strCY)) + { + //italian vs. english + if (StringAt(workingString, current, strCIO, strCIE, strCIA)) + { + metaphoneData.Add(strS, strX); + } + else + { + metaphoneData.Add(strS); + } + current += 2; + break; + } + + //else + metaphoneData.Add(strK); + + //name sent in 'mac caffrey', 'mac gregor + if (StringAt(workingString, (current + 1), strspC, strspQ, strspG)) + { + current += 3; + } + else if (StringAt(workingString, (current + 1), strC, strK, strQ) + && !StringAt(workingString, (current + 1), strCE, strCI)) + { + current += 2; + } + else + { + current += 1; + } + break; + + case charD: + if (StringAt(workingString, current, strDG)) + { + if (StringAt(workingString, (current + 2), strI, strE, strY)) + { + //e.g. 'edge' + metaphoneData.Add(strJ); + current += 3; + break; + } + else + { + //e.g. 'edgar' + metaphoneData.Add(strTK); + current += 2; + break; + } + } + + if (StringAt(workingString, current, strDT, strDD)) + { + metaphoneData.Add(strT); + current += 2; + break; + } + + //else + metaphoneData.Add(strT); + current += 1; + break; + + case charF: + if (workingString[current + 1] == charF) + { + current += 2; + } + else + { + current += 1; + } + metaphoneData.Add(strF); + break; + + case charG: + if (workingString[current + 1] == charH) + { + if ((current > 0) && !IsVowel(workingString[current - 1])) + { + metaphoneData.Add(strK); + current += 2; + break; + } + + if (current < 3) + { + //'ghislane', ghiradelli + if (current == 0) + { + if (workingString[current + 2] == charI) + { + metaphoneData.Add(strJ); + } + else + { + metaphoneData.Add(strK); + } + current += 2; + break; + } + } + //Parker's rule (with some further refinements) - e.g., 'hugh' + if (((current > 1) && StringAt(workingString, (current - 2), strB, strH, strD)) //e.g., 'bough' + || ((current > 2) && StringAt(workingString, (current - 3), strB, strH, strD)) //e.g., 'broughton' + || ((current > 3) && StringAt(workingString, (current - 4), strB, strH))) + { + current += 2; + break; + } + else + { + //e.g., 'laugh', 'McLaughlin', 'cough', 'gough', 'rough', 'tough' + if ((current > 2) && (workingString[current - 1] == charU) + && StringAt(workingString, (current - 3), strC, strG, strL, strR, strT)) + { + metaphoneData.Add(strF); + } + else if ((current > 0) && workingString[current - 1] != charI) + { + metaphoneData.Add(strK); + } + + current += 2; + break; + } + } + + if (workingString[current + 1] == charN) + { + if ((current == 1) && IsVowel(workingString[0]) && !isSlavoGermanic) + { + metaphoneData.Add(strKN, strN); + } + else + //not e.g. 'cagney' + if (!StringAt(workingString, (current + 2), strEY) + && (workingString[current + 1] != charY) && !isSlavoGermanic) + { + metaphoneData.Add(strN, strKN); + } + else + { + metaphoneData.Add(strKN); + } + current += 2; + break; + } + + //'tagliaro' + if (StringAt(workingString, (current + 1), strLI) && !isSlavoGermanic) + { + metaphoneData.Add(strKL, strL); + current += 2; + break; + } + + //-ges-,-gep-,-gel-, -gie- at beginning + if ((current == 0) + && ((workingString[current + 1] == charY) + || StringAt(workingString, (current + 1), strES, strEP, strEB, strEL, strEY, strIB, strIL, strIN, strIE, strEI, strER))) + { + metaphoneData.Add(strK, strJ); + current += 2; + break; + } + + // -ger-, -gy- + if ((StringAt(workingString, (current + 1), strER) + || (workingString[current + 1] == charY)) + && !StringAt(workingString, 0, strDANGER, strRANGER, strMANGER) + && !StringAt(workingString, (current - 1), strE, strI) + && !StringAt(workingString, (current - 1), strRGY, strOGY)) + { + metaphoneData.Add(strK, strJ); + current += 2; + break; + } + + // italian e.g, 'biaggi' + if (StringAt(workingString, (current + 1), strE, strI, strY) + || StringAt(workingString, (current - 1), strAGGI, strOGGI)) + { + //obvious germanic + if ((StringAt(workingString, 0, strVANsp, strVONsp) + || StringAt(workingString, 0, strSCH)) + || StringAt(workingString, (current + 1), strET)) + { + metaphoneData.Add(strK); + } + else + //always soft if french ending + if (StringAt(workingString, (current + 1), strIERsp)) + { + metaphoneData.Add(strJ); + } + else + { + metaphoneData.Add(strJ, strK); + } + current += 2; + break; + } + + if (workingString[current + 1] == charG) + { + current += 2; + } + else + { + current += 1; + } + metaphoneData.Add(strK); + break; + + case 'H': + //only keep if first & before vowel or btw. 2 vowels + if (((current == 0) || IsVowel(workingString[current - 1])) && IsVowel(workingString[current + 1])) + { + metaphoneData.Add(strH); + current += 2; + } + else //also takes care of 'HH' + { + current += 1; + } + break; + + case 'J': + //obvious spanish, 'jose', 'san jacinto' + if (StringAt(workingString, current, strJOSE) || StringAt(workingString, 0, strSANsp)) + { + if (((current == 0) && (workingString[current + 4] == ' ')) || StringAt(workingString, 0, strSANsp)) + { + metaphoneData.Add(strH); + } + else + { + metaphoneData.Add(strJ, strH); + } + current += 1; + break; + } + + if ((current == 0) && !StringAt(workingString, current, strJOSE)) + { + metaphoneData.Add(strJ, strA); //Yankelovich/Jankelowicz + } + else + //spanish pron. of e.g. 'bajador' + if (current > 0 + && IsVowel(workingString[current - 1]) + && !isSlavoGermanic && ((workingString[current + 1] == charA) + || (workingString[current + 1] == charO))) + { + metaphoneData.Add(strJ, strH); + } + else if (current == last) + { + metaphoneData.Add(strJ, sp); + } + else if (!StringAt(workingString, (current + 1), strL, strT, strK, strS, strN, strM, strB, strZ) + && !StringAt(workingString, (current - 1), strS, strK, strL)) + { + metaphoneData.Add(strJ); + } + + if (workingString[current + 1] == charJ) //it could happen! + { + current += 2; + } + else + { + current += 1; + } + break; + + case charK: + if (workingString[current + 1] == charK) + { + current += 2; + } + else + { + current += 1; + } + metaphoneData.Add(strK); + break; + + case charL: + if (workingString[current + 1] == charL) + { + //spanish e.g. 'cabrillo', 'gallegos' + if (((current == (input.Length - 3)) + && StringAt(workingString, (current - 1), strILLO, strILLA, strALLE)) + || ((StringAt(workingString, (last - 1), strAS, strOS) + || StringAt(workingString, last, strA, strO)) + && StringAt(workingString, (current - 1), strALLE))) + { + metaphoneData.Add(strL, sp); + current += 2; + break; + } + current += 2; + } + else + { + current += 1; + } + metaphoneData.Add("L"); + break; + + case charM: + if ((StringAt(workingString, (current - 1), strUMB) + && (((current + 1) == last) + || StringAt(workingString, (current + 2), strER))) //'dumb','thumb' + || (workingString[current + 1] == charM)) + { + current += 2; + } + else + { + current += 1; + } + metaphoneData.Add("M"); + break; + + case charN: + if (workingString[current + 1] == charN) + { + current += 2; + } + else + { + current += 1; + } + metaphoneData.Add(strN); + break; + + case charOdash: + current += 1; + metaphoneData.Add(strN); + break; + + case charP: + if (workingString[current + 1] == charH) + { + metaphoneData.Add(strF); + current += 2; + break; + } + + //also account for "campbell", "raspberry" + if (StringAt(workingString, (current + 1), strP, strB)) + { + current += 2; + } + else + { + current += 1; + } + metaphoneData.Add(strP); + break; + + case charQ: + if (workingString[current + 1] == charQ) + { + current += 2; + } + else + { + current += 1; + } + metaphoneData.Add(strK); + break; + + case charR: + //french e.g. 'rogier', but exclude 'hochmeier' + if ((current == last) && !isSlavoGermanic + && StringAt(workingString, (current - 2), strIE) + && !StringAt(workingString, (current - 4), strME, strMA)) + { + metaphoneData.Add(string.Empty, strR); + } + else + { + metaphoneData.Add(strR); + } + + if (workingString[current + 1] == charR) + { + current += 2; + } + else + { + current += 1; + } + break; + + case charS: + //special cases 'island', 'isle', 'carlisle', 'carlysle' + if (StringAt(workingString, (current - 1), strISL, strYSL)) + { + current += 1; + break; + } + + //special case 'sugar-' + if ((current == 0) && StringAt(workingString, current, strSUGAR)) + { + metaphoneData.Add(strX, strS); + current += 1; + break; + } + + if (StringAt(workingString, current, strSH)) + { + //germanic + if (StringAt(workingString, (current + 1), strHEIM, strHOEK, strHOLM, strHOLZ)) + { + metaphoneData.Add(strS); + } + else + { + metaphoneData.Add(strX); + } + current += 2; + break; + } + + //italian & armenian + if (StringAt(workingString, current, strSIO, strSIA) || StringAt(workingString, current, strSIAN)) + { + if (!isSlavoGermanic) + { + metaphoneData.Add(strS, strX); + } + else + { + metaphoneData.Add(strS); + } + current += 3; + break; + } + + //german & anglicisations, e.g. 'smith' match 'schmidt', 'snider' match 'schneider' + //also, -sz- in slavic language altho in hungarian it is pronounced 's' + if (((current == 0) + && StringAt(workingString, (current + 1), strM, strN, strL, strW)) + || StringAt(workingString, (current + 1), strZ)) + { + metaphoneData.Add(strS, strX); + if (StringAt(workingString, (current + 1), strZ)) + { + current += 2; + } + else + { + current += 1; + } + break; + } + + if (StringAt(workingString, current, strSC)) + { + //Schlesinger's rule + if (workingString[current + 2] == charH) + { + //dutch origin, e.g. 'school', 'schooner' + if (StringAt(workingString, (current + 3), strOO, strER, strEN, strUY, strED, strEM)) + { + //'schermerhorn', 'schenker' + if (StringAt(workingString, (current + 3), strER, strEN)) + { + metaphoneData.Add(strX, strSK); + } + else + { + metaphoneData.Add(strSK); + } + current += 3; + break; + } + else + { + if ((current == 0) && !IsVowel(workingString[3]) && (workingString[3] != charW)) + { + metaphoneData.Add(strX, strS); + } + else + { + metaphoneData.Add(strX); + } + current += 3; + break; + } + } + + if (StringAt(workingString, (current + 2), strI, strE, strY)) + { + metaphoneData.Add(strS); + current += 3; + break; + } + //else + metaphoneData.Add(strSK); + current += 3; + break; + } + + //french e.g. 'resnais', 'artois' + if ((current == last) && StringAt(workingString, (current - 2), strAI, strOI)) + { + metaphoneData.Add(string.Empty, strS); + } + else + { + metaphoneData.Add(strS); + } + + if (StringAt(workingString, (current + 1), strS, strZ)) + { + current += 2; + } + else + { + current += 1; + } + break; + + case charT: + if (StringAt(workingString, current, strTION)) + { + metaphoneData.Add(strX); + current += 3; + break; + } + + if (StringAt(workingString, current, strTIA, strTCH)) + { + metaphoneData.Add(strX); + current += 3; + break; + } + + if (StringAt(workingString, current, strTH) || StringAt(workingString, current, strTTH)) + { + //special case 'thomas', 'thames' or germanic + if (StringAt(workingString, (current + 2), strOM, strAM) + || StringAt(workingString, 0, strVANsp, strVONsp) || StringAt(workingString, 0, strSCH)) + { + metaphoneData.Add(strT); + } + else + { + metaphoneData.Add(strO, strT); + } + current += 2; + break; + } + + if (StringAt(workingString, (current + 1), strT, strD)) + { + current += 2; + } + else + { + current += 1; + } + metaphoneData.Add(strT); + break; + + case charV: + if (workingString[current + 1] == charV) + { + current += 2; + } + else + { + current += 1; + } + metaphoneData.Add(strF); + break; + + case charW: + //can also be in middle of word + if (StringAt(workingString, current, strWR)) + { + metaphoneData.Add(strR); + current += 2; + break; + } + + if ((current == 0) && (IsVowel(workingString[current + 1]) + || StringAt(workingString, current, strWH))) + { + //Wasserman should match Vasserman + if (IsVowel(workingString[current + 1])) + { + metaphoneData.Add(strA, strF); + } + else + { + //need Uomo to match Womo + metaphoneData.Add(strA); + } + } + + //Arnow should match Arnoff + if ((current == last && current > 0 && IsVowel(workingString[current - 1])) + || StringAt(workingString, (current - 1), strEWSKI, strEWSKY, strOWSKI, strOWSKY) + || StringAt(workingString, 0, strSCH)) + { + metaphoneData.Add(string.Empty, strF); + current += 1; + break; + } + + //polish e.g. 'filipowicz' + if (StringAt(workingString, current, strWICZ, strWITZ)) + { + metaphoneData.Add(strTS, strFX); + current += 4; + break; + } + + //else skip it + current += 1; + break; + + case charX: + //french e.g. breaux + if (!((current == last) + && (StringAt(workingString, (current - 3), strIAU, strEAU) + || StringAt(workingString, (current - 2), strAU, strOU)))) + { + metaphoneData.Add(strKS); + } + + if (StringAt(workingString, (current + 1), strC, strX)) + { + current += 2; + } + else + { + current += 1; + } + break; + + case charZ: + //chinese pinyin e.g. 'zhao' + if (workingString[current + 1] == charH) + { + metaphoneData.Add(strJ); + current += 2; + break; + } + else if (StringAt(workingString, (current + 1), strZO, strZI, strZA) + || (isSlavoGermanic && ((current > 0) && workingString[current - 1] != charT))) + { + metaphoneData.Add(strS, strTS); + } + else + { + metaphoneData.Add(strS); + } + + if (workingString[current + 1] == charZ) + { + current += 2; + } + else + { + current += 1; + } + break; + + default: + current += 1; + break; + } + } + + return metaphoneData.ToString(); + } + + + static bool IsVowel(this char self) + { + return (self == charA) || (self == charE) || (self == charI) + || (self == charO) || (self == charU) || (self == charY); + } + + private const char charA = 'A'; + private const char charW = 'W'; + private const char charK = 'K'; + private const string strCZ = "CZ"; + private const string strWITZ = "WITZ"; + private const string strGN = "GN"; + private const string strKN = "KN"; + private const string strPN = "PN"; + private const string strWR = "WR"; + private const string strPS = "PS"; + private const char charX = 'X'; + private const string strS = "S"; + private const char charE = 'E'; + private const char charI = 'I'; + private const char charO = 'O'; + private const char charU = 'U'; + private const char charY = 'Y'; + private const char charB = 'B'; + private const char charAdash = 'Ã'; + private const string strACH = "ACH"; + private const string strBACHER = "BACHER"; + private const string strMACHER = "MACHER"; + private const string strK = "K"; + private const string strCAESAR = "CAESAR"; + private const string strCHIA = "CHIA"; + private const string strCH = "CH"; + private const string strCHAE = "CHAE"; + private const string strX = "X"; + private const string strHARAC = "HARAC"; + private const string strHARIS = "HARIS"; + private const string strHOR = "HOR"; + private const string strHYM = "HYM"; + private const string strHIA = "HIA"; + private const string strHEM = "HEM"; + private const string strCHORE = "CHORE"; + private const string strVANsp = "VAN "; + private const string strVONsp = "VON "; + private const string strSCH = "SCH"; + private const string strORCHES = "ORCHES"; + private const string strARCHIT = "ARCHIT"; + private const string strORCHID = "ORCHID"; + private const string strT = "T"; + private const string strA = "A"; + private const string strO = "O"; + private const string strU = "U"; + private const string strE = "E"; + private const string strL = "L"; + private const string strR = "R"; + private const string strN = "N"; + private const string strM = "M"; + private const string strB = "B"; + private const string strH = "H"; + private const string strF = "F"; + private const string strV = "V"; + private const string strW = "W"; + private const string sp = " "; + private const string strMC = "MC"; + private const string strWICZ = "WICZ"; + private const string strCIA = "CIA"; + private const string strCC = "CC"; + private const char charM = 'M'; + private const string strI = "I"; + private const string strHU = "HU"; + private const string strUCCEE = "UCCEE"; + private const string strUCCES = "UCCES"; + private const string strKS = "KS"; + private const string strCK = "CK"; + private const string strCG = "CG"; + private const string strCQ = "CQ"; + private const string strCI = "CI"; + private const string strCE = "CE"; + private const string strCY = "CY"; + private const string strCIO = "CIO"; + private const string strCIE = "CIE"; + private const string strspC = " C"; + private const string strspQ = " Q"; + private const string strspG = " G"; + private const string strC = "C"; + private const string strQ = "Q"; + private const char charC = 'C'; + private const char charD = 'D'; + private const string strDG = "DG"; + private const string strY = "Y"; + private const string strJ = "J"; + private const string strTK = "TK"; + private const string strDT = "DT"; + private const string strDD = "DD"; + private const char charF = 'F'; + private const char charG = 'G'; + private const char charH = 'H'; + private const string strD = "D"; + private const string strG = "G"; + private const char charN = 'N'; + private const string strEY = "EY"; + private const string strLI = "LI"; + private const string strKL = "KL"; + private const string strES = "ES"; + private const string strEP = "EP"; + private const string strEB = "EB"; + private const string strEL = "EL"; + private const string strIB = "IB"; + private const string strIL = "IL"; + private const string strIN = "IN"; + private const string strIE = "IE"; + private const string strEI = "EI"; + private const string strER = "ER"; + private const string strDANGER = "DANGER"; + private const string strRANGER = "RANGER"; + private const string strMANGER = "MANGER"; + private const string strRGY = "RGY"; + private const string strOGY = "OGY"; + private const string strAGGI = "AGGI"; + private const string strOGGI = "OGGI"; + private const string strIERsp = "IER "; + private const string strJOSE = "JOSE"; + private const string strSANsp = "SAN "; + private const string strZ = "Z"; + private const char charJ = 'J'; + private const char charL = 'L'; + private const string strILLO = "ILLO"; + private const string strILLA = "ILLA"; + private const string strALLE = "ALLE"; + private const string strAS = "AS"; + private const string strOS = "OS"; + private const string strUMB = "UMB"; + private const char charOdash = 'Ð'; + private const char charP = 'P'; + private const string strP = "P"; + private const char charQ = 'Q'; + private const string strME = "ME"; + private const string strMA = "MA"; + private const char charR = 'R'; + private const char charS = 'S'; + private const string strISL = "ISL"; + private const string strYSL = "YSL"; + private const string strSUGAR = "SUGAR"; + private const string strSH = "SH"; + private const string strHEIM = "HEIM"; + private const string strHOEK = "HOEK"; + private const string strHOLM = "HOLM"; + private const string strHOLZ = "HOLZ"; + private const string strSIO = "SIO"; + private const string strSIA = "SIA"; + private const string strSIAN = "SIAN"; + private const string strSC = "SC"; + private const string strOO = "OO"; + private const string strEN = "EN"; + private const string strUY = "UY"; + private const string strED = "ED"; + private const string strEM = "EM"; + private const string strSK = "SK"; + private const string strAI = "AI"; + private const string strOI = "OI"; + private const string strTION = "TION"; + private const string strTIA = "TIA"; + private const string strTCH = "TCH"; + private const char charT = 'T'; + private const string strTH = "TH"; + private const string strTTH = "TTH"; + private const string strOM = "OM"; + private const string strAM = "AM"; + private const char charV = 'V'; + private const string strWH = "WH"; + private const string strEWSKI = "EWSKI"; + private const string strEWSKY = "EWSKY"; + private const string strOWSKI = "OWSKI"; + private const string strOWSKY = "OWSKY"; + private const string strFX = "FX"; + private const string strTS = "TS"; + private const string strEAU = "EAU"; + private const string strIAU = "IAU"; + private const string strAU = "AU"; + private const string strOU = "OU"; + private const char charZ = 'Z'; + private const string strZA = "ZA"; + private const string strZI = "ZI"; + private const string strZO = "ZO"; + private const string strET = "ET"; + + + static bool StartsWith(this string self, StringComparison comparison, params string[] strings) + { + foreach (string str in strings) + { + if (self.StartsWith(str, comparison)) + { + return true; + } + } + return false; + } + + static bool StringAt(this string self, int startIndex, params string[] strings) + { + if (startIndex < 0) + { + startIndex = 0; + } + + foreach (string str in strings) + { + if (self.IndexOf(str, startIndex, StringComparison.OrdinalIgnoreCase) >= startIndex) + { + return true; + } + } + return false; + } + + + private class MetaphoneData + { + readonly StringBuilder _primary = new StringBuilder(5); + readonly StringBuilder _secondary = new StringBuilder(5); + + + #region Properties + + internal bool Alternative { get; set; } + internal int PrimaryLength + { + get + { + return _primary.Length; + } + } + + internal int SecondaryLength + { + get + { + return _secondary.Length; + } + } + + #endregion + + + internal void Add(string main) + { + if (main != null) + { + _primary.Append(main); + _secondary.Append(main); + } + } + + internal void Add(string main, string alternative) + { + if (main != null) + { + _primary.Append(main); + } + + if (alternative != null) + { + Alternative = true; + if (alternative.Trim().Length > 0) + { + _secondary.Append(alternative); + } + } + else + { + if (main != null && main.Trim().Length > 0) + { + _secondary.Append(main); + } + } + } + + public override string ToString() + { + string ret = (Alternative ? _secondary : _primary).ToString(); + //only give back 4 char metaph + if (ret.Length > 4) + { + ret = ret.Substring(0, 4); + } + + return ret; + } + } + } +} diff --git a/Editor/DuoVia.FuzzyStrings/DoubleMetaphoneExtensions.cs.meta b/Editor/DuoVia.FuzzyStrings/DoubleMetaphoneExtensions.cs.meta new file mode 100644 index 0000000..ef45e2c --- /dev/null +++ b/Editor/DuoVia.FuzzyStrings/DoubleMetaphoneExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8a308f8acdb7424eba6c45fcfda3aab0 +timeCreated: 1662837350 \ No newline at end of file diff --git a/Editor/DuoVia.FuzzyStrings/LevenshteinDistanceExtensions.cs b/Editor/DuoVia.FuzzyStrings/LevenshteinDistanceExtensions.cs new file mode 100644 index 0000000..3f426e5 --- /dev/null +++ b/Editor/DuoVia.FuzzyStrings/LevenshteinDistanceExtensions.cs @@ -0,0 +1,72 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Derived from http://www.codeguru.com/vb/gen/vb_misc/algorithms/article.php/c13137__1/Fuzzy-Matching-Demo-in-Access.htm + * and from http://www.berghel.net/publications/asm/asm.php + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +namespace DuoVia.FuzzyStrings +{ + public static class LevenshteinDistanceExtensions + { + /// + /// Levenshtein Distance algorithm with transposition.
+ /// A value of 1 or 2 is okay, 3 is iffy and greater than 4 is a poor match + ///
+ /// + /// + /// + /// + public static int LevenshteinDistance(this string input, string comparedTo, bool caseSensitive = false) + { + if (string.IsNullOrWhiteSpace(input) || string.IsNullOrWhiteSpace(comparedTo)) return -1; + if (!caseSensitive) + { + input = input.ToLower(); + comparedTo = comparedTo.ToLower(); + } + + int[,] matrix = new int[input.Length + 1, comparedTo.Length + 1]; + + //initialize + for (int i = 0; i <= matrix.GetUpperBound(0); i++) matrix[i, 0] = i; + for (int i = 0; i <= matrix.GetUpperBound(1); i++) matrix[0, i] = i; + + //analyze + for (int i = 1; i <= matrix.GetUpperBound(0); i++) + { + var si = input[i - 1]; + for (int j = 1; j <= matrix.GetUpperBound(1); j++) + { + var tj = comparedTo[j - 1]; + int cost = (si == tj) ? 0 : 1; + + var above = matrix[i - 1, j]; + var left = matrix[i, j - 1]; + var diag = matrix[i - 1, j - 1]; + var cell = FindMinimum(above + 1, left + 1, diag + cost); + + //transposition + if (i > 1 && j > 1) + { + var trans = matrix[i - 2, j - 2] + 1; + if (input[i - 2] != comparedTo[j - 1]) trans++; + if (input[i - 1] != comparedTo[j - 2]) trans++; + if (cell > trans) cell = trans; + } + matrix[i, j] = cell; + } + } + return matrix[matrix.GetUpperBound(0), matrix.GetUpperBound(1)]; + } + + private static int FindMinimum(params int[] p) + { + if (null == p) return int.MinValue; + int min = int.MaxValue; + for (int i = 0; i < p.Length; i++) + { + if (min > p[i]) min = p[i]; + } + return min; + } + } +} diff --git a/Editor/DuoVia.FuzzyStrings/LevenshteinDistanceExtensions.cs.meta b/Editor/DuoVia.FuzzyStrings/LevenshteinDistanceExtensions.cs.meta new file mode 100644 index 0000000..291df6d --- /dev/null +++ b/Editor/DuoVia.FuzzyStrings/LevenshteinDistanceExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4b63a2615de249edb0b1b3cbb416fb07 +timeCreated: 1662837350 \ No newline at end of file diff --git a/Editor/DuoVia.FuzzyStrings/LongestCommonSubsequenceExtensions.cs b/Editor/DuoVia.FuzzyStrings/LongestCommonSubsequenceExtensions.cs new file mode 100644 index 0000000..5175343 --- /dev/null +++ b/Editor/DuoVia.FuzzyStrings/LongestCommonSubsequenceExtensions.cs @@ -0,0 +1,128 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Derived from http://www.codeproject.com/KB/recipes/lcs.aspx + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +using System; + +namespace DuoVia.FuzzyStrings +{ + public static class LongestCommonSubsequenceExtensions + { + /// + /// Longest Common Subsequence. A good value is greater than 0.33. + /// + /// + /// + /// + /// Returns a Tuple of the sub sequence string and the match coeficient. + public static Tuple LongestCommonSubsequence(this string input, string comparedTo, bool caseSensitive = false) + { + if (string.IsNullOrWhiteSpace(input) || string.IsNullOrWhiteSpace(comparedTo)) return new Tuple(string.Empty, 0.0d); + if (!caseSensitive) + { + input = input.ToLower(); + comparedTo = comparedTo.ToLower(); + } + + int inputLen = input.Length; + int comparedToLen = comparedTo.Length; + + int[,] lcs = new int[inputLen + 1, comparedToLen + 1]; + LcsDirection[,] tracks = new LcsDirection[inputLen + 1, comparedToLen + 1]; + int[,] w = new int[inputLen + 1, comparedToLen + 1]; + int i, j; + + for (i = 0; i <= inputLen; ++i) + { + lcs[i, 0] = 0; + tracks[i, 0] = LcsDirection.North; + + } + for (j = 0; j <= comparedToLen; ++j) + { + lcs[0, j] = 0; + tracks[0, j] = LcsDirection.West; + } + + for (i = 1; i <= inputLen; ++i) + { + for (j = 1; j <= comparedToLen; ++j) + { + if (input[i - 1].Equals(comparedTo[j - 1])) + { + int k = w[i - 1, j - 1]; + //lcs[i,j] = lcs[i-1,j-1] + 1; + lcs[i, j] = lcs[i - 1, j - 1] + Square(k + 1) - Square(k); + tracks[i, j] = LcsDirection.NorthWest; + w[i, j] = k + 1; + } + else + { + lcs[i, j] = lcs[i - 1, j - 1]; + tracks[i, j] = LcsDirection.None; + } + + if (lcs[i - 1, j] >= lcs[i, j]) + { + lcs[i, j] = lcs[i - 1, j]; + tracks[i, j] = LcsDirection.North; + w[i, j] = 0; + } + + if (lcs[i, j - 1] >= lcs[i, j]) + { + lcs[i, j] = lcs[i, j - 1]; + tracks[i, j] = LcsDirection.West; + w[i, j] = 0; + } + } + } + + i = inputLen; + j = comparedToLen; + + string subseq = ""; + double p = lcs[i, j]; + + //trace the backtracking matrix. + while (i > 0 || j > 0) + { + if (tracks[i, j] == LcsDirection.NorthWest) + { + i--; + j--; + subseq = input[i] + subseq; + //Trace.WriteLine(i + " " + input1[i] + " " + j); + } + + else if (tracks[i, j] == LcsDirection.North) + { + i--; + } + + else if (tracks[i, j] == LcsDirection.West) + { + j--; + } + } + + double coef = p / (inputLen * comparedToLen); + + Tuple retval = new Tuple(subseq, coef); + return retval; + } + + private static int Square(int p) + { + return p * p; + } + } + + internal enum LcsDirection + { + None, + North, + West, + NorthWest + } +} diff --git a/Editor/DuoVia.FuzzyStrings/LongestCommonSubsequenceExtensions.cs.meta b/Editor/DuoVia.FuzzyStrings/LongestCommonSubsequenceExtensions.cs.meta new file mode 100644 index 0000000..1604949 --- /dev/null +++ b/Editor/DuoVia.FuzzyStrings/LongestCommonSubsequenceExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d8202090d5db46c4abcacc1a935d5ffd +timeCreated: 1662837350 \ No newline at end of file diff --git a/Editor/DuoVia.FuzzyStrings/StringExtensions.cs b/Editor/DuoVia.FuzzyStrings/StringExtensions.cs new file mode 100644 index 0000000..428c64f --- /dev/null +++ b/Editor/DuoVia.FuzzyStrings/StringExtensions.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace DuoVia.FuzzyStrings +{ + public static class StringExtensions + { + public static bool FuzzyEquals(this string strA, string strB, double requiredProbabilityScore = 0.75) + { + return strA.FuzzyMatch(strB) > requiredProbabilityScore; + } + + public static double FuzzyMatch(this string strA, string strB) + { + string localA = Strip(strA.Trim().ToLower()); + string localB = Strip(strB.Trim().ToLower()); + if (localA.Contains(space) && localB.Contains(space)) + { + var partsA = localA.Split(' '); + var partsB = localB.Split(' '); + var weightedHighCoefficients = new double[partsA.Length]; + var distanceRatios = new double[partsA.Length]; + for (int i = 0; i < partsA.Length; i++) + { + double high = 0.0; + int indexDistance = 0; + for (int x = 0; x < partsB.Length; x++) + { + var coef = CompositeCoefficient(partsA[i], partsB[x]); + if (coef > high) + { + high = coef; + indexDistance = Math.Abs(i - x); + } + } + double distanceWeight = indexDistance == 0 ? 1.0 : 1.0 - (indexDistance / (partsA.Length * 1.0)); + weightedHighCoefficients[i] = high * distanceWeight; + } + double avgWeightedHighCoefficient = weightedHighCoefficients.Sum() / (partsA.Length * 1.0); + return avgWeightedHighCoefficient < 0.999999 ? avgWeightedHighCoefficient : 0.999999; //fudge factor + } + else + { + var singleComposite = CompositeCoefficient(localA, localB); + return singleComposite < 0.999999 ? singleComposite : 0.999999; //fudge factor + } + } + + private static readonly string space = " "; + private static Regex stripRegex = new Regex(@"[^a-zA-Z0-9 -]*"); + + private static string Strip(string str) + { + return stripRegex.Replace(str, string.Empty); + } + + private static double CompositeCoefficient(string strA, string strB) + { + double dice = strA.DiceCoefficient(strB); + var lcs = strA.LongestCommonSubsequence(strB); + int leven = strA.LevenshteinDistance(strB); + double levenCoefficient = 1.0 / (1.0 * (leven + 0.2)); //may want to tweak offset + string strAMp = strA.ToDoubleMetaphone(); + string strBMp = strB.ToDoubleMetaphone(); + int matchCount = 0; + if (strAMp.Length == 4 && strBMp.Length == 4) + { + for (int i = 0; i < strAMp.Length; i++) + { + if (strAMp[i] == strBMp[i]) matchCount++; + } + } + double mpCoefficient = matchCount == 0 ? 0.0 : matchCount / 4.0; + double avgCoefficent = (dice + lcs.Item2 + levenCoefficient + mpCoefficient) / 4.0; + return avgCoefficent; + } + } +} diff --git a/Editor/DuoVia.FuzzyStrings/StringExtensions.cs.meta b/Editor/DuoVia.FuzzyStrings/StringExtensions.cs.meta new file mode 100644 index 0000000..8e4f136 --- /dev/null +++ b/Editor/DuoVia.FuzzyStrings/StringExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3e005595ee4949c1b37494739e8e812f +timeCreated: 1662837350 \ No newline at end of file diff --git a/Editor/EcsLiteHierarchyWindow.cs b/Editor/EcsLiteHierarchyWindow.cs new file mode 100644 index 0000000..1c91194 --- /dev/null +++ b/Editor/EcsLiteHierarchyWindow.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DuoVia.FuzzyStrings; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Nomnom.EcsLiteDebugger.Editor { + internal class EcsLiteHierarchyWindow: EditorWindow { + [MenuItem("Tools/Nomnom/ecsLite Hierarchy")] + private static void Open() { + GetWindow("ecsLite Hierarchy").Show(); + } + + private DropdownField _worldMenu; + private Toolbar _toolbar; + private ToolbarSearchField _searchField; + private ScrollView _hierarchy; + + private readonly List _defaultOption = new List(1) { "None" }; + + private void CreateGUI() { + EditorApplication.playModeStateChanged -= PlayModeStateChanged; + EditorApplication.playModeStateChanged += PlayModeStateChanged; + + VisualTreeAsset baseWindow = Resources.Load("ecsLite/HierarchyWindow"); + + baseWindow.CloneTree(rootVisualElement); + + _worldMenu = new DropdownField(_defaultOption, 0); + + VisualElement toolbarBtn = _worldMenu.ElementAt(0); + toolbarBtn.AddToClassList("unity-toolbar-button"); + toolbarBtn.style.borderBottomLeftRadius = 0; + toolbarBtn.style.borderBottomRightRadius = 0; + toolbarBtn.style.borderTopLeftRadius = 0; + toolbarBtn.style.borderTopRightRadius = 0; + + _worldMenu.SetEnabled(false); + _worldMenu.RegisterValueChangedCallback(e => + { + EcsLiteInspectorWindow.SetEntity(default, default, true); + _hierarchy.Clear(); + + // fake some mutations to propagate the list + if (!(WorldDebugView.Views == null || WorldDebugView.Views.Count == 0)) { + WorldDebugView.Views[_worldMenu.index].CreateDummyList(); + } + + Refresh(); + }); + + _toolbar = rootVisualElement.Q("toolbar"); + _toolbar.Add(_worldMenu); + + _worldMenu.SendToBack(); + + _searchField = rootVisualElement.Q("search").ElementAt(0) as ToolbarSearchField; + _searchField.RegisterValueChangedCallback(e => + { + FilterEntities(); + }); + + _hierarchy = rootVisualElement.Q("hierarchy"); + + rootVisualElement.Q