Problem
For a course I following and my code is approved I had to make some code that finds the person with the biggest loss where the input is a string[]
The string arrray looks like this :
{ "Tom, 1, 3, -1", "Gillie, 2, 3, 1", "Thor, 1000, 1001, 1002" };
I solved it this way but it’s not a clean way. I must say we were not allowed to use classes. that will be the next chapter of the course.
My solution looks now like this :
public static string FindPersonWithBiggestLoss(string[] peopleAndBalances)
{
if (IsInValidInput(peopleAndBalances))
{
return InValidOutput;
}
var highestLossEver = decimal.MinValue;
var personWithHighestLoss = new StringBuilder();
for (int i = 0; i <= peopleAndBalances.Length - 1; i++)
{
var currentPersonData = peopleAndBalances[i].Split(',');
var allAmounts = currentPersonData[1..];
// calculateLoss
if (allAmounts.Length <= 1)
{
return InValidOutput;
}
for (int j = 0; j < allAmounts.Length - 1; j++)
{
try
{
var amount1 = decimal.Parse(allAmounts[j], NumberStyles.Currency | NumberStyles.Number);
var amount2 = decimal.Parse(allAmounts[j + 1], NumberStyles.Currency | NumberStyles.Number);
var lossForCurrentPerson = amount1 - amount2;
//check if loss is greater then the current highest loss
if (lossForCurrentPerson > highestLossEver)
{
highestLossEver = lossForCurrentPerson;
personWithHighestLoss.Clear();
personWithHighestLoss.Append(currentPersonData[0]);
}
}
catch (FormatException ex)
{
Console.WriteLine($"there is not a valid character in the database. Reason{ex}");
break;
}
}
}
if (personWithHighestLoss.ToString().Contains(','))
{
ReplaceCommaWithAnd(personWithHighestLoss);
}
return $"{personWithHighestLoss} lost the most money. -¤{highestLossEver}.";
}
it works but also I need a lot of nested control statements to make it work.
Is there a cleaner way to make this work ?
Solution
A few things I noticed:
Using the range option of the array index(var allAmounts = currentPersonData[1..];
) doesn’t really help here. All that’s needed is to use the appropriate indexes of the original array.
It’s quite inefficient to use a try
block to test if a string is a valid number. Using the tryParse
method is much more efficient.
A hard coded currency symbol means that your output might not format properly on someone else’s machine. I would suggest using the format overload of the ToString
method with the currency format.
I appears you’re using a StringBuilder
to change the name of whichever person has the highest loss. If I’m not mistaken this will copy the string you already have. I would suggest just use the string array itself. This will be assigned by reference and won’t require recopying the string.
This:
if (personWithHighestLoss[0].Contains(','))
{
ReplaceCommaWithAnd(personWithHighestLoss);
}
doesn’t really make much sense. The Split
method already removes the commas. If you’re worried about extra commas use the StringSplitOptions.RemoveEmptyEntries
option of the Split
method.
Here’s what the modified code could look like:
public static string FindPersonWithBiggestLoss(string[] peopleAndBalances)
{
if (IsInValidInput(peopleAndBalances))
{
return InValidInput;
}
var highestLossEver = decimal.MinValue;
string[] personWithHighestLoss = new string[] { "" };
for (int i = 0; i <= peopleAndBalances.Length - 1; i++)
{
var currentPersonData = peopleAndBalances[i].Split(',',StringSplitOptions.RemoveEmptyEntries);
// calculateLoss
if (currentPersonData.Length < 4)
{
return InValidInput;
}
for (int j = 1; j < currentPersonData.Length - 1; j++)
{
var amount1 = 0.0M;
var amount2 = 0.0M;
if (!(decimal.TryParse(currentPersonData[j], NumberStyles.Currency | NumberStyles.Number, null, out amount1) &&
decimal.TryParse(currentPersonData[j + 1], NumberStyles.Currency | NumberStyles.Number, null, out amount2)))
{
Console.WriteLine($"there is not a valid character in the database");
}
var lossForCurrentPerson = amount1 - amount2;
//check if loss is greater then the current highest loss
if (lossForCurrentPerson > highestLossEver)
{
highestLossEver = lossForCurrentPerson;
personWithHighestLoss = currentPersonData;
}
}
}
return $"{personWithHighestLoss[0]} lost the most money. -{highestLossEver.ToString("C")}.";
}
I wrote this code with a simpler version.
At first I created a class to store parsed information.
public class PersonWithMinLoss
{
public string Name { get; set; }
public decimal MaxLoss { get; set; }
private readonly List<decimal> Amounts;
public PersonWithMinLoss(string currentPersonData)
{
var parts = currentPersonData.Split(',');
this.Amounts = parts[1..]
.Select(_ => decimal.Parse(_, NumberStyles.Currency | NumberStyles.Number)).ToList();
this.Name = parts[0];
this.MaxLoss = GetMaxLoss();
}
private decimal GetMaxLoss()
{
decimal maxLoss = int.MinValue;
for (int i = 0; i < Amounts.Count - 1; i++)
{
var loss = Amounts[i] - Amounts[i + 1];
if (loss > maxLoss) maxLoss = loss;
}
return maxLoss;
}
}
After that we just have to do several Linq
queries. For this case
public static string FindPersonWithBiggestLoss(string[] peopleAndBalances)
{
var personsWithMaxLoss = peopleAndBalances.Select(_ => new PersonWithMinLoss(_));
var maxValue = personsWithMaxLoss.Max(_ => _.MaxLoss);
var personWithMaxLoss = personsWithMaxLoss.Where(x => (x.MaxLoss - maxValue) < 0.0001m).First();
return $"{personWithMaxLoss.Name} lost the most money. -¤{personWithMaxLoss.MaxLoss}.";
}
I stored amounts in List<decimal> Amounts
so that we can use it after other operations(Not needed for this specific case).
And in var personWithMaxLoss = personsWithMaxLoss.Where(x => (x.MaxLoss - maxValue) < 0.0001m).First();
, we can use .ToList()
instead of .First()
to find all the persons with max loss.
In this way, complexity is O(n*m)
where n is number of person and m is the number of values each person has.
Alternatively we also can use aggregate funtion to find person of max loss.
var personsWithMaxLoss = peopleAndBalances.Select(_ => new PersonWithMinLoss(_));
var personWithMaxLoss = personsWithMaxLoss.Aggregate((cur, next) => (cur == null || cur.MaxLoss < next.MaxLoss) ? next : cur);
A few things can be noted.
- Find the largest value by using
Max()
which is aLINQ
extension. - Don’t initialize integers variables with
MinValue
orMaxValue
such asdecimal.MinValue
. Use a default value of0
or-1
(which are the common used ones` for better code management). IsInValidInput
is not presented in your post, but I assume it’s a method where you have validate the input based on your business needs, I just hoped that would be present as well to make the picture clearer.- Using
Try/Catch
block inside this method is unnecessary. TheTry/Catch
should be outside this scope. - Parsing string, then returning a string is a bad practice. You always need to parse once, and work through the parsed typed, then use
ToString()
whenever needed, this would create a string while keeping the actual source untouched. Using the current solution means that you’re parsing the values in every step in the application. If you see multiple parsing to the same data, try to unify it to make it parsed once, and let the application uses the actual types, then convert it to string whenever needed (like, sending it to database or over web request ..etc). - Use
decimal.TryParse
instead ofdecimal.Parse
as you don’t know what values are passed to the method, so validate these values before parse them. Also, usestring.IsNullOrEmpty
since you’re working withstring
. It’s minor changes, but it would avoid invalid inputs. Even if it’s already validated inIsInValidInput
, as long as the method is exposed, you should always consider the implement the method validation process. - Make use of generic classes such as
IEnumerable
. instead of acceptingstring[]
you could converted toIEnumerable<string>
which would extend your acceptable collections types, and would work with any collection that implementsIEnumerable<string>
. This would make your method flixable and adaptable to other classes out-of-the-box. - using
StringBuilder
is a good choice in your case, but you’re misused it when you added a string at the return statement. What you need to do is to keep using theStringBuilder
save the results inside it, and then callToString()
on the return statement.
for the current work, where you get a string’s array and output one string containing the person information who has the biggest loss. You can simplify it to :
public static string FindPersonWithBiggestLoss(string[] peopleAndBalances)
{
if (IsInValidInput(peopleAndBalances)) { return InValidOutput; }
decimal max = 0;
var personWithHighestLoss = new StringBuilder();
for (int x = 0; x < peopleAndBalances.Length; ++x)
{
// convert string to array
var data = peopleAndBalances[x].Split(',');
// since index 0 would hold the person name, then we can start with index 1 to get balances
for (int i = 1; i < data.Length; i++)
{
if (decimal.TryParse(data[i], out decimal amount) && amount > max)
{
personWithHighestLoss.Clear();
personWithHighestLoss
.Append(data[0])
.Append(" lost the most money. -¤")
.Append(amount)
.Append('.');
max = amount;
}
}
}
return personWithHighestLoss.ToString();
}
if you want a better version which would be more practical in actual work environment, then you would need to do something like this :
// Make use of `IEnumerable` and `KeyValuePair<string, decimal>`
public static IEnumerable<KeyValuePair<string, decimal>> FindPersonWithBiggestLoss(IEnumerable<string> peopleAndBalances)
{
if (IsInValidInput(peopleAndBalances)) { return InValidOutput; }
foreach (var person in peopleAndBalances)
{
decimal max = 0;
// check the string array
if (!string.IsNullOrEmpty(person))
{
// convert string to array
var data = person.Split(',');
// since index 0 would hold the person name, then we can start with index 1 to get balances
for (int i = 1; i < data.Length; i++)
{
// check the string and validate if it's an integer
// if not valid, just ignore and get the next value.
if (decimal.TryParse(data[i], out decimal amount) && amount > max)
{
max = amount;
}
}
yield return new KeyValuePair<string, decimal>(data[0], max);
}
}
}
this can be simplified using Linq
to this :
public static IEnumerable<KeyValuePair<string, decimal>> FindPersonWithBiggestLoss(IEnumerable<string> peopleAndBalances)
{
if (IsInValidInput(peopleAndBalances)) { return InValidOutput; }
foreach (var person in peopleAndBalances)
{
var data = person.Split(',');
yield return new KeyValuePair<string, decimal>(
data[0], // you can use Array.Find(data, x => !decimal.TryParse(x, out _)) as well.
data.Where(x => decimal.TryParse(x, out _)).Max(x => decimal.Parse(x))
);
}
}
I would rather use the LINQ Query Syntax for simplicity and readability
var input = new List<string> { "Tom, 1, 3, -1", "Gillie, 2, 3, 1", "Thor, 1000, 1001, 1002" };
var personsWithLoss =
from pl in input
let splitted = pl.Split(",")
let person = splitted[0]
let maxLoss = splitted.Skip(1).OrderByDescending(x=>x).First()
select new {person,maxLoss};
var personWithMaxLoss = personsWithLoss.OrderBy(x=>x.maxLoss).First();
Console.WriteLine($"{personWithMaxLoss.person} - {personWithMaxLoss.maxLoss}");