Bobson
Bobson

Reputation: 233

Parsing values from string array

I am trying to create a save/load function for 2d objects that has been drawn into a form.

type circle = { X : int; Y : int; Diameter : int; Brush : Brush}
type Square = { X : int; Y : int; Length : int; Height: int; Brush : Brush}

When i create the object i put them into 2 lists 1 for each type. My initial thought was to read and write these objects to a textfile, see below:

saveFile.Click.Add(fun _ ->
 for c in listOfCircles do 
   myfile.WriteLine("Circle," + c.X.ToString() + "," + c.Y.ToString() + "," + c.Diameter.ToString() + "," + c.Brush.ToString())
 for s in listOfSquares do
   myfile.WriteLine("Square," + s.X.ToString() + "," + s.Y.ToString() + "," + s.Height.ToString() + "," + s.Length.ToString() + "," + s.Brush.ToString())
 myfile.Close() // close the file

And in the textfile it looks like this

Circle,200,200,50,System.Drawing.SolidBrush
Square,50,55,45,55,System.Drawing.SolidBrush

From here i want to read these values and then be able to parse them and recreate the objects by adding the objects the lists and redraw them.

let readCircle =
  System.IO.File.ReadAllLines path
  |> Array.choose (fun s ->
    match s.Split ',' with
    | [|x; y ; z ; b ; _|] when x = "Circle" -> Some (y, z, b)
    | _ -> None )

let readSquare =  
  System.IO.File.ReadAllLines path
  |> Array.choose (fun s ->
    match s.Split ',' with
    | [|x; y ; z ; b ; a ; _|] when x = "Square" -> Some (y, z, b, a)
    | _ -> None )

These functions gives me

val readCircle : (string * string * string) [] = [|("200", "200", "50")|]
val readSquare : (string * string * string * string) [] = [|("50", "55", "45", "55")|]

The problem i have now is im not sure how to obtain the values from the array. Beneath is example with multiple circles.

val readCircle : (string * string * string) [] =  [|("200", "200", "50"); ("200", "200","50")|]

Any ideas or comments about how to go from here/how to resolve this issue is very appreciated! Question summary: how could i get the values from the array and put them in for example my already created add functions, see below:

 listOfCircles.Add({ X = 200; Y = 200; Diameter = 50; Brush = Brushes.Black})

Upvotes: 2

Views: 681

Answers (3)

plinth
plinth

Reputation: 49179

This is fun - I approached it in a totally different way than Daniel (but I agree with him that you classes might be a better approach for your shapes). Instead, I took advantage of discriminated unions (and there are better ways to do this - more later):

First, I define a type for a list of parameters for making a shape:

type Parameter =
    | Label of string
    | Number of int

Now let's convert a string to a parameter:

let toParameter s =
    match Int32.TryParse(s) with
    | (true, i) -> Number(i)
    | (_, _) -> Label(s)

Now to convert a list of strings to a list of Parameter:

let stringListToParameterList stringlist = stringlist |> List.map(function s -> toParameter s)

Now to convert a comma-separated string to a list of string:

let commastringToList (s:string) = s.Split(',') |> Array.toList

OK - great - let's define your records and a master Shape:

type circlerec = { X : int; Y : int; Diameter : int; Brush : Brush}
type squarerec = { X : int; Y : int; Length : int; Height: int; Brush : Brush}
type Shape =
    | Circle of circlerec
    | Square of squarerec

With this we need a way to make a Shape from a parameter list. This is brute force, but it reads well enough:

let toShape list =
    match list with
    | Label("Circle") :: Number(x) :: Number(y) :: Number(diam) :: Label(colorName) :: [] ->
        Circle({X = x; Y = y; Diameter = diam; Brush = new SolidBrush(Color.FromName(colorName)); })
    | Label("Circle") :: rest -> raise <| new ArgumentException("parse error:expected Circle num num num color but got " + list.ToString())
    | Label("Square") :: Number(x) :: Number(y) :: Number(length) :: Number(height) :: Label(colorName) :: [] ->
        Square({X = x; Y = y; Length = length; Height = height; Brush = new SolidBrush(Color.FromName(colorName)); })
    | Label("Square") :: rest -> raise <| new ArgumentException("parse error:expected Square num num num num color but got " + list.ToString())
    | _ -> raise <| new ArgumentException("parse error: unknown shape: " + list.ToString())

It's dense, but I'm using F#'s pattern matching to spot the various parameters for each shape. Note that you could now do things like have Square,x,y,size,colorName in your file and make a Square where Length and Height are equal to size by just adding in the pattern.

Finally comes the piece de resistance, converting your file into shapes:

let toShapes path =
    System.IO.File.ReadAllLines path |> Array.toList |>
        List.map(function s -> s |> commastringToList |>
        stringListToParameterList |> toShape)

which maps every line in the file to a list of string which then maps each line to a shape, but piping the comma string to the list converter and then through the parameter list and then to a shape.

Now where this is bad is that the error checking is pretty horrid and that the Parameter type should really include Pigment of Color, which would allow you to look at the string that comes in and if it's valid Color name, map it to a Pigment else a Label.

Upvotes: 1

Leaf Garland
Leaf Garland

Reputation: 3687

You could convert the arrays of string tuples you have using Array.map, e.g.

[|("200", "200", "50"); ("200", "200","50")|]
|> Array.map (fun (x,y,d) -> {X = int32 x; Y = int32 y; Diameter = int32 d; Brush = Brushes.Black})

It might be a bit clearer if you converted to circle or square as you parsed the file, then you would have an array of circle or an array of square that you can add directly to your lists.

let readCircle =
  System.IO.File.ReadAllLines path
  |> Array.choose (fun s ->
    match s.Split ',' with
    | [|t; x; y; d; _|] when t = "Circle" -> 
        Some {X = int32 x; Y = int32 y; Diameter = int32 d; Brush = Brushes.Red}
    | _ -> None )

But... if you wanted to make larger changes, you could use discriminated unions to represent your shapes, they would then share a common type of Shape and you could parse circles and squares in the same function.

type Shape = 
| Circle of X : int * Y : int * Diameter : int * Brush : Brush
| Square of X : int * Y : int * Length : int * Height: int * Brush : Brush 

let readShapes (data: string array) =
  data
  |> Array.choose (fun s ->
    match s.Split ',' with
    | [|t; x; y; d; _|] when t = "Circle" -> 
        Some (Circle(X = int32 x, Y = int32 y, Diameter = int32 d, Brush = Brushes.Red))
    | [|t; x; y; l; h; _|] when t = "Square" -> 
        Some (Square(X = int32 x, Y = int32 y, Length = int32 l, Height = int32 h, Brush = Brushes.Red))
    | _ -> None )

let listOfShapes = ResizeArray<_>()

let testInput = """
Circle,200,200,50,System.Drawing.SolidBrush
Square,50,55,45,55,System.Drawing.SolidBrush"""

testInput.Split('\n') // System.IO.File.ReadAllLines path
|> readShapes
|> Array.iter (listOfShapes.Add)

Which would result in

val it : System.Collections.Generic.List<Shape> =
  seq
    [Circle (200,200,50,System.Drawing.SolidBrush {Color = Color [Red];});
     Square (50,55,45,55,System.Drawing.SolidBrush {Color = Color [Red];})]

You could then use pattern matching to draw each type of shape

let drawShape shape =
    match shape with
    | Circle(x,y,d,b) -> 
        printfn "Pretend I just drew a circle at %d,%d with diameter %d." x y d
    | Square(x,y,l,h,b) -> 
        printfn "Pretend I just drew a rectangle at %d,%d that was %d long and %d high." x y l h

listOfShapes |> Seq.iter drawShape

Giving

Pretend I just drew a circle at 200,200 with diameter 50.
Pretend I just drew a rectangle at 50,55 that was 45 long and 55 high.

Upvotes: 3

Daniel
Daniel

Reputation: 47904

If I understand your goal, this is how I would go about it. I've only implemented Circle; you'll need to modify it to handle Square.

open System
open System.Collections.Generic
open System.Drawing
open System.IO

let memoize f =
  let cache = Dictionary()
  fun key ->
    match cache.TryGetValue(key) with
    | true, value -> value
    | _ ->
      let value = f key
      cache.Add(key, value)
      value

let getBrush =
  memoize (fun name -> typeof<Brushes>.GetProperty(name).GetValue(null) :?> SolidBrush)

type Circle = 
  { X : int
    Y : int
    Diameter : int
    Brush : SolidBrush } with
  override this.ToString() = 
    sprintf "Circle,%d,%d,%d,%s" this.X this.Y this.Diameter this.Brush.Color.Name
  static member Parse(s: string) =
    match s.Split(',') with
    | [|"Circle";x;y;diameter;brushName|] -> {X=int x; Y=int y; Diameter=int diameter; Brush=getBrush brushName}
    | _ -> invalidArg "s" "Cannot parse string"

let writeShapesToFile fileName shapes =
  File.WriteAllLines(fileName, Seq.map (sprintf "%O") shapes)

let readShapesFromFile fileName =
  File.ReadAllLines(fileName) |> Array.map Circle.Parse

Also, you might consider using a class hierarchy instead of records since much of the structure and behavior of Circle and Square are shared.

Upvotes: 2

Related Questions