How can I create a more user-friendly string.format syntax?

前端 未结 6 1980
慢半拍i
慢半拍i 2020-11-27 10:29

I need to create a very long string in a program, and have been using String.Format. The problem I am facing is keeping track of all the numbers when you have more than 8-10

相关标签:
6条回答
  • 2020-11-27 11:05

    As of C#6, this kind of string interpolation is now possible using the new string interpolation syntax:

    var formatted = $"You are {age} years old and your last name is {name}";
    
    0 讨论(0)
  • 2020-11-27 11:05

    not quite the same but sort of spoofing it... use an extension method, a dictionary and a little code:

    something like this...

      public static class Extensions {
    
            public static string FormatX(this string format, params KeyValuePair<string, object> []  values) {
                string res = format;
                foreach (KeyValuePair<string, object> kvp in values) {
                    res = res.Replace(string.Format("{0}", kvp.Key), kvp.Value.ToString());
                }
                return res;
            }
    
        }
    
    0 讨论(0)
  • 2020-11-27 11:10

    What about if age/name is an variable in your application. So you would need a sort syntax to make it almost unique like {age_1}?

    If you have trouble with 8-10 parameters: why don't use

    "You are " + age + " years old and your last name is " + name + "
    
    0 讨论(0)
  • 2020-11-27 11:13

    primitive implementation:

    public static class StringUtility
    {
      public static string Format(string pattern, IDictionary<string, object> args)
      {
        StringBuilder builder = new StringBuilder(pattern);
        foreach (var arg in args)
        {
          builder.Replace("{" + arg.Key + "}", arg.Value.ToString());
        }
        return builder.ToString();
      }
    }
    

    Usage:

    StringUtility.Format("You are {age} years old and your last name is {name} ",
      new Dictionary<string, object>() {{"age" = 18, "name" = "Foo"}});
    

    You could also use a anonymous class, but this is much slower because of the reflection you'll need.

    For a real implementation you should use regular expression to

    • allow escaping the {}
    • check if there are placeholders that where not replaced, which is most probably a programming error.
    0 讨论(0)
  • 2020-11-27 11:24

    Although C# 6.0 can now do this with string interpolation, it's sometimes necessary to do this with dynamic format strings at runtime. I've been unable to use other methods that require DataBinder.Eval due to them not being available in .NET Core, and have been dissatisfied with the performance of Regex solutions.

    With that in mind, here's a Regex free, state machine based parser that I've written up. It handles unlimited levels of {{{escaping}}} and throws FormatException when input contains unbalanced braces and/or other errors. Although the main method takes a Dictionary<string, object>, the helper method can also take an object and use its parameters via reflection.

    public static class StringExtension {
        /// <summary>
        /// Extension method that replaces keys in a string with the values of matching object properties.
        /// </summary>
        /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
        /// <param name="injectionObject">The object whose properties should be injected in the string</param>
        /// <returns>A version of the formatString string with keys replaced by (formatted) key values.</returns>
        public static string FormatWith(this string formatString, object injectionObject) {
            return formatString.FormatWith(GetPropertiesDictionary(injectionObject));
        }
    
        /// <summary>
        /// Extension method that replaces keys in a string with the values of matching dictionary entries.
        /// </summary>
        /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
        /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
        /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
        public static string FormatWith(this string formatString, IDictionary<string, object> dictionary) {
            char openBraceChar = '{';
            char closeBraceChar = '}';
    
            return FormatWith(formatString, dictionary, openBraceChar, closeBraceChar);
        }
            /// <summary>
            /// Extension method that replaces keys in a string with the values of matching dictionary entries.
            /// </summary>
            /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
            /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
            /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
        public static string FormatWith(this string formatString, IDictionary<string, object> dictionary, char openBraceChar, char closeBraceChar) {
            string result = formatString;
            if (dictionary == null || formatString == null)
                return result;
    
            // start the state machine!
    
            // ballpark output string as two times the length of the input string for performance (avoids reallocating the buffer as often).
            StringBuilder outputString = new StringBuilder(formatString.Length * 2);
            StringBuilder currentKey = new StringBuilder();
    
            bool insideBraces = false;
    
            int index = 0;
            while (index < formatString.Length) {
                if (!insideBraces) {
                    // currently not inside a pair of braces in the format string
                    if (formatString[index] == openBraceChar) {
                        // check if the brace is escaped
                        if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) {
                            // add a brace to the output string
                            outputString.Append(openBraceChar);
                            // skip over braces
                            index += 2;
                            continue;
                        }
                        else {
                            // not an escaped brace, set state to inside brace
                            insideBraces = true;
                            index++;
                            continue;
                        }
                    }
                    else if (formatString[index] == closeBraceChar) {
                        // handle case where closing brace is encountered outside braces
                        if (index < formatString.Length - 1 && formatString[index + 1] == closeBraceChar) {
                            // this is an escaped closing brace, this is okay
                            // add a closing brace to the output string
                            outputString.Append(closeBraceChar);
                            // skip over braces
                            index += 2;
                            continue;
                        }
                        else {
                            // this is an unescaped closing brace outside of braces.
                            // throw a format exception
                            throw new FormatException($"Unmatched closing brace at position {index}");
                        }
                    }
                    else {
                        // the character has no special meaning, add it to the output string
                        outputString.Append(formatString[index]);
                        // move onto next character
                        index++;
                        continue;
                    }
                }
                else {
                    // currently inside a pair of braces in the format string
                    // found an opening brace
                    if (formatString[index] == openBraceChar) {
                        // check if the brace is escaped
                        if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) {
                            // there are escaped braces within the key
                            // this is illegal, throw a format exception
                            throw new FormatException($"Illegal escaped opening braces within a parameter - index: {index}");
                        }
                        else {
                            // not an escaped brace, we have an unexpected opening brace within a pair of braces
                            throw new FormatException($"Unexpected opening brace inside a parameter - index: {index}");
                        }
                    }
                    else if (formatString[index] == closeBraceChar) {
                        // handle case where closing brace is encountered inside braces
                        // don't attempt to check for escaped braces here - always assume the first brace closes the braces
                        // since we cannot have escaped braces within parameters.
    
                        // set the state to be outside of any braces
                        insideBraces = false;
    
                        // jump over brace
                        index++;
    
                        // at this stage, a key is stored in current key that represents the text between the two braces
                        // do a lookup on this key
                        string key = currentKey.ToString();
                        // clear the stringbuilder for the key
                        currentKey.Clear();
    
                        object outObject;
    
                        if (!dictionary.TryGetValue(key, out outObject)) {
                            // the key was not found as a possible replacement, throw exception
                            throw new FormatException($"The parameter \"{key}\" was not present in the lookup dictionary");
                        }
    
                        // we now have the replacement value, add the value to the output string
                        outputString.Append(outObject);
    
                        // jump to next state
                        continue;
                    } // if }
                    else {
                        // character has no special meaning, add it to the current key
                        currentKey.Append(formatString[index]);
                        // move onto next character
                        index++;
                        continue;
                    } // else
                } // if inside brace
            } // while
    
            // after the loop, if all braces were balanced, we should be outside all braces
            // if we're not, the input string was misformatted.
            if (insideBraces) {
                throw new FormatException("The format string ended before the parameter was closed.");
            }
    
            return outputString.ToString();
        }
    
        /// <summary>
        /// Creates a Dictionary from an objects properties, with the Key being the property's
        /// name and the Value being the properties value (of type object)
        /// </summary>
        /// <param name="properties">An object who's properties will be used</param>
        /// <returns>A <see cref="Dictionary"/> of property values </returns>
        private static Dictionary<string, object> GetPropertiesDictionary(object properties) {
            Dictionary<string, object> values = null;
            if (properties != null) {
                values = new Dictionary<string, object>();
                PropertyDescriptorCollection props = TypeDescriptor.GetProperties(properties);
                foreach (PropertyDescriptor prop in props) {
                    values.Add(prop.Name, prop.GetValue(properties));
                }
            }
            return values;
        }
    }
    

    Ultimately, all the logic boils down into 10 main states - For when the state machine is outside a bracket and likewise inside a bracket, the next character is either an open brace, an escaped open brace, a closed brace, an escaped closed brace, or an ordinary character. Each of these conditions is handled individually as the loop progresses, adding characters to either an output StringBuffer or a key StringBuffer. When a parameter is closed, the value of the key StringBuffer is used to look up the parameter's value in the dictionary, which then gets pushed into the output StringBuffer.

    EDIT:

    I've turned this into a full on project at https://github.com/crozone/FormatWith

    0 讨论(0)
  • 2020-11-27 11:29

    How about the following, which works both for anonymous types (the example below), or regular types (domain entities, etc):

    static void Main()
    {
        string s = Format("You are {age} years old and your last name is {name} ",
            new {age = 18, name = "Foo"});
    }
    

    using:

    static readonly Regex rePattern = new Regex(
        @"(\{+)([^\}]+)(\}+)", RegexOptions.Compiled);
    static string Format(string pattern, object template)
    {
        if (template == null) throw new ArgumentNullException();
        Type type = template.GetType();
        var cache = new Dictionary<string, string>();
        return rePattern.Replace(pattern, match =>
        {
            int lCount = match.Groups[1].Value.Length,
                rCount = match.Groups[3].Value.Length;
            if ((lCount % 2) != (rCount % 2)) throw new InvalidOperationException("Unbalanced braces");
            string lBrace = lCount == 1 ? "" : new string('{', lCount / 2),
                rBrace = rCount == 1 ? "" : new string('}', rCount / 2);
    
            string key = match.Groups[2].Value, value;
            if(lCount % 2 == 0) {
                value = key;
            } else {
                if (!cache.TryGetValue(key, out value))
                {
                    var prop = type.GetProperty(key);
                    if (prop == null)
                    {
                        throw new ArgumentException("Not found: " + key, "pattern");
                    }
                    value = Convert.ToString(prop.GetValue(template, null));
                    cache.Add(key, value);
                }
            }
            return lBrace + value + rBrace;
        });
    }
    
    0 讨论(0)
提交回复
热议问题