Modifying a dictionary collection - c#

I currently have a dictionary(string,int) which will hold values like the following
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[1] , 91
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[2] , 92
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[2]/FormA1[1] , 93
And this collection is created using a simple method CreatePathCollection(string path, int entityKey)
However the challenge I am facing is the following.
Suppose I receive a key and value into my method, which has values like
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[1] , 94
I would like to update the the following keys in the collection from
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[1] , 91
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[2] , 92
TO
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[2] , 91
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[3] , 92
And then Add
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[1] , 94
So the final collection will be
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[1] , 94
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[2] , 91
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[3] , 92
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[2]/FormA1[1] , 93
Is there an elegant way to accomplish this?

Create a model to represent your key like this:
The following class represents part of the path like /ReturnState[1], and it contains a method (the constructor) to parse the data from a string and another method to convert the data to the string format.
public class Part
{
public string Name { get; set; }
public int Index { get; set; }
public Part(string str)
{
int location_of_bracket_start = str.LastIndexOf("[");
if(location_of_bracket_start == -1)
throw new Exception("Unexpected format");
Name = str.Substring(0, location_of_bracket_start);
string rest = str.Substring(location_of_bracket_start);
Index = int.Parse(rest.Substring(1, rest.Length - 2));
}
public string ConvertToStringFormat()
{
return string.Format("/{0}[{1}]", Name, Index);
}
}
The following class represents a full path (e.g. /ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[1]) as a list of parts. It also contains method to construct an the object from a string and to convert to a string.
public class NodePath : List<Part>
{
public NodePath(string path)
{
string[] parts = path.Split(new []{"/"}, StringSplitOptions.RemoveEmptyEntries);
foreach (string part in parts)
{
this.Add(new Part(part));
}
}
public string ConvertToStringFormat()
{
return string.Join("", this.Select(x => x.ConvertToStringFormat()));
}
}
The following class contains the logic that you need:
public class PathClass
{
private readonly Dictionary<string, int> m_Dictionary;
public PathClass()
{
m_Dictionary = new Dictionary<string, int>();
}
public Dictionary<string, int> Dictionary
{
get { return m_Dictionary; }
}
public void Add(string path, int number)
{
if (m_Dictionary.ContainsKey(path))
MoveOne(path);
m_Dictionary.Add(path, number);
}
public void MoveOne(string path)
{
int number = m_Dictionary[path];
m_Dictionary.Remove(path);
var moved_node_path = IncrementPath(path);
if (m_Dictionary.ContainsKey(moved_node_path))
MoveOne(moved_node_path);
m_Dictionary.Add(moved_node_path, number);
}
private string IncrementPath(string path)
{
NodePath node_path = new NodePath(path);
node_path.Last().Index++;
return node_path.ConvertToStringFormat();
}
}
When the consumer tries to add a path, it checks if it exists, if it does, it moves the existing one (increments the index of the last path Part). It does this recursively in case the dictionary also contains an item where we are trying to move to.
I tested this like this:
PathClass path_class = new PathClass();
path_class.Add("/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[1]" , 1);
path_class.Add("/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[1]", 2);
path_class.Add("/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[2]/FormA1[1]", 3);
path_class.Add("/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[2]/FormA1[2]", 4);
path_class.Add("/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[2]/FormA1[1]", 5);
I got the following results:
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[2], 1
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[1]/FormA1[1], 2
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[2]/FormA1[2], 3
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[2]/FormA1[3], 4
/ReturnState[1]/ReturnDataState[1]/Form6[1]/Body[1]/Member[2]/FormA1[1], 5
Please note that another way to do this is to use a Dictionary<NodePath,int>, this means that you would need to implement Equals and GetHashCode for NodePath.
UPDATE:
If you don't care about the model, you can replace the IncrementPath method with this (and remove the model) for performance reasons:
private string IncrementPath(string path)
{
int location_of_bracket_start = path.LastIndexOf("[");
if (location_of_bracket_start == -1)
throw new Exception("Unexpected format");
string before_bracket = path.Substring(0, location_of_bracket_start);
string rest = path.Substring(location_of_bracket_start);
int index = int.Parse(rest.Substring(1, rest.Length - 2));
index ++;
return string.Format("{0}[{1}]", before_bracket, index);
}

Here is what I end up - not very elegant, but should do the job
static void UpdatePathCollection(Dictionary<string, int> target, string path, int entityKey)
{
int start, index;
if (path == null || path.Length < 3 || path[path.Length - 1] != ']'
|| (start = path.LastIndexOf('[', path.Length - 2)) < 0
|| !int.TryParse(path.Substring(start + 1, path.Length - start - 2), out index)
|| index < 0) throw new ArgumentException("path");
var prefix = path.Substring(0, start + 1);
var nextKey = path;
var nextValue = entityKey;
while (true)
{
int oldValue;
if (!target.TryGetValue(nextKey, out oldValue))
{
target.Add(nextKey, nextValue);
break;
}
target[nextKey] = nextValue;
index++;
nextKey = prefix + index + "]";
nextValue = oldValue;
}
}

As far as I understand, the strings you use to define the paths will occur in alphabetical order (it will depend on whether you have more than 9 elements per index). In this scenario, you might use SortedDictionary and proceed as follows:
private readonly SortedDictionary<string, int> sortedDictionary = CreatePathCollection(path, entityKey);
public void Set(string path, int index)
{
sortedDictionary.Remove(path);
var i = 91;
foreach (var key in sortedDictionary.Keys)
sortedDictionary[key] = i++;
sortedDictionary[path] = index;
}
Unfortunately, I may not understand your problem in all detail, but I hope this gives you some ideas.

Related

Build the string dynamically based on the length in c#

My aim is to generate a string. which has following conditions.
First i will take an empty string and start building the string based on length.
Example:
I have an empty string "".
In the first step i want to add a string till 8 characters, means first my string is "" then till 8 characters my string should contain the value Hiwor so finally my string will be Hiwor if there is no value empty value should be padded in the string.
In the second step i want to add the string meena till 10 positions , so my final string should be Hiwor meena. In this way i want to build my string. Ho can i achieve this. Can you please help me.
Sample:
initial string ""
first step adding string Hiwor till 8 positions,
so final string should be Hiwor
second step adding string meena till 10 postions
so final string should be Hiwor meena .
Till now i tried like this
Dictionary<string, Int16> transLine = new Dictionary<string, Int16>();
transLine.Add("ProductCode", 1);
transLine.Add("ApplicantFirstName", 12);
transLine.Add("ApplicantMiddleInitial", 1);
transLine.Add("partner", 1);
transLine.Add("employee", 8);
List<string> list = new List<string>();
list.Add("grouplife");
list.Add("meena");
list.Add("d");
list.Add("yes");
list.Add("yes");
StringBuilder sb = new StringBuilder();
foreach (var listItem in list)
{
foreach (var item in transLine)
{
if (listItem == item.Key)
{
var length = item.Value;
sb.Insert(length, item.Key);
}
}
}
but it is throwing me an exception.Index was out of range. Must be non-negative and less than the size of the collection.
Firstly define an extension method for StringBuilder:
public static class StringBuilderExtensions
{
public static void AppendPadded(this StringBuilder builder, string value, int length);
{
builder.Append($"{value}{new string(' ', length)}".Substring(0, length));
}
public static void AppendPadded(this StringBuilder builder, int value, int length);
{
builder.Append($"{new string('0', length)}{value}".Reverse().ToString().Substring(0, length).Reverse().ToString());
}
}
Then use:
StringBuilder builder = new StringBuilder();
builder.AppendPadded("Hiwor", 8);
builder.AppendPadded("meena", 10);
return builder.ToString();
Or with your example:
foreach (string item in list)
builder.AppendPadded(item, transLine[item]);
EDIT: Ok, so looks like you want to be able to define a format then build the string using it. Try:
(you will need to reference System.ComponentModel.DataAnnotations and System.Reflection for this)
public abstract class AnItem
{
private static int GetLength(PropertyInfo property)
{
StringLengthAttribute attribute = property.GetCustomAttributes(typeof(StringLengthAttribute), true).FirstOrDefault() as StringLengthAttribute;
if (attribute == null)
throw new Exception($"StringLength not specified for {property.Name}");
return attribute.MaxLength();
}
private string GetPropertyValue(PropertyInfo property)
{
if (property.PropertyType == typeof(string))
return property.GetValue(this);
else if (property.PropertyType == typeof(int))
return property.GetValue(this).ToString();
else
throw new Exception($"Property '{property.Name}' is not a supported type");
}
private static void SetPropertyValue(object item, PropertyInfo property, string value)
{
if (property.PropertyType == typeof(string))
property.SetValue(item, value, null);
else if (property.PropertyType == typeof(int))
property.SetValue(item, int.Parse(value), null);
else
throw new Exception($"Property '{property.Name}' is not a supported type");
}
public string GetPaddedString()
{
StringBuilder builder = new StringBuilder();
PropertyInfo[] properties = GetType().GetProperties();
foreach (PropertyInfo property in properties)
builder.AppendPadded(GetPropertyValue(property), GetLength(property));
return builder.ToString();
}
public static T CreateFromPaddedString<T>(string paddedString) where T : AnItem, new()
{
T item = new T();
int offset = 0;
PropertyInfo[] properties = typeof(T).GetProperties();
foreach (PropertyInfo property in properties)
{
int length = GetLength(property);
if (offset + length > paddedString.Length)
throw new Exception("The string is too short");
SetPropertyValue(item, property, paddedString.Substring(offset, length)));
offset += length;
}
if (offset < paddedString.Length)
throw new Exception("The string is too long");
return item;
}
}
public class MyItem : AnItem
{
[StringLength(1)]
public string ProductCode { get; set; }
[StringLength(12)]
public string ApplicantFirstName { get; set; }
[StringLength(1)]
public string ApplicantMiddleInitial { get; set; }
[StringLength(1)]
public string Partner { get; set; }
[StringLength(8)]
public string Employee { get; set; }
}
Then use it:
MyItem item = new MyItem
{
ProductCode = "grouplife",
ApplicantFirstName = "meena",
ApplicantMiddleInitial = "d",
Partner = "yes",
Employee = "yes"
};
string paddedString = item.GetPaddedString();
And to read a string to get an item:
MyItem item = AnItem.CreateFromPaddedString<MyItem>(paddedString);
At first I want to say something more about your exception:
Index was out of range. Must be non-negative and less than the size of the collection.
As the exception already said. The problem is that you want to access a position within your new StringBuilder sb which does not exists.
StringBuilder sb = new StringBuilder();
After this line your new sb is empty. There is no single character in it. So you can only access the index at position 0. But almost in your first iteration of the inner for-each loop you want to target the index 1 and try to insert your string at the position 1, which does not exists.
// length: 1 and item.Key: ProductCode
sb.Insert(length, item.Key);
So how to solve this. You can use a feature from String.Format() or since C#6 the string interpolation.
So for example:
String.Format()
var sb = new StringBuilder(string.Empty); // sb: []
sb.Append(string.Format("{0, -8}", "Hiwor")); // sb: [Hiwor ]
sb.Append(string.Format("{0,-10}", "meena")); // sb: [Hiwor meena ]
C#6 String Interpolation
var sb = new StringBuilder(string.Empty); // sb: []
sb.Append($"{"Hiwor", -8}"); // sb: [Hiwor ]
sb.Append($"{"meena", -10}"); // sb: [Hiwor meena ]
// ...
Targeting your edit:
With your given list items you will never get a match with any of your dictionary keys.

How to compare two csv files by 2 columns?

I have 2 csv files
1.csv
spain;russia;japan
italy;russia;france
2.csv
spain;russia;japan
india;iran;pakistan
I read both files and add data to lists
var lst1= File.ReadAllLines("1.csv").ToList();
var lst2= File.ReadAllLines("2.csv").ToList();
Then I find all unique strings from both lists and add it to result lists
var rezList = lst1.Except(lst2).Union(lst2.Except(lst1)).ToList();
rezlist contains this data
[0] = "italy;russia;france"
[1] = "india;iran;pakistan"
At now I want to compare, make except and union by second and third column in all rows.
1.csv
spain;russia;japan
italy;russia;france
2.csv
spain;russia;japan
india;iran;pakistan
I think I need to split all rows by symbol ';' and make all 3 operations (except, distinct and union) but cannot understand how.
rezlist must contains
india;iran;pakistan
I added class
class StringLengthEqualityComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
...
}
public int GetHashCode(string obj)
{
...
}
}
StringLengthEqualityComparer stringLengthComparer = new StringLengthEqualityComparer();
var rezList = lst1.Except(lst2,stringLengthComparer ).Union(lst2.Except(lst1,stringLengthComparer),stringLengthComparer).ToList();
Your question is not very clear: for instance, is india;iran;pakistan the desired result primarily because russia is at element[1]? Isn't it also included because element [2] pakistan does not match france and japan? Even though thats unclear, I assume the desired result comes from either situation.
Then there is this: find all unique string from both lists which changes the nature dramatically. So, I take it that the desired results are because "iran" appears in column[1] no where else in column[1] in either file and even if it did, that row would still be unique due to "pakistan" in col[2].
Also note that a data sample of 2 leaves room for a fair amount of error.
Trying to do it in one step makes it very confusing. Since eliminating dupes found in 1.CSV is pretty easy, do it first:
// parse "1.CSV"
List<string[]> lst1 = File.ReadAllLines(#"C:\Temp\1.csv").
Select(line => line.Split(';')).
ToList();
// parse "2.CSV"
List<string[]> lst2 = File.ReadAllLines(#"C:\Temp\2.csv").
Select(line => line.Split(';')).
ToList();
// extracting once speeds things up in the next step
// and leaves open the possibility of iterating in a method
List<List<string>> tgts = new List<List<string>>();
tgts.Add(lst1.Select(z => z[1]).Distinct().ToList());
tgts.Add(lst1.Select(z => z[2]).Distinct().ToList());
var tmpLst = lst2.Where(x => !tgts[0].Contains(x[1]) ||
!tgts[1].Contains(x[2])).
ToList();
That results in the items which are not in 1.CSV (no matching text in Col[1] nor Col[2]). If that is really all you need, you are done.
Getting unique rows within 2.CSV is trickier because you have to actually count the number of times each Col[1] item occurs to see if it is unique; then repeat for Col[2]. This uses GroupBy:
var unique = tmpLst.
GroupBy(g => g[1], (key, values) =>
new GroupItem(key,
values.ToArray()[0],
values.Count())
).Where(q => q.Count == 1).
GroupBy(g => g.Data[2], (key, values) => new
{
Item = string.Join(";", values.ToArray()[0]),
Count = values.Count()
}
).Where(q => q.Count == 1).Select(s => s.Item).
ToList();
The GroupItem class is trivial:
class GroupItem
{
public string Item { set; get; } // debug aide
public string[] Data { set; get; }
public int Count { set; get; }
public GroupItem(string n, string[] d, int c)
{
Item = n;
Data = d;
Count = c;
}
public override string ToString()
{
return string.Join(";", Data);
}
}
It starts with tmpList, gets the rows with a unique element at [1]. It uses a class for storage since at this point we need the array data for further review.
The second GroupBy acts on those results, this time looking at col[2]. Finally, it selects the joined string data.
Results
Using 50,000 random items in File1 (1.3 MB), 15,000 in File2 (390 kb). There were no naturally occurring unique items, so I manually made 8 unique in 2.CSV and copied 2 of them into 1.CSV. The copies in 1.CSV should eliminate 2 if the 8 unique rows in 2.CSV making the expected result 6 unique rows:
NepalX and ItalyX were the repeats in both files and they correctly eliminated each other.
With each step it is scanning and working with less and less data, which seems to make it pretty fast for 65,000 rows / 130,000 data elements.
your GetHashCode()-Method in EqualityComparer are buggy. Fixed version:
public int GetHashCode(string obj)
{
return obj.Split(';')[1].GetHashCode();
}
now the result are correct:
// one result: "india;iran;pakistan"
btw. "StringLengthEqualityComparer"is not a good name ;-)
private void GetUnion(List<string> lst1, List<string> lst2)
{
List<string> lstUnion = new List<string>();
foreach (string value in lst1)
{
string valueColumn1 = value.Split(';')[0];
string valueColumn2 = value.Split(';')[1];
string valueColumn3 = value.Split(';')[2];
string result = lst2.FirstOrDefault(s => s.Contains(";" + valueColumn2 + ";" + valueColumn3));
if (result != null)
{
if (!lstUnion.Contains(result))
{
lstUnion.Add(result);
}
}
}
}
class Program
{
static void Main(string[] args)
{
var lst1 = File.ReadLines(#"D:\test\1.csv").Select(x => new StringWrapper(x)).ToList();
var lst2 = File.ReadLines(#"D:\test\2.csv").Select(x => new StringWrapper(x));
var set = new HashSet<StringWrapper>(lst1);
set.SymmetricExceptWith(lst2);
foreach (var x in set)
{
Console.WriteLine(x.Value);
}
}
}
struct StringWrapper : IEquatable<StringWrapper>
{
public string Value { get; }
private readonly string _comparand0;
private readonly string _comparand14;
public StringWrapper(string value)
{
Value = value;
var split = value.Split(';');
_comparand0 = split[0];
_comparand14 = split[14];
}
public bool Equals(StringWrapper other)
{
return string.Equals(_comparand0, other._comparand0, StringComparison.OrdinalIgnoreCase)
&& string.Equals(_comparand14, other._comparand14, StringComparison.OrdinalIgnoreCase);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is StringWrapper && Equals((StringWrapper) obj);
}
public override int GetHashCode()
{
unchecked
{
return ((_comparand0 != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(_comparand0) : 0)*397)
^ (_comparand14 != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(_comparand14) : 0);
}
}
}

Is there a way to check the data in advance of runtime if it's not the correct type?

I occasionally get data that is not completely clean, and during runtime I get error messages because the data doesn't match the expected type. For example, sometimes the data has a string where there should be an int, or an int where there should be a date.
Is there a way to scan the data first for bad data, so that I can fix it all at once instead of finding out during run-time and fixing it iteratively?
Here's my code which works:
class TestScore{
public string Name;
public int Age;
public DateTime Date;
public DateTime Time;
public double Score;
}
//read data
var Data = File.ReadLines(FilePath).Select(line => line.Split('\t')).ToArray();
//select data
var query = from x in Data
select new { Name = x[3], Age = x[1], Date = x[2], Time = x[5], Score = x[7] };
//create List and put data into List
List<TestScore> Results = new List<TestScore>();
for (int i = 0; i < query.Count; i++)
{
TestScore TS = new TestScore();
TS.Name = query[i].Name;
TS.Age = query[i].Age;
TS.Date = query[i].Date;
TS.Time = query[i].Time;
TS.Score = query[i].Score;
Results.Add(TS);
}
Is there a way to scan the data first for bad data, so that I can fix
it all at once instead of finding out during run-time and fixing it
iteratively?
Scanning is a runtime operation. However, it's fairly straightforward to implement a solution that gives you enough information to "fix it all at once".
The following code shows a pattern for validating the file in its entirety, and doesn't attempt to load any data unless it completely succeeds.
If it fails, a collection of all errors encountered is returned.
internal sealed class ParseStatus
{
internal bool IsSuccess;
internal IReadOnlyList<string> Messages;
}
private ParseStatus Load()
{
string filePath = "foo";
var data = File.ReadLines( filePath ).Select( line => line.Split( '\t' ) ).ToArray();
var results = from x in data
select new { Name = x[3], Age = x[1], Date = x[2], Time = x[5], Score = x[7] };
var errors = new List<string>();
int row = 0;
// first pass: look for errors by testing each value
foreach( var line in results )
{
row++;
int dummy;
if( !int.TryParse( line.Age, out dummy ) )
{
errors.Add( "Age couldn't be parsed as an int on line " + row );
}
// etc...use exception-free checks on each property
}
if( errors.Count > 0 )
{
// quit, and return errors list
return new ParseStatus { IsSuccess = false, Messages = errors };
}
// otherwise, it is safe to load all rows
// TODO: second pass: load the data
return new ParseStatus { IsSuccess = true };
}
For not finding out the errors during run-time, the best thing that I can think of would be to correct the data manually before your program runs ..
But as we are trying do things constructive, I think that using a static readonly field to indicate the data error would be helpful. The following is a simple example which doesn't take the failed items, you might want to modify it when you are going to do some advanced handling.
public partial class TestScore {
public static TestScore Parse(String plainText) {
var strings=plainText.Split('\t');
var result=new TestScore();
if(
strings.Length<5
||
!double.TryParse(strings[4], out result.Score)
||
!DateTime.TryParse(strings[3], out result.Time)
||
!DateTime.TryParse(strings[2], out result.Date)
||
!int.TryParse(strings[1], out result.Age)
)
return TestScore.Error;
result.Name=strings[0];
return result;
}
public String Name;
public int Age;
public DateTime Date;
public DateTime Time;
public double Score;
public static readonly TestScore Error=new TestScore();
}
public static partial class TestClass {
public static void TestMethod() {
var path=#"some tab splitted file";
var lines=File.ReadAllLines(path);
var format=""
+"Name: {0}; Age: {1}; "
+"Date: {2:yyyy:MM:dd}; Time {3:hh:mm}; "
+"Score: {4}";
var list=(
from line in lines
where String.Empty!=line
let result=TestScore.Parse(line)
where TestScore.Error!=result
select result).ToList();
foreach(var item in list) {
Console.WriteLine(
format,
item.Name, item.Age, item.Date, item.Time, item.Score
);
}
}
}

How do i create the functions of List<float> in the options file?

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

Get Max() of alphanumeric value

I have a dictionary containg ID which are alphanumeric (e.g. a10a10 & d10a9) from which I want the biggest ID, meaning 9 < 10 < a ...
When I use the following code, d10a9 is MAX since 9 is sorted before 10
var lsd = new Dictionary<string, string>();
lsd.Add("a", "d10a10");
lsd.Add("b", "d10a9");
string max = lsd.Max(kvp => kvp.Value);
How can I get the Max value of the IDs with the Longest string combined?
I think you may try to roll your own IComparer<string>
class HumanSortComparer : IComparer<string>
{
public int Compare(string x, string y)
{
// your human sorting logic here
}
}
Usage:
var last = collection.OrderBy(x => x.Value, new HumanSortComparer()).LastOrDefault();
if (last != null)
string max = last.Value;
this works like a charm assuming IDs always start with "d10a":
int max = lsd.Max(kvp => Convert.ToInt32(kvp.Value.Substring(4)));
Console.Write(string.Format("d10a{0}", max));
One way would be to do this
string max =lsd.Where(kvp=>kvp.Value.Length==lsd.Max(k=>k.Value.Length)).Max(kvp => kvp.Value);
however I think that this method would evalute the max length for each item so you may be better to extract it to a variable first
int maxLength=lsd.Max(kvp=>kvp.Value.Length);
string max = lsd.Where(kvp=>kvp.Value.Length == maxLength).Max(kvp => kvp.Value);
If you are going to have null strings in there you may need to perform null checks too
int maxLength=lsd.Max(kvp=>(kvp.Value??String.Empty).Length);
string max = lsd.Where(kvp=>(kvp.Value??String.Empty).Length == maxLength).Max(kvp => kvp.Value);
Alternatively treat your string as Base36 number and convert to long for the max function and then convert back again to get the max string.
string max =lsd.Max(tvp=>tvp.Value.FromBase36()).ToBase36();
public static class Base36 {
public static long FromBase36(this string src) {
return src.ToLower().Select(x=>(int)x<58 ? x-48 : x-87).Aggregate(0L,(s,x)=>s*36+x);
}
public static string ToBase36(this long src) {
StringBuilder result=new StringBuilder();
while(src>0) {
var digit=(int)(src % 36);
digit=(digit<10) ? digit+48 :digit+87;
result.Insert(0,(char)digit);
src=src / 36;
}
return result.ToString();
}
}
Finally just just the Agregate extension method instead of Max as this lets you do all the comparison logic....
lsd.Agregate(string.Empty,(a,b)=> a.Length == b.Length ? (a>b ? a:b) : (a.Length>b.Length ? a:b));
This could doesn't have null checks but you easily add them in.
I think if you did this:
var max = lsd.OrderByDescending(x => x.Value)
.GroupBy(x => x.Value.Length)
.OrderByDescending(x => x.Key)
.SelectMany(x => x)
.FirstOrDefault();
It may give you what you want.
You need StringComparer.OrdinalIgnoreCase.
Without the need to use linq, the function that do that is quite simple.
Complexity is, of course, O(n).
public static KeyValuePair<string, string> FindMax(IEnumerable<KeyValuePair<string, string>> lsd)
{
var comparer = StringComparer.OrdinalIgnoreCase;
var best = default(KeyValuePair<string, string>);
bool isFirst = true;
foreach (KeyValuePair<string, string> kvp in lsd)
{
if (isFirst || comparer.Compare(kvp.Value, best.Value) > 0)
{
isFirst = false;
best = kvp;
}
}
return best;
}
Okay - I think you need to first turn each key into a series of strings and numbers - since you need the whole number to be able to determine the comparison. Then you implement an IComparer - I've tested this with your two input strings as well as with a few others and it appears to do what you want. The performance could possibly be improved - but I was brainstorming it!
Create this class:
public class ValueChain
{
public readonly IEnumerable<object> Values;
public int ValueCount = 0;
private static readonly Regex _rx =
new Regex("((?<alpha>[a-z]+)|(?<numeric>([0-9]+)))",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public ValueChain(string valueString)
{
Values = Parse(valueString);
}
private IEnumerable<object> Parse(string valueString)
{
var matches = _rx.Matches(valueString);
ValueCount = matches.Count;
foreach (var match in matches.Cast<Match>())
{
if (match.Groups["alpha"].Success)
yield return match.Groups["alpha"].Value;
else if (match.Groups["numeric"].Success)
yield return int.Parse(match.Groups["numeric"].Value);
}
}
}
Now this comparer:
public class ValueChainComparer : IComparer<ValueChain>
{
private IComparer<string> StringComparer;
public ValueChainComparer()
: this(global::System.StringComparer.OrdinalIgnoreCase)
{
}
public ValueChainComparer(IComparer<string> stringComparer)
{
StringComparer = stringComparer;
}
#region IComparer<ValueChain> Members
public int Compare(ValueChain x, ValueChain y)
{
//todo: null checks
int comparison = 0;
foreach (var pair in x.Values.Zip
(y.Values, (xVal, yVal) => new { XVal = xVal, YVal = yVal }))
{
//types match?
if (pair.XVal.GetType().Equals(pair.YVal.GetType()))
{
if (pair.XVal is string)
comparison = StringComparer.Compare(
(string)pair.XVal, (string)pair.YVal);
else if (pair.XVal is int) //unboxing here - could be changed
comparison = Comparer<int>.Default.Compare(
(int)pair.XVal, (int)pair.YVal);
if (comparison != 0)
return comparison;
}
else //according to your rules strings are always greater than numbers.
{
if (pair.XVal is string)
return 1;
else
return -1;
}
}
if (comparison == 0) //ah yes, but were they the same length?
{
//whichever one has the most values is greater
return x.ValueCount == y.ValueCount ?
0 : x.ValueCount < y.ValueCount ? -1 : 1;
}
return comparison;
}
#endregion
}
Now you can get the max using OrderByDescending on an IEnumerable<ValueChain> and FirstOrDefault:
[TestMethod]
public void TestMethod1()
{
List<ValueChain> values = new List<ValueChain>(new []
{
new ValueChain("d10a9"),
new ValueChain("d10a10")
});
ValueChain max =
values.OrderByDescending(v => v, new ValueChainComparer()).FirstOrDefault();
}
So you can use this to sort the string values in your dictionary:
var maxKvp = lsd.OrderByDescending(kvp => new ValueChain(kvp.Value),
new ValueChainComparer()).FirstOrDefault();

Categories