Skip to content

Instantly share code, notes, and snippets.

@markusl
Created October 28, 2011 12:26
Show Gist options
  • Save markusl/1322155 to your computer and use it in GitHub Desktop.
Save markusl/1322155 to your computer and use it in GitHub Desktop.
Economic order quantity calculator in F#
/// Read more about EOQ from http://en.wikipedia.org/wiki/Economic_order_quantity
/// Using functional approach to handle user interface interaction using reactive programming style.
/// Any comments and improvements welcome :-)
/// -Markus L/2011
#if INTERACTIVE
#r "PresentationCore"
#r "PresentationFramework"
#r "System.Xaml"
#r "UIAutomationTypes"
#r "WindowsBase"
#endif
open System.Windows
open System.Windows.Controls
open System.Windows.Data
open System.Windows.Media
open System.Windows.Shapes
open Microsoft.Win32
[<Measure>]
type Money
(** Struct for storing data for doing Economic order quantity calculations.
* Read more from http://en.wikipedia.org/wiki/Economic_order_quantity *)
type EOQ = {
(** Annual demand quantity *)
d : float;
(** Purchase cost per unit *)
p : float<Money>;
(** Fixed cost per order *)
s : float<Money>;
(** Annual holding cost (percent of unit price) *)
f : float;
}
with
(** Annual holding cost/unit *)
member this.h = this.f * this.p
(** Calculate the optimal order quantity *)
member this.EOQ = sqrt((2. * this.d * this.s)/(this.h))
member this.costPerYear quantity = (this.s * this.d / quantity) + 0.5*quantity*this.h
static member Construct = { new EOQ
with d = 70.
and p = 15.5<Money>
and s = 5.<Money>
and f = 0.1
}
end
/// Type for visualizing EOQ data
type EOQ_Render() =
do ()
(** Renders the given EOQ data to a canvas that is then returned *)
static member renderScene (data:EOQ) width height =
/// Get TextBlock describing the input values
let getEOQDescription (eoq:EOQ) =
let text = (sprintf"Annual demand: %f\r\nPurchase cost/unit: %f\r\nCost per order: %f\r\nHolding cost: %f"
eoq.d (eoq.p/1.<Money>) (eoq.s/1.<Money>) eoq.f)
new TextBlock(Text=text, Foreground=SolidColorBrush(Colors.WhiteSmoke))
/// Get the brush to draw a single column, returns different brush for column that is emphasized
let graphBrush highlight = if highlight then SolidColorBrush(Color.FromRgb(27uy, 170uy, 90uy))
else SolidColorBrush(Colors.White)
let backgroundBrush = SolidColorBrush(Color.FromRgb(14uy, 109uy, 56uy))
/// Render EOQ data visually in columns
let createBars (sceneCanvas:Controls.Panel) best (nth,value) =
let groundLevel = height-40.
let x = nth * 4 + 2
let line = new Line(X1 = float x, Y1 = groundLevel, X2=float x, Y2 = groundLevel - value,
StrokeThickness = 2., Stroke=graphBrush (nth = best))
sceneCanvas.Children.Add(line) |> ignore
let sceneCanvas = new Canvas(Background=backgroundBrush, Width=width, Height=height)
let values = [1 .. 100] |> List.map (fun nth -> nth,(data.costPerYear ((float)nth)) / 1.<Money>)
let best = values |> List.sortBy snd |> List.head |> fst
values |> List.iter (createBars sceneCanvas best)
sceneCanvas.Children.Add(getEOQDescription data) |> ignore
sceneCanvas
type EOQWindow() as this =
inherit Window()
/// Define controls with labels, min-max values and initial values and change action
let controlsAndActions = [(1., 100., 70., "Annual demand", (fun newValue (eoq:EOQ) -> {eoq with d = newValue}));
(5., 200., 15.5, "Purchase cost/unit", (fun newValue (eoq:EOQ) -> {eoq with p = newValue*1.<Money>}));
(1., 40., 5., "Cost per order", (fun newValue (eoq:EOQ) -> {eoq with s = newValue*1.<Money>}));
(0.05, 0.95, 0.1, "Holding cost%", (fun newValue (eoq:EOQ) -> {eoq with f = newValue}))]
/// Create controls on given panel, add defined change action and merge all observables
let createAndGetControlEvents (panel:Controls.Panel) =
let createControlAndRegisterEvent (min, max, initial, label, action) =
panel.Children.Add(new Label(Content=label)) |> ignore
let slider = new Slider(Minimum=min, Maximum=max, Value=initial)
panel.Children.Add(slider) |> ignore
slider.ValueChanged |> Observable.map (fun rea -> action rea.NewValue)
controlsAndActions
|> List.map (createControlAndRegisterEvent)
|> List.reduce Observable.merge
/// Re-render the screen with changed data
let renderScene (mainPanel:Controls.Panel) data =
mainPanel.Children.RemoveAt(1)
mainPanel.Children.Add(EOQ_Render.renderScene data (this.Width-70.) this.Height) |> ignore
let initControlEventsAndAddListener leftPanel mainPanel =
let controlEvents = createAndGetControlEvents leftPanel
controlEvents
|> Observable.scan (fun a b -> b a) (EOQ.Construct)
|> Observable.add (renderScene mainPanel)
let createMainPanel =
let mainPanel = new StackPanel(Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Top)
let leftPanel = new StackPanel(Orientation = Orientation.Vertical)
initControlEventsAndAddListener leftPanel mainPanel
mainPanel.Children.Add(leftPanel) |> ignore
mainPanel.Children.Add(new TextBlock(Text="\r\n\r\n\r\n«« Start by modifying the values on the left")) |> ignore
mainPanel
do
this.Content <- createMainPanel
[<System.STAThread>]
do
(new Application()).Run(EOQWindow(Width=500., Height=350., Title="EOQ calculator")) |> ignore
@markusl
Copy link
Author

markusl commented Oct 28, 2011

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment