Example of Ruby Programming

by Matt Heffernan

In order to satisfy my desire to combine my passions of football and programming, I developed a simple formula for ranking the teams of the NFL. This was in the mid-90s, and the best available tool I had for my PC was Microsoft Visual C++. I created a program to work out my formula and generate the results to standard output, which could then be captured to a file and posted on my website. Below is the last revision I made in C (in 1999). Please note that the conferences were kept track of, but never used in the program.


 /* nflrank99.c */

 #include <stdio.h>
 #include <string.h>

 #define NUM_TEAMS    31

 #define AFC_EAST     0
 #define AFC_WEST     1
 #define AFC_CENTRAL  2
 #define NFC_EAST     3
 #define NFC_WEST     4
 #define NFC_CENTRAL  5

 #define TOKEN_SEPS " \t\n"

 void ParseTeams();
 void RateTeams();
 void SortTeams();
 void PrintTeams();

 int GetTeamByName(char *strName);

 struct Victory
 {
    int nTeam;
    Victory *next;
 };

 struct Team
 {
    int nConf, nWins, nLosses, nWeight;
    Victory *victories;
    char szName[21];
 };

 Team aTeams[NUM_TEAMS] = 
 {
    { NFC_EAST,    0,0,0, NULL, "Arizona Cardinals   " },
    { NFC_WEST,    0,0,0, NULL, "Atlanta Falcons     " },
    { AFC_CENTRAL, 0,0,0, NULL, "Baltimore Ravens    " },
    { AFC_EAST,    0,0,0, NULL, "Buffalo Bills       " },
    { NFC_WEST,    0,0,0, NULL, "Carolina Panthers   " },
    { NFC_CENTRAL, 0,0,0, NULL, "Chicago Bears       " },
    { AFC_CENTRAL, 0,0,0, NULL, "Cincinnati Bengals  " },
    { AFC_CENTRAL, 0,0,0, NULL, "Cleveland Browns    " },
    { NFC_EAST,    0,0,0, NULL, "Dallas Cowboys      " },
    { AFC_WEST,    0,0,0, NULL, "Denver Broncos      " },
    { NFC_CENTRAL, 0,0,0, NULL, "Detroit Lions       " },
    { NFC_CENTRAL, 0,0,0, NULL, "Green Bay Packers   " },
    { AFC_EAST,    0,0,0, NULL, "Indianapolis Colts  " },
    { AFC_CENTRAL, 0,0,0, NULL, "Jacksonville Jaguars" },
    { AFC_WEST,    0,0,0, NULL, "Kansas City Chiefs  " },
    { AFC_EAST,    0,0,0, NULL, "Miami Dolphins      " },
    { NFC_CENTRAL, 0,0,0, NULL, "Minnesota Vikings   " },
    { AFC_EAST,    0,0,0, NULL, "New England Patriots" },
    { NFC_WEST,    0,0,0, NULL, "New Orleans Saints  " },
    { NFC_EAST,    0,0,0, NULL, "New York Giants     " },
    { AFC_EAST,    0,0,0, NULL, "New York Jets       " },
    { AFC_WEST,    0,0,0, NULL, "Oakland Raiders     " },
    { NFC_EAST,    0,0,0, NULL, "Philadelphia Eagles " },
    { AFC_CENTRAL, 0,0,0, NULL, "Pittsburg Steelers  " },
    { NFC_WEST,    0,0,0, NULL, "St. Louis Rams      " },
    { AFC_WEST,    0,0,0, NULL, "San Diego Chargers  " },
    { NFC_WEST,    0,0,0, NULL, "San Francisco 49ers " },
    { AFC_WEST,    0,0,0, NULL, "Seattle Seahawks    " },
    { NFC_CENTRAL, 0,0,0, NULL, "Tampa Bay Buccaneers" },
    { AFC_CENTRAL, 0,0,0, NULL, "Tennessee Titans    " },
    { NFC_EAST,    0,0,0, NULL, "Washington Redskins " }
 };

 int order[NUM_TEAMS];

 void main ()
 {
    ParseTeams();
    RateTeams();
    SortTeams();
    PrintTeams();
 }


 int GetTeamByName(char *strName)
 {
    if (!strcmp(strName, "ari"))
       return 0;

    if (!strcmp(strName, "atl"))
       return 1;

    if (!strcmp(strName, "bal"))
       return 2;

    if (!strcmp(strName, "buf"))
       return 3;

    if (!strcmp(strName, "car"))
       return 4;

    if (!strcmp(strName, "chi"))
       return 5;

    if (!strcmp(strName, "cin"))
       return 6;

    if (!strcmp(strName, "cle"))
       return 7;

    if (!strcmp(strName, "dal"))
       return 8;

    if (!strcmp(strName, "den"))
       return 9;

    if (!strcmp(strName, "det"))
       return 10;

    if (!strcmp(strName, "gbp"))
       return 11;

    if (!strcmp(strName, "ind"))
       return 12;

    if (!strcmp(strName, "jax"))
       return 13;

    if (!strcmp(strName, "kcc"))
       return 14;

    if (!strcmp(strName, "mia"))
       return 15;

    if (!strcmp(strName, "min"))
       return 16;

    if (!strcmp(strName, "nep"))
       return 17;

    if (!strcmp(strName, "nos"))
       return 18;

    if (!strcmp(strName, "nyg"))
       return 19;

    if (!strcmp(strName, "nyj"))
       return 20;

    if (!strcmp(strName, "oak"))
       return 21;

    if (!strcmp(strName, "phi"))
       return 22;

    if (!strcmp(strName, "pit"))
       return 23;

    if (!strcmp(strName, "stl"))
       return 24;

    if (!strcmp(strName, "sdc"))
       return 25;

    if (!strcmp(strName, "sff"))
       return 26;

    if (!strcmp(strName, "sea"))
       return 27;

    if (!strcmp(strName, "tbb"))
       return 28;

    if (!strcmp(strName, "ten"))
       return 29;

    if (!strcmp(strName, "was"))
       return 30;

    return NUM_TEAMS;
 }


 void ParseTeams()
 {
    FILE *f;
    Victory *last, *temp;
    char teamLine[70];
    char *teamName;
    int i;

    f = fopen("nflteams.txt", "r");
    for (i=0; i<NUM_TEAMS; i++)
    {
       last = NULL;
       fgets(teamLine,70,f);
       teamName = strtok(teamLine, TOKEN_SEPS); /* get team name */
       teamName = strtok(NULL, TOKEN_SEPS); /* get first victory */
       while (teamName != NULL)
       {
          temp = new Victory;
          temp->nTeam = GetTeamByName(teamName);
          temp->next = last;
          aTeams[i].nWins++;
          aTeams[temp->nTeam].nLosses++;
          last=temp;
          teamName = strtok(NULL, TOKEN_SEPS); /* get next victory */ 
       }
       aTeams[i].victories = last;
    }

    fclose(f);
 }


 void RateTeams()
 {
    Victory *temp;
    int i;

    for (i=0; i<NUM_TEAMS; i++)
    {
       temp = aTeams[i].victories;
       while (temp != NULL)
       {
          aTeams[i].nWeight += (1 + aTeams[temp->nTeam].nWins);
          temp = temp->next;
       }
    }
 }


 void SortTeams()
 {
    int i, j, last, next;

    for (i=1; i<NUM_TEAMS; i++)
    {
       j=0;

       while ((j<i) && (aTeams[i].nWeight <= aTeams[order[j]].nWeight)) 
       {
          j++;
       }

       last = i;
       while (j<i)
       {
          next = order[j];
          order[j] = last;
          last = next;
          j++;
       }
       order[j] = last;
    }
 }


 void PrintTeams()
 {
    int i, rank, lastWeight;

    printf("Rank Team                 Record  Weight\n");
    rank = 0;
    lastWeight = 0;
    for (i=0;i<NUM_TEAMS;i++)
    {
       if (aTeams[order[i]].nWeight != lastWeight)
       {
          rank = i+1;
          lastWeight = aTeams[order[i]].nWeight;
       }

       printf("%2d.  %s %2d-%2d   %3d\n",
          rank, aTeams[order[i]].szName, aTeams[order[i]].nWins,
          aTeams[order[i]].nLosses, lastWeight);
    }
 }
Fig. 1: nflrank99.c

The program took an input file like the one below, which lists all the wins each team gets. The first acronym on each line stands for a team, and the acronyms that follow represent each of the teams it has beaten (which occasionally happens twice for a particular team).


 ari dal cle was
 atl sff car car
 bal pit jax cin cle jax cin ten dal cle
 buf ten gbp sdc nyj nep chi kcc
 car sff sea sff stl gbp
 chi gbp ind tbb
 cin den cle
 cle cin pit nep
 dal was car ari cin
 den atl oak sdc cle nyj oak sdc sea
 det nos was chi gbp tbb atl nyg nep
 gbp phi ari sff min ind
 ind kcc jax buf sea nep det nyj
 jax cle cin dal pit
 kcc sdc den sea stl sea
 mia sea bal nep cin buf gbp det sdc ind
 min chi mia nep det tbb chi buf ari car dal 
 nep den ind cin
 nos sdc chi car atl ari sff car stl
 nyg ari phi chi atl dal phi cle ari
 nyj gbp nep buf tbb nep mia mia chi
 oak sdc ind cle sff kcc sea sdc kcc nos atl
 phi dal nos atl ari chi dal pit ari was
 pit jax nyj cin cle bal cin
 stl den sea sff atl sdc atl sff nyg
 sdc kcc
 sff dal ari kcc atl
 sea nos sdc sdc jax
 tbb nep chi det min atl gbp buf
 ten kcc pit nyg cin jax bal was pit cle jax
 was car nyg tbb phi bal jax stl
Fig. 2: nflteams.txt

This resulted in the following output.


 Rank Team                 Record  Weight
  1.  Tennessee Titans     10- 2    64
  2.  Minnesota Vikings    10- 2    62
  3.  Washington Redskins   7- 5    57
  4.  Miami Dolphins        9- 3    55
  5.  New York Jets         8- 4    54
  6.  Detroit Lions         8- 4    52
  7.  Oakland Raiders      10- 2    51
  8.  Philadelphia Eagles   9- 4    50
  9.  Denver Broncos        8- 4    48
 10.  Baltimore Ravens      9- 4    47
 11.  Indianapolis Colts    7- 5    46
 11.  Tampa Bay Buccaneers  7- 5    46
 13.  New York Giants       8- 4    45
 14.  St. Louis Rams        8- 4    43
 15.  Buffalo Bills         7- 5    42
 16.  New Orleans Saints    8- 4    40
 17.  Green Bay Packers     5- 7    38
 18.  Pittsburg Steelers    6- 6    34
 19.  Carolina Panthers     5- 7    30
 19.  Kansas City Chiefs    5- 7    30
 21.  Chicago Bears         3- 9    22
 22.  Dallas Cowboys        4- 8    21
 23.  New England Patriots  3- 9    20
 24.  Jacksonville Jaguars  4- 8    19
 24.  San Francisco 49ers   4- 8    19
 26.  Seattle Seahawks      4- 8    18
 27.  Arizona Cardinals     3- 9    17
 27.  Atlanta Falcons       3-10    17
 29.  Cleveland Browns      3-10    14
 30.  Cincinnati Bengals    2-10    13
 31.  San Diego Chargers    1-11     6
Fig. 3: Output from nflrank99.c (Fig. 1) given input of the nflteams.txt of Fig. 2

Unfortunately, this implementation was rather rigid, making it less flexible when the league experienced changes, like teams moving and expansion. Eventually, I came around to Ruby, a new scripting language that made text processing like this much easier and more flexible. My first significant goal in learning this language was to re-write this program with Ruby to use the same input and be able to get the same output on any platform, including a webserver (if only my server allowed Ruby scripts, but that's another issue). Besides, I had to add the Houston Texans to the mix, so the program had to be updated anyway.

The thing with Ruby is, you can't get away with half-assed object orientation like I did in the C program. So, the first step in creating this program was to define a Team class, shown below.


 #team.rb - Team class for NFLRank 

 class Team
   attr_reader :wins
   attr_accessor :losses
   attr_accessor :points
   attr_reader :name
   attr_reader :winString

   def initialize(nm, ws)
     @wins = ws.split(" ").length 
     @losses = 0
     @points = 0
     @name = nm
     @winString = ws
   end
 end
Fig. 4: team.rb

Simple enough. Each Team object, as with the Team struct in the C program, is responsible for keeping track of its own statistics. However, Ruby is so loosely typed, classes can be defined by whoever creates an object of that class. Team.name and Team.winString end up being strings in my implementation, but it doesn't have to be that way, so the Team class doesn't have to worry about allocation and clean-up of any string buffers.

Moving on, a League class is required to keep track of all the teams, and (in this case) to count up all the wins and losses between the teams in the league. Below is the implementation, which uses the same nflteams.txt input file as shown in Fig. 2.


 #league.rb - League class for NFLRank

 class League
   attr_reader :arTeams

   def findTeam(name)
      for t in @arTeams
        if t.name == name
          break
        end
      end
      t
   end   

   def initialize
      f = File.open("nflteams.txt")
      first = true
      while line = f.gets
        line.chomp!
        if (line.length > 0) && (line =~ /\S/) 
          name = line[0..2]
          wins = line[4..-1]
	         if wins == nil
	           wins = " "
	         end
          t = Team.new(name,wins)
          if first
            @arTeams = [t]
	           first = false
          else
            @arTeams += [t]
          end
        end
      end
      f.close
     
      for t in @arTeams
	       arWins = t.winString.split(" ")
	       for win in arWins
	         loser = findTeam(win)
          t.points += 1 + loser.wins
          loser.losses += 1
	       end
      end
    end
 end
Fig. 5: league.rb

Notice again, because this is an interpreted scripting language, there is no explicit inclusion of team.rb in league.rb. It's up to whoever declares a League object to include the desired Team object as well. Whether that's convenient or lazy programming is up to the reader. In either case, the program works and is flexible -- more on that later.

Now, we will come to the actual script that is executed, which defines what acronyms will be used for what teams, and then sorts them by their point value and prints the results to standard output. That's all the below script does, as it leaves most of the heavy lifting to the instantiation of the League object.


 #nflrank.rb - Main executing script for NFLRank 2004

 load "team.rb"
 load "league.rb"

 nameHash = {
   "ari" => "Arizona Cardinals     ",
   "atl" => "Atlanta Falcons       ",
   "bal" => "Baltimore Ravens      ",
   "buf" => "Buffalo Bills         ",
   "car" => "Carolina Panthers     ",
   "chi" => "Chicago Bears         ",
   "cin" => "Cincinnati Bengals    ",
   "cle" => "Cleveland Browns      ",
   "dal" => "Dallas Cowboys        ",
   "den" => "Denver Broncos        ",
   "det" => "Detroit Lions         ",
   "gbp" => "Green Bay Packers     ",
   "hou" => "Houston Texans        ",
   "ind" => "Indianapolis Colts    ",
   "jax" => "Jacksonville Jaguars  ",
   "kcc" => "Kansas City Chiefs    ",
   "mia" => "Miami Dolphins        ",
   "min" => "Minnesota Vikings     ",
   "nep" => "New England Patriots  ",
   "nos" => "New Orleans Saints    ",
   "nyg" => "New York Giants       ",
   "nyj" => "New York Jets         ",
   "oak" => "Oakland Raiders       ",
   "phi" => "Philadelphia Eagles   ",
   "pit" => "Pittsburgh Steelers   ",
   "stl" => "St. Louis Rams        ",
   "sdc" => "San Diego Chargers    ",
   "sff" => "San Francisco 49ers   ",
   "sea" => "Seattle Seahawks      ",
   "tbb" => "Tampa Bay Buccaneers  ",
   "ten" => "Tennessee Titans      ",
   "was" => "Washington Redskins   "
   }

 nfl = League.new
 nfl.arTeams.sort! {|a,b| b.points <=> a.points}
 print "Rank  Team                  Record  Weight\n"
 rank = 0
 i = 0
 lastPoints = 0
 for t in nfl.arTeams
   i += 1
   if t.points != lastPoints
      rank = i;
      lastPoints = t.points;
   end
   printf "%3d.  %s%2d-%2d   %3d\n", rank, nameHash[t.name], t.wins, t.losses, t.points 
 end
Fig. 6: nflrank.rb

And that's it. Three files, but a lot less code, which is now much more portable and flexible. Adding a new team to the mix is as easy as adding an entry to the nameHash declaration. The only resposibility lies in the creation of a sound nflteams.txt file, in which the correct acronyms are used. As long as the lines in the input are in alpahabetical order by team name, the output will list equally weighted teams alphabetically, just as before. Since I'm the only one creating these input files, I can be confident that nobody sees a bad output. I leave error checking to programs I put in the wild.

So, I accomplished my goal with this program. It's proven flexible enough to use it for ranking college football, after I came up with a new hash table and a slightly modified points system, the latter of which was simply a preference of mine. Indeed, one could use this same ranking system for any team sport. In the case of ties (which actually happened twice in one season since I've been doing this), I just pretend the game never happened. Soccer fans might not be satisfied, but they have their own crazy ranking system to deal with. Anyway, I hope you enjoyed seeing this example of Ruby in action and that you check out my rankings this season.


©2004 Matt Heffernan
Please don't claim this code as your own. Use it all you like, just give me credit, mmmkay?