This question already has answers here:
Is there a string math evaluator in .NET?
(18 answers)
Closed 2 years ago.
I have a situation where i need to parse multiple n number of related fields (Do not want to evaluate):
string exp1 = "10";
string exp2 = "20";
string exp3= "exp1 + exp2 + 30";
string exp4 = "exp5 - exp3";
string exp5 = "exp3 / 10";
Dictionary<string,string> expressions = new Dictionary<string,string>();
expressions.Add("exp1", exp1);
expressions.Add("exp2", exp2);
expressions.Add("exp3", exp3);
expressions.Add("exp4", exp3);
expressions.Add("exp5", exp5);
Now we want to loop through all the expression fields and parse the expression with the actual values (Bodmas should also be applied) .So, after parsing, We want below as output:
exp1 = "10";
exp2 = "20";
exp3= "10 + 20 + 30";
exp4 = "((10 + 20 + 30 )/10) - (10+ 20 + 30)";
exp5 = "(10 + 29 + 30)/ 10";
What would be the data structure I should use here to parse it using a generic way? Would it be Binary Tree, graphs, ExpressionTrees?
The Code
using System.Collections.Generic;
using Microsoft.VisualBasic; //the code was written in VB and converted. make appropriate changes if you don't want to use this namespace
class Exp
{
private Dictionary<string, string> AllExpressions = new Dictionary<string, string>();
public void Add(string name, string value)
{
AllExpressions.Add(name, value);
}
public string ValueOf(string name)
{
var parts = Strings.Split(AllExpressions[name]);
for (int i = 0; i <= parts.Length - 1; i++)
{
if (AllExpressions.ContainsKey(parts[i]))
{
var partVal = ValueOf(parts[i]);
parts[i] = Information.IsNumeric(partVal) ? partVal : $"({partVal})";
}
}
return Strings.Join(parts);
}
}
Usage
Exp myExp = new Exp();
myExp.Add("exp1", "10");
myExp.Add("exp2", "20");
myExp.Add("exp3", "exp1 + exp2 + 30");
myExp.Add("exp4", "exp5 - exp3");
myExp.Add("exp5", "exp3 / 10");
// Test to see if we can get correct values
Console.WriteLine(myExp.ValueOf("exp1"));
Console.WriteLine(myExp.ValueOf("exp2"));
Console.WriteLine(myExp.ValueOf("exp3"));
Console.WriteLine(myExp.ValueOf("exp4"));
Console.WriteLine(myExp.ValueOf("exp5"));
The Result
10
20
10 + 20 + 30
((10 + 20 + 30) / 10) - (10 + 20 + 30)
(10 + 20 + 30) / 10
String replacement is unavoidable, but we can decrease the number of loops and replace operations by having a class to represent our expressions. When we have our arithmetic expressions wrapped in a reference type, we can add features and use them later:
public class ArithmeticExpression
{
public string Value;
public bool IsLiteral;
public bool IsFinalized;
public string ArithmeticSafeValue { get { return IsLiteral ? Value : "(" + Value + ")"; } }
public ArithmeticExpression(string value)
{
Value = value;
int dummy;
IsFinalized = IsLiteral = int.TryParse(value, out dummy);
}
}
And here is the methods to evaluate the final versions of our expressions, changing them to literals:
private static void Resolve(Dictionary<string, ArithmeticExpression> expressions)
{
foreach (string expressionKey in expressions.Keys.ToArray())
{
Resolve(expressionKey, expressions);
}
}
private static void Resolve(string expressionKey, Dictionary<string, ArithmeticExpression> expressions)
{
ArithmeticExpression expression = expressions[expressionKey];
if (expression.IsFinalized) return;
List<string> containedExpressions = expressions.Keys.Where(x => expression.Value.Contains(x)).ToList();
if (containedExpressions.Count > 0)
{
containedExpressions.ForEach((x) => Resolve(x, expressions));
containedExpressions.ForEach((x) => expression.Value = expression.Value.Replace(x, expressions[x].ArithmeticSafeValue));
expression.IsFinalized = true;
}
}
And here is how to use it:
public static void Main()
{
string exp1 = "10";
string exp2 = "20";
string exp3 = "exp1 + exp2 + 30";
string exp4 = "exp5 - exp3";
string exp5 = "exp3 / 10";
Dictionary<string, ArithmeticExpression> expressions = new Dictionary<string, ArithmeticExpression>();
expressions.Add("exp1", new ArithmeticExpression(exp1));
expressions.Add("exp2", new ArithmeticExpression(exp2));
expressions.Add("exp3", new ArithmeticExpression(exp3));
expressions.Add("exp4", new ArithmeticExpression(exp4));
expressions.Add("exp5", new ArithmeticExpression(exp5));
Resolve(expressions);
expressions.Keys.ToList().ForEach
(
(x) => Console.WriteLine("{0}: {1}", x, expressions[x].Value)
);
}
Hope this helps.
Well, not the most elegant solution, but I think it works
string exp1 = "10";
string exp2 = "20";
string exp3 = "exp1 + exp2 + 30";
string exp4 = "exp5 - exp3";
string exp5 = "exp3 / 10";
var dic = new Dictionary<String, String>();
dic.Add("exp1", exp1);
dic.Add("exp2", exp2);
dic.Add("exp3", exp3);
dic.Add("exp4", exp4);
dic.Add("exp5", exp5);
for (int i = 0; i < dic.Count; i++)
{
var dicItem = dic.ElementAt(i);
var splitted = dicItem.Value.Split(' ');
var sb = new StringBuilder();
foreach (var splittedItem in splitted)
{
if(dic.ContainsKey(splittedItem))
{
sb.Append(dic[splittedItem]);
}
else
{
sb.Append(splittedItem);
}
sb.Append(" ");
}
var parsed = sb.ToString();
if (parsed.Contains("exp"))
i--; //go back and try to evaluate it again
dic.Remove(dicItem.Key);
dic.Add(dicItem.Key, sb.ToString());
}
and not to make it more readable you can use DynamicExpression
var p = Expression.Parameter(typeof(String));
foreach (var exp in dic.Values)
{
var e = System.Linq.Dynamic.DynamicExpression.ParseLambda(new[] { p }, null, exp);
System.Diagnostics.Trace.WriteLine(e.Body);
}
OUTPUT
10
20
((10 + 20) + 30)
(((((10 + 20) + (30 / 10)) - 10) + 20) + 30)
((10 + 20) + (30 / 10))
This solution only replaces the variables:
var results = expressions.ToDictionary(x => x.Key, x => x.Value);
bool expanded;
do
{
expanded = false;
foreach (var key in expressions.Keys)
{
foreach (var kvpReplaceWith in expressions)
{
if (key == kvpReplaceWith.Key)
{
continue;
}
var lengthBefore = results[key].Length;
results[key] = results[key].Replace(kvpReplaceWith.Key, "(" + kvpReplaceWith.Value + ")");
var lengthAfter = results[key].Length;
expanded = expanded || lengthBefore != lengthAfter;
}
}
}
while (expanded);
Related
I would like to write C# code that parses nested parenthesis to array elements, but only on first level. An example is needed for sure:
I want this string:
"(example (to (parsing nested paren) but) (first lvl only))"
tp be parsed into:
["example", "(to (parsing nested paren) but)", "(first lvl only)"]
I was thinking about using regex but can't figure out how to properly use them without implementing this behaviour from scratch.
In the case of malformed inputs I would like to return an empty array, or an array ["error"]
I developed a parser for your example. I also checked some other examples which you can see in the code.
using System;
using System.Collections;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
string str = "(example (to (parsing nested paren) but) (first lvl only))"; // => [example , (to (parsing nested paren) but) , (first lvl only)]
//string str = "(first)(second)(third)"; // => [first , second , third]
//string str = "(first(second)third)"; // => [first , (second) , third]
//string str = "(first(second)(third)fourth)"; // => [first , (second) , (third) , fourth]
//string str = "(first((second)(third))fourth)"; // => [first , ((second)(third)) , fourth]
//string str = "just Text"; // => [ERROR]
//string str = "start with Text (first , second)"; // => [ERROR]
//string str = "(first , second) end with text"; // => [ERROR]
//string str = ""; // => [ERROR]
//string str = "("; // => [ERROR]
//string str = "(first()(second)(third))fourth)"; // => [ERROR]
//string str = "(((extra close pareanthese))))"; // => [ERROR]
var res = Parser.parse(str);
showRes(res);
}
static void showRes(ArrayList res)
{
var strings = res.ToArray();
var theString = string.Join(" , ", strings);
Console.WriteLine("[" + theString + "]");
}
}
public class Parser
{
static Dictionary<TokenType, TokenType> getRules()
{
var rules = new Dictionary<TokenType, TokenType>();
rules.Add(TokenType.OPEN_PARENTHESE, TokenType.START | TokenType.OPEN_PARENTHESE | TokenType.CLOSE_PARENTHESE | TokenType.SIMPLE_TEXT);
rules.Add(TokenType.CLOSE_PARENTHESE, TokenType.SIMPLE_TEXT | TokenType.CLOSE_PARENTHESE);
rules.Add(TokenType.SIMPLE_TEXT, TokenType.SIMPLE_TEXT | TokenType.CLOSE_PARENTHESE | TokenType.OPEN_PARENTHESE);
rules.Add(TokenType.END, TokenType.CLOSE_PARENTHESE);
return rules;
}
static bool isValid(Token prev, Token cur)
{
var rules = Parser.getRules();
return rules.ContainsKey(cur.type) && ((prev.type & rules[cur.type]) == prev.type);
}
public static ArrayList parse(string sourceText)
{
ArrayList result = new ArrayList();
int openParenthesesCount = 0;
Lexer lexer = new Lexer(sourceText);
Token prevToken = lexer.getStartToken();
Token currentToken = lexer.readNextToken();
string tmpText = "";
while (currentToken.type != TokenType.END)
{
if (currentToken.type == TokenType.OPEN_PARENTHESE)
{
openParenthesesCount++;
if (openParenthesesCount > 1)
{
tmpText += currentToken.token;
}
}
else if (currentToken.type == TokenType.CLOSE_PARENTHESE)
{
openParenthesesCount--;
if (openParenthesesCount < 0)
{
return Parser.Error();
}
if (openParenthesesCount > 0)
{
tmpText += currentToken.token;
}
}
else if (currentToken.type == TokenType.SIMPLE_TEXT)
{
tmpText += currentToken.token;
}
if (!Parser.isValid(prevToken, currentToken))
{
return Parser.Error();
}
if (openParenthesesCount == 1 && tmpText.Trim() != "")
{
result.Add(tmpText);
tmpText = "";
}
prevToken = currentToken;
currentToken = lexer.readNextToken();
}
if (openParenthesesCount != 0)
{
return Parser.Error();
}
if (!Parser.isValid(prevToken, currentToken))
{
return Parser.Error();
}
if (tmpText.Trim() != "")
{
result.Add(tmpText);
}
return result;
}
static ArrayList Error()
{
var er = new ArrayList();
er.Add("ERROR");
return er;
}
}
class Lexer
{
string _txt;
int _index;
public Lexer(string text)
{
this._index = 0;
this._txt = text;
}
public Token getStartToken()
{
return new Token(-1, TokenType.START, "");
}
public Token readNextToken()
{
if (this._index >= this._txt.Length)
{
return new Token(-1, TokenType.END, "");
}
Token t = null;
string txt = "";
if (this._txt[this._index] == '(')
{
txt = "(";
t = new Token(this._index, TokenType.OPEN_PARENTHESE, txt);
}
else if (this._txt[this._index] == ')')
{
txt = ")";
t = new Token(this._index, TokenType.CLOSE_PARENTHESE, txt);
}
else
{
txt = this._readText();
t = new Token(this._index, TokenType.SIMPLE_TEXT, txt);
}
this._index += txt.Length;
return t;
}
private string _readText()
{
string txt = "";
int i = this._index;
while (i < this._txt.Length && this._txt[i] != '(' && this._txt[i] != ')')
{
txt = txt + this._txt[i];
i++;
}
return txt;
}
}
class Token
{
public int position
{
get;
private set;
}
public TokenType type
{
get;
private set;
}
public string token
{
get;
private set;
}
public Token(int position, TokenType type, string token)
{
this.position = position;
this.type = type;
this.token = token;
}
}
[Flags]
enum TokenType
{
START = 1,
OPEN_PARENTHESE = 2,
SIMPLE_TEXT = 4,
CLOSE_PARENTHESE = 8,
END = 16
}
well, regex will do the job:
var text = #"(example (to (parsing nested paren) but) (first lvl only))";
var pattern = #"\(([\w\s]+) (\([\w\s]+ \([\w\s]+\) [\w\s]+\)) (\([\w\s]+\))\)*";
try
{
Regex r = new Regex(pattern, RegexOptions.IgnoreCase);
Match m = r.Match(text);
string group_1 = m.Groups[1].Value; //example
string group_2 = m.Groups[2].Value; //(to (parsing nested paren) but)
string group_3 = m.Groups[3].Value; //(first lvl only)
return new string[]{group_1,group_2,group_3};
}
catch(Exception ex){
return new string[]{"error"};
}
hopefully this helps, tested here in dotnetfiddle
Edit:
this might get you started into building the right expression according to whatever patterns you are falling into and maybe build a recursive function to parse the rest into the desired output :)
RegEx is not recursive. You either count bracket level, or recurse.
An non-recursive parser loop I tested for the example you show is..
string SplitFirstLevel(string s)
{
List<string> result = new List<string>();
int p = 0, level = 0;
for (int i = 0; i < s.Length; i++)
{
if (s[i] == '(')
{
level++;
if (level == 1) p = i + 1;
if (level == 2)
{
result.Add('"' + s.Substring(p, i - p) + '"');
p = i;
}
}
if (s[i] == ')')
if (--level == 0)
result.Add('"' + s.Substring(p, i - p) + '"');
}
return "[" + String.Join(",", result) + "]";
}
Note: after some more testing, I see your specification is unclear. How to delimit orphaned level 1 terms, that is terms without bracketing ?
For example, my parser translates
(example (to (parsing nested paren) but) (first lvl only))
to:
["example ","(to (parsing nested paren) but) ","(first lvl only)"]
and
(example (to (parsing nested paren)) but (first lvl only))
to:
["example ","(to (parsing nested paren)) but ","(first lvl only)"]
In either case, "example" gets a separate term, while "but" is grouped with the first term. In the first example this is logical, it is in the bracketing, but it may be unwanted behaviour in the second case, where "but" should be separated, like "example", which also has no bracketing (?)
I need to check whether all parts of a string like
A=1&AW=43&KO=96&R=7&WW=15&ZJ=80
are in a big string like:
A=1&AG=77&AW=43&.....&KF=11&KO=96&.....&QW=55&R=7&....&WV=1&WW=15&....ZJ=80&
My code splits the first string on & and uses Contains. But the duration is too long, as the big string is up to 800000 characters.
Is there a better/faster method for this?
public partial class UserDefinedFunctions
{
[Microsoft.SqlServer.Server.SqlFunction]
public static SqlInt32 EquipmentCompare(SqlString equip, SqlString comp)
{
SqlInt32 result = 1;
if (comp.IsNull)
{
result = 1;
}
else
{
string equipment = "&" + equip.ToString();
string compString = comp.ToString() + "! ";
while (compString.Length > 1)
{
string sub = compString.Substring(0, compString.IndexOf("!"));
compString = compString.Substring(compString.IndexOf("!")+1);
string[] elements = sub.Split('&');
foreach (string i in elements)
{
if (i.StartsWith("~"))
{
if (equipment.Contains("&" + i.Substring(1) + "&"))
{
result = 0;
break;
}
}
else if (!equipment.Contains("&" + i + "&"))
{
result = 0;
break;
}
else
{
result = 1;
continue;
}
}
if (result == 1)
{
break;
}
}
}
return result;
}
}
I think you may speed up your code by using HashSet. Try this:
var str1 = "A=1&AW=43&KO=96&R=7&WW=15&ZJ=80";
var str2 = "A=1&AG=77&AW=43&.....&KF=11&KO=96&.....&QW=55&R=7&....&WV=1&WW=15&....ZJ=80&";
var largeStringSet = new HashSet<string>(str2.Split('&'));
var allPartsIncluded = str1.Split('&').All(s => largeStringSet.Contains(s));
Code:
string animals = "cat98dog75";
What i try to achieve :
string a = "cat98";
string b = "dog75";
Question :
How do i split the string using some range delimiter?
example :
animals.split();
I suggest matching with a help of regular expressions:
using System.Text.RegularExpressions;
...
string animals = "cat98dog75";
string[] items = Regex
.Matches(animals, "[a-zA-Z]+[0-9]*")
.OfType<Match>()
.Select(match => match.Value)
.ToArray();
string a = items[0];
string b = items[1];
Concole.Write(string.Join(", ", items));
Outcome:
cat98, dog75
In case you want to split the initial string by equal size chunks:
int size = 5;
string[] items = Enumerable
.Range(0, animals.Length / size + (animals.Length % size > 0 ? 1 : 0))
.Select(index => (index + 1) * size <= animals.Length
? animals.Substring(index * size, size)
: animals.Substring(index * size))
.ToArray();
string a = items[0];
string b = items[1];
This might do the trick for you
string animals = "cat98dog75";
string[] DiffAnimals = Regex.Split(animals, "(?<=[0-9]{2})")
.Where(s => s != String.Empty) //Just to Remove Empty Entries.
.ToArray();
If you want to split the name of the animal and number, try following..
I know its too long....
private static void SplitChars()
{
string animals = "cat98dog75";
Dictionary<string, string> dMyobject = new Dictionary<string, string>();
string sType = "",sCount = "";
bool bLastOneWasNum = false;
foreach (var item in animals.ToCharArray())
{
if (char.IsLetter(item))
{
if (bLastOneWasNum)
{
dMyobject.Add(sType, sCount);
sType = ""; sCount = "";
bLastOneWasNum = false;
}
sType = sType + item;
}
else if (char.IsNumber(item))
{
bLastOneWasNum = true;
sCount = sCount + item;
}
}
dMyobject.Add(sType, sCount);
foreach (var item in dMyobject)
{
Console.WriteLine(item.Key + "- " + item.Value);
}
}
You will get output as
cat - 98
dog - 75
Basically, you are getting type and numbers so if you want to use the count, you don't need to split again...
I have in the options file two functions GetKey and SetKey.
I set a key then in the settings_file.txt it will look like:
text = hello where text is the key then = and hello is the value for the current key.
Now i need to add another two functions the first one is type of List that get a string and return a List
And a another function that get a Key and a List.
So this is the first two functions allready working GetKey and SetKey:
/*----------------------------------------------------------------
* Module Name : OptionsFile
* Description : Saves and retrievs application options
* Author : Danny
* Date : 10/02/2010
* Revision : 1.00
* --------------------------------------------------------------*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.IO;
using System.Configuration;
/*
* Introduction :
*
* This module helps in saving application options
*
*
* Typical file could look like this:
* user_color=Red
* time_left=30
*
*
*
*
*
* */
namespace DannyGeneral
{
class OptionsFile
{
/*----------------------------------------
* P R I V A T E V A R I A B L E S
* ---------------------------------------*/
/*---------------------------------
* P U B L I C M E T H O D S
* -------------------------------*/
string path_exe;
string temp_settings_file;
string temp_settings_dir;
string Options_File;
StreamWriter sw;
StreamReader sr;
/*----------------------------------------------------------
* Function : OptionsFile
* Description : Constructor
* Parameters : file_name is the name of the file to use
* Return : none
* --------------------------------------------------------*/
public OptionsFile(string settings)
{
if (!File.Exists(settings))
{
if (!Directory.Exists(Path.GetDirectoryName(settings)))
{
Directory.CreateDirectory(Path.GetDirectoryName(settings));
}
File.Create(settings).Close();
}
path_exe = Path.GetDirectoryName(Application.LocalUserAppDataPath);
Options_File = settings;
}
/*----------------------------------------------------------
* Function : GetKey
* Description : gets the value of the key.
* Parameters : key
* Return : value of the key if key exist, null if not exist
* --------------------------------------------------------*/
public string GetKey(string key)
{
// string value_of_each_key;
string key_of_each_line;
string line;
int index;
string key_value;
key_value = null;
sr = new StreamReader(Options_File);
while (null != (line = sr.ReadLine()))
{
index = line.IndexOf("=");
// value_of_each_key = line.Substring(index+1);
if (index >= 1)
{
key_of_each_line = line.Substring(0, index);
if (key_of_each_line == key)
{
key_value = line.Substring(key.Length + 1);
}
}
else
{
}
}
sr.Close();
return key_value;
}
/*----------------------------------------------------------
* Function : SetKey
* Description : sets a value to the specified key
* Parameters : key and a value
* Return : none
* --------------------------------------------------------*/
public void SetKey(string key , string value)
{
bool key_was_found_inside_the_loop;
string value_of_each_key;
string key_of_each_line ;
string line;
int index;
key_was_found_inside_the_loop = false;
temp_settings_file = "\\temp_settings_file.txt";
temp_settings_dir = path_exe + #"\temp_settings";
if (!Directory.Exists(temp_settings_dir))
{
Directory.CreateDirectory(temp_settings_dir);
}
sw = new StreamWriter(temp_settings_dir+temp_settings_file);
sr = new StreamReader(Options_File);
while (null != (line = sr.ReadLine()))
{
index = line.IndexOf("=");
key_of_each_line = line.Substring(0, index);
value_of_each_key = line.Substring( index + 1);
// key_value = line.Substring(0,value.Length);
if (key_of_each_line == key)
{
sw.WriteLine(key + " = " + value);
key_was_found_inside_the_loop = true;
}
else
{
sw.WriteLine(key_of_each_line+"="+value_of_each_key);
}
}
if (!key_was_found_inside_the_loop)
{
sw.WriteLine(key + "=" + value);
}
sr.Close();
sw.Close();
File.Delete(Options_File);
File.Move(temp_settings_dir + temp_settings_file, Options_File);
return;
}
After this two functions i did:
public List<float> GetListFloatKey(string keys)
{
int j;
List<float> t;
t = new List<float>();
int i;
for (i = 0; ; i++)
{
j = Convert.ToInt32(GetKey((keys + i).ToString()));
if (j == 0)
{
break;
}
else
{
t.Add(j);
}
}
if (t.Count == 0)
return null;
else
return t;
}
public void SetListFloatKey(string key, List<float> Values)
{
int i;
for (i = 0; i < Values.Count; i++)
{
string indexed_key;
indexed_key = string.Format("{0}{1}", key, i);
// indexed_key = Key + i.ToString();
SetKey(indexed_key, Values[i].ToString());
}
}
But they are not good.
The last one the SetListFloatKey when i put a List in it the result in the text file settings_file.txt is for exmaple:
coordinates01 = 123
coordinates02 = 144
coordinates03 = 145
For every cell/index in the List i get its making a key. What i need is that the List i get will have one key the format in the text file should be like this:
coordinates = 123,144,145......and so on one key and then all the values from the List i get.
Then in the GetListFloatKey i need re format the values according to the key for example coordinates and return a List with the values in index 0 123 in 1 144 in 2 145 and so on....
The qustion if the function the way im doing them are good in the way im using in both GetKey and SetKey ? And how do i format and re format the values ?
At the moment you are calling SetKey within SetListFloatKey for every item in the list. Instead, you need to build a string and call it once, along the lines of (basic testing done):
public static void SetListFloatKey(string key, List<float> Values)
{
StringBuilder sb = new StringBuilder();
foreach (float value in Values)
{
sb.AppendFormat("{0},", value);
}
SetKey(key, sb.ToString());
}
Note I am getting lazy here - the last item will have a comma after it. Then when loading the list:
public static List<float> GetListFloatKey(string keys)
{
List<float> result = new List<float>();
string s = GetKey(keys);
string[] items = s.Split(new char[] { ',' });
float f;
foreach (string item in items)
{
if (float.TryParse(item, out f))
result.Add(f);
}
return result;
}
However, given you are reading and writing an options file, you might want to investigate options around serializing your objects to and from files.
EDIT There are a few ways you can get rid of the extra comma. One way is to not put it in in the first place...
string sep = "";
foreach (float value in Values)
{
sb.AppendFormat("{0}{1}", sep, value);
if (sep == "") sep = ",";
}
...and another is to exclude it in the call to SetKey...
foreach (float value in Values)
{
sb.AppendFormat(",{0}", value);
}
SetKey(key, sb.ToString().Substring(1));
..note that in both of these cases I moved the comma to the start to make life easier. Alternatively, you could store the numbers in an array and use Array.Join.
I think that you are wasting too much time thinking about how to format the file each time you make a change, you are also causing a lot of file overhead each time you check for a key.
Consider using a class like
public class Options
{
public static string FILENAME = #"C:\Test\testfile.txt";
public List<KeyValuePair<string, string>> OrderedKeys { get; set; }
public Dictionary<string, KeyValuePair<string, string>> Pairs { get; set; }
public string GetKey(string key)
{
return this.Pairs[key].Value;
}
public void SetKey(string key, string value)
{
if(this.Pairs.ContainsKey(key))
{
KeyValuePair<string, string> pair = new KeyValuePair<string, string>(key, value);
this.OrderedKeys.Insert(this.OrderedKeys.IndexOf(this.Pairs[key]), pair);
this.Pairs[key] = pair;
}
}
public Options()
{
LoadFile();
}
~Options()
{
WriteFile();
}
private void LoadFile()
{
Regex regex = new Regex(#"(?<key>\S*?)\s*=\s*(?<val>\S*?)\s*\r\n");
MatchCollection matches = regex.Matches(File.ReadAllText(FILENAME));
this.OrderedKeys = new List<KeyValuePair<string, string>>();
this.Pairs = new Dictionary<string, KeyValuePair<string, string>>();
foreach (Match match in matches)
{
KeyValuePair<string, string> pair =
new KeyValuePair<string,string>(match.Groups["key"].Value, match.Groups["val"].Value);
this.OrderedKeys.Add(pair);
this.Pairs.Add(pair.Key, pair);
}
}
private void WriteFile()
{
if (File.Exists(FILENAME))
File.Delete(FILENAME);
using (System.IO.StreamWriter file = new System.IO.StreamWriter(FILENAME))
{
foreach (KeyValuePair<string, string> pair in this.OrderedKeys)
{
file.WriteLine(pair.Key + " = " + pair.Value);
}
}
}
}
Notice that the options object will read from the file once, and writeout when it is destroyed, meanwhile it will hold a local dictionary of the values in your file. You can then GetKey() and SetKey() to get and set your options.
I modified my original post to use a list and a dictionary, this is because a Dictionary on its own does not maintain the original order that pairs are added, so the list ensures that the options are always written to the file in the correct order.
You will also notice I threw in a Regular Expression to parse your file, makes things much easier and quicker and allows for things like extra whitespace in the options file.
Once you have done this it is easy to add functions like
public List<float> GetListFloatKey(string keybase)
{
List<float> ret = new List<float>();
foreach (string key in this.Pairs.Keys)
{
if (Regex.IsMatch(key, keybase + "[0-9]+"))
ret.Add(float.Parse(this.Pairs[key].Value));
}
return ret;
}
public void SetListFloatKey(string keybase, List<float> values)
{
List<string> oldkeys = new List<string>();
int startindex = -1;
foreach (string key in this.Pairs.Keys)
{
if (Regex.IsMatch(key, keybase + "[0-9]+"))
{
if (startindex == -1)
startindex = this.OrderedKeys.IndexOf(this.Pairs[key]);
oldkeys.Add(key);
}
}
foreach (string key in oldkeys)
{
this.OrderedKeys.Remove(this.Pairs[key]);
this.Pairs.Remove(key);
}
for (int i = 0; i < values.Count; i++)
{
KeyValuePair<string, string> pair = new KeyValuePair<string, string>(keybase + i.ToString(), values[i].ToString());
if (startindex != -1)
this.OrderedKeys.Insert(startindex + i, pair);
else
this.OrderedKeys.Add(pair);
this.Pairs.Add(pair.Key, pair);
}
}
It is easier to do that at this point because you have abstracted the actual file structure away and are now just dealing with a Dictionary
Instead of using {0} {1}, etc. I want to use {title} instead. Then fill that data in somehow (below I used a Dictionary). This code is invalid and throws an exception. I wanted to know if i can do something similar to what i want. Using {0 .. N} is not a problem. I was just curious.
Dictionary<string, string> d = new Dictionary<string, string>();
d["a"] = "he";
d["ba"] = "llo";
d["lol"] = "world";
string a = string.Format("{a}{ba}{lol}", d);
No, but this extension method will do it
static string FormatFromDictionary(this string formatString, Dictionary<string, string> valueDict)
{
int i = 0;
StringBuilder newFormatString = new StringBuilder(formatString);
Dictionary<string, int> keyToInt = new Dictionary<string,int>();
foreach (var tuple in valueDict)
{
newFormatString = newFormatString.Replace("{" + tuple.Key + "}", "{" + i.ToString() + "}");
keyToInt.Add(tuple.Key, i);
i++;
}
return String.Format(newFormatString.ToString(), valueDict.OrderBy(x => keyToInt[x.Key]).Select(x => x.Value).ToArray());
}
Check this one, it supports formating:
public static string StringFormat(string format, IDictionary<string, object> values)
{
var matches = Regex.Matches(format, #"\{(.+?)\}");
List<string> words = (from Match matche in matches select matche.Groups[1].Value).ToList();
return words.Aggregate(
format,
(current, key) =>
{
int colonIndex = key.IndexOf(':');
return current.Replace(
"{" + key + "}",
colonIndex > 0
? string.Format("{0:" + key.Substring(colonIndex + 1) + "}", values[key.Substring(0, colonIndex)])
: values[key] == null ? string.Empty : values[key].ToString());
});
}
How to use:
string format = "{foo} is a {bar} is a {baz} is a {qux:#.#} is a really big {fizzle}";
var dictionary = new Dictionary<string, object>
{
{ "foo", 123 },
{ "bar", true },
{ "baz", "this is a test" },
{ "qux", 123.45 },
{ "fizzle", DateTime.Now }
};
StringFormat(format, dictionary)
You can implement your own:
public static string StringFormat(string format, IDictionary<string, string> values)
{
foreach(var p in values)
format = format.Replace("{" + p.Key + "}", p.Value);
return format;
}
Phil Haack discussed several methods of doing this on his blog a while back: http://haacked.com/archive/2009/01/14/named-formats-redux.aspx. I've used the "Hanselformat" version on two projects with no complaints.
It's possible now
With Interpolated Strings of C# 6.0 you can do this:
string name = "John";
string message = $"Hi {name}!";
//"Hi John!"
static public class StringFormat
{
static private char[] separator = new char[] { ':' };
static private Regex findParameters = new Regex(
"\\{(?<param>.*?)\\}",
RegexOptions.Compiled | RegexOptions.Singleline);
static string FormatNamed(
this string format,
Dictionary<string, object> args)
{
return findParameters.Replace(
format,
delegate(Match match)
{
string[] param = match.Groups["param"].Value.Split(separator, 2);
object value;
if (!args.TryGetValue(param[0], out value))
value = match.Value;
if ((param.Length == 2) && (param[1].Length != 0))
return string.Format(
CultureInfo.CurrentCulture,
"{0:" + param[1] + "}",
value);
else
return value.ToString();
});
}
}
A little more involved than the other extension method, but this should also allow non-string values and formatting patterns used on them, so in your original example:
Dictionary<string, object> d = new Dictionary<string, object>();
d["a"] = DateTime.Now;
string a = string.FormatNamed("{a:yyyyMMdd-HHmmss}", d);
Will also work...
Since C# 6 released you are able to use String Interpolation feature
Code that solves your question:
string a = $"{d["a"]}{d["ba"]}{d["lol"]}";
Why a Dictionary? It's unnecessary and overly complicated. A simple 2 dimensional array of name/value pairs would work just as well:
public static string Format(this string formatString, string[,] nameValuePairs)
{
if (nameValuePairs.GetLength(1) != 2)
{
throw new ArgumentException("Name value pairs array must be [N,2]", nameof(nameValuePairs));
}
StringBuilder newFormat = new StringBuilder(formatString);
int count = nameValuePairs.GetLength(0);
object[] values = new object[count];
for (var index = 0; index < count; index++)
{
newFormat = newFormat.Replace(string.Concat("{", nameValuePairs[index,0], "}"), string.Concat("{", index.ToString(), "}"));
values[index] = nameValuePairs[index,1];
}
return string.Format(newFormat.ToString(), values);
}
Call the above with:
string format = "{foo} = {bar} (really, it's {bar})";
string formatted = format.Format(new[,] { { "foo", "Dictionary" }, { "bar", "unnecessary" } });
Results in: "Dictionary = unnecessary (really, it's unnecessary)"
public static string StringFormat(this string format, IDictionary<string, object> values)
{
return Regex.Matches(format, #"\{(?!\{)(.+?)\}")
.Select(m => m.Groups[1].Value)
.Aggregate(format, (current, key) =>
{
string[] splits = key.Split(":");
string replacement = splits.Length > 1
? string.Format($"{{0:{splits[1]}}}", values[splits[0]])
: values[key].ToString();
return Regex.Replace(current, "(.|^)("+ Regex.Escape($"{{{key}}}")+")(.|$)",
m => m.Groups[1].ToString() == "{" && m.Groups[3].ToString() == "}"
? m.Groups[2].ToString()
: m.Groups[1] + replacement + m.Groups[3]
);
});
}
This is similar to another answer, but it considers escaping with {{text}}.
I took #LPCRoy's answer and refactored for generic dictionary types, added parameter validation, it only replaces fields it can find, and attempts to solve the "double curly brace" issue by escaping them first, then replacing them at the end with single braces in the same way that string.Format() does.
public static string FormatFromDictionary<K, T>(this string formatString, IDictionary<K, T> valueDict)
{
if (string.IsNullOrWhiteSpace(formatString)) return formatString;
if (valueDict == null || !valueDict.Any()) return formatString;
bool escapedDoubleCurlyBraces = false;
if (formatString.Contains("{{") || formatString.Contains("}}"))
{
formatString = formatString.Replace("{{", "\\{\\{").Replace("}}", "\\}\\}");
escapedDoubleCurlyBraces = true;
}
int i = 0;
StringBuilder newFormatString = new StringBuilder(formatString);
Dictionary<K, int> keyToInt = new Dictionary<K, int>();
string field;
//StringBuilder.Replace() is faster than string.Replace().
foreach (var kvp in valueDict)
{
field = "{" + kvp.Key.ToString() + "}";
if (formatString.Contains(field))
{
newFormatString = newFormatString.Replace(field, "{" + i.ToString() + "}");
keyToInt.Add(kvp.Key, i);
i++;
}
}
//Any replacements to make?
if (keyToInt.Any())
{
formatString = string.Format(
newFormatString.ToString(),
keyToInt.OrderBy(kvp => kvp.Value).Select(kvp => (object)valueDict[kvp.Key]).ToArray()
);
}
if (escapedDoubleCurlyBraces)
{
//Converts "{{" and "}}" to single "{" and "}" for the final result just like string.format().
formatString = formatString.Replace("\\{\\{", "{").Replace("\\}\\}", "}");
}
return formatString;
}
Here is a nice solution that is very useful when formatting emails: http://www.c-sharpcorner.com/UploadFile/e4ff85/string-replacement-with-named-string-placeholders/
Edited:
public static class StringExtension
{
public static string Format( this string str, params Expression<Func<string,object>>[] args)
{
var parameters = args.ToDictionary( e=>string.Format("{{{0}}}",e.Parameters[0].Name), e=>e.Compile()(e.Parameters[0].Name));
var sb = new StringBuilder(str);
foreach(var kv in parameters)
{
sb.Replace( kv.Key, kv.Value != null ? kv.Value.ToString() : "");
}
return sb.ToString();
}
}
Example usage:
public string PopulateString(string emailBody)
{
User person = _db.GetCurrentUser();
string firstName = person.FirstName; // Peter
string lastName = person.LastName; // Pan
return StringExtension.Format(emailBody.Format(
firstname => firstName,
lastname => lastName
));
}
(your Dictionary + foreach + string.Replace) wrapped in a sub-routine or extension method?
Obviously unoptimized, but...