11 minute read
article
dotnet
csharp
microsoft
advent
Comments
Update: December 18, 2022: Per the comment on this post from James Curran, I updated
the post and the code in GitHub.
While surfing around GitHub recently, looking for repos related to
Christmas, I ran across this and was immediately intrigued.
After reading through it, I was inspired and decided to use the idea for one of my
two 2022 Advent posts! Check out the post I wrote for the .NET Advent calendar on December 10.
The gist of this is to take a data file that contains the bare minimum information
necessary to programmatically generate all 12 verses of
The 12 Days of
Christmas! It includes some interesting challenges, but for my version,
I'm going to simplify things a bit.
In this post, I'll walk you through the code and talk about some of the
things I did to overcome the challenges presented in the exercise.
The requirements
Load the data needed for the song from this data file
Day
Day.in.Words
Gift.Item
Verb
Adjective
Location
1
first
partridge
NA
NA
in a pear tree
2
second
dove
NA
turtle
NA
3
third
hen
NA
french
NA
4
fourth
bird
NA
calling
NA
5
fifth
ring
NA
golden
NA
6
sixth
goose
a-laying
NA
NA
7
seventh
swan
a-swimming
NA
NA
8
eighth
maid
a-milking
NA
NA
9
ninth
lady
dancing
NA
NA
10
tenth
lord
a-leaping
NA
NA
11
eleventh
piper
piping
NA
NA
12
twelfth
drummer
drumming
NA
NA
Write a function to pluralize a word.
stringpluralizeWord(stringword);
All the gifts in the file are singular and must be pluralized.
"For example, the word "rings" should not appear anywhere in
the function. I should be able to give it any gift and get back the plural of
that gift." The function should be able to handle any word, not just the gifts
listed in the file.
Write a function that takes in the necessary data from the data file and outputs
the line(s) for a single day. Remember, each day looks like this:
On the {nth} day of Christmas my true love gave to me
{gift}
Use the data in the file to generate the 12 verses of the song.
The Code
To get started, create a new folder for the console application
and then change into it:
mkdir 12days
cd 12days
Now create the project
dotnet new console -o 12days.console
With the project created, change into the 12days.console directory and open program.cs in
your editor of choice!
Requirement number one is to read the data file in so we can use it later to generate
the verses. In my solution, I did the simplest thing that works. Before getting into
the requirements, I removed the "hello, world" and replaced it with
Console.WriteLine("The 12 Days of Christmas");Console.WriteLine();
and then this bit of code to read in all the lines of the file into an array
string[]lines=File.ReadAllLines("xmas.csv");
I'll talk about using the data a bit later in the post.
Pluralizing words
The next challenge to be faced with this project is the idea of pluralizing words.
The English language is full of strange, inconsistent rules, and pluralizing isn't
as simple as tacking on an 's' or 'es' to a word.
stringpluralizeWord(stringword){// what goes here?}
I Googled and found a decent reference for
pluralizing words and wrote the following code.
stringpluralize(stringword){if(specialWords.TryGetValue(word,outstringspecialWord)){returnspecialWord;}// -s, -x, -sh, -ch, -ss or -zif(word.EndsWith("s")||word.EndsWith("x")||word.EndsWith("sh")||word.EndsWith("ch")||word.EndsWith("ss")||word.EndsWith("z")){returnword+"es";}if(word.EndsWith("y")){// example: ladyvarnextToLast=word[^2];if(!vowels.Contains(nextToLast)){returnword[..^1]+"ies";}else{returnword+"s";}}returnword+"s";}
This code works for all of the gifts in the data file, but anything beyond that and
I offer no guarantees.
Because English has these strange rules, there are some words that don't have
any rules for pluralization so you just need to "know" what the plural is.
Goose is a great example! Child is another. Because of that, I created a simple
dictionary to handle that particular case (allowing for the addition of other words):
I also added this to support my pluralization code:
// per a suggestion from James Curran in the comments, // simplifying this.varvowels="aeiou";
Is that cheating? Well, requirement number 2 says we can't hard-code words, but unless
one of my readers can tell me a way to handle special words like "goose", then I'm
going with "it works", and I'm ok with it. If you want a more reliable,
robust solution for pluralizing words, you can make life easy by using a library:
The code for Pluralizer.NET is on GitHub, and
is actually a nice library that allows you to plugin your own rules as well as using the
built-in rules. If you do look at the code for Pluralize.NET, pay attention to how *it*
handles words like "goose"!
Reading the data and constructing verses
The data is loaded into a string array named "lines", there's a method to pluralize words, so now it's time
to start generating the verses! Before jumping into the code, I want to walk through an approach so
there's a map for getting from "data is loaded" to the full 12 verses.
Since the data in the file is ordered, I want to iterate over the lines in the file (skipping the first),
building up each verse, and then displaying what's been generated.
At a high level, this is what I want to do:
for each line
split into the elements
pass the elements to a method that will construct the output for a single day
build up the verse
output
Starting with a loop, split each line from the array into the separate elements:
foreach(varlineinlines.Skip(1)){// 0=day, 1=Day.In.Words, 2=Gift, 3=Verb, 4=Adjective, 5=LocationvardayElements=line.Split(",");// pass the data into a method to generate a portion of the songvardayPortion=getOutputForDay(dayElements);}
I questioned whether I should do the split in the loop or in getOutputForDay, but decided
it wasn't the responsiblity of getOutputForDay so I left it in the loop and passed in the
array of elements instead.
I did look at some CSV parsing libraries on Nuget, but most of them seemed like overkill for
this exercise, so I did some very basic manipulation of the data to pull out the individual
elements.
In the following code, I'm handling the cases where it contains "NA" since in those cases I
want nothing (a blank). Since the CSV is all strings, I am casting some of the data to more
appropriate types. You'll notice the call to "pluralize", but only if the gift is on days
2 through 12 (I don't want "A partridges in a pear tree").
(stringline1,stringline2)getOutputForDay(string[]day){varactualDay=Convert.ToInt16(day[0]);stringgift=actualDay>1?pluralize(day[2]):day[2];vardoing=day[3]=="NA"?"":day[3];varadjective=day[4]=="NA"?"":day[4];varlocation=day[5]=="NA"?"":day[5];stringnumberOfThings=numberToText(actualDay);varline1=$"On the {day[1]} day of Christmas my true love gave to me";varline2=new[]{numberOfThings,adjective,gift,doing,location}.Where(s=>!string.IsNullOrEmpty(s)).Aggregate("",(acc,cur)=>acc+" "+cur).Trim();return(line1,line2);}
Notice the numberToText function? That's to convert the number of gifts (element 0 "Day")
to a string representation of the number, so instead of the number 2 showing up, it'll be the
word "two". That's nothing more than a wrapper around a dictionary lookup:
Finally, notice how I decided to return a tuple? There's a reason for that. If I had
just returned a single string that contained one day, I would have run into trouble
when I tried to build the whole song because of the way the song is constructed.
For example, while the first verse is:
On the first day of Christmas my true love gave to me
A partridge in a pear tree
The second verse only cares about "a partridge in a pear tree":
On the second day of Christmas my true love gave to me
two turtle doves
and a partridge in a pear tree
Back in the main loop, I'm able to use the tuple to construct a full verse because I
kept track of the previous line. Here's a new version of the loop. I'm now keeping track
of the current day as well as the previous "line 2". I'm also storing the verses in a
dictionary so I can dump everything out at once or just as easily just, output a single verse:
The 12 Days of Christmas
On the first day of Christmas my true love gave to me
A partridge in a pear tree
On the second day of Christmas my true love gave to me
two turtle doves
And a partridge in a pear tree
On the third day of Christmas my true love gave to me
three french hens
two turtle doves
And a partridge in a pear tree
On the fourth day of Christmas my true love gave to me
four calling birds
three french hens
two turtle doves
And a partridge in a pear tree
On the fifth day of Christmas my true love gave to me
five golden rings
four calling birds
three french hens
two turtle doves
And a partridge in a pear tree
On the sixth day of Christmas my true love gave to me
six geese a-laying
five golden rings
four calling birds
three french hens
two turtle doves
And a partridge in a pear tree
On the seventh day of Christmas my true love gave to me
seven swans a-swimming
six geese a-laying
five golden rings
four calling birds
three french hens
two turtle doves
And a partridge in a pear tree
On the eighth day of Christmas my true love gave to me
eight maids a-milking
seven swans a-swimming
six geese a-laying
five golden rings
four calling birds
three french hens
two turtle doves
And a partridge in a pear tree
On the ninth day of Christmas my true love gave to me
nine ladies dancing
eight maids a-milking
seven swans a-swimming
six geese a-laying
five golden rings
four calling birds
three french hens
two turtle doves
And a partridge in a pear tree
On the tenth day of Christmas my true love gave to me
ten lords a-leaping
nine ladies dancing
eight maids a-milking
seven swans a-swimming
six geese a-laying
five golden rings
four calling birds
three french hens
two turtle doves
And a partridge in a pear tree
On the eleventh day of Christmas my true love gave to me
eleven pipers piping
ten lords a-leaping
nine ladies dancing
eight maids a-milking
seven swans a-swimming
six geese a-laying
five golden rings
four calling birds
three french hens
two turtle doves
And a partridge in a pear tree
On the twelfth day of Christmas my true love gave to me
twelve drummers drumming
eleven pipers piping
ten lords a-leaping
nine ladies dancing
eight maids a-milking
seven swans a-swimming
six geese a-laying
five golden rings
four calling birds
three french hens
two turtle doves
And a partridge in a pear tree
Wrap-up
And there it is! The 12 Days of Christmas built with the bare minimum data. This was a
fun little exercise, and I hope you enjoyed it!
You can check out the code on GitHub.
Shoutout to my friends Brian Friesen and Matt Davis for reviewing this and making
suggestions that have improved both the content and the code!
Here are the best and most interesting articles, blog posts, videos, podcasts, and GitHub repositories I’ve run into over the last week (March 3, 2024 - Marc...
Here are the best and most interesting articles, blog posts, videos, podcasts, and GitHub repositories I’ve run into over the last week (February 24, 2024 - ...
Here are the best and most interesting articles, blog posts, videos, podcasts, and GitHub repositories I’ve run into over the last week (February 17, 2024 - ...
Here are the best and most interesting articles, blog posts, videos, podcasts, and GitHub repositories I’ve run into over the last week (February 10, 2024 - ...
Comments