benjamin-hodgson / Pidgin

A lightweight and fast parsing library for C#.
https://www.benjamin.pizza/Pidgin/
MIT License
883 stars 68 forks source link

Parsing pseudo freeform text #141

Closed powerdude closed 1 year ago

powerdude commented 1 year ago

Hi,

I'm trying to write a parser to read something like a recipe. If I have a simple version like this:

"Grandma's bread"
1 oz. sugar
2 cup cream of wheat

Mix thoroughly until done

I think I have the pieces I need, but not sure how to assemble them to get this to get this to work.

public class RecipeParser
{
    public class IngredientModel
    {
        public int Amount { get; set; }
        public string Units { get; set; }
        public string Name { get; set; }
    }
    public class RecipeModel
    {
        public string Name { get; set; }

        public List<IngredientModel> Ingredients { get; set; } = new();
        public string Instructions { get; set; }
    }
    public abstract class RecipeBuilder
    {
        public abstract void Build(RecipeModel recipe);
    }

    public class SetName : RecipeBuilder
    {
        private readonly string name;

        public SetName(string name)
        {
            this.name = name;
        }

        /// <inheritdoc />
        public override void Build(RecipeModel recipe) => recipe.Name=name;
    }
    public class SetInstructions : RecipeBuilder
    {
        private readonly string value;

        public SetInstructions(string value)
        {
            this.value = value;
        }

        /// <inheritdoc />
        public override void Build(RecipeModel recipe) => recipe.Instructions=value;
    }
    public class SetIngredient : RecipeBuilder
    {
        private readonly int amount;

        private readonly string units;

        private readonly string name;

        public SetIngredient(int amount, string units, string name)
        {
            this.amount = amount;
            this.units = units;
            this.name = name;
        }

        /// <inheritdoc />
        public override void Build(RecipeModel recipe) => recipe.Ingredients.Add(new IngredientModel(){Amount=amount,Name=name,Units=units});
    }

    private static Parser<char, T> Tok<T>(Parser<char, T> token) => Try(token).Before(SkipWhitespaces);

    private static Parser<char, string> Tok(string token) => Tok(String(token));

    private static readonly Parser<char, char> _quote = Char('"');

    private static readonly Parser<char, string> _quotedString = Token(c => c != '"').ManyString().Between(_quote);

    private static readonly Parser<char, string> Units = Tok("oz").Or(Tok("cup"));
    private static readonly Parser<char, string> Word =
        Tok(Parser.Letter.Then(Parser.LetterOrDigit.Or(Char('-')).Or(Char('\'')).ManyString(), (h, t) => h + t))
           .Labelled("word");

    public static Parser<char, char> Space => Char(' ');

    private static Parser<char, IEnumerable<string>> Words => Word.SeparatedAndOptionallyTerminated(Space)
                                                                  .Trace(x => $"words={string.Join(" ", x)}")

                                                                  .Labelled("words");

    private static Parser<char, string> WordsString => Words.Select(x => string.Join(" ", x))
                                                            .Trace(x => $"wordstring={x}")

                                                            .Labelled("words string");

    private static readonly Parser<char, int> Number = Tok(Parser.Num).Labelled("number");

    private static readonly Parser<char, RecipeBuilder> name = _quotedString.Select<RecipeBuilder>(x=>new SetName(x));
    private static readonly Parser<char, RecipeBuilder> instructions = _quotedString.Select<RecipeBuilder>(x=>new SetInstructions(x));
    private static readonly Parser<char, RecipeBuilder> ingredient = Parser.Map((num, u,n) => (RecipeBuilder)new SetIngredient(num, u,n), Number, Units,Word);

    public static Result<char, RecipeBuilder> Parse(string input) => ???;

}

Ultimately, I'd like to be able to parse something like this:

"Grandma's bread and Icing"
Bread
1 oz. sugar
2 cup cream of wheat

Mix thoroughly until done.

Rroyal Icing
3 g. something
4 ml. something else

other instructions.

Any help greatly appreciated.

benjamin-hodgson commented 1 year ago

What exactly do you need help with? You can compose the name, instructions, ingredient parsers something like this:

from n in name.Before(NewLine)
from ingreds in ingredient.SeparatedBy(NewLine)
from instrs in instructions
select new RecipeModel(name, ingreds, instructions);
powerdude commented 1 year ago

what is NewLine?

benjamin-hodgson commented 1 year ago

Apologies, I meant EndOfLine.

powerdude commented 1 year ago

nevermind, I've made some progress. Thanks!