How to prevent a System.IndexOutOfRangeException in a LINQ WHERE? - c#

I'm facing this exception when I'm using String.Split with random strings.
List<string> linhas = new List<string>();
linhas.Add("123;abc");
linhas.Add("456;def");
linhas.Add("789;ghi");
linhas.Add("chocolate");
var novas = linhas.Where(l => l.ToString().Split(';')[1]=="def");

The last string "chocolate"doesn't contain a ";", so String.Split returns an array with a single string "chocolate". That's why you get the exception if you try to accesss the second.
You could use ElementAtOrDefault which returns null for strings instead:
var novas = linhas.Where(l => l.Split(';').ElementAtOrDefault(1) == "def");
A longer approach using an anonymous type:
var novas = linhas
.Select(l => new { Line = l, Split = l.Split(';') })
.Where(x => x.Split.Length >= 2 && x.Split[1] == "def")
.Select(x => x.Line);

I'm going to expand a little on Tim's answer and show a way to do a few extra things within your LINQ queries.
You can expand the logic within you Where clause to do some additional processes, which can make your code a bit more readable. This would be good for something small:
var novas = linhas.Where(l =>
{
var parts = l.Split(':');
return parts.Length > 1 ? parts[1] == "def" : false;
});
If you need multiple statements, you can wrap the body of your clause within curly braces, but then you need the return keyword.
Alternatively, if you have a lot of information that would make something inline like that unreadable, you can also use a separate method within your query.
public void FindTheStringImLookingFor()
{
var linhas = new List<string>();
linhas.Add("123;abc");
linhas.Add("456;def");
linhas.Add("789;ghi");
linhas.Add("chocolate");
var words = linhas.Where(GetTheStringIWant);
}
private bool GetTheStringIWant(string s)
{
var parts = s.Split(':');
// Do a lot of other operations that take a few lines.
return parts.Length > 1 ? parts[1] == "def" : false;
}

Related

Is there best practice to obtain elements/variables from collection based on different conditions

Or in general how to filter some elements from collection based on different and complex conditions in single pass
Let's say we have collection of elements
var cats = new List<Cat>{ new Cat("Fluffy"), new Cat("Meowista"), new Cat("Scratchy")};
And somewhere we use this collection
public CatFightResult MarchBoxing(List<Cat> cats, string redCatName, string blueCatName)
{
var redCat = cats.First(cat => cat.Name == redCatName);
var blueCat = cats.First(cat => cat.Name == blueCatName);
var redValue = redCat.FightValue();
var blueValue = blueCat.FightValue();
if (Cat.FightValuesEqualWithEpsilon(redValue, blueValue))
return new CatFightResult{IsDraw: true};
return new CatFightResult{Winner: redValue > blueValue ? redCat : blueCat};
}
Question: Is there a nice way to obtain multiple variables from collection based on some condition(s)?
The question probably requires some sort of uniqueness in collection, let's first assume there is some (i.e. HashSet/Dictionary)
AND preferably:
SINGLE pass/cycle on collection (the most important reason of question, as you can see there are 2 filter operations in above method)
oneliner or like that, with readability, and the shorter the better
generic way (IEnumerable<T> I think, or ICollection<T>)
typos error-prone and changes/additions safe (minimal use of actual conditions in code, preferably checked
null/exception check, because my intention that null is valid result for obtained variable
Would be also cool to have ability to provide custom conditions, which probably could be done via Func parameters, but I didn't tested yet.
There are my attempts, which I've posted in my repo https://github.com/phomm/TreeBalancer/blob/master/TreeTraverse/Program.cs
Here is the adaptation to example with Cats:
public CatFightResult MarchBoxing(List<Cat> cats, string redCatName, string blueCatName)
{
var redCat = null;
var blueCat = null;
//1 kinda oneliner, but hard to read and not errorprone
foreach (var c in cats) _ = c.Name == redCatName ? redCat = n : n.Name == blueCatName ? blueCat = n : null;
//2 very good try, because errorprone and easy to read (and find mistake in assignment), but not oneliner and not elegant (but fast) redundant fetching and not single pass at all, up to O(N*N) with FirstOrDefault
var filter = new [] { redCatName, blueCatName }.ToDictionary(x => x.Key, x => cats.FirstOrDefault(n => n.Name == x.Key));
redCat = filter[redCatName];
blueCat = filter[blueCatName];
//3 with readability and ckecks for mistakenly written searching keys (dictionary internal dupe key check) , but not oneliner and not actualy single pass
var dic = new Dictionary<int, Func<Cat, Cat>> { { redCatName, n => redCat = n }, { blueCatName, n => blueCat = n } };
cats.All(n => dic.TryGetValue(n.Name, out var func) ? func(n) is null : true);
//4 best approach, BUT not generic (ofc one can write simple generic IEnumerable<T> ForEach extension method, and it would be strong candidate to win)
cats.ForEach(n => _ = n.Name == redCatName ? redCat = n : n.Name == blueCatName ? blueCat = n : null);
//5 nice approach, but not single pass, enumerating collection twice
cats.Zip(cats, (n, s) => n.Name == redCatName ? redCat = n : n.Name == blueCatName ? blueCat = n : null);
//6 the one I prefer best, however it's arguable due to breaking functional approach of Linq, causing side effects
cats.All(n => (n.Name == redCatName ? redCat = n : n.Name == blueCatName ? blueCat = n : null) is null);
}
All the options with ternary op are not extensible easily and relatively error-prone, but are quite short and Linq-ish, they also rely (some trade-off with confusion) on not returning/using actual results of ternary (with discard "_" or "is null" as bool). I think the approach with Dictionary of Funcs is a good candidate to implement custom conditions, just bake-in them with variables.
Thank you, looking forward your solutions ! :)
I'm not sure if it's possible with Linq out of the box but if writing a custom extension once is an option for you, retrieving some values from a collection with arbitrary number of conditions may later be put in pretty concise manner.
For example, you may write something like
var (redCat, blueCat) = cats.FindFirsts(
x => x.Name == redCatName,
x => x.Name == blueCatName);
If you introduce the FindFirsts() extension as follows:
public static class FindExtensions
{
public static T[] FindFirsts<T>(this IEnumerable<T> collection,
params Func<T, bool>[] conditions)
{
if (conditions.Length == 0)
return new T[] { };
var unmatchedConditions = conditions.Length;
var lookupWork = conditions
.Select(c => (
value: default(T),
found: false,
cond: c
))
.ToArray();
foreach (var item in collection)
{
for (var i = 0; i < lookupWork.Length; i++)
{
if (!lookupWork[i].found && lookupWork[i].cond(item))
{
lookupWork[i].found = true;
lookupWork[i].value = item;
unmatchedConditions--;
}
}
if (unmatchedConditions <= 0)
break;
}
return lookupWork.Select(x => x.value).ToArray();
}
}
The full demo can be hound here: https://dotnetfiddle.net/QdVJUd
Note: In order to deconstruct the result array (i.e. use var (redCat, blueCat) = ...), you have to define a deconstruction extension. I borrowed some code from this thread to do so.

How to find the nearest string in a List in LINQ?

If I want to find the exact match or the next nearest for a string.
Using SQL, I can do :
SELECT TOP 1 *
FROM table
WHERE Code >= #searchcode
ORDER BY Code
How might I achieve this using LINQ and a List of the records.
I was expecting to be able to do something like:
var find = ListDS.Where(c => c.Code >= searchcode).First();
but you can't compare strings that way.
Note that Code is an alpha string, letters, numbers, symbols, whatever..
Nearest means if you have a list containing "England", "France", "Spain", and you search for "France" then you get "France". If you search for "Germany" you get "Spain".
Here is a simple code may help you
List<string> ls = new List<string>();
ls.Add("ddd");
ls.Add("adb");
var vv = from p in ls where p.StartsWith("a") select p;
select all element with starting string "a"
If Code is an int this might work:
var find = ListDS.Where(c => c.Code >= searchcode).OrderBy(c => c.Code).First();
otherwise you need to convert it to one:
int code = int.Parse(searchcode);
var find = ListDS.Where(c => Convert.ToInt32(c.Code) >= code).OrderBy(c => Convert.ToInt32(c.Code)).First();
Try this solution:
class Something
{
public string Code;
public Something(string code)
{
this.Code = code;
}
}
class Program
{
static void Main(string[] args)
{
List<Something> ListDS = new List<Something>();
ListDS.Add(new Something("test1"));
ListDS.Add(new Something("searchword1"));
ListDS.Add(new Something("test2"));
ListDS.Add(new Something("searchword2"));
string searchcode = "searchword";
var find = ListDS.First(x => x.Code.Contains(searchcode));
Console.WriteLine(find.Code);
Console.ReadKey();
}
}
I replaced your >= with .Contains. You can also add the action into First, no need for Where.
It will not find the "nearest", just the first word containg your search parameters.
You could compare string in C#, it will use alphabetically order:
var find = ListDS.Where(c => c.Code.CompareTo(searchcode) >= 0)
.OrderBy(c => c) // get closer one, need to order
.First();
See the CompareTo docs.
Note that with this method, "10" > "2".

Performing operations on a collection of string arrays

I am reading a file that contains rows like
pathName; additionalString; maybeSomeNumbers
I read it using
var lines = File.ReadAllLines(fileListFile);
var fileListEntries = from line in lines
where !line.StartsWith("#")
select line.Split(';').ToArray();
This works well so far. However I would like to change the drive letter in the pathName. I could convert fileListEntries to an array and loop across elements [i][0], but is there a way that I could do this operation on the collection directly?
Use the LINQ extension method syntax in order to be able to use code blocks { ... } in the lambda expressions. If you do so, you have to include an explicit return-statement.
var fileListEntries = lines
.Where(l => !l.StartsWith("#"))
.Select(l => {
string[] columns = l.Split(';');
if (Path.IsPathRooted(column[0])) {
string root = Path.GetPathRoot(columns[0]);
columns[0] = Path.Combine(#"X:\", columns[0].Substring(root.Length));
}
return columns;
})
.ToArray();
I think you can do it inline with the LINQ.
File.ReadAllLines() returns a string array, so you should be able to perform Replace() on the line from the collection.
var replace = "The string to replace the drive letter";
var lines = File.ReadAllLines(fileListFile);
var fileListEntries = from line in lines
where !line.StartsWith("#")
select (line.Replace(line[0], replace).Split(';')).ToArray();
You could just call a method in your select that modifies the text in the manner that you would like.
static void Main(string[] args)
{
var fileListEntries = from line in lines
where !(line.StartsWith("#"))
select ( ModifyString(line));
}
private static string[] ModifyString(string line)
{
string[] elements = line.Split(';');
elements[0] = "modifiedString";
return elements;
}
lines.Where(l => !l.StartsWith("#").
Select(l => string.Concat(driveLetter, l.Substring(1))).
Select(l => l.Split(';');

Linq Where condition to match array

I have a linq list obtained from database in my Model. Now I have a string array obtained from my controller. I want to construct a statement
pseudo-code
List<object> objlist = db.objects.tolist();
string[] strarray; // obtained from a long code.
var k = objlist.Where(u => u.somecol == strarray[0] || u.somecol == strarray[1]........strarray[n]).toList();
I am little bit confused how to accomplish this since my strarray[] is variable length and can contain upto 1000 words.
You can check if an array contains some item using the Array.IndexOf<T> Method:
bool strarrayContainsX = Array.IndexOf<string>(strarray, "X") >= 0;
However, I'd recommend you use a HashSet<string> instead of a string array for anything more than a few items. The HashSet<T> Class provides a Contains Method to check if a set contains some item:
HashSet<string> strset = new HashSet<string>(strarray);
bool strsetContainsX = strset.Contains("X");
The resulting query then looks like this:
var k = objlist.Where(u => strset.Contains(u.somecol)).ToList();
Use Contains:
var k = objlist.Where(u => strarray.Contains(u.somecol)).toList();
Try this:
List<object> objlist = db.objects.tolist();
string[] strarray; // obtained from a long code.
var k = objlist.Where(u => strarray.Contains(u.somecol)).toList();
var k = objlist.Where(u => strarray.Any(x=>x == u.somecol)).ToList();

Is there a cleaner way to split delimited text into data structures?

I have this code:
private IEnumerable<FindReplacePair> ConstructFindReplacePairs(string inputFilePath)
{
var arrays = from line in File.ReadAllLines(Path.GetFullPath(inputFilePath))
select line.Split('|');
var pairs = from array in arrays
select new FindReplacePair { Find = array[0], Replace = array[1] };
return pairs;
}
I'm wondering if there is a clean linq syntax to do this operation in only one query, because it feels like there should be.
I tried chaining the from clauses (a SelectMany), but it splits up the data too much and I could not get to the separate arrays to select from (instead I got individual strings one at a time).
IEnumerable<FindReplacePair> ConstructFindReplacePairs(string inputFilePath)
{
return File.ReadAllLines(Path.GetFullPath(inputFilePath))
.Select(line => line.Split('|'))
.Select(array => new FindReplacePair {
Find = array[0],
Replace = array[1]
});
}
OR
IEnumerable<FindReplacePair> ConstructFindReplacePairs(string inputFilePath)
{
return from line in File.ReadAllLines(Path.GetFullPath(inputFilePath))
let array = line.Split('|')
select new FindReplacePair {
Find = array[0], Replace = array[1]
};
}
You can also add where condition to check if array has more than one element.
Not sure if this is cleaner, just a little bit shorter.
IEnumerable<FindReplacePair> allFindReplacePairs = File.ReadLines(inputFilePath)
.Select(l => new FindReplacePair { Find = l.Split('|')[0], Replace = l.Split('|')[1] });
Note that i'm using File.ReadLines which does not need to read all lines into memory first. it works like a StreamReader.
When it comes down to prettifying LINQ, I usually write out simple loop and Resharper will suggest a better LINQ optimisation, e.g.
foreach (var split in File.ReadAllLines(inputFilePath).Select(l => l.Split('|')))
yield return new FindReplacePair { Find = split[0], Replace = split[1] };
R# convertes it to
return File.ReadAllLines(inputFilePath).Select(l => l.Split('|')).Select(split => new FindReplacePair { Find = split[0], Replace = split[1] });
That said you might as well use builtin type, e.g. .ToDictionary(l => l[0], l => l[1]) or add a method on FindReplacePair, i.e.
return File.ReadAllLines(inputFilePath).Select(l => l.Split('|')).Select(FindReplacePair.Create);
public static FindReplacePair Create(string[] split)
{
return new FindReplacePair { Find = split.First(), Replace = split.Last() };
}

Categories

Resources