Problem sorting objects based on value of its properties - c#

I'm sitting here with a school project I can't seem to figure out; I am to create a console application that let's the user enter a number of salesmen for a hypothetical company. The information about the salesmen includes their name, district and number of sold items. The salesmen are then to be sorted into different levels according to the number of sold items. Finally, the salesmen are to be printed to the console. The printing should be done one level at the time, if for instance two salesmen reached level one and one reached level 2, the console should look something like:
John Johnsson, someDistrict, 33 items sold
Mary Mara, someOtherDistrict, 40 items sold
2 salesmen reached level 1
Judy Juggernut, anotherDistrict, 67 items sold
1 salesmen reached level 2
And it's the printing part in question that gives me trouble. When the user enters information a new object of a salesman-class is created and stored in an array of salesmen. The number of items sold for each salesman is then checked and each salesman is assigned a level. The array is then sorted using bubblesort, to have the salesman with the least amount of sales on salesmanArray[0] and so on.
Everything works fine until its time to print the results to the console. I tried to write a method for it:
public static void sortering(Salesman[] salesmenArray)
{
Salesman[] level1 = new Salesman[salesmenArray.Length];
Salesman[] level2 = new Salesman[salesmenArray.Length];
Salesman[] level3 = new Salesman[salesmenArray.Length];
Salesman[] level4 = new Salesman[salesmenArray.Length];
for (int i = 0; i < salesmenArray.Length - 1; i++)
{
if (salesmenArray[i].level == 1)
{
level1[i] = salesmenArray[i];
} else if (salesmenArray[i].level == 2)
{
level2[i] = salesmenArray[i];
} else if (salesmenArray[i].level == 3)
{
level3[i] = salesmenArray[i];
} else if (salesmenArray[i].level == 4)
{
level4[i] = salesmenArray[i];
}
}
if (level1.Length != 0)
{
for (int i = 0; i < level1.Length - 1; i++)
{
Console.WriteLine("Name: " + level1[i].name);
Console.WriteLine("District: " + level1[i].district);
Console.WriteLine("Items sold: " + level1[i].itemsSold);
}
Console.WriteLine("" + (level1.Length - 1) + " sellers have reached level 1");
}
//Same thing for level 2, 3 and 4
}
What I'm trying to do is 4 new arrays for the different levels. I then loop through the array with all the salesmen and place the salesmen into the arrays in accordance to the number of sold items. I then check if the level-arrays are empty. If they aren't, I loop through them printing out the name, district and items sold for each salesman. Finally also printing out how many sellers there are in each level. When running the program, I get an error on the line
Console.WriteLine("Name: " + level1[i].name);
Saying "System.NullReferenceException has been thrown "Object reference not set to an instance if an object".
I would assume that means level1[i].name isn't referencing to an object but I don't really know how to go from there... Any advice or pointers would be greatly appriciated!

You are getting a System.NullReferenceException because you are initializing the level arrays with the same length as the salesmen array, but you are only adding salesmen to the level arrays based on their level.
So there will be not initialized null elements in the level arrays, and when you try to access the name property of a null element, you get the exception because you try to read property of absent element.
To fix this, you may use List<Salesman> instead of Salesman[]. List<T> is a generic dynamic array and you can iterate over its items in the same way:
public static void sortering(Salesman[] salesmenArray)
{
var level1 = new List<Salesman>();
var level2 = new List<Salesman>();
var level3 = new List<Salesman>();
var level4 = new List<Salesman>();
for (int i = 0; i < salesmenArray.Length; i++)
{
if (salesmenArray[i].level == 1)
{
level1.Add(salesmenArray[i]);
}
else if (salesmenArray[i].level == 2)
{
level2.Add(salesmenArray[i]);
}
else if (salesmenArray[i].level == 3)
{
level3.Add(salesmenArray[i]);
}
else if (salesmenArray[i].level == 4)
{
level4.Add(salesmenArray[i]);
}
}
if (level1Count > 0)
{
for (int i = 0; i < level1.Count; i++)
{
Console.WriteLine("Name: " + level1[i].name);
Console.WriteLine("District: " + level1[i].district);
Console.WriteLine("Items sold: " + level1[i].itemsSold);
}
Console.WriteLine("" + level1Count + " sellers have reached level 1");
}
//Same thing for level 2, 3 and 4
}
Here is some other improvments then you can do with your code. For example if Salesman.level may contains only values form the list [1, 2, 3, 4] you can store levels in the List of List<Salesman> or in the array of List<Salesman> and add items in the easier way. Also string interpolation is an easier, faster, and more readable string concatenation syntax.
// here we creates a new array of lists and initialize it with 4 empty lists of Salesman
var levels = new List<Salesman>[]
{
new List<Salesman>(),
new List<Salesman>(),
new List<Salesman>(),
new List<Salesman>()
};
foreach(var salesmen in salesmenArray)
{
// (salesmen.level - 1)-th list stores salesmen with that level
levels[salesmen.level - 1].Add(salesmen);
}
// you can iterate salesmen of all levels with nested loops
for(int level = 0; level < levels.Lenth; level++)
{
foreach(var salesman in levels[level])
{
Console.WriteLine($"Name: {salesman.name}");
Console.WriteLine($"District: {salesman.district}");
Console.WriteLine($"Items sold: {salesman.itemsSold}");
}
// Count property gets the number of elements contained in the List<T> so you don't need to decrement this value for display the number of salesmen with this level
Console.WriteLine($"{levels[level].Count} sellers have reached level {level + 1}");
}
Finally there is an interesting mechanism to manipulate collections in .NET called LINQ. You can use LINQ syntax to select, filter, group and aggregate data. LINQ is readable efficiency and powerful tool. Here's a sample of your code rewritten with LINQ:
foreach(var group in salesmenArray
.GroupBy(salesman => salesman.level)
.OrderBy(groups => groups.Key))
{
foreach(var salesman in group)
{
Console.WriteLine($"Name: {salesman.name}");
Console.WriteLine($"District: {salesman.district}");
Console.WriteLine($"Items sold: {salesman.itemsSold}");
}
Console.WriteLine($"{group.Count()} sellers have reached level {group.Key}");
}

Where is the bubble sort? Sort the array first, then loop through the array with counters to count each level and print the output from the same loop.
// bubble sort
for (int i = 0; i < salesmenArray.Length; i++)
for (int j = 0; j < salesmenArray.Length - 1; j++)
if(salesmenArray[j].itemsSold > salesmenArray[j+1].itemsSold)
{
//swap positions
//...
}
int counter = 0;
int lastLevel = 1; //if 1 is the min level
for (int i = 0; i < salesmenArray.Length; i++)
{
if(salesmenArray[j].level != lastLevel)
{
//print summary
//...
counter = 0; //reset counter
}
// print detail lines
Console.WriteLine("Name: " + level1[i].name);
Console.WriteLine("District: " + level1[i].district);
Console.WriteLine("Items sold: " + level1[i].itemsSold);
counter++;
}
//print final summary for last level
//...
The ... are lines for you to fill.

Vadim's answer details why your code is failing. He proposed a way to solve the problem via Lists. I would also follow that road.
On the other hand, your approach was valid, but not very efficient and has a few traps for yourself (as Vadim mentioned, you are creating 4 level arrays with the same size of the total of salesmen, then you assign them to each level via i, leaving some null gaps). If you want your approach to work, in the printing for-loop, before getting level1[i].name, check that level1[i] is not null.
If you are using an IDE, I would recommend you to put a breakpoint inside the for-loop and see the contents of level1.
Good luck learning!

Related

Foreach loop not acting like I thought it would [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 2 years ago.
Improve this question
I have been working on on this problem for awhile I have two arrays in parallel that are the same length. I have nested foreach loops to compare the arrays so I can tell what number is greater. One number is randomly generated and the other is gathered from user input. The problem I'm having is the number that is randomly generated is being compared to each user input before moving to the next randomly generated number. This is my code:
Random r = new Random();
int length = 10;
int[] ranNum = new int[length];
int[] userNum = new int[length];
for (int i = 0; i < length; i++)
{
ranNum[i] = r.Next(1, 100);
}
for (int i = 0; i < length; i++)
{
Console.WriteLine("Enter a number between 1 and 100");
userNum[i] = Convert.ToInt32(Console.ReadLine());
}
foreach(int rn in ranNum)
{
foreach(int ui in userNum)
{
if (rn < ui)
{
Console.WriteLine(rn + " is less than " + ui);
}
else if (rn > ui)
{
Console.WriteLine(rn + " is greater than " + ui);
}
else
{
Console.WriteLine(rn + " is equal to " + ui);
}
}
}
I'm sure I'm missing something obvious any help would be appreciated Thank you in advance
Nested foreach loops are exactly like nested for loops in that they loop through each of the outer values with all of the inner values. If you're just trying to compare the values one-to-one, you'll need to iterate through both arrays at the same time. You can do this by grabbing their iterators and using a while loop; You can use LINQ and Zip both arrays; Or, you can use another for loop, like you used to generate your arrays, and use a common index to iterate through both loops:
for (int i = 0; i < length; i++) {
var rn = ranNum[i];
var ui = userNum[i];
if (rn < ui) {
Console.WriteLine(rn + " is less than " + ui);
} else if (rn > ui) {
Console.WriteLine(rn + " is greater than " + ui);
} else {
Console.WriteLine(rn + " is equal to " + ui);
}
}
Depending on what you're doing with these values, you might consider consolidating these loops, but this is how you would iterate through both at the same time.
There are TWO nested foreach loops here, which will create every possible combination of values. If you want to just match by index you can use a single for loop, or you can use the IEnumerable.Zip() function:
var results = ranNum.Zip(userNum, (rn, ui) => {
if (rn < ui) return $"{rn} is less than {ui}";
if (rn > ui) return $"{rn} is greater than {ui}";
return $"{rn} is equal to {ui}";
});
foreach (var result in results)
{
Console.WriteLine(result);
}

HackerRank Climbing the Leaderboard

This question has to do with this challenge on HackerRank. It seems to be failing some cases, but I'm not clear what's wrong with the algorithm (many people seem to have problem with timeouts, that's not an issue here, everything runs plenty fast, and all the cases that are visible to me pass, so I don't have a specific case that's failing).
The essential outline of how the algorithm works is as follows:
First be sure that Alice isn't already winning over the existing highest score (degenerate case), if she is just tell the world she's #1 from start to finish. Otherwise, at least one score on the leaderboard beats Alice's first try.
Start by walking down the scores list from the highest until we find a place where Alice fits in and record the scores that beat Alice's initial score along the way.
If we reach the end of the scores list before finding a place for Alice's bottom score, pretend there is a score at the bottom of the list which matches Alice's first score (this is just convenient for the main loop and reduces the problem to one where Alice's first score is on the list somewhere)
At this point we have a (sorted) array of scores with their associated ranks, rankAry[r - 1] is the minimum score needed for Alice to attain rank r as of the end of the if clause following the first while loop.
From there, the main algorithm takes over where we walk through Alice's scores and note her rank as we go by comparing against the benchmarks from the scores array that we setup as rankAry earlier. curRank is our candidate rank at each stage which we've definitely achieved by the time this loop starts (by construction).
If we're at rank 1 we will be forever more, so just populate the current rank as 1 and move on.
If we're currently tied with or beating the current benchmark and that's not the end of the line, keep peeking at the next one and if we're also beating that next one, decrease the current benchmark location and iterate
Once this terminates, we've found the one we're going to supplant and we cannot supplant anything further, so assign this rank to this score and repeat until done
As far as I can tell this handles all cases correctly, even if Alice has repeated values or increases between the benchmarks from scores, we should stay at the same rank until we hit the new benchmarks, but the site feedback indicates there must be a bug somewhere.
All the other approaches I've been able to find seem to be some variation on doing a binary search to find the score each time, but I prefer not having to constantly search each time and just use the auxiliary space, so I'm a little stumped on what could be off.
static int[] climbingLeaderboard(int[] scores, int[] alice) {
int[] res = new int[alice.Length];
if (scores.Length == 0 || alice[0] >= scores[0]) { //degenerate cases
for (int i = 0; i < alice.Length; ++i) {
res[i] = 1;
}
return res;
}
int[] rankAry = new int[scores.Length + 1];
rankAry[0] = scores[0]; //top score rank
int curPos = 1; //start at the front and move down
int curRank = 1; //initialize
//initialize from the front. This way we can figure out ranks as we go
while (curPos < scores.Length && scores[curPos] > alice[0]) {
if (scores[curPos] < scores[curPos-1]) {
rankAry[curRank] = scores[curPos]; //update the rank break point
curRank++; //moved down in rank
}
curPos++; //move down the array
}
if (curPos == scores.Length) { //smallest score still bigger than Alice's first
rankAry[curRank] = alice[0]; //pretend there was a virtual value at the end
curRank++; //give rank Alice will have for first score when we get there
}
for (int i = 0; i < alice.Length; ++i) {
if (curRank == 1) { //if we're at the top, we're going to stay there
res[i] = 1;
continue;
}
//Non-degenerate cases
while (alice[i] >= rankAry[curRank - 1]) {
if (curRank == 1 || alice[i] < rankAry[curRank - 2]) {
break;
}
curRank--;
}
res[i] = curRank;
}
return res;
}
You have a couple of bugs in your algorithm.
Wrong mapping
Your rankAry must map a rank (your index) to a score. However, with this line rankAry[0] = scores[0];, the highest score is mapped to 0, but the highest possible rank is 1 and not 0. So, change that to:
rankAry[1] = scores[0];
Wrong initial rank
For some reason, your curRank is set to 1 as below:
int curRank = 1; //initialize
However, it's wrong since your alice[0] is less than scores[0] because of the following block running at the beginning of your method:
if (scores.Length == 0 || alice[0] >= scores[0]) { //degenerate cases
for (int i = 0; i < alice.Length; ++i) {
res[i] = 1;
}
return res;
}
So, at best your curRank is 2. Hence, change it to:
int curRank = 2;
Then, you can also remove curRank++ as your curRank has a correct initial value from:
if (curPos == scores.Length) { //smallest score still bigger than Alice's first
rankAry[curRank] = alice[0]; //pretend there was a virtual value at the end
curRank++; // it's not longer needed so remove it
}
Improve "Non-degenerate cases" handling
Your break condition should consider rankAry at curRank - 1 and not curRank - 2 as it's enough to check the adjacent rank value. Also, a value at curRank - 2 will produce wrong results for some input but I won't explain for which cases specifically - I'll leave it up to you to find out.
Fixed Code
So, I fixed your method according to my comment above and it passed it all the tests. Here it is.
static int[] climbingLeaderboard(int[] scores, int[] alice) {
int[] res = new int[alice.Length];
if (scores.Length == 0 || alice[0] >= scores[0]) { //degenerate cases
for (int i = 0; i < alice.Length; ++i) {
res[i] = 1;
}
return res;
}
int[] rankAry = new int[scores.Length + 1];
rankAry[1] = scores[0]; //top score rank
int curPos = 1; //start at the front and move down
int curRank = 2; //initialize
//initialize from the front. This way we can figure out ranks as we go
while (curPos < scores.Length && scores[curPos] > alice[0]) {
if (scores[curPos] < scores[curPos-1]) {
rankAry[curRank] = scores[curPos]; //update the rank break point
curRank++; //moved down in rank
}
curPos++; //move down the array
}
if (curPos == scores.Length) { //smallest score still bigger than Alice's first
rankAry[curRank] = alice[0]; //pretend there was a virtual value at the end
}
for (int i = 0; i < alice.Length; ++i) {
if (curRank == 1) { //if we're at the top, we're going to stay there
res[i] = 1;
continue;
}
//Non-degenerate cases
while (alice[i] >= rankAry[curRank - 1]) {
if (curRank == 1 || alice[i] < rankAry[curRank - 1]) {
break;
}
curRank--;
}
res[i] = curRank;
}
return res;
}

Where is the flaw in my algorithm for consolidating gold mines?

The setup is that, given a list of N objects like
class Mine
{
public int Distance { get; set; } // from river
public int Gold { get; set; } // in tons
}
where the cost of moving the gold from one mine to the other is
// helper function for cost of a move
Func<Tuple<Mine,Mine>, int> MoveCost = (tuple) =>
Math.Abs(tuple.Item1.Distance - tuple.Item2.Distance) * tuple.Item1.Gold;
I want to consolidate the gold into K mines.
I've written an algorithm, thought it over many times, and don't understand why it isn't working. Hopefully my comments help out. Any idea where I'm going wrong?
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class Mine
{
public int Distance { get; set; } // from river
public int Gold { get; set; } // in tons
}
class Solution
{
static void Main(String[] args)
{
// helper function for reading lines
Func<string, int[]> LineToIntArray = (line) => Array.ConvertAll(line.Split(' '), Int32.Parse);
int[] line1 = LineToIntArray(Console.ReadLine());
int N = line1[0], // # of mines
K = line1[1]; // # of pickup locations
// Populate mine info
List<Mine> mines = new List<Mine>();
for(int i = 0; i < N; ++i)
{
int[] line = LineToIntArray(Console.ReadLine());
mines.Add(new Mine() { Distance = line[0], Gold = line[1] });
}
// helper function for cost of a move
Func<Tuple<Mine,Mine>, int> MoveCost = (tuple) =>
Math.Abs(tuple.Item1.Distance - tuple.Item2.Distance) * tuple.Item1.Gold;
// all move combinations
var moves = from m1 in mines
from m2 in mines
where !m1.Equals(m2)
select Tuple.Create(m1,m2);
// moves in ascending order of cost
var ordered = from m in moves
orderby MoveCost(m)
select m;
int sum = 0; // running total of move costs
var spots = Enumerable.Repeat(1, N).ToArray(); // spots[i] = 1 if hasn't been consildated into other mine, 0 otherwise
var iter = ordered.GetEnumerator();
while(iter.MoveNext() && spots.Sum() != K)
{
var move = iter.Current; // move with next smallest cost
int i = mines.IndexOf(move.Item1), // index of source mine in move
j = mines.IndexOf(move.Item2); // index of destination mine in move
if((spots[i] & spots[j]) == 1) // if the source and destination mines are both unconsolidated
{
sum += MoveCost(move); // add this consolidation to the total cost
spots[i] = 0; // "remove" mine i from the list of unconsolidated mines
}
}
Console.WriteLine(sum);
}
}
An example of a test case I'm failing is
3 1
11 3
12 2
13 1
My output is
3
and the correct output is
4
The other answer does point out a flaw in the implementation, but it fails to mention that in your code, you aren't actually changing the Gold values in the remaining Mine objects. So even if you did re-sort the data, it wouldn't help.
Furthermore, at each iteration all you really care about is the minimum value. Sorting the entire list of data is overkill. You can just scan it once to find the minimum-valued item.
You also don't really need the separate array of flags. Just maintain your move objects in a list, and after choosing a move, remove the move objects that include the Mine you would otherwise have flagged as no longer valid.
Here is a version of your algorithm that incorporates the above feedback:
static void Main(String[] args)
{
string input =
#"3 1
11 3
12 2
13 1";
StringReader reader = new StringReader(input);
// helper function for reading lines
Func<string, int[]> LineToIntArray = (line) => Array.ConvertAll(line.Split(' '), Int32.Parse);
int[] line1 = LineToIntArray(reader.ReadLine());
int N = line1[0], // # of mines
K = line1[1]; // # of pickup locations
// Populate mine info
List<Mine> mines = new List<Mine>();
for (int i = 0; i < N; ++i)
{
int[] line = LineToIntArray(reader.ReadLine());
mines.Add(new Mine() { Distance = line[0], Gold = line[1] });
}
// helper function for cost of a move
Func<Tuple<Mine, Mine>, int> MoveCost = (tuple) =>
Math.Abs(tuple.Item1.Distance - tuple.Item2.Distance) * tuple.Item1.Gold;
// all move combinations
var moves = (from m1 in mines
from m2 in mines
where !m1.Equals(m2)
select Tuple.Create(m1, m2)).ToList();
int sum = 0, // running total of move costs
unconsolidatedCount = N;
while (moves.Count > 0 && unconsolidatedCount != K)
{
var move = moves.Aggregate((a, m) => MoveCost(a) < MoveCost(m) ? a : m);
sum += MoveCost(move); // add this consolidation to the total cost
move.Item2.Gold += move.Item1.Gold;
moves.RemoveAll(m => m.Item1 == move.Item1 || m.Item2 == move.Item1);
unconsolidatedCount--;
}
Console.WriteLine("Moves: " + sum);
}
Without more detail in your question, I can't guarantee that this actually meets the specification. But it does produce the value 4 for the sum. :)
When you consolidate mine i into mine j, the amount of gold in the mine j is increased. This makes consolidations from mine j to other mines more expensive potentially making the ordering of the mines by the move cost invalid. To fix this, you could re-sort the list of mines at the beginning of each iteration of your while-loop.

How to count multiple items in SteamBot

In Jassecar's SteamBot, is there a way to count items of different defindexes and add them up?
I tried this:
switch(message.ToLower())
{
case "ticket":
foreach (ulong id in Trade.OtherOfferedItems)
{
int totalScrap = 0;
Trade.SendMessage("Please pay 3.44 ref");
var items = Trade.OtherOfferedItems;
var itemType = Trade.OtherInventory.GetItem(id);
if (itemType.Defindex == 5002)
{
totalScrap = items.Count * 9;
}
else if (itemType.Defindex == 5001)
{
totalScrap = items.Count * 3;
}
else if (itemType.Defindex == 5000)
{
totalScrap = items.Count;
}
Trade.RemoveAllItems();
if (totalScrap > 31)
{
Trade.AddItemByDefindex(725);
int Change = 31 - totalScrap;
while(Change > 0)
{
Trade.AddItemByDefindex(5000);
Change - 1;
}
}
else
{
Trade.SendMessage("You have only added a total of " + totalScrap + " Scrap, please put up the correct amount and type ticket again");
}
}
break;
But it will count 1 Scrap (item Defindex of 5000) and 1 Refined Metal (item Defindex of 5002) as both 9 and say to the user he has added a total of 18 scrap where he as only added 10. (1 refined = 9 Scrap)
You're iterating over id's in Trade.OtherOfferedItems.
Error#1
On every iteration of the loop you're clearing totalScrap, by setting it to 0.
Error#2
In here:
`totalScrap = items.Count * 9`
you're saying "the total amount of scrap is the ammount of my items multiplied by 9" which is wrong, because if you've got 2 items with possibly different Defindexes (5002 and 5000 in your case), it gives you 18.
Error#3
Then you've got:
Trade.RemoveAllItems();
which I suppose will remove all the items from the collection you're actually iterating over - I'm really amazed that your loop doesn't crash.
Put the totalScrap outside. Iterate over every item and THEN do all the clearing, removing and checking whether the sum is>31 or not.

Card Game without using arrays

We have a task to create a random hand of cards (5 cards). Where the cards can not be the same. We have yet not learnd how to use arrays so it would be nice if anyone could help us to get started without using arrays.
This is how we have started, but we can not figure out how to not get the same card twice.
static void Cards()
{
var rnd = new Random();
var suit, rank, count = 0;
while (count < 5)
{
rank = rnd.Next(13) + 1;
suit = rnd.Next(4) + 1;
if (suit == 1)
{
Console.WriteLine("Spader " + rank);
}
else if (suit == 2)
{
Console.WriteLine("Hjärter " + rank);
}
else if (suit == 3)
{
Console.WriteLine("Ruter " + rank);
}
else
{
Console.WriteLine("Klöver " + rank);
}
count++;
}
}
Thanks!
This is the sort of problem that arrays can deal with, so it would be easier to learn how to use them. Without them you need to store your 5 cards in variables (string card1, string card2, etc) then on each iteration check to see if the card matches any of these and discard it if it does, else save it. But then you have a whole bunch of conditional code to see which variable to store it in...
Much easier to just have an array
string[] cards = new string[5];
then you can just loop over the array looking for a match (something like this)
for(int idx=0; idx<5; idx++){
if(cards[idx]==thecardyouhavejustcreatedwithrandomcode){
break; //bail out of the for on a match
}
cards[iAntalKort]=thecardyouhavejustcreatedwithrandomcode;
}

Categories

Resources