diff --git a/CalcBinding/CalcBinding.cs b/CalcBinding/CalcBinding.cs index 659f445..f9bc2f2 100644 --- a/CalcBinding/CalcBinding.cs +++ b/CalcBinding/CalcBinding.cs @@ -52,7 +52,7 @@ public override object ProvideValue(IServiceProvider serviceProvider) if (sourcePropertiesPathesWithPositions.Count() == 1) { - var binding = new System.Windows.Data.Binding(sourcePropertiesPathesWithPositions.Single().Item1) + var binding = new System.Windows.Data.Binding(sourcePropertiesPathesWithPositions.Single().LocalPath) { Mode = Mode, NotifyOnSourceUpdated = NotifyOnSourceUpdated, @@ -113,7 +113,7 @@ public override object ProvideValue(IServiceProvider serviceProvider) foreach (var sourcePropertyPathWithPositions in sourcePropertiesPathesWithPositions) { - var binding = new System.Windows.Data.Binding(sourcePropertyPathWithPositions.Item1); + var binding = new System.Windows.Data.Binding(sourcePropertyPathWithPositions.LocalPath); if (Source != null) binding.Source = Source; @@ -153,55 +153,62 @@ private Type GetPropertyType(IServiceProvider serviceProvider) /// /// /// - private string GetExpressionTemplate(string path, List>> pathes) + private string GetExpressionTemplate(string fullpath, List pathes) { - var result = ""; - var sourceIndex = 0; - - while (sourceIndex < path.Length) + //Flatten parsed paths, get all merged indexes and corresponding localpath keys + var pathmap = new List>(); //Item1 = LocalPath, Item2 = order of localpath, Item3 = position of localpath in fullpath + + for (int i = 0; i < pathes.Count; i++) { - var replaced = false; - for (int index = 0; index < pathes.Count; index++) + foreach (var index in pathes[i].MergedIndexes) { - var replace = index.ToString("{0}"); - var positions = pathes[index].Item2; - var sourcePropertyPath = pathes[index].Item1; + var t = new Tuple(pathes[i].LocalPath, i, index); + pathmap.Add(t); + } + } + pathmap = pathmap.OrderBy(x => x.Item3).ToList(); - if (positions.Contains(sourceIndex)) - { - result += replace; - sourceIndex += sourcePropertyPath.Length; - replaced = true; - break; - } + //Iterate through ascending index values and rebuild the fullpath with replaced values + StringBuilder sb = new StringBuilder(); + int currentindex = 0; + foreach (var m in pathmap) + { + var index = m.Item3; + var localpath = m.Item1; + var pathorder = m.Item2; + + //Either we are appending the local path, or we are appending what is between the consecutive map values + if (currentindex < index) + { + var s = fullpath.Substring(currentindex, index - currentindex); + sb.Append(s); + currentindex += s.Length; } - if (!replaced) + if (currentindex == index) { - result += path[sourceIndex]; - sourceIndex++; + sb.AppendFormat("{{{0}}}", pathorder); + currentindex += localpath.Length; } } + //Fill in the remaining characters + var remaininglength = fullpath.Length - currentindex; + var substr = fullpath.Substring(currentindex, remaininglength); + sb.Append(substr); + var result = sb.ToString(); return result; } /// /// Find and return all sourceProperties pathes in Path string /// - /// + /// /// List of pathes and its start positions - private List>> GetSourcePropertiesPathes(string normPath) + private List GetSourcePropertiesPathes(string fullPath) { - var operators = new [] - { - "(", ")", "+", "-", "*", "/", "%", "^", "&&", "||", - "&", "|", "?", ":", "<=", ">=", "<", ">", "==", "!=", "!", "," - }; - // temporary solution of problem: all string content shouldn't be parsed. Solution - remove strings from sourcePath. //todo: better solution is to use parser PARSER!! - var pathsList = GetPathes(normPath, operators); //detect all start positions @@ -215,44 +222,116 @@ private List>> GetSourcePropertiesPathes(string normPath // not other symbols. So, following code perform this check //may be that task solved by using PARSER! - var pathIndexList = pathsList - .Select(path => new Tuple>(path, new List())) + + //Get the set of valid paths + var pathsList = GetPathes(fullPath) + .Where(p => p.ValidPath) + .Where(p => p.LocalPath != "null") .ToList(); - foreach (var path in pathIndexList) + //Merge the groups. + //We are combining each distinct value of the LocalPath, merging the differing indexes into the MergedIndexes list + //This is so that we don't have duplicate property evaluations for the same data, improving performance + var pathGroups = pathsList.GroupBy(p => p.LocalPath); + foreach (var g in pathGroups) { - var indexes = Regex.Matches(normPath, path.Item1).Cast().Select(m => m.Index).ToList(); + var first = g.First(); + first.MergedIndexes = g.Select(x => x.StartIndex).ToList(); + } - foreach (var index in indexes) - { - bool startPosIsOperator = index == 0; + pathsList = pathsList.Where(p => p.MergedIndexes?.Count > 0).ToList(); + + return pathsList; + } - foreach (var op in operators) - if (index >= op.Length && normPath.Substring(index - op.Length, op.Length) == op) - startPosIsOperator = true; + class ParsedPath + { + public static string[] operators = new[] + { + "(", ")", "+", "-", "*", "/", "%", "^", "&&", "||", + "&", "|", "?", ":", "<=", ">=", "<", ">", "==", "!=", "!", "," + }; - bool endPosIsOperator = index + path.Item1.Length == normPath.Length; + static string[] opsWithoutParenthesis = operators.Except(new[] { "(", ")" }).ToArray(); - foreach (var op in operators) - if (index + path.Item1.Length <= normPath.Length - op.Length && normPath.Substring(index + path.Item1.Length, op.Length) == op) - endPosIsOperator = true; + public string FullPath { get; set; } + public string LocalPath { get; set; } + public int StartIndex { get; set; } + public int EndIndex { get { return StartIndex + LocalPath.Length - 1; } } + public List MergedIndexes { get; set; } - if (startPosIsOperator && endPosIsOperator) - path.Item2.Add(index); + public bool ValidPath + { + get + { + return StartPosIsOperator && EndPosIsOperator; + } + } + + public bool StartPosIsOperator + { + get + { + if (StartIndex == 0) + return true; + return operators.Any(op => + op.Length <= StartIndex && + op == FullPath.Substring(StartIndex - op.Length, op.Length)); + } + } + + public bool EndPosIsOperator + { + get + { + if (StartIndex + LocalPath.Length == FullPath.Length) + return true; + return operators.Any(op => + FullPath.Length - op.Length >= StartIndex + LocalPath.Length && + op == FullPath.Substring(StartIndex + LocalPath.Length, op.Length)); + } + } + + public bool LocalPathIsNotAtEdge + { + get + { + return (StartIndex > 0 && EndIndex < FullPath.Length - 1); + } + } + + public bool SurroundedByParenthesis + { + get + { + if (!LocalPathIsNotAtEdge) + return false; + char start = FullPath[StartIndex - 1]; + char end = FullPath[EndIndex + 1]; + return start == '(' && end == ')'; } } - return pathIndexList; + + public override string ToString() + { + return LocalPath; + } } /// - /// Returns all strings that are pathes + /// Returns all strings that are pathes. Includes locations of each path in relation to the fullPath input. /// - /// - /// + /// /// - private List GetPathes(string path, string[] operators) + private List GetPathes(string fullPath) { - var substrings = path.Split(new[] { "\"" }, StringSplitOptions.None); + var originalPath = fullPath; + var operators = ParsedPath.operators; + + var substrings = originalPath.Split(new[] { "\"" }, StringSplitOptions.None); + + //Keep track of quoted string lengths in order to preserve token indexes + var stringlengths = new Queue(); if (substrings.Length > 0) { @@ -262,30 +341,162 @@ private List GetPathes(string path, string[] operators) if (i % 2 == 0) pathWithoutStringsBuilder.Append(substrings[i]); else + { pathWithoutStringsBuilder.Append("\"\""); + stringlengths.Enqueue(substrings[i].Length); + } } - path = pathWithoutStringsBuilder.ToString(); + fullPath = pathWithoutStringsBuilder.ToString(); } - var matches = path.Split(operators, StringSplitOptions.RemoveEmptyEntries).Distinct(); + + + var matches = SplitWithIndexes(fullPath, operators); + + //If containing text has no operators, but surrounded by ( ), treat the enclosing parenthesis + //as part of the string // detect all pathes - var pathsList = new List(); + var pathsList = new List(); - foreach (var match in matches) + //Scan through the full path to locate the indexes of the matches + int currentIndex = 0; + int totalStringLengths = 0; + for(int matchIdx = 0; matchIdx < matches.Count; matchIdx++) { + var match = matches[matchIdx].Item2; + var matchFoundAtLoc = matches[matchIdx].Item1; + if (matchFoundAtLoc < currentIndex) //Already parsed recursively, skip + continue; + + if (match == "\"\"") + { + //Keep track of the current number of characters removed up to this point + //So that an offset can be calculated + totalStringLengths += stringlengths.Dequeue(); + } + + currentIndex = fullPath.IndexOf(match, currentIndex); + var parsed = new ParsedPath() + { + FullPath = originalPath, + LocalPath = match, + StartIndex = currentIndex + totalStringLengths + }; + currentIndex += match.Length; + + //Since (Canvas.Left) is a valid path but Canvas.Left is not, we need to account for this. + if (parsed.SurroundedByParenthesis) + { + //Expand local path by one character in each direction, to include the surrounding parenthesis + parsed.LocalPath = parsed.FullPath.Substring(parsed.StartIndex - 1, parsed.LocalPath.Length + 2); + parsed.StartIndex--; + } + if (!isDouble(match) && !match.Contains("\"")) { // math detection - if (!Regex.IsMatch(match, @"Math.\w+\(\w+\)") && !Regex.IsMatch(match, @"Math.\w+")) - if (match != "null") - pathsList.Add(match); + var mathmatches1 = Regex.Matches(match, @"Math.\w+\(\w+\)").Cast(); + var mathmatches2 = Regex.Matches(match, @"Math.\w+").Cast(); + bool isMath = mathmatches1.Count() > 0 || mathmatches2.Count() > 0; + + if (isMath) + { + //Get what is inside the math function, parse it, and unbox + int mathIdx = (mathmatches1.FirstOrDefault()?.Index ?? 0) + (mathmatches2.FirstOrDefault()?.Index ?? 0); + int beginIdxInner = parsed.FullPath.IndexOf("(", parsed.StartIndex + mathIdx) + 1; + + //Traverse string until we get to the closing parenthesis + //An exception will be thrown if there is no closing parenthesis + int endIdxInner = beginIdxInner; + int depth = 1; + bool found = false; + for (int i = beginIdxInner; i < parsed.FullPath.Length; i++, endIdxInner++) + { + if (parsed.FullPath[i] == '(') + depth++; + if (parsed.FullPath[i] == ')') + { + depth--; + if (depth == 0) + { + found = true; + break; + } + } + } + if (!found) + throw new InvalidOperationException($"Unclosed parenthesis: {parsed.FullPath}"); + + string innerArg = parsed.FullPath.Substring(beginIdxInner, endIdxInner - beginIdxInner); + var innerRecursiveParsedPaths = GetPathes(innerArg); + + //merge paths + foreach (var innerpath in innerRecursiveParsedPaths) + { + innerpath.FullPath = parsed.FullPath; + innerpath.StartIndex += beginIdxInner; //innerpath index is just an offset until this point. Convert to absolute index + pathsList.Add(innerpath); + } + + //advance currentindex past the point at which we procesed the fullpath, as we do not want to parse the same region twice! + // currentIndex += + } + else + pathsList.Add(parsed); } } return pathsList; } + //Splits a string using splitby as a delimiter, returns each substring along with the original index at which the substring resides + private List> SplitWithIndexes(string input, string[] splitby) + { + //Remove empty strings + splitby = splitby.Where(s => s != null && s.Length > 0).ToArray(); + + var list = new List>(); + + int idx = 0; + while (idx < input.Length) + { + //Get the first location that any of the splitby args are located in input, starting from idx + var next = splitby.Select(s => + { + int loc = input.IndexOf(s, idx); + if (loc < 0) + loc = int.MaxValue; + return new { op = s, loc = loc }; + }); + var minidx = next.Min(o => o.loc); + + if (minidx == int.MaxValue) //nothing found, finish up + { + var remainingText = input.Substring(idx); + if (remainingText.Length > 0) + list.Add(new Tuple(idx, remainingText)); + break; + } + + //Get the next delimiter that the string is split by + var nextop = next.Where(x => x.loc == minidx).First(); + + //Get the end index of the current substring that we are including in the results + int endIdxCurrent = nextop.loc - 1; + + //Add the item to the results + string nextSubstring = input.Substring(idx, endIdxCurrent - idx + 1); + if (nextSubstring.Length > 0) + list.Add(new Tuple(idx, nextSubstring)); + + //Get the next index to continue string traversal + idx = nextop.loc + nextop.op.Length; + } + + return list; + } + /// /// Return true, is string can be converted to Double type, and false otherwise ///