Tigris
Tigris

Reputation: 129

C# Array.FindIndex index of current object

I'm doing my school homework which is just a basic programming exercise, and at the minute I'm stuck. I tried to solve everything as simple and as elegant as possible, whereas my classmates just used a bunch of for loops, which I consider disgusting. I could do the same, that wouldn't be a challange for me, but I think I should do the way it helps me improve more.

So the task is we have a txt file with some data about radio programmes. The first line tells the number of all the songs played, and each line after that consists of four items, the number of the radio channel, the length of the song (minutes and seconds) and the name of the song. We have to be able to read it, and answer some questions based on it. I'm stuck at the following:

"Tell exactly how much time went past since the beginning and the end of the first and last Eric Clapton song on radio channel #1."

The file looks like this:

677 
1 5 3 Deep Purple:Bad Attitude
2 3 36 Eric Clapton:Terraplane Blues
3 2 46 Eric Clapton:Crazy Country Hop
3 3 25 Omega:Ablakok
2 4 23 Eric Clapton:Catch Me If You Can
1 3 27 Eric Clapton:Willie And The Hand Jive
3 4 33 Omega:A szamuzott
2 6 20 Eric Clapton:Old love
...

I read it using this code:

const int N_COL = 4;
const int N_ROW = 1000;

string[][] RDATA = new string[N_COL][];

for (int i = 0; i < N_COL; i++)
    RDATA[i] = new string[N_ROW];

using (StreamReader sr = new StreamReader("musor.txt"))
{
    int n_lines = Convert.ToInt32(sr.ReadLine());

    for (int i = 0; i < n_lines; i++)
    {
        int idx = 0;
        foreach (string s in (sr.ReadLine().Split(new char[] {' '}, 4)))
            RDATA[idx++][i] = s;
    }
}

And this is how I tried to answer the question:

int[] first = new int[] { Array.FindIndex(RDATA[3], x => x.Contains("Eric Clapton")), 0 };
int[] last = new int[] { Array.FindLastIndex(RDATA[3], RDATA[3].Count(x => x != null) - 1, x => x.Contains("Eric Clapton")), 0 }; 

for (int i = 0; i < first[0]; i++)
    if (RDATA[0][i] == RDATA[0][first[0]])
        first[1] += Convert.ToInt32(RDATA[1][i]) * 60 + Convert.ToInt32(RDATA[2][i]);

for (int i = 0; i <= last[0]; i++)
    if (RDATA[0][i] == RDATA[0][last[0]])
        last[1] += Convert.ToInt32(RDATA[1][i]) * 60 + Convert.ToInt32(RDATA[2][i]);

Console.WriteLine("\nDifference: {0}", TimeSpan.FromSeconds(last[1] - first[1]));

Well, it works perfectly, but the problem is, it's not the answer for the question above, it only answers how much time went past in the whole section, on all three channels. How could I implement that to search for only the ones on channel #1? Is it possible to get the current index of x (object) in the FindIndex method itself? Should I use LINQ instead?

Please help me, I found it so clean with these one line methods, and now I'm facing a problem I can not solve. I have no idea either.

Upvotes: 3

Views: 701

Answers (3)

Olivier Jacot-Descombes
Olivier Jacot-Descombes

Reputation: 112259

I appreciate your effort to make it better than just writing a few loops. However, there is still room for improvement.

Your logic would become clearer if you stored your songs in an appropriate class

public class Song
{
    public int Channel { get; set; }
    public TimeSpan Length { get; set; }
    public string Author { get; set; }
    public string Name { get; set; }
}

I would then store the songs in an array of Song, since you know the exact number of records in advance. Otherwise a List<Song> would be more appropriate.

int n_lines = Convert.ToInt32(sr.ReadLine());
var songs = new Song[n_lines];
for (int i = 0; i < n_lines; i++) {
    string line = sr.ReadLine();
    string[] columns = line.Split(new char[] { ' ' }, 4);
    string[] subcolumns = columns[3].Split(':');
    int minutes = Convert.ToInt32(columns[1]);
    int seconds = Convert.ToInt32(columns[2]);
    songs[i] = new Song {
        Channel = Convert.ToInt32(columns[0]),
        Length = new TimeSpan(0, minutes, seconds),
        Author = subcolumns[0],
        Name = subcolumns[1]
    };
}

Now we can formulate the query much easier:

int lastSongIndex = Array.FindLastIndex(songs, 
    s => s.Channel == 1 && s.Author == "Eric Clapton");
int totalSeconds = songs
    .SkipWhile(s => s.Channel != 1 || s.Author != "Eric Clapton")
    .Select((s, i) => new { Song = s, Index = i })
    .TakeWhile(x => x.Index <= lastSongIndex)
    .Sum(x => (int)x.Song.Length.TotalSeconds);

LINQ has an overload of Select which provides the index of the item as second parameter. From this we have to construct an anonymous type which contains a song as well as the index. Then we can stop summing up until we reach the index of the last song. However, we still need Array.FindLastIndex in order to find it.


LINQ does not make the life much easier here, since we need to consider songs beginning and ending with specific songs. This requires quite a few tricks with LINQ because LINQ processes the items one by one in a strict sequence. Therefore, a good old for-loop would be very appropriate in this case.

Upvotes: 2

Jake Rote
Jake Rote

Reputation: 2247

What about this is simple readable code, is it what your after? Storing it in an object makes it easier to read and to query for data.

internal class Program
{
    private static readonly Regex LineMatch = new Regex( @"(\d+) (\d+) (\d+) (.*)", RegexOptions.Compiled );

    private static void Main( string[] args )
    {
        var filePath = @"";
        var songPlays = File.ReadAllLines( filePath ).Select( GetSongPlay );

        var totalTime = songPlays.Where( x => x.RadioStation == 1 && x.SongName.Contains( "Eric Clapton" ) ).Aggregate( new TimeSpan( 0 ), ( timeSpan, songPlay ) => timeSpan.Add( songPlay.TimeSpan ) );
    }

    private static SongPlay GetSongPlay( string arg )
    {
        var match = LineMatch.Match( arg );
        return new SongPlay
        {
            RadioStation = Convert.ToInt32( match.Groups[ 1 ].Value ),
            SongName = match.Groups[ 4 ].Value,
            TimeSpan = new TimeSpan( 0, Convert.ToInt32( match.Groups[ 2 ].Value ), Convert.ToInt32( match.Groups[ 3 ].Value ) )
        };
    }
}

public class SongPlay
{
    public int RadioStation { get; set; }
    public TimeSpan TimeSpan { get; set; }
    public string SongName { get; set; }
}

Upvotes: 1

ProgrammingLlama
ProgrammingLlama

Reputation: 38727

    public Form1()
    {
        InitializeComponent();

        List<RadioProgrammeDetails> Programmes = new List<RadioProgrammeDetails>();
        using (StreamReader sr = new StreamReader("musor.txt"))
        {
            int n_lines = Convert.ToInt32(sr.ReadLine());

            for (int i = 0; i < n_lines; i++)
            {
                string [] data = sr.ReadLine().Split(new char[] { ' ' }, 4);
                int channel = 0;
                int minutes = 0;
                int seconds = 0;
                string artist = "";
                string song = "";

                int.TryParse(data[0], out channel);
                int.TryParse(data[1], out minutes);
                int.TryParse(data[2], out seconds);
                string[] artistSong = data[3].Split(new char[] { ':' });
                artist = artistSong[0];
                song = artistSong[1];

                Programmes.Add(new RadioProgrammeDetails() { Artist = artist, SongName = song, Channel = channel, Length = new TimeSpan(0, minutes, seconds) });
            }

            var radioOne = Programmes.Where(x => x.Channel == 1);
            double elapsedSeconds = radioOne.SkipWhile(x => x.Artist != "Eric Clapton").Reverse().SkipWhile(x=>x.Artist!="Eric Clapton").Sum(x=>x.Length.TotalSeconds);
            Console.WriteLine(elapsedSeconds);
        }

    }

    public class RadioProgrammeDetails
    {
        public int Channel { get; set; }
        public TimeSpan Length { get; set; }
        public string Artist { get; set; }
        public string SongName { get; set; }
    }

Upvotes: 1

Related Questions