type Html = | Elem of string * Html list | Attr of string * string | Text of string with static member toString elem = let rec toString indent elem = let spaces = String.replicate indent " " match elem with | Attr(name,value) -> name+"=\""+value+"\"" | Elem(tag, [Text s]) -> spaces+"<"+tag+">"+s+"\r\n" | Elem(tag, content) -> let isAttr = function Attr _ -> true | _ -> false let attrs, elems = content |> List.partition isAttr let attrs = if attrs = [] then "" else " " + String.concat " " [for attr in attrs -> toString 0 attr] spaces+"<"+tag+attrs+">\r\n"+ String.concat "" [for e in elems -> toString (indent+1) e] + spaces+"\r\n" | Text(text) -> spaces + text + "\r\n" toString 0 elem override this.ToString() = Html.toString this let elem tag content = Elem(tag,content) let html = elem "html" let head = elem "head" let title = elem "title" let style = elem "style" let body = elem "body" let table = elem "table" let thead = elem "thead" let tbody = elem "tbody" let tfoot = elem "tfoot" let tr = elem "tr" let td = elem "td" let th = elem "th" let strong = elem "strong" let (~%) s = [Text(s.ToString())] let (%=) name value = Attr(name,value) // [snippet:Multi-currency domain] type Money = private { Amount:decimal; Currency:Currency } with static member ( * ) (lhs:Money,rhs:decimal) = { lhs with Amount=lhs.Amount * rhs } static member ( + ) (lhs:Money,rhs:Money) = if lhs.Currency <> rhs.Currency then invalidOp "Currency mismatch" { lhs with Amount=lhs.Amount + rhs.Amount} override money.ToString() = sprintf "%M%s" money.Amount money.Currency and Currency = string type RateTable = { To:Currency; From:Map } let exchangeRate (rates:RateTable) cy = if rates.To = cy then 1.0M else rates.From.[cy] let convertCurrency (rates:RateTable) money = let rate = exchangeRate rates money.Currency { Amount=money.Amount / rate; Currency=rates.To } // [/snippet] // [snippet:Multi-currency report model] type Report = { Rows:Row list; Total:Money } and Row = { Position:Position; Total:Money } and Position = { Instrument:string; Shares:int; Price:Money } let generateReport rates positions = let rows = [for position in positions -> let total = position.Price * decimal position.Shares { Position=position; Total=total } ] let total = rows |> Seq.map (fun row -> convertCurrency rates row.Total) |> Seq.reduce (+) { Rows=rows; Total=total } // [/snippet] // [snippet:Multi-currency report view] let toHtml (report:Report) = html [ head [ title %"Multi-currency report" ] body [ table [ "border"%="1" "style"%="border-collapse:collapse;" "cellpadding"%="8" thead [ tr [th %"Instrument"; th %"Shares"; th %"Price"; th %"Total"] ] tbody [ for row in report.Rows -> let p = row.Position tr [td %p.Instrument; td %p.Shares; td %p.Price; td %row.Total] ] tfoot [ tr [td ("colspan"%="3"::"align"%="right"::[strong %"Total"]) td %report.Total] ] ] ] ] // [/snippet] // [snippet:Example] let USD amount = { Amount=amount; Currency="USD" } let CHF amount = { Amount=amount; Currency="CHF" } let positions = [{Instrument="IBM"; Shares=1000; Price=USD( 25M)} {Instrument="Novartis"; Shares= 400; Price=CHF(150M)}] let inUSD = { To="USD"; From=Map.ofList ["CHF",1.5M] } let positionsInUSD = generateReport inUSD positions let report = positionsInUSD |> toHtml |> Html.toString // [/snippet] // [snippet:Show report embedded] #r "System.Windows.Forms.dll" open System.Windows.Forms let form = new Form(Text="Multi-currency report") let web = new WebBrowser(Dock=DockStyle.Fill) form.Controls.Add(web) web.Navigate("about:blank") web.Document.Write(report) form.Show() // [/snippet] // [snippet:Write report & launch in browser] open System.IO let name = System.Guid.NewGuid().ToString() let path = System.IO.Path.GetTempPath() + name + ".html" let writer = File.CreateText(path) writer.Write(report) writer.Close() System.Diagnostics.Process.Start(path) // [/snippet]