I have a dictionary Dictionary<string, List<string>> I want order it alphabetically by the keys and convert it into a string that can be written into a CSV file with the keys as column headers and the values as values for that column.
My onordered dictionary looks like:
{
"Name" : ["John", "Ciara", "Moses"],
"Age" : ["23", "16", "37"],
"State" : ["Alabama", "Florida", "New York"]
}
The end result will look like:
Age,Name,State
23,John,Alabama
16,Ciara,Florida
37,Moses,New York
Please how I can achieve this in C#?
For clarity, here is a link to what the task entail.
And below is my approach of solving it. I converted the string into a dictionary with the column headings as keys. My problem now is converting the dictionary back to the string format.
public static string SortCsvColumns( string csv_data )
{
var data = csv_data.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
var values = data.Skip(1).ToArray();
var splittedValues = new List<List<string>>();
var dataSet = data[0].Split(new string[] {","}, StringSplitOptions.None).ToDictionary(x => x, x => new List<string>());
for(int i = 0; i < values.Length; i++)
{
splittedValues.Add(values[i].Split(new string[] { "," }, StringSplitOptions.None).ToList());
}
for(int i = 0; i < splittedValues.Count(); i++) {
var splittedValue = splittedValues[i];
for(int j = 0; j < splittedValue.Count(); j++) {
dataSet.Values.ElementAt(i).Add(splittedValue[j]);
}
}
dataSet = dataSet.OrderBy(key => key.Key);
}
Can someone suggest the best approach to do this please.
First, create a map to re-order the columns in sorted order by column header:
var map = new StringReader(csv_data).ReadLine() // get header line
.Split(';') // split into array of headers
.Select((h, n) => new { header = h, OrigPos = n }) // remember original position
.OrderBy(hn => hn.header, StringComparer.CurrentCultureIgnoreCase) // sort into new position
.Select(hn => hn.OrigPos) // return just old position new new order
.ToList();
Then remap each CSV line into the new order and recombine into a string:
using var sr = new StringReader(csv_data);
var ans = String.Join("\n",
sr.ReadLines()
.Select(line => line.Split(';'))
.Select(columns => String.Join(";", map.Select(pos => columns[pos]))));
This requires an extension method on TextReader to enumerate the lines of a TextReader:
public static class StringReaderExt {
public static IEnumerable<string> ReadLines(this TextReader sr) {
string line;
while ((line = sr.ReadLine()) != null)
yield return line;
}
}
Related
I'm currently making a program that tracks certain things (basic INT Values and the Date when they were saved).
My goal is to add up the INT values with the same Date.
20.11.2018 00:00:00; 1;1;1;1;1
20.11.2018 00:00:00; 1;1;1;1;1
22.11.2018 00:00:00; 1;1;1;1;1
Should basically look like this
20.11.2018 00:00:00; 2;2;2;2;2
22.11.2018 00:00:00; 1;1;1;1;1
The Saving Data and even the adding the 2 "Lines" together is working perfectly fine.
The problem is that When I add the Lines together, the 2 Lines with the 1 obviously don't get deleted.
This is the Method that Compares the Date and adds the lines together:
public static Dictionary<DateTime, int[]> CompareDateMethod(Dictionary<DateTime, int[]> oDateTimeAndIntDictionary,string[][] ReadData)
{
Dictionary<DateTime, int[]> oPrintRealData = new Dictionary<DateTime, int[]>();
Dictionary<DateTime, int[]> oAddRealData = new Dictionary<DateTime, int[]>();
for (int i = 0 ; i < ReadData.Length; i++)
{
DateTime dtDateValue;
if (DateTime.TryParse(ReadData[i][0], out dtDateValue))
{
int[] iValuesToAdd = ConvertArrayToInt(ReadData[i]);
if (dtDateValue.Date == DateTime.Now.Date)
{
for (int j = 0; j < iValuesToAdd.Length; j++)
{
oDateTimeAndIntDictionary[dtDateValue.Date][j] += iValuesToAdd[j];
}
}
else if (dtDateValue.Date != DateTime.Now.Date)
{
goto Endloop;
}
}
}
Endloop:
return oDateTimeAndIntDictionary;
This is the method that Writes the Data into the .CSV file
Dictionary<DateTime, int[]> oDateTimeAndIntDictionary = new Dictionary<DateTime, int[]>();
string[][] OldData= AddVariables.ReadOldData();
int[] iNewDataArray = new int[] { iVariable1, iVariable2, iVariable3, iVariable4, iVariable5};
oDateTimeAndIntDictionary.Add(DateTime.Now.Date, iNewDataArray);
using (System.IO.FileStream fileStream = new System.IO.FileStream(#"C: \Users\---\Csvsave\SaveDatei.csv", System.IO.FileMode.Append, System.IO.FileAccess.Write))
using (System.IO.StreamWriter streamWriter = new System.IO.StreamWriter(fileStream))
{
foreach (KeyValuePair<DateTime, int[]> kvp in AddVariables.CompareDateMethod(oDateTimeAndIntDictionary, OldData))
{
streamWriter.WriteLine("{0}; {1}", kvp.Key, string.Join(";", kvp.Value));
}
}
I tried so hard to come up with something but nothing worked (I tried deleting lines from the .csv which seems really hard, I tried reading the file in backwards which didnt work etc.)
If someone can give me some pointers I would really appreciate it.
I think the problem with the original code is that it's a bit confused about what happens when. I've restructured it so that things happen in a logical order (and updated it a bit, simplifying variable names, etc). There's one function for combining rows with the same date, which is separate from the CSV writing code (which hasn't changed)
static void Main(string[] args)
{
var oldData = ReadOldData();
// Do the work
var results = SumValuesForSameDate(oldData);
// Write the file
using (System.IO.FileStream fileStream = new System.IO.FileStream(#"C: \Users\---\Csvsave\SaveDatei.csv", System.IO.FileMode.Append, System.IO.FileAccess.Write))
using (System.IO.StreamWriter streamWriter = new System.IO.StreamWriter(fileStream))
{
foreach (KeyValuePair<DateTime, int[]> kvp in results)
{
streamWriter.WriteLine("{0}; {1}", kvp.Key, string.Join(";", kvp.Value));
}
}
}
public static Dictionary<DateTime, int[]> SumValuesForSameDate(string[][] readData)
{
var oDateTimeAndIntDictionary = new Dictionary<DateTime, int[]>();
var currentDate = DateTime.MinValue;
foreach (var row in readData)
{
DateTime dateValue;
if(!DateTime.TryParse(row[0], out dateValue)) continue;
dateValue = dateValue.Date;
var intValues = ConvertArrayToInt(row);
if (dateValue == currentDate)
{
for (var j = 0; j < intValues.Length; j++)
{
oDateTimeAndIntDictionary[dateValue][j] += intValues[j];
}
}
else
{
oDateTimeAndIntDictionary.Add(dateValue, intValues);
currentDate = dateValue;
}
}
return oDateTimeAndIntDictionary;
}
static int[] ConvertArrayToInt(string[] strings)
{
var output = new int[strings.Length - 1];
for (var i = 1; i < strings.Length; i++)
{
output[i - 1] = int.Parse(strings[i]);
}
return output;
}
static string[][] ReadOldData()
{
// Fake data
var data = new string[][]
{
new string[] { "20.11.2018 00:00:00", "1", "1", "1", "1", "1" },
new string[] { "20.11.2018 00:00:00", "1", "1", "1", "1", "1" },
new string[] { "22.11.2018 00:00:00", "1", "1", "1", "1", "1" },
};
return data;
}
}
For overwriting the previous CSV just use System.IO.FileMode.Create instead of Append. This will overwrite any previous data.
You need to overwrite the csv anyways to get rid of the written row.
So instead of returning oDateTimeAndIntDictionary from CompareDateMethod method, return ReadData and overwrite the parsed values of ReadData.
Something like this,
public static Dictionary<DateTime, int[]> CompareDateMethod(Dictionary<DateTime, int[]> oDateTimeAndIntDictionary,string[][] ReadData)
{
Dictionary<DateTime, int[]> oPrintRealData = new Dictionary<DateTime, int[]>();
Dictionary<DateTime, int[]> oAddRealData = new Dictionary<DateTime, int[]>();
for (int i = 0 ; i < ReadData.Length; i++)
{
DateTime dtDateValue;
if (DateTime.TryParse(oDateTimeAndIntDictionary[0][0], out dtDateValue))
{
int[] iValuesToAdd = ConvertArrayToInt(ReadData[i]);
if (dtDateValue.Date == DateTime.Now.Date)
{
for (int j = 0; j < iValuesToAdd.Length; j++)
{
//Add the ReadData values here and store at ReadData[i][j]
}
}
else if (dtDateValue.Date != DateTime.Now.Date)
{
goto Endloop;
}
}
}
Endloop:
return ReadData;
}
Hope this helps...
I readed your comment about not using linq and 3rd part lib too late.
But let me show you what you are missing.
Here it's a little Linq + CSVHelper
First Lest define your data, and define how to map them in the CSV
public sealed class data
{
public DateTime TimeStamp { get; set; }
public List<int> Numbers { get; set; }
}
public sealed class dataMapping : ClassMap<data>
{
public dataMapping()
{
Map(m => m.TimeStamp).Index(0);
Map(m => m.Numbers)
.ConvertUsing(
row =>
new List<int> {
row.GetField<int>(1),
row.GetField<int>(2),
row.GetField<int>(3)
}
);
}
}
And now this is a short demo:
class CsvExemple
{
string inputPath = "datas.csv";
string outputPath = "datasOut.csv";
List<data> datas;
public void Demo()
{
//no duplicate row in orginal input
InitialiseFile();
LoadExistingData();
//add some new row and some dupe
NewDatasArrived();
//save to an other Path, to Compare.
SaveDatas();
}
private void SaveDatas()
{
using (TextWriter writer = new StreamWriter(outputPath))
using (var csvWriter = new CsvWriter(writer))
{
csvWriter.Configuration.RegisterClassMap<dataMapping>();
csvWriter.Configuration.Delimiter = ";";
csvWriter.Configuration.HasHeaderRecord = false;
csvWriter.WriteRecords(datas);
}
}
static List<int> SuperZip(params List<int>[] sourceLists)
{
for (var i = 1; i < sourceLists.Length; i++)
{
sourceLists[0] = sourceLists[0].Zip(sourceLists[i], (a, b) => a + b).ToList();
}
return sourceLists[0];
}
private void NewDatasArrived()
{
var now = DateTime.Today;
// New rows
var outOfInitialDataRange = Enumerable.Range(11, 15)
.Select(x => new data { TimeStamp = now.AddDays(-x), Numbers = new List<int> { x, x, x } });
// Duplicate rows
var inOfInitialDataRange = Enumerable.Range(3, 7)
.Select(x => new data { TimeStamp = now.AddDays(-x), Numbers = new List<int> { x, x, x } });
//add all of them them together
datas.AddRange(outOfInitialDataRange);
datas.AddRange(inOfInitialDataRange);
// all this could have been one Line
var grouped = datas.GroupBy(x => x.TimeStamp);
var temp = grouped.Select(g => new { TimeStamp = g.Key, ManyNumbers = g.Select(x => x.Numbers).ToArray() });
// We can combine element of 2 list using Zip. ListA.Zip(ListB, (a, b) => a + b)
datas = temp.Select(x => new data { TimeStamp = x.TimeStamp, Numbers = SuperZip(x.ManyNumbers) }).ToList();
}
private void LoadExistingData()
{
if (File.Exists(inputPath))
{
using (TextReader reader = new StreamReader(inputPath))
using (var csvReader = new CsvReader(reader))
{
csvReader.Configuration.RegisterClassMap<dataMapping>();
csvReader.Configuration.HasHeaderRecord = false;
csvReader.Configuration.Delimiter = ";";
datas = csvReader.GetRecords<data>().ToList();
}
}
else
{
datas = new List<data>();
}
}
private void InitialiseFile()
{
if (File.Exists(inputPath))
{
return;
}
var now = DateTime.Today;
var ExistingData = Enumerable.Range(0, 10)
.Select(x => new data { TimeStamp = now.AddDays(-x), Numbers = new List<int> { x, x, x } });
using (TextWriter writer = new StreamWriter(inputPath))
using (var csvWriter = new CsvWriter(writer))
{
csvWriter.Configuration.RegisterClassMap<dataMapping>();
csvWriter.Configuration.Delimiter = ";";
csvWriter.Configuration.HasHeaderRecord = false;
csvWriter.WriteRecords(ExistingData);
}
}
}
I have my code like,
string firstLineOfRecord = "front images,Currency Code,Date,BackImages,Domination";
string[] fieldArrayRecord = firstLineOfRecord.Split(',');
string fields = "FrontImages,BackImages,Domination,CurrencyCode,SerialNumber";
string[] fieldArrayList = fields.Split(',');
List<int> mappedList = new List<int>();
for (int i = 0; i< fieldArrayList.Count(); i++)
{
for (int j = 0; j < fieldArrayRecord.Count(); j++)
{
if (fieldArrayList[i] == fieldArrayRecord[j])
{
mappedList.Add(j);
}
}
}
How can i map, the "front Images" with "FrontImages".
As iam the beginner i dont know how to solve this.Kindly tell me how to achieve this.
For such a fuzzy match, you first need to identify the valid identifiers to ignore (in this case a space).
You could do something like this: You strip out all those identifiers. Then compare case and culture insensitive.
string normalizedHeaderString = "FrontImages";
string normalizedInputString = "front images";
foreach (string c in new[] { " " }) /* the strings to strip out */
{
normalizedHeaderString = normalizedHeaderString.Replace(c, null);
normalizedInputString = normalizedInputString.Replace(c, null);
}
if (string.Equals( normalizedHeaderString
, normalizedInputString
, StringComparison.OrdinalIgnoreCase
)
)
{ /* do your logic, like saving the index, etc */ }
This is a little hacky, but you get the idea. You'd better use a custom implementation of a StringComparer that just ignores the characters to strip out.
As I understand from your question, your problems are spaces and case sensitivity
so you can use
fieldArrayList[i].Replace(" ","").ToLower() ==
fieldArrayRecord[j].Replace(" ","").ToLower()
class Program
{
static void Main(string[] args)
{
string firstLineOfRecord = "front images,Currency Code,Date,BackImages,Domination";
string[] fieldArrayRecord = firstLineOfRecord.Split(',');
string fields = "FrontImages,BackImages,Domination,CurrencyCode,SerialNumber";
string[] fieldArrayList = fields.Split(',');
List<int> mappedList = new List<int>();
for (int i = 0; i < fieldArrayRecord.Length; i++)
{
if (fieldArrayList.Any(s => string.Equals( fieldArrayRecord[i].Replace(" ", string.Empty), s, StringComparison.OrdinalIgnoreCase)))
{
mappedList.Add(i);
}
}
foreach (int index in mappedList)
{
Console.WriteLine(index);
}
}
}
Output:
0
1
3
4
Or using a dictionary:
class Program
{
static void Main(string[] args)
{
string firstLineOfRecord = "front images,Currency Code,Date,BackImages,Domination";
string fields = "FrontImages,BackImages,Domination,CurrencyCode,SerialNumber";
var dataFields = firstLineOfRecord.Split(',').Select((x, index) => new { FieldName = x.Replace(" ", string.Empty), Index = index });
var tableFields = fields.Split(',').Select((x, index) => new { FieldName = x, Index = index });
Dictionary<int, int> mapping = (from dataField in dataFields
let tableField = tableFields.SingleOrDefault(x => string.Equals(dataField.FieldName, x.FieldName, StringComparison.OrdinalIgnoreCase))
where tableField != null
select new { DF = dataField.Index, TF = tableField.Index })
.ToDictionary(c => c.DF, c => c.TF);
// Test:
string[] dataFieldsArray = firstLineOfRecord.Split(',');
string[] tableFieldsArray = fields.Split(',');
foreach (KeyValuePair<int,int> pair in mapping)
{
Console.WriteLine(
"TableField '{0}' Index {1} has to be mapped to DataField '{2}' Index {3}",
tableFieldsArray[pair.Value], pair.Value, dataFieldsArray[pair.Key],pair.Key);
}
}
}
Output:
TableField 'FrontImages' Index 0 has to be mapped to DataField 'front images' Index 0
TableField 'CurrencyCode' Index 3 has to be mapped to DataField 'Currency Code' Index 1
TableField 'BackImages' Index 1 has to be mapped to DataField 'BackImages' Index 3
TableField 'Domination' Index 2 has to be mapped to DataField 'Domination' Index 4
Here's a way to do it with a LINQ query:
string firstLineOfRecord = "front images,Currency Code,Date,BackImages,Domination";
string[] fieldArrayRecord = firstLineOfRecord.Split(',')
.Select(x => x.Replace(" ", string.Empty))
.ToArray();
// Test it, prints True.
fieldArrayRecord.Contains("FrontImages", StringComparer.OrdinalIgnoreCase)
Note this will replace any white space between those letters and will alter the given fieldArrayRecord.
I am using the below code to read data from a text file row by row. I would like to assign each row into an array. I must be able to find the number or rows/arrays and the number of elements on each one of them.
I would also like to do some manipulations on some or all rows and return their values.
I get the number of rows, but is there a way to to loop something like:
*for ( i=1 to number of rows)
do
mean[i]<-row[i]
done
return mean*
var data = System.IO.File.ReadAllText("Data.txt");
var arrays = new List<float[]>();
var lines = data.Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var lineArray = new List<float>();
foreach (var s in line.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries))
{
lineArray.Add(Convert.ToSingle(s));
}
arrays.Add(lineArray.ToArray());
}
var numberOfRows = lines.Count();
var numberOfValues = arrays.Sum(s => s.Length);
var arrays = new List<float[]>();
//....your filling the arrays
var averages = arrays.Select(floats => floats.Average()).ToArray(); //float[]
var counts = arrays.Select(floats => floats.Count()).ToArray(); //int[]
Not sure I understood the question. Do you mean something like
foreach (string line in File.ReadAllLines("fileName.txt")
{
...
}
Is it ok for you to use Linq? You might need to add using System.Linq; at the top.
float floatTester = 0;
List<float[]> result = File.ReadLines(#"Data.txt")
.Where(l => !string.IsNullOrWhiteSpace(l))
.Select(l => new {Line = l, Fields = l.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) })
.Select(x => x.Fields
.Where(f => Single.TryParse(f, out floatTester))
.Select(f => floatTester).ToArray())
.ToList();
// now get your totals
int numberOfLinesWithData = result.Count;
int numberOfAllFloats = result.Sum(fa => fa.Length);
Explanation:
File.ReadLines reads the lines of a file (not all at once but straming)
Where returns only elements for which the given predicate is true(f.e. the line must contain more than empty text)
new { creates an anonymous type with the given properties(f.e. the fields separated by comma)
Then i try to parse each field to float
All that can be parsed will be added to an float[] with ToArray()
All together will be added to a List<float[]> with ToList()
Found an efficient way to do this. Thanks for your input everybody!
private void ReadFile()
{
var lines = File.ReadLines("Data.csv");
var numbers = new List<List<double>>();
var separators = new[] { ',', ' ' };
/*System.Threading.Tasks.*/
Parallel.ForEach(lines, line =>
{
var list = new List<double>();
foreach (var s in line.Split(separators, StringSplitOptions.RemoveEmptyEntries))
{
double i;
if (double.TryParse(s, out i))
{
list.Add(i);
}
}
lock (numbers)
{
numbers.Add(list);
}
});
var rowTotal = new double[numbers.Count];
var rowMean = new double[numbers.Count];
var totalInRow = new int[numbers.Count()];
for (var row = 0; row < numbers.Count; row++)
{
var values = numbers[row].ToArray();
rowTotal[row] = values.Sum();
rowMean[row] = rowTotal[row] / values.Length;
totalInRow[row] += values.Length;
}
First of all sorry for my mistakes in English its not my primary language
i have a problem , i have a array like following
string[] arr1 = new string[] {
"Pakistan:4,India:3,USA:2,Iran:1,UK:0",
"Pakistan:4,India:3,USA:2,Iran:1,UK:0",
"India:4,USA:3,Iran:2,UK:1,Pakistan:0"
};
now i just want to know that how many times Pakistan comes with 1 ,
how many times with 2 , 3 , 4
and i need to know this about all India , USA , Iran , UK
Thanks in advance , you guys are my last hope .
This linq will convert the array into a Dictionary>, where the outer dictionary contains the countries names, and inner dictionaries will contain the ocurrence number (the number after ':') and the count for each ocurrence.
string[] arr1 = new string[]
{
"Pakistan:4,India:3,USA:2,Iran:1,UK:0",
"Pakistan:4,India:3,USA:2,Iran:1,UK:0",
"India:4,USA:3,Iran:2,UK:1,Pakistan:0"
};
var count = arr1
.SelectMany(s => s.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
.GroupBy(s => s.Split(':')[0], s => s.Split(':')[1])
.ToDictionary(g => g.Key,
g =>
{
var items = g.Distinct();
var result = new Dictionary<String, int>();
foreach (var item in items)
result[item] = g.Count(gitem => gitem == item);
return result;
});
// print the result
foreach(var country in count.Keys)
{
foreach(var ocurrence in count[country].Keys)
{
Console.WriteLine("{0} : {1} = {2}", country, ocurrence, count[country][ocurrence]);
}
}
I would use the String.Split(char[]) method and the String.SubString(int, int) method to inspect every 'country' inside your array and to get the number postfix of each country.
Try the following:
(The following code is now compiled and tested.)
Use a simple data structure to facilitate the task of holding the result of your operation.
public struct Result {
string Country { get; set; }
int Number { get; set; }
int Occurrences { get; set; }
}
// define what countries you are dealing with
string[] countries = new string[] { "Pakistan", "India", "USA", "Iran", "UK", }
Method to provide the overall result:
public static Result[] IterateOverAllCountries () {
// range of numbers forming the postfix of your country strings
int numbersToLookFor = 4;
// provide an array that stores all the local results
// numbersToLookFor + 1 to respect that numbers are starting with 0
Result[] result = new Result[countries.Length * (numbersToLookFor + 1)];
string currentCountry;
int c = 0;
// iterate over all countries
for (int i = 0; i < countries.Length; i++) {
currentCountry = countries[i];
int j = 0;
// do that for every number beginning with 0
// (according to your question)
int localResult;
while (j <= numbersToLookFor) {
localResult = FindCountryPosition(currentCountry, j);
// add another result to the array of all results
result[c] = new Result() { Country = currentCountry, Number = j, Occurrences = localResult };
j++;
c++;
}
}
return result;
}
Method to provide a local result:
// iterate over the whole array and search the
// occurrences of one particular country with one postfix number
public static int FindCountryPosition (string country, int number) {
int result = 0;
string[] subArray;
for (int i = 0; i < arr1.Length; i++) {
subArray = arr1[i].Split(',');
string current;
for (int j = 0; j < subArray.Length; j++) {
current = subArray[j];
if (
current.Equals(country + ":" + number) &&
current.Substring(current.Length - 1, 1).Equals(number + "")
)
result++;
}
}
return result;
}
The following should enable you to run the algorithm
// define what countries you are dealing with
static string[] countries = new string[] { "Pakistan", "India", "USA", "Iran", "UK", };
static string[] arr1 = new string[] {
"Pakistan:4,India:3,USA:2,Iran:1,UK:0",
"Pakistan:4,India:3,USA:2,Iran:1,UK:0",
"India:4,USA:3,Iran:2,UK:1,Pakistan:0"
};
static void Main (string[] args) {
Result[] r = IterateOverAllCountries();
}
The data structure you are using is not rich enough to provide you with that information. Hence you need to parse your string and create a new data structure to be able to provide (sring[][]):
string[] arr1 = new string[] {
"Pakistan,India,USA,Iran,UK",
"Pakistan,India,USA,Iran,UK",
"India,USA,Iran,UK,Pakistan"
};
string[][] richerArray = arr1.Select(x=> x.Split('\'')).ToArray();
var countPakistanIsFirst = richerArray.Select(x=>x[0] == "Pakistan").Count();
UPDATE
You seem to have changed your question. The answer applies to the original question.
I'm looking to combine the contents of two string arrays, into a new list that has the contents of both joined together.
string[] days = { "Mon", "Tue", "Wed" };
string[] months = { "Jan", "Feb", "Mar" };
// I want the output to be a list with the contents
// "Mon Jan", "Mon Feb", "Mon Mar", "Tue Jan", "Tue Feb" etc...
How can I do it ? For when it's only two arrays, the following works and is easy enough:
List<string> CombineWords(string[] wordsOne, string[] wordsTwo)
{
var combinedWords = new List<string>();
foreach (var wordOne in wordsOne)
{
foreach (string wordTwo in wordsTwo)
{
combinedWords.Add(wordOne + " " + wordTwo);
}
}
return combinedWords;
}
But I'd like to be able to pass varying numbers of arrays in (i.e. to have a method with the signature below) and have it still work.
List<string> CombineWords(params string[][] arraysOfWords)
{
// what needs to go here ?
}
Or some other solution would be great. If it's possible to do this simply with Linq, even better!
What you want to do is actually a cartesian product of all the arrays of words, then join the words with spaces. Eric Lippert has a simple implementation of a Linq cartesian product here. You can use it to implement CombineWords:
List<string> CombineWords(params string[][] arraysOfWords)
{
return CartesianProduct(arraysOfWords)
.Select(x => string.Join(" ", x))
.ToList();
}
To cross join on any amount of arrays of strings:
// Define other methods and classes here
List<string> CombineWords(params string[][] arraysOfWords)
{
if (arraysOfWords.Length == 0)
return new List<string>();
IEnumerable<string> result = arraysOfWords[0];
foreach( string[] words in arraysOfWords.Skip(1) )
{
var tempWords = words;
result = from r in result
from w in tempWords
select string.Concat(r, " ", w);
}
return result.ToList();
}
Code below works for any number of arrays (and uses linq to some degree):
List<string> CombineWords(params string[][] wordsToCombine)
{
if (wordsToCombine.Length == 0)
return new List<string>();
IEnumerable<string> combinedWords = wordsToCombine[0].ToList();
for (int i = 1; i < wordsToCombine.Length; ++i)
{
var temp = i;
combinedWords = (from x in combinedWords from y in wordsToCombine[temp]
select x + " " + y);
}
return combinedWords.ToList();
}
public static List<string> CombineWords(params string[][] arraysOfWords)
{
var strings = new List<string>();
if (arraysOfWords.Length == 0)
{
return strings;
}
Action<string, int> combineWordsInternal = null;
combineWordsInternal = (baseString, index) =>
{
foreach (var str in arraysOfWords[index])
{
string str2 = baseString + " " + str;
if (index + 1 < arraysOfWords.Length)
{
combineWordsInternal(str2, index + 1);
}
else
{
strings.Add(str2);
}
}
};
combineWordsInternal(string.Empty, 0);
return strings;
}
Second try... I'm not able to do it in LINQ... A little too much complex to linquize correctly :-)
I'm using a local anonymous function (and showing that it's quite complex to recurse on anonymous functions, because you have to declare them separately)
This is a non-recursive solution which buffers strings as it progresses, to reduce the number of concatenations. Therefore it should also be usable for more arrays.
It also preserves your desired order - the items in the first array will always be at the beginning of the resulting string.
var s1 = new string[] { "A", "B", "C" };
var s2 = new string[] { "1", "2", "3", "4" };
var s3 = new string[] { "-", "!", "?" };
var res = Combine(s1, s2, s3);
And the function in question:
private List<string> Combine(params string[][] arrays)
{
if (arrays.Length == 1)
{
// The trivial case - exit.
return new List<string>(arrays[0]);
}
IEnumerable<string> last = arrays[arrays.Length - 1];
// Build from the last array, progress forward
for (int i = arrays.Length - 2; i >= 0; i--)
{
var buffer = new List<string>();
var current = arrays[i];
foreach (var head in current)
{
foreach (var tail in last)
{
// Concatenate with the desired space.
buffer.Add(head + " " + tail);
}
}
last = buffer;
}
return (List<string>)last;
}
Could you try this method ?
static List<string> CombineWords(string[] wordsOne, string[] wordsTwo)
{
var combinedWords = new List<string>();
for(int x = 0; (x <= wordsOne.Length - 1); ++x)
{
for(int y = 0; (x <= wordsTwo.Length - 1); ++y)
{
combinedWords.Add(string.Format("{0} {1}", wordsOne[x], wordsTwo[y]));
}
}
return combinedWords;
}
Kris