Is it possible to use LINQ to detect duplicate adjacent characters? - c#

I want to verify that a string does not contain any duplicate characters (from a set of bad characters) in adjacent positions. Previous stack overflow answers on this subject seem to mostly be of the general form:
for(int i = 0; i < testString.Length-1; i++){
if(testString[i] == testString[i+1] && testString[i] == badChar){
//Handle rejection here
}
}
Is it possible to do this kind of verification/validation in LINQ? More generically: is it possible within LINQ to compare the value of each character in a string to the next character in a
testString.Any(c => /*test goes here*/) call?

Anytime you have a class that has Count (or equivalent) property and indexer, you can use Enumerable.Range as base for the LINQ query and perform inside an indexed access similar to the non LINQ code:
bool test = Enumerable.Range(0, testString.Length - 1).Any(i = >
testString[i] == testString[i + 1] && testString[i] == badChar)

You could use Pairwise from moreLINQ library:
if(testString.Pairwise((n, m) => new {n, m}).Any(x => x.n == x.m && x.n == badChar))
// do something
If you want to use pure LINQ you could hack it with Skip/Zip combination:
if(testString.Zip(testString.Skip(1), (n, m) => new {n, m})).Any(x => x.n == x.m && x.n == badChar))
// do something
But both these solutions will be much slower then for loop-based solution, so I'd advice against doing that.

How about the egregious misuse of the aggregate function? I like to think this answer is more of an example of what not to do, even if it is possible. A while and string.indexOf are probably the most appropriate to this problem.
var items = "ab^cdeef##gg";
var badChars = new[] {'^', '#', '~'};
var doesAdjacentDupeExist = false;
var meaninglessAggregate = items.Aggregate((last, current) =>
{
if (last == current && badChars.Contains(last))
{
doesAdjacentDupeExist = true;
};
return current;
});
This is not as clever, but it does work. It trades the setting of an outside variable inside the query (bad), for relying on index and elementAt (not great).
var items = "abcdefffghhijjk";
var badChars = new[] { 'f', 'h' };
var indexCieling = items.Count() - 1;
var badCharIndexes = items.Select((item, index) =>
{
if (index >= indexCieling)
{
return null as int?;
}
else
{
if (item == items.ElementAt(index + 1) && badChars.Contains(item))
{
return index as int?;
}
else
{
return null as int?;
}
}
});
var doesAdjacentDupeExist = badCharIndexes.Any(x => x.HasValue);

Related

Is it possible to know what actions have been called on an IEnumerable?

I have several conditions that might affect what filters (.Where(...)) are used on a list. And at some point an exception is thrown, and I would like to know what actions have been called upon the list up until this point.
Is something like this possible?
var myList = new List<SomeClass>();
myList = myList.Where(item => item.property == value);
.
.
.
myList = myList.Where(item => item.otherProperty < otherValue);
Console.WriteLine(myList.ToActionsString());
It might print something like this:
list.Where(i => i.property == <the actual value>)
.Where(i => i.otherProperty < <the actual otherValue>)
Just calling toString() on the list does not exactly give any relevant information, and just listing the items in the list is not of interest.
Warning: there is overhead to this as the compiler has to create all of the Expression objects (which then have to be allocated and compiled at runtime). Use this sparingly.
You can do this using AsQueryable and relying on the ToString logic of the built in EnumerableQuery and Expression classes. The following extension method will convert your query to it's textual representation:
public static string GetText<T>(this IQueryable<T> query) {
retury query.Expression.ToString();
}
It can be used like:
var list = new List<int>();
var query = list.AsQueryable()
.Select((c, i) => c * (i + 1))
.Where(c => c > 5)
.Where(c => c < 10 && c != 7)
.Take(2)
.OrderBy(x => 1);
var text = query.GetText();
This results in the following:
System.Collections.Generic.List`1[System.Int32].Select((c, i) => (c * (i + 1))).Where(c => (c > 5)).Where(c => ((c < 10) AndAlso (c != 7))).Take(2).OrderBy(x => 1)
We can throw an anonymous type in the mix just to see how it looks:
var query = list.AsQueryable()
.Select((c, i) => c * (i + 1))
.Select(x => new { Value = x, ValueSquared = x * x });
var result = query.GetText();
Which will print:
System.Collections.Generic.List`1[System.Int32].Select((c, i) => (c * (i + 1))).Select(x => new <>f__AnonymousType0`2(Value = x, ValueSquared = (x * x)))
Through the use of Expression manipulation, we can make this method a little bit more robust. We can add in line breaks between the method calls and optionally strip off the name of the list's type.
public static string GetText<T>(this IQueryable<T> query, bool lineBreaks, bool noClassName)
{
var text = query.Expression.ToString();
if (!lineBreaks && !noClassName)
return text;
var expression = StripQuotes(query.Expression);
if (!(expression is MethodCallExpression mce))
return text;
if (lineBreaks)
{
var strings = new Stack<string>();
strings.Push(mce.ToString());
while (mce.Arguments.Count > 0 && mce.Arguments[0] is MethodCallExpression me)
{
strings.Push(me.ToString());
mce = me;
}
var sb = new StringBuilder(strings.Pop());
var len = sb.Length;
while (strings.TryPop(out var item))
{
sb.AppendLine().Append(item.Substring(len));
len = item.Length;
}
text = sb.ToString();
}
if (mce.Arguments.Count > 0 && mce.Arguments[0] is ConstantExpression ce)
{
var root = ce.Value.ToString();
if (root != null && text.StartsWith(root))
{
text = noClassName
? text.Substring(root.Length + 1)
: text.Insert(root.Length, Environment.NewLine);
}
}
return text;
}
// helper in case we get an actual Queryable in there
private static Expression StripQuotes(Expression e)
{
while (e.NodeType == ExpressionType.Quote)
e = ((UnaryExpression)e).Operand;
return e;
}
We can call this method as follows:
var list = new List<int>();
var query = list.AsQueryable()
.Select((c, i) => c * (i + 1))
.Where(c => c > 5)
.Where(c => c < 10 && c != 7)
.Take(2)
.OrderBy(x => 1);
var text = query.GetText(true, true);
Which will produce the following:
Select((c, i) => (c * (i + 1)))
.Where(c => (c > 5))
.Where(c => ((c < 10) AndAlso (c != 7)))
.Take(2)
.OrderBy(x => 1)
Note that this is very basic. It's not going to cover the case of closures (passing variables in) you'll get the <>DisplayClass objects written into your query. We can resolve that with an ExpressionVisitor that walks the expression and evaluates the ConstantExpressions representing the closures.
(Unfortunately I do not have time at the moment to provide that ExpressionVisitor solution, but stay tuned for an update)
No, it is not possible, at least not in a direct way as described in your question.
The List is not filtered step by step (i.e., apply Where(expr1) for all elements, then Where(expr2) for all remaining elements, ...), but in a deferred way:
If you request the first item of the resulting IEnumerable, LINQ evaluates the expr1 clause for each item until one item matches.
Then it checks whether this list item also matches expr2.
If it does, return it (or pass to further Where stages).
If it doesn't match, go back to step 1 and continue finding an item which matches expr1.
So logging by simply calling some ToActionsString() is difficult here. As already noted in the comments, it is probably much easier to just log when adding the Where-clauses, since you are in a known state then anyway:
if(condition1)
{
myList = myList.Where(item => item.Property == value);
Log($"Adding expression 1 with value '{value}'");
}
If your concern is that value could change before the IEnumerable is actually evaluated (captured variable), and you cannot restructure your control flow adequately, a workaround may be to create Func<T> objects which output the captured variables, and to evaluate those immediately before iterating the list:
List<Func<int>> values = new List<Func<int>>();
if(condition1)
{
myList = myList.Where(item => item.Property == value);
values.Add(() => value);
}
...
foreach(var v in values)
Log($"List will be filtered by {v()}");
var filteredList = myList.ToList();
Finally, you could call some logging function in your expressions, which logs the conditions and/or catches exceptions when evaluating those conditions:
myList.Where(item =>
{
Log(item, value);
return item.Property == value;
});

How to take last records from list

I have list as follows
static List<MessageDetail> CurrentMessage = new List<MessageDetail>();
Dynamically, values assigned to this list for example:
CurrentMessage.Add(new MessageDetail { UserName = 123,GroupName = somegrp, Message = somemsg });
Here, I want to take last 5 or so records.
// this returns first 5 result, dont want to user orderby clause either
CurrentMessagesForGroup = CurrentMessage
.Where(c => c.GroupName == groupName)
.Take(5).ToList();
Is there a way to implement TakeLast() attribute? Or any kind of help will be appreciated. Thanks for your time.
Use skip:
CurrentMessagesForGroup = CurrentMessage
.Where(c => c.GroupName == groupName).Skip(Math.Max(0, CurrentMessage.Count() - 5)).ToList();
EDIT: I also find this that I think it is more easier to use (MoreLinq):
using MoreLinq;
var CurrentMessagesForGroup2 = CurrentMessage.TakeLast(5);
Use an OrderBy (ASC or DESC) to get the records lined up correctly for your Take operation.
Ascending:
CurrentMessagesForGroup = CurrentMessage
.Where(c => c.GroupName == groupName)
.OrderBy(c => c.GroupName)
.Take(5)
.ToList();
or Descending:
CurrentMessagesForGroup = CurrentMessage
.Where(c => c.GroupName == groupName)
.OrderByDescending(c => c.GroupName)
.Take(5)
.ToList();
If anyone using DotNet Core 2 or above or DotNet Standard 2.1 or above then you can use Linq's built in .TakeLast()
Reference: Microsoft Documentation here
You could use Reverse(), which is slightly perverse.
CurrentMessagesForGroup = CurrentMessage
.Where(c => c.GroupName == groupName)
.Reverse()
.Take(5).ToList();
I use an extension method for this.
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int numElements)
{
return source.Skip(Math.Max(0, source.Count() - numElements));
}
And to use it:
CurrentMessagesForGroup = CurrentMessage.Where(c => c.GroupName == groupName).TakeLast(5).ToList();
Edit: Credit to Using Linq to get the last N elements of a collection?
I like this implementation, it uses a circular buffer.
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> input, int n)
{
if (n == 0)
yield break;
int tail = 0;
int head = 0;
int count = 0;
T[] buffer = new T[n];
foreach (T item in input)
{
buffer[tail] = item;
tail = (tail + 1) % n;
if (count < n)
count++;
else
head = (head + 1) % n;
}
for (int i = 0; i < count; i++)
yield return buffer[(head + i) % n];
}
If the MessageDetails class has numeric Id or Created date time we can use
var lastRecords= CurrentMessage.OrderByDescending(i=>i.Id).Where(p=>p.GroupName==groupName).Take(5).ToList();

How to OR a foreach looped linq lambda expression

I am trying to find in my db where an item may or may not contain particular words in its title by running a loop on an array of words. I know how to loop absolute contains on the query but i don't want to do that. here is my code:
C#
var item = "the-batman-returns-hd-version";
var id = 0;
//split slug by dash into an array
var keywords = item.Split('-'); //gives me (the, batman, returns, hd, version)
//remove any work less than 4 chracters
var result = _contentService.Products;
foreach (var word in keywords )
{
if (word.Length <= 4) continue;
var key = word;
result = result.Where(c => c.Title.Contains(key));
}
var firstOrDefault = result.FirstOrDefault();
if (firstOrDefault != null)
{
id = firstOrDefault.Id;
}
//loop query to search where all parts exist
return (id);
The above works. But the product is called "Batman Returns" so it returns 0 because it couldn't find items that contained "hd" and "version" in anything.
is there anyway to OR the looped linq. Such as:
foreach (var word in keywords )
{
if (word.Length <= 4) continue;
var key = word;
result = result.Where(c => c.Title.Contains(key) ||); //or; then loop again.
}
You can use the .Any() method:
result = result.Where(c => keywords.Any(k => c.Title.Contains(k)));
You can further filter out words less than 5 characters like it seems you might want:
result = result.Where(c => keywords.Where(k => k.Length > 4).Any(k => c.Title.Contains(k)));
Although it would be more efficient to do it once when constructing your keywords array:
var keywords = item.Split('-').Where(k => k.Length > 4).ToArray();

Find indexes in bool array changed from false to true?

I have two arrays:
bool[] oldValues = GetCurrentValuesFromSomewhere ();
ChangeCurrentValues ();
bool[] newValues = GetCurrentValuesFromSomewhere ();
List<int> whichIndexsHasBeenChangedFromFalseToTrue = /* linq */
Any idea? Instead of list, it can be bool[] array too.
You could do use something like this:
var changedValues =
(from i in Enumerable.Range(0, oldValues.Length)
where !oldValues[i] && newValues[i]
select i)
.ToList();
Or if you prefer fluent syntax:
var changedValues = Enumerable
.Range(0, oldValues.Length)
.Where(i => !oldValues[i] && newValues[i])
.ToList();
If you wanted a bool[] result, you can use this:
var changedValues =
(from i in Enumerable.Range(0, oldValues.Length)
select !oldValues[i] && newValues[i])
.ToArray();
Or in fluent syntax:
var changedValues = Enumerable
.Range(0, oldValues.Length)
.Select(i => !oldValues[i] && newValues[i])
.ToArray();
I would prefer using the lambda that gives you the index, so you do not have to generate the range:
var changed = newValues.
Select((value, index) => oldValues[index] == value ? -1 : index).
Where(i => i >= 0);
This should return a list of the indexes that have changed; .Count() will give you how many values have changed.
UPDATE: An alternative version
var changed = newValues.
Select((value, index) =>
value ? (oldValues[index] ? 0 : index + 1) : (oldValues[index] ? - (index + 1) : 0)).
Where(i => i != 0);
Will give you as index+1 those values that were false and are now true, and as -(index + 1) those values that were true and now are false. I am learning LINQ myself so I like to play with it quite a bit.
If there are always the same number of new and old, and you're just doing a diff, which is what you seem to be doing, I'd do something like this:
int index;
whichIndexsHasBeenChangedFromFalseToTrue = oldValues.Zip(newValues, (old, new) =>
{
int result = -1;
if(old != new) result = index;
index++;
return result;
}).Where(x => x != -1);
This is only for changed, but if you specifically want false to true, that's just a change to the if.
EDIT: Fixed a serious issue.

Getting the key value paired differences in two data rows?

Alright, so I need to get the key value paired differences of two data rows. In short, I'm sending an email to let a user know they've made specific changes to their profile. I already know the rows are different because I'm using the SequenceEqual to determine that.
At the moment I've written and debugged the following code:
if (currentRow.ItemArray.SequenceEqual(updatedRow)) { return; }
var updates = currentRow.ItemArray
.Where((o, i) =>
{
if (o == null && updatedRow[i] == null) { return false; }
else if (o == null && updatedRow[i] != null) { return true; }
else if (o.Equals(updatedRow[i])) { return false; }
return true;
})
.Select((o, i) =>
{
return new AppServices.NotificationData
{
Key = updatedRow.Table.Columns[i].ColumnName,
Value = Convert.ToString(updatedRow[i])
};
}).ToList();
But there are two problems with this code:
It seems really inefficient to me because it's going through each value in the ItemArray and then building a key value pair if the values differ.
It doesn't actually work because the i sent into the Select isn't correct (e.g. if the second column changed, 1, the index sent into the Select is actually 0. Honestly, that makes sense, but I'm not sure exactly how to get what I want here.
CONSTRAINT: I'd like to use LINQ here.
NOTE: I'm only comparing two rows (i.e. it's not going to be going through a list of rows).
What is the appropriate LINQ statement for what I'm trying to do here?
UPDATE: It really feels like I just need to use:
currentRow.ItemArray.Intersect(updatedRow.ItemArray)
but the problem with that is I don't have any idea what field that is so I can't build a key value pair. In other words, I get back only the differences, but I've no clue what the index is so I can't go get a column name based off of those values.
Honestly you're not going to lose much code clarity by using a for loop.
public IEnumerable<AppServices.NotificationData> GetUpdates(DataRow currentRow, DataRow updatedRow)
{
if (currentRow.ItemArray.SequenceEqual(updatedRow)) yield break;
var length = currentRow.ItemArray.Length;
for(var i = 0; i < length; i++)
{
var currentCol = currentRow[i];
var updatedCol = updatedRow[i];
if (currentCol == null && updatedCol == null) continue;
else if (currentCol == null && updatedCol != null) continue;
else if (currentCol.Equals(updatedCol)) continue;
yield return new AppServices.NotificationData
{
Key = updatedRow.Table.Columns[i].ColumnName,
Value = Convert.ToString(updatedCol)
};
}
}
var updates = currentRow.ItemArray
.Select((o, i) => new { Row = o, Index = i })
.Where(r => (r.Row == null && updatedRow[r.Index] != null)
|| (r.Row != null && updatedRow[r.Index] != null
&& !r.Row.Equals(updatedRow[r.Index])))
.Select(r => new
{
Key = updatedRow.Table.Columns[r.Index].ColumnName,
Value = Convert.ToString(updatedRow[r.Index])
}).ToList();
In general, I consider using array index values in LINQ to be a "code smell", and this is a good example of why: the Where clause, in generating a new sequence of values, destroys the illusion that the Select clause is working on the same collection as before.
A quick hack to get around this right now (though I don't think it is quite yet the right solution), would be to swap your Where and Select clauses, essentially:
if (currentRow.ItemArray.SequenceEqual(updatedRow)) { return; }
var updates = currentRow.ItemArray
.Select((o, i) =>
{
if (o == null && updatedRow[i] == null || o.Equals(updatedRow[i])) { return null; }
else return new AppServices.NotificationData
{
Key = updatedRow.Table.Columns[i].ColumnName,
Value = Convert.ToString(updatedRow[i])
};
}).Where(o => o != null).ToList();

Categories

Resources