Phillip Trelford's Array

POKE 36879,255

Calendar Types

Welcome to the 2015 F# Advent Calendar and one of 2 posts for December 3rd. For last year’s advent calendar I tried to follow the seasonal theme and produced an article on generating snowflakes. This year I thought I’d be more literal and look at producing calendar types using F#’s Type Provider mechanism resurrecting a project from 2014, FSharp.Date.

FSharp.Date

FSharp.Date is a simple F# Type Provider, inspired by VB.Net’s date literal feature,that lets you define dates and time values in F# by pressing dot and selecting only valid values:

2015 December 3rd

Further inspired by the advent calendar theme I’ve added a new feature that lets you visualize the calendar month as a tooltip in your editor:

2015 December Calendar

The source is available on BitBucket or you can download the package from Nuget.

But wait there’s more…

On This Day

That got me thinking, what if you could provide topical information on a particular day direct to the editor, and then I found the BBC news site On This Day.

The web site contains a set of both historically significant and quirky stories from the same day in the past.

First I needed a way of finding an article and scraping the news items from the page.

Thankfully the BBC pages use a uniform URL taking the month and day making it easy to get a specific page:

let getPage (month,day) =
    let date = System.DateTime(2005,month,day)    
    let monthName = date.ToString("MMMM").ToLower()
    let url = sprintf "http://news.bbc.co.uk/onthisday/low/dates/stories/%s/%d/default.stm"
                 monthName date.Day
    use client = new System.Net.WebClient()
    client.DownloadString(url)  

The page’s HTML is not well formed so I resorted to a regular expression to parse the news items:

let getNewsItems html =
    let pattern = """<a href="([^"]*)"><span class="h1">(.*?)</span></a><br>(.*?)<br clear="ALL">"""
    let matches = Regex.Matches(html, pattern, RegexOptions.Singleline)
    let newsItems = [for m in matches -> [for i in 1..m.Groups.Count-1 -> m.Groups.[i].Value]]
    [for newsItem in newsItems do
        match newsItem with
        | [link;title;description] ->
            yield "http://news.bbc.co.uk" + link, title.Trim(), description.Trim().Replace("\n","").Replace("\r","")
        | _ -> ()
    ]

Which returns a list of news items for the specified month and day:

> (12,3)|> getPage |> getNewsItems;;
val it : (string * string * string) list =
  [("http://news.bbc.co.uk/onthisday/low/dates/stories/december/3/newsid_2698000/2698709.stm",
    "1984: Hundreds die in Bhopal chemical accident",
    "A dense cloud of lethal gas escapes from a chemical factory in the central Indian city of Bhopal, killing hundreds of people.");
   ("http://news.bbc.co.uk/onthisday/low/dates/stories/december/3/newsid_2519000/2519715.stm",
    "1992: Bomb explosions in Manchester",
    "Emergency services are dealing with casualties at the scene of two bomb blasts in the centre of Manchester.");
   ("http://news.bbc.co.uk/onthisday/low/dates/stories/december/3/newsid_4119000/4119950.stm",
    "1989: Malta summit ends Cold War",
    "The leaders of the two world superpowers declare an end to the Cold War after two days of storm-lashed talks at the Malta summit.");
   ("http://news.bbc.co.uk/onthisday/low/dates/stories/december/3/newsid_4119000/4119070.stm",
    "1965: White jury convicts Ku Klux Klansmen",
    "For the first time an all-white jury convicts members of the KKK over the murder of a white civil rights activist Viola Liuzzo.");
   ("http://news.bbc.co.uk/onthisday/low/dates/stories/december/3/newsid_2519000/2519133.stm",
    "1971: Pakistan intensifies air raids on India",
    "India declares a state of emergency as  airports are hit during a Pakistani attack on the country.");
   ("http://news.bbc.co.uk/onthisday/low/dates/stories/december/3/newsid_2519000/2519451.stm",
    "1988: Egg industry fury over salmonella claim",
    "Claims by a health minister that eggs contain salmonella are branded alarmist and incorrect.")]
> 

Now to create a type provider.

Creating your own Type Provider

If you’re new to writing your own type provider I’d recommend starting with Michael Newton’s Type Providers From the Ground Up post.

First off we need to reference two F# files from the FSharp.TypeProviders.StarterPack.

Paket

Rather than make a static copy I used Paket, a handy .Net dependency management tool, to reference the files directly from GitHub.

This required a simple paket.dependencies file:

source https://nuget.org/api/v2

github fsprojects/FSharp.TypeProviders.StarterPack src/ProvidedTypes.fsi 
github fsprojects/FSharp.TypeProviders.StarterPack src/ProvidedTypes.fs 

and a paket.references file:

File:ProvidedTypes.fsi
File:ProvidedTypes.fs

With that in place all that’s left to do is run > paket install.

Defining the Type Provider

To create a type provider you need a type with the TypeProvider attribute that inherits from TypeProviderFromNamespaces. For this example I only need a single type OnThisDay which provides the news items via a set of static properties:

[<TypeProvider>]
type OnThisDayProvider (config:TypeProviderConfig) as this = 
   inherit TypeProviderForNamespaces ()

   let getProperties newsItems =
       [for (url, title, description) in newsItems ->
           let property = 
               ProvidedProperty(title, typeof<string>, IsStatic=true,
                  GetterCode=fun _ -> <@@ url @@>)
           property.AddXmlDoc(description)
           property]

   let ns = "FsAdvent"
   let asm = System.Reflection.Assembly.GetExecutingAssembly()
   let today = System.DateTime.Now
   let providedType = ProvidedTypeDefinition(asm, ns, "OnThisDay", Some typeof<obj>)
   do  providedType.AddXmlDoc(today.ToLongDateString())
   do  providedType.AddMembersDelayed(fun () ->             
            (today.Month,today.Day) |> getPage |> getNewsItems |> getProperties
       )
   do  this.AddNamespace(ns, [providedType])

Once it’s built you can reference the type provider and get a list of topical news items for the day directly in your editor:

Tunnel

The selected property returns the URL as the value, which you can easily launch with your default browser using Process.Start:

OnThisDay.``1995: Rogue trader jailed for six years``
|> System.Diagnostics.Process.Start

If you fancy a play the source code is available on GitHub: https://github.com/ptrelford/OnThisDay

Happy holidays!

Comments are closed