Related
the information that the user enters can be sorted in many ways ascending and descending and i'm trying to make the user choose how he want to see the data:
so is there any way to set a variable that the user enter and sort the data without repeating the code multiple time depending on the input like:
var empName = el.Select(i => new { i.ID, i.FullName });
if(emsort.Text="text1")
empName.OrderBy(i.text1);
else if...
by doing something shorter like:
string sort=emsort.Text ;
empName.OrderBy(sort);
If I understood your problem right, that you want to sort dynamically with the property name which is stored in emsort.Text, then you can use Expressions:
assuming empName is IEnumerable<Employee>, then use this:
private static Func<Employee, dynamic> GetSortable(string sortablePoperty)
{
var param = Expression.Parameter(typeof(Employee), "e");
var member = Expression.Property(param, sortablePoperty);
Expression memberAsObject = Expression.Convert(member, typeof(object));
return Expression.Lambda<Func<Employee, dynamic>>(memberAsObject, param).Compile();
}
then use it:
string sort=emsort.Text ;
empName.OrderBy(GetSortable(sort));
if empName is IQueryable<Employee>, then use this:
private static Expression<Func<Employee, dynamic>> GetSortable(string sortablePoperty)
{
var param = Expression.Parameter(typeof(Employee), "e");
var member = Expression.Property(param, sortablePoperty);
Expression memberAsObject = Expression.Convert(member, typeof(object));
return Expression.Lambda<Func<Employee, dynamic>>(memberAsObject, param);
}
If the User in your question is some software that calls your function, then the user is aware of the type of the objects in the el sequence: he knows which properties the elements in the el sequence are, and he knows how he wants them ordered.
Why not let this user give you the keySelector to use in the OrderBy?
Creating an extension function:
static class QueryableExtensions
{
IQueryable<MyType> SelectAndSort<TSource, TResult>(this IQueryable<TSource source,
Expression<Func<TSource, TResult>> selector,
Expression<Func<TSource, TKey>> sortKeySelector,
System.ComponentModel.ListSortDirection sortDirection)
{
var selectBeforeOrdering = myRepository.el.Select(selector);
// note: this is still an IQueryable, only the query has been made,
// the database is not accessed yet!
IQueryable<TResult> result = (sortDirection == ListSortDirectrion.Ascending) ?
// sort in ascending order:
selectBeforeOrdering.OrderBy(sortKeySelector) :
// else: selec in descending order:
selectBeforeOrdering.OrderByDescending(sortKeySelector);
return result;
}
}
Usage: suppose your user knows your el, and he want several fields of it, ordered by one of the properties of your el
using (var myDbContext = new DbContext(...))
{
IQueryable myEls = myDbContext.el;
var mySelectedItems = myEls.SelectAndSore(
// selector: what properties do I as a user want?
el => new
{
Id = el.Id,
FullName = el.FullName,
... // other desired properties
},
// sortSelector: how do I want my selected items to be sorted?
selectedItem => selectedItem.FullName,
// direction:
ListSortDirection.Descending);
// note: until now the database has not been accessed
// only the Expression of the query has been created.
// depending on what you want as a result: ToList / ToArray / First / ...
return mySelectedItems.ToList();
}
If, on the other hand your user is not software, but an operator, who has to select by which column he wants his items to be ordered, he has to have a method to tell the computer which column should be sorted.
This is usually done by clicking the column. Another method could be by selecting a value in a combo box. Anyway, you'll have to attach something to the column or to the combobox values that holds the sortKeySelector:
class MySortableColumn<TDisplayedProperty> : DataGridViewColumn
// or whatever column class you are using
{
public Expression<Func<MyDisplayedItem, TDisplayedProperty>> SortKeySelector{get;set;}
}
Now you have several columns that each display one of the properties of MyDisplayedItem:
var columnId = new MySortableColumn<int>()
{
SortKeySelector = displayedItem => myDisplayedItem.Id,
};
var columnFullName = new MyStortableColumn<string>()
{
SortKeySelector = displayedItem => myDisplayedItem.Id,
}
// etc.
Whenever the operator clicks on a column, the sortKey is extracted and used as parameter to sort the items:
void SortColumn(SortableColumn column)
{
var sortedItems = Sort(column.SortKeySelector, ListSortOrder...);
Display(sortedItems);
}
As I assume, el is materialized(i.e., for example, it is not just entry point to database), so you can use reflection:
var sort = emsort.Text;
PropertyInfo property = null;
var sortedData = el.Select(i => new { i.ID, i.FullName })
.OrderBy(x =>
{
property = property ?? x.GetType().GetProperty(sort);
return property.GetValue(x);
}).ToList();
this is a part of the code that i had to write to do it :
if (emord.Text == "Ascending")
{
if (emsort.Text == "ID")
{
var empName = el.Select(i => new { i.ID, i.FullName }).OrderBy(x => x.ID);
var empNamenAmp = el.Select(i => new { i.ID, i.FullName, i.Salary, i.Currency, i.Per }).OrderBy(x => x.ID);
var empNamenAmpnwh = el.Select(i => new { i.ID, i.FullName, i.Salary, i.Currency, i.Per, i.Hours }).OrderBy(x => x.ID);
var empNamenwh = el.Select(i => new { i.ID, i.FullName, i.Hours }).OrderBy(x => x.ID);
var empNamenbd = el.Select(i => new { i.ID, i.FullName, i.Date }).OrderBy(x => x.ID);
var empNamenad = el.Select(i => new { i.ID, i.FullName, i.Location }).OrderBy(x => x.ID);
var empNamenpn = el.Select(i => new { i.ID, i.FullName, i.PhoneNb }).OrderBy(x => x.ID);
var empNamena = el.Select(i => new { i.ID, i.FullName, i.Age }).OrderBy(x => x.ID);
if (empfilcomb.Text == "Name")
{
dispfil.Clear();
foreach (var x in empName)
dispfil.Text += x.ID + ", " + x.FullName + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Amount paid")
{
dispfil.Clear();
foreach (var x in empNamenAmp)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Salary + " " + x.Currency + " " + x.Per + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Work Hours")
{
dispfil.Clear();
foreach (var x in empNamenwh)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Hours + Environment.NewLine;
}
else if (empfilcomb.Text == "Name,Amount paid and Work Hours")
{
dispfil.Clear();
foreach (var x in empNamenAmpnwh)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Salary + " " + x.Currency + " " + x.Per + ", " + x.Hours + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Birthday")
{
dispfil.Clear();
foreach (var x in empNamenbd)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Date + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Address")
{
dispfil.Clear();
foreach (var x in empNamenad)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Location + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Phone Number")
{
dispfil.Clear();
foreach (var x in empNamenpn)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.PhoneNb + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Age")
{
dispfil.Clear();
foreach (var x in empNamena)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Age + Environment.NewLine;
}
}
else if (emsort.Text == "Name")
{
var empName = el.Select(i => new { i.ID, i.FullName }).OrderBy(x => x.FullName);
var empNamenAmp = el.Select(i => new { i.ID, i.FullName, i.Salary, i.Currency, i.Per }).OrderBy(x => x.FullName);
var empNamenAmpnwh = el.Select(i => new { i.ID, i.FullName, i.Salary, i.Currency, i.Per, i.Hours }).OrderBy(x => x.FullName);
var empNamenwh = el.Select(i => new { i.ID, i.FullName, i.Hours }).OrderBy(x => x.FullName);
var empNamenbd = el.Select(i => new { i.ID, i.FullName, i.Date }).OrderBy(x => x.FullName);
var empNamenad = el.Select(i => new { i.ID, i.FullName, i.Location }).OrderBy(x => x.FullName);
var empNamenpn = el.Select(i => new { i.ID, i.FullName, i.PhoneNb }).OrderBy(x => x.FullName);
var empNamena = el.Select(i => new { i.ID, i.FullName, i.Age }).OrderBy(x => x.FullName);
if (empfilcomb.Text == "Name")
{
dispfil.Clear();
foreach (var x in empName)
dispfil.Text += x.ID + ", " + x.FullName + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Amount paid")
{
dispfil.Clear();
foreach (var x in empNamenAmp)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Salary + " " + x.Currency + " " + x.Per + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Work Hours")
{
dispfil.Clear();
foreach (var x in empNamenwh)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Hours + Environment.NewLine;
}
else if (empfilcomb.Text == "Name,Amount paid and Work Hours")
{
dispfil.Clear();
foreach (var x in empNamenAmpnwh)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Salary + " " + x.Currency + " " + x.Per + ", " + x.Hours + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Birthday")
{
dispfil.Clear();
foreach (var x in empNamenbd)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Date + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Address")
{
dispfil.Clear();
foreach (var x in empNamenad)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Location + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Phone Number")
{
dispfil.Clear();
foreach (var x in empNamenpn)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.PhoneNb + Environment.NewLine;
}
else if (empfilcomb.Text == "Name and Age")
{
dispfil.Clear();
foreach (var x in empNamena)
dispfil.Text += x.ID + ", " + x.FullName + ", " + x.Age + Environment.NewLine;
}
}
and i was asking if there is a shorter way to do it:withing what i'm allowed to use. Seems that there isn't but thanks a lot for the help i'm sure what you guys provided is useful.
I am having some issues with doing a search. I am trying to find a customer on either Name+Lastname, reference or CompanyName. I managed to get it working, however the the ordering is not 100%.
Here is my original object
var q = from row in DataAccess.metadata.db_Customer
where row.accountID == accountID
&& row.isActive
orderby row.Name
select new bl_customerNames
{
customerID = row.customerID,
CustomerName = row.Name + " " + row.LastName,
Company = row.Company,
Reference = row.reference,
Email = row.Email,
CurrencyCode = row.CurrencyCode,
isSuspended = row.isSuspended
};
Then I did the following filter.
if (!string.IsNullOrEmpty(search))
{
q = q.Where(x => (x.CustomerName.Contains(search) || x.Reference.Contains(search) || x.Company.Contains(search)));
}
Although this does a fair job the list does not really make logical sense as its does not favor starts with above Contains.
I then came up with a ranking solution where I would rank Starts with higher than Contains.
Here is my code for that:
var q = from row in DataAccess.metadata.db_Customer
where row.accountID == accountID
&& row.isActive
orderby row.Name
select new bl_customerNames
{
customerID = row.customerID,
CustomerName = row.Name + " " + row.LastName,
Company = row.Company,
Reference = row.reference,
Email = row.Email,
CurrencyCode = row.CurrencyCode,
isSuspended = row.isSuspended,
Rank = ((row.Name + " " + row.LastName).StartsWith(search) || row.reference.StartsWith(search) || row.Company.StartsWith(search)) ? 1 : ((row.Name + " " + row.LastName).Contains(search) || row.reference.Contains(search) || row.Company.Contains(search)) ? 2 : 0
};
I would then filter like this:
q = q.Where(r=>r.Rank > 0).OrderBy(r => r.Rank);
However if I search on reference it does not return anything.
So My question is will my current method work also why does nothing return if I search on reference or Company Name? What would the correct way of doing a StartsWith then Contains and still keeping some kind of alphabetical ordering after StartsWith List.
Please keep in mind I am still a junior developer and any help would be appreciated.
I went back to the basics and this is what I came up with.
//Get customers that starts with
var qSW = from row in DataAccess.metadata.db_Customer
where row.accountID == accountID
&& row.isActive
where (row.Name + " " + row.LastName).StartsWith(search)
|| row.Company.StartsWith(search)
|| row.reference.StartsWith(search)
select new bl_customerNames
{
customerID = row.customerID,
CustomerName = row.Name + " " + row.LastName,
Company = row.Company,
Reference = row.reference,
Email = row.Email,
CurrencyCode = row.CurrencyCode,
isSuspended = row.isSuspended
};
//Order
qSW = qSW.OrderBy(r => r.CustomerName).ThenBy(r => r.Company).ThenBy(r => r.Reference);
//Get customers that contains
var qC = from row in DataAccess.metadata.db_Customer
where row.accountID == accountID
&& row.isActive
where (row.Name + " " + row.LastName).Contains(search)
|| row.Company.Contains(search)
|| row.reference.Contains(search)
select new bl_customerNames
{
customerID = row.customerID,
CustomerName = row.Name + " " + row.LastName,
Company = row.Company,
Reference = row.reference,
Email = row.Email,
CurrencyCode = row.CurrencyCode,
isSuspended = row.isSuspended
};
//If search is less then 3 chars limit to 10 to help with performance
if(search.Count() > 2)
qC = qC.OrderBy(r => r.CustomerName).ThenBy(r => r.Company).ThenBy(r => r.Reference);
else
qC = qC.OrderBy(r => r.CustomerName).ThenBy(r => r.Company).ThenBy(r => r.Reference).Take(10);
//Concat list leaving out duplicates
var SList = qSW.Concat(qC);
// Return List
return SList.ToList();
It seems to be doing the job and performance seems very good.
Click to see the Output:
I have an Employee class
public class Employee
{
public int ID { get; set; }
public string Name { get; set; }
public string Age { get; set; }
public string Address { get; set; }
public string ContactNo { get; set; }
}
and have fill method to fill lists
private static void FillEmployeeList(ref List<Employee> lt1, ref List<Employee> lt2)
{
lt1 = new List<Employee> {new Employee{ID=1,Name="Kavya",Age="24",Address="No.1,Nehru Street,Chennai",ContactNo="9874521456"},
new Employee{ID=2,Name="Ravi",Age="24",Address="Flat No.25/A1,Gandhi Street,Chennai",ContactNo="9658745258"},
new Employee{ID=3,Name="Lavnya",Age="30",Address="No.12,Shastri nagar,Chennai",ContactNo="5214587896"},
new Employee{ID=4,Name="Rupa",Age="31",Address="No.23/5,Nehru Street,Chennai",ContactNo="9874521256"},
new Employee{ID=5,Name="Divya",Age="32",Address="No.1/227,Nehru Street,Chennai",ContactNo="8541256387"},
};
lt2 = new List<Employee> {new Employee{ID=1,Name="Kavya",Age="24",Address="No.1,Nehru Street,Chennai",ContactNo="9874521456"},
new Employee{ID=2,Name="Ravindran",Age="30",Address="Flat No.25/A1,Gandhi Street,Chennai",ContactNo="9658745258"},
new Employee{ID=3,Name="Chandru",Age="30",Address="No.12,Shastri nagar,Chennai",ContactNo="5214587896"},
new Employee{ID=4,Name="Rakesh",Age="32",Address="No.23/5,Nehru Street,Chennai",ContactNo="9874021256"},
new Employee{ID=5,Name="Suresh",Age="32",Address="No.1/227,Nehru Street,Chennai",ContactNo="8541056387"},
new Employee{ID=11,Name="Suryakala",Age="28",Address="No.1,Pillayar koil Street,Chennai",ContactNo="9541204782"},
new Employee{ID=12,Name="Thivya",Age="41",Address="No.42,Ellaiamman koil Street,Chennai",ContactNo="9632140874"},
};
}
Comparing two list of objects
protected List<Employee> ListCompare(List<Employee> lt1, List<Employee> lt2)
{
FillEmployeeList(ref lt1, ref lt2);
List<Employee> lst = new List<Employee>();
if (lt1.Count > 0 && lt2.Count > 0)
{
// Displaying Matching Records from List1 and List2 by ID
var result = (from l1 in lt1
join l2 in lt2
on l1.ID equals l2.ID
orderby l1.ID
select new
{
ID = l1.ID,
Name = (l1.Name == l2.Name) ? "$" : (l2.Name + " (Modified)"),
Age = (l1.Age == l2.Age) ? "$" : (l2.Age + " (Modified)"),
Address = (l1.Address == l2.Address) ? "$" : (l2.Address + " (Modified)"),
ContactNo = (l1.ContactNo == l2.ContactNo) ? "$" : (l2.ContactNo + " (Modified)")
}).ToList();
// Displaying Records from List1 which is not in List2
var result1 = from l1 in lt1
where !(from l2 in lt2
select l2.ID).Contains(l1.ID)
orderby l1.ID
select new
{
ID = l1.ID,
Name = " Deleted",
Age = " Deleted",
Address = " Deleted",
ContactNo = " Deleted"
};
// Displaying Records from List1 which is not in List2
var result2 = from l1 in lt2
where !(from l2 in lt1
select l2.ID).Contains(l1.ID)
orderby l1.ID
select new
{
ID = l1.ID,
Name = l1.Name + " (Added)",
Age = l1.Age + " (Added)",
Address = l1.Address + " (Added)",
ContactNo = l1.ContactNo + " (Added)"
};
var res1 = result.Concat(result1).Concat(result2);
foreach (var item in res1)
{
Employee emp = new Employee();
//Response.Write(item + "<br/>");
emp.ID = item.ID;
emp.Name = item.Name;
emp.Age = item.Age;
emp.Address = item.Address;
emp.ContactNo = item.ContactNo;
lst.Add(emp);
}
}
return lst;
}
Here I am calling compareList method and returns the result and displaying it on the html table.
List<Employee> lt1 = new List<Employee>();
List<Employee> lt2 = new List<Employee>();
List<Employee> resultset = new List<Employee>();
//string value = "ID";
StringBuilder htmlTable = new StringBuilder();
htmlTable.Append("<table border='1'>");
htmlTable.Append("<tr><th>ID</th><th>Name</th><th>Age</th><th>Address</th><th>ContactNo</th></tr>");
resultset = ListCompare(lt1, lt2);
foreach(var item in resultset)
{
htmlTable.Append("<tr>");
htmlTable.Append("<td>" + item.ID + "</td>");
htmlTable.Append("<td>" + item.Name + "</td>");
htmlTable.Append("<td>" + item.Age + "</td>");
htmlTable.Append("<td>" + item.Address + "</td>");
htmlTable.Append("<td>" + item.ContactNo + "</td>");
htmlTable.Append("</tr>");
}
htmlTable.Append("</table>");
PlaceHolder1.Controls.Add(new Literal { Text = htmlTable.ToString() });
My question is how to generalize this coding. I may have any class(like Employee or Student). I want the coding just i will pass the two lists of objects to CompareMethod(to compare method i will pass any type of list of objects) which will return the list as result. How to proceed, pls give any idea.
If you are interesting in collection equality, I would like to recomend next stuff:
Enumerable.SequenceEqual
Determines whether two sequences are equal by comparing their elements by using a specified IEqualityComparer. MSDN
in case of unit testing:
CollectionAssert.AreEquivalent
Verifies that two specified collections are equivalent. The assertion fails if the collections are not equivalent. MSDN
if you need to know about difference:
Enumerable.Except
Produces the set difference of two sequences. MSDN
You can create your compare method as the following:
protected List<TResult> ListCompare<TKey, TInput, TResult>(List<TInput> lt1, List<TInput> lt2, Func<TInput, TKey> key, Func<TInput, TInput, TResult> modified, Func<TInput, TResult> added, Func<TInput, TResult> deleted)
{
// Displaying Matching Records from List1 and List2 by ID
var matchingEmployees = lt1.Join(lt2, key, key, modified);
// Displaying Records from List1 which is not in List2
var lt1NotInlt2 = lt1
.Where(e1 => !lt2.Any(e2 => key(e2).Equals(key(e1))))
.Select(deleted);
// Displaying Records from List2 which is not in List1
var lt2NotInlt1 = lt2
.Where(e2 => !lt1.Any(e1 => key(e1).Equals(key(e2))))
.Select(added);
return matchingEmployees.Concat(lt1NotInlt2).Concat(lt2NotInlt1).ToList();
}
If you don't want to use the key, then your compare method should look like this and your class should implement the Equals method:
protected List<TResult> ListCompare<TInput, TResult>(List<TInput> lt1, List<TInput> lt2, Func<TInput, TInput, TResult> modified, Func<TInput, TResult> added, Func<TInput, TResult> deleted)
{
// Displaying Matching Records from List1 and List2 by ID
var matchingEmployees = lt1
.Where(e1 => lt2.Any(e2 => e2.Equals(e1)))
.Select(e1 =>
{
var e2 = lt2.First(e => e.Equals(e1));
return modified(e1, e2);
});
// Displaying Records from List1 which is not in List2
var lt1NotInlt2 = lt1
.Where(e1 => !lt2.Any(e2 => e2.Equals(e1)))
.Select(deleted);
// Displaying Records from List2 which is not in List1
var lt2NotInlt1 = lt2
.Where(e2 => !lt1.Any(e1 => e1.Equals(e2)))
.Select(added);
return matchingEmployees.Concat(lt1NotInlt2).Concat(lt2NotInlt1).ToList();
}
Then you can call the compare method like this:
var result = ListCompare<int, Employee, Employee>(
lt1,
lt2,
e => e.ID,
(e1, e2) => new Employee
{
ID = e1.ID,
Name = (e1.Name == e2.Name) ? "$" : (e2.Name + " (Modified)"),
Age = (e1.Age == e2.Age) ? "$" : (e2.Age + " (Modified)"),
Address = (e1.Address == e2.Address) ? "$" : (e2.Address + " (Modified)"),
ContactNo = (e1.ContactNo == e2.ContactNo) ? "$" : (e2.ContactNo + " (Modified)")
},
e => new Employee
{
ID = e.ID,
Name = e.Name + " (Added)",
Age = e.Age + " (Added)",
Address = e.Address + " (Added)",
ContactNo = e.ContactNo + " (Added)"
},
e => new Employee
{
ID = e.ID,
Name = " Deleted",
Age = " Deleted",
Address = " Deleted",
ContactNo = " Deleted"
});
I am using linq to get data and insert the data into a IEnumerable>. But sometimes i get duplicate keyvaluepairs and i dont want that because i am doing a ToDictionary(pair => pair.Key, pair => pair.Value) on the IEnumerable.
this is my code:
public Dictionary<Guid, string> GetCitizensWithUnwarrentedAbsence(Guid counselorId, DateTime date)
{
var now = DateTime.Now;
var startInterval = Convert.ToDateTime(date.Date.ToShortDateString());
var endInterval = Convert.ToDateTime(date.Date.ToShortDateString()).AddHours(23).AddMinutes(59);
var list = (from c in _context.CitizenCounselorSet
join p in _context.ActivityLessonParticipantSet on c.Citizen.Id equals p.UserId
where c.CounselorId == counselorId
&& c.StartDate < now
&& (c.EndDate == null || (c.EndDate.HasValue && c.EndDate.Value > now))
&& p.WasUnwarrantedAbsent
&& !p.ActivityLesson.IsDeleted
&& !p.ActivityLesson.IsCancelled
&& p.ActivityLesson.From >= startInterval
&& p.ActivityLesson.From <= endInterval
select new
{
UserId = p.UserId,
UserName = p.User.FullName,
CPR = p.User.UserName
}).ToList().Select(a => new KeyValuePair<Guid, string>(a.UserId, a.UserName + " (" + EncryptionUtility.DecryptString(a.CPR).Insert(6, "-") + ")"));
return list.ToDictionary(pair => pair.Key, pair => pair.Value);
}
How would i make sure not to get duplicates or remove the duplicates after i get the data ??
I would make couple of changes at the end of the query. Let's save space and start in your }).ToList() when your main query logic gets executed and redefine the rest to get your dictionary:
var yourExistingQueryLogic = ...
}).ToList();
var yourUserDictionary = yourExistingQueryLogic
.Select(x=>new {x.UserId, UserName = x.UserName+ " (" + EncryptionUtility.DecryptString(a.CPR).Insert(6, "-") + ")"}) //you can simply build an anonymous object here
.Distinct() //this will eliminate duplicates
.ToDictionary(x=>x.UserId, x=>x.UserName); // DONE!
I have a linq query that does something simple like:
var k = people.Select(x=>new{x.ID, x.Name});
I then want a function or linq lambda, or something that will output the names in sentence format using commas and "ands".
{1, John}
{2, Mark}
{3, George}
to
"1:John, 2:Mark and 3:George"
I'm fine with hardcoding the ID + ":" + Name part, but it could be a ToString() depending on the type of the linq query result. I'm just wondering if there is a neat way to do this with linq or String.Format().
public string ToPrettyCommas<T>(
List<T> source,
Func<T, string> stringSelector
)
{
int count = source.Count;
Func<int, string> prefixSelector = x =>
x == 0 ? "" :
x == count - 1 ? " and " :
", ";
StringBuilder sb = new StringBuilder();
for(int i = 0; i < count; i++)
{
sb.Append(prefixSelector(i));
sb.Append(stringSelector(source[i]));
}
string result = sb.ToString();
return result;
}
Called with:
string result = ToPrettyCommas(people, p => p.ID.ToString() + ":" + p.Name);
Why Linq?
StringBuilder sb = new StringBuilder();
for(int i=0;i<k.Count();i++)
{
sb.Append(String.Format("{0}:{1}", k[i].ID, k[i].Name);
if(i + 2 < k.Count())
sb.Append(", ");
else if(i + 1 < k.Count())
sb.Append(" and ");
}
Really, all Linq will let you do is hide the loop.
Also, make sure you do or do not want the "Oxford Comma"; this algorithm will not insert one, but a small change will (append the comma and space after every element except the last, and also append "and " after the next-to-last).
Just for fun, here’s something that really uses functional LINQ — no loop and no StringBuilder. Of course, it’s pretty slow.
var list = new[] { new { ID = 1, Name = "John" },
new { ID = 2, Name = "Mark" },
new { ID = 3, Name = "George" } };
var resultAggr = list
.Select(item => item.ID + ":" + item.Name)
.Aggregate(new { Sofar = "", Next = (string) null },
(agg, next) => new { Sofar = agg.Next == null ? "" :
agg.Sofar == "" ? agg.Next :
agg.Sofar + ", " + agg.Next,
Next = next });
var result = resultAggr.Sofar == "" ? resultAggr.Next :
resultAggr.Sofar + " and " + resultAggr.Next;
// Prints 1:John, 2:Mark and 3:George
Console.WriteLine(result);
Much like the rest, this isn't better than using a string builder, but you can go (ignoring the ID, you can add it in):
IEnumerable<string> names = new[] { "Tom", "Dick", "Harry", "Abe", "Bill" };
int count = names.Count();
string s = String.Join(", ", names.Take(count - 2)
.Concat(new [] {String.Join(" and ", names.Skip(count - 2))}));
This approach pretty much abuses Skip and Take's ability to take negative numbers, and String.Join's willingness to take a single parameter, so it works for one, two or more strings.
Using the Select operation that gives you an index, this can be written as a ONE LINE extension method:
public static string ToAndList<T>(this IEnumerable<T> list, Func<T, string> formatter)
{
return string.Join(" ", list.Select((x, i) => formatter(x) + (i < list.Count() - 2 ? ", " : (i < list.Count() - 1 ? " and" : ""))));
}
e.g.
var list = new[] { new { ID = 1, Name = "John" },
new { ID = 2, Name = "Mark" },
new { ID = 3, Name = "George" } }.ToList();
Console.WriteLine(list.ToAndList(x => (x.ID + ": " + x.Name)));
Improving(hopefully) on KeithS's answer:
string nextBit = "";
var sb = new StringBuilder();
foreach(Person person in list)
{
sb.Append(nextBit);
sb.Append(", ");
nextBit = String.Format("{0}:{1}", person.ID, person.Name);
}
sb.Remove(sb.Length - 3, 2);
sb.Append(" and ");
sb.Append(nextBit);
This is not pretty but will do the job using LINQ
string s = string.Join(",", k.TakeWhile(X => X != k.Last()).Select(X => X.Id + ":" + X.Name).ToArray()).TrimEnd(",".ToCharArray()) + " And " + k.Last().Id + ":" + k.Last().Name;
Y'all are making it too complicated:
var list = k.Select(x => x.ID + ":" + x.Name).ToList();
var str = list.LastOrDefault();
str = (list.Count >= 2 ? list[list.Count - 2] + " and " : null) + str;
str = string.Join(", ", list.Take(list.Count - 2).Concat(new[]{str}));
How about this?
var k = people.Select(x=>new{x.ID, x.Name});
var stringified = people
.Select(x => string.Format("{0} : {1}", x.ID, x.Name))
.ToList();
return string.Join(", ", stringified.Take(stringified.Count-1).ToArray())
+ " and " + stringified.Last();
I have refined my previous answer and I believe this is the most elegant solution yet.
However it would only work on reference types that don't repeat in the collection (or else we'd have to use different means for finding out if item is first/last).
Enjoy!
var firstGuy = guys.First();
var lastGuy = guys.Last();
var getSeparator = (Func<Guy, string>)
(guy => {
if (guy == firstGuy) return "";
if (guy == lastGuy) return " and ";
return ", ";
});
var formatGuy = (Func<Guy, string>)
(g => string.Format("{0}:{1}", g.Id, g.Name));
// 1:John, 2:Mark and 3:George
var summary = guys.Aggregate("",
(sum, guy) => sum + getSeparator(guy) + formatGuy(guy));
This can be the way you can achieve your goal
var list = new[] { new { ID = 1, Name = "John" },
new { ID = 2, Name = "Mark" },
new { ID = 3, Name = "George" }
}.ToList();
int i = 0;
string str = string.Empty;
var k = list.Select(x => x.ID.ToString() + ":" + x.Name + ", ").ToList();
k.ForEach(a => { if (i < k.Count() - 1) { str = str + a; } else { str = str.Substring(0, str.Length -2) + " and " + a.Replace("," , ""); } i++; });
Here's a method that doesn't use LINQ, but is probably as efficient as you can get:
public static string Join<T>(this IEnumerable<T> list,
string joiner,
string lastJoiner = null)
{
StringBuilder sb = new StringBuilder();
string sep = null, lastItem = null;
foreach (T item in list)
{
if (lastItem != null)
{
sb.Append(sep);
sb.Append(lastItem);
sep = joiner;
}
lastItem = item.ToString();
}
if (lastItem != null)
{
if (sep != null)
sb.Append(lastJoiner ?? joiner);
sb.Append(lastItem);
}
return sb.ToString();
}
Console.WriteLine(people.Select(x => x.ID + ":" + x.Name).Join(", ", " and "));
Since it never creates a list, looks at an element twice, or appends extra stuff to the StringBuilder, I don't think you can get more efficient. It also works for 0, 1, and 2 elements in the list (as well as more, obviously).
StringBuilder Approach
Here's an Aggregate with a StringBuilder. There's some position determinations that are made to clean up the string and insert the "and" but it's all done at the StringBuilder level.
var people = new[]
{
new { Id = 1, Name = "John" },
new { Id = 2, Name = "Mark" },
new { Id = 3, Name = "George" }
};
var sb = people.Aggregate(new StringBuilder(),
(s, p) => s.AppendFormat("{0}:{1}, ", p.Id, p.Name));
sb.Remove(sb.Length - 2, 2); // remove the trailing comma and space
var last = people.Last();
// index to last comma (-2 accounts for ":" and space prior to last name)
int indexComma = sb.Length - last.Id.ToString().Length - last.Name.Length - 2;
sb.Remove(indexComma - 1, 1); // remove last comma between last 2 names
sb.Insert(indexComma, "and ");
// 1:John, 2:Mark and 3:George
Console.WriteLine(sb.ToString());
A String.Join approach could have been used instead but the "and" insertion and comma removal would generate ~2 new strings.
Regex Approach
Here's another approach using regex that is quite understandable (nothing too cryptic).
var people = new[]
{
new { Id = 1, Name = "John" },
new { Id = 2, Name = "Mark" },
new { Id = 3, Name = "George" }
};
var joined = String.Join(", ", people.Select(p => p.Id + ":" + p.Name).ToArray());
Regex rx = new Regex(", ", RegexOptions.RightToLeft);
string result = rx.Replace(joined, " and ", 1); // make 1 replacement only
Console.WriteLine(result);
The pattern is simply ", ". The magic lies in the RegexOptions.RightToLeft which makes the match occur from the right and thereby makes the replacement occur at the last comma occurrence. There is no static Regex method that accepts the number of replacements with the RegexOptions, hence the instance usage.
Here's one using a slightly modified version of my answer to Eric Lippert's Challenge which is IMHO the most concise with easy to follow logic (if you're familiar with LINQ).
static string CommaQuibblingMod<T>(IEnumerable<T> items)
{
int count = items.Count();
var quibbled = items.Select((Item, index) => new { Item, Group = (count - index - 2) > 0})
.GroupBy(item => item.Group, item => item.Item)
.Select(g => g.Key
? String.Join(", ", g)
: String.Join(" and ", g));
return String.Join(", ", quibbled); //removed braces
}
//usage
var items = k.Select(item => String.Format("{0}:{1}", item.ID, item.Name));
string formatted = CommaQuibblingMod(items);
static public void Linq1()
{
var k = new[] { new[] { "1", "John" }, new[] { "2", "Mark" }, new[] { "3", "George" } };
Func<string[], string> showPerson = p => p[0] + ": " + p[1];
var res = k.Skip(1).Aggregate(new StringBuilder(showPerson(k.First())),
(acc, next) => acc.Append(next == k.Last() ? " and " : ", ").Append(showPerson(next)));
Console.WriteLine(res);
}
could be optimized by moving k.Last() computation to before the loop
public static string ToListingCommaFormat(this List<string> stringList)
{
switch(stringList.Count)
{
case 0:
return "";
case 1:
return stringList[0];
case 2:
return stringList[0] + " and " + stringList[1];
default:
return String.Join(", ", stringList.GetRange(0, stringList.Count-1))
+ ", and " + stringList[stringList.Count - 1];
}
}
This is the method is faster than the 'efficient' Join method posted by Gabe. For one and two items, it is many times faster, and for 5-6 strings, it is about 10% faster. There is no dependency on LINQ. String.Join is faster than StringBuilder for small arrays, which are typical for human-readable text. In grammar, these are called listing commas, and the last comma should always be included to avoid ambiguity. Here is the resulting code:
people.Select(x=> x.ID.ToString() + ":" + x.Name).ToList().ToListingCommaFormat();
There are ways to optimize this since it isn't very efficient, but something like this may work:
var k = people.Select(x => new {x.ID, x.Name}).ToList();
var last = k.Last();
k.Aggregate(new StringBuilder(), (sentence, item) => {
if (sentence.Length > 0)
{
if (item == last)
sentence.Append(" and ");
else
sentence.Append(", ");
}
sentence.Append(item.ID).Append(":").Append(item.Name);
return sentence;
});