String Matcher

5

Klepałem kod ostatnio, bo uznałem że w Regexy są passé i nie znam jakiejś alternatywy, a jako że po co tracić 5min na szukanie, gdy można poklepać pół łikendu, to powstało takie coś

YetAnotherStringMatcher library tries to make string matching easier, more readable while being somewhat expressive.

It tries to replace Regex to some extent, mostly when it comes to relatively basic cases.

For now it is in very early version and apart from the errors, then it may lack of "obvious things" and be kinda inconsistent in its APIs - mostly due to maintainer being retard.

// Polish Postal Code
var input = "12-345";

var result = new Matcher(input)
                 .MatchDigitsOfLength(2)
                 .Then("-")
                 .ThenDigitsOfLength(3)
                 .Check();

Assert.True(result.Success);
var matcher = new Matcher("[2021-09-05] ERROR: Message1! Exception!")
                  .Match("[2021-09-05]")
                  .ThenAnything()
                  .Then("Exception!")
                  .Check();
				  
Assert.True(matcher.Success);
var input = new List<string>
{
	"[2021-09-05] ERROR: Message1",
	"[2021-09-05] WARNING: Message1",
	"[2021-09-05] INFO: Message1",
	"[2021-09-07] WARNING: Message1",
};

var pattern = new Matcher()
			  .Match("[2021-09-05] ")
			  .ThenAnyOf("WARNING:", "ERROR:");

Assert.True(pattern.Check(input[0]).Success);
Assert.True(pattern.Check(input[1]).Success);

Assert.False(pattern.Check(input[2]).Success);
Assert.False(pattern.Check(input[3]).Success);
var matcher = new Matcher("Apple Watermelon")
                  .MatchAnyOf("Apple", "Banana")
                  .Then(" ")
                  .ThenAnyOf("Giraffe", "melon", "Watermelon")
                  .Check();

Assert.True(matcher.Success);
var matcher = new Matcher("TEST")
                  .Match("test").IgnoreCase()
                  .Check();

Assert.True(matcher.Success);
// Sample Phone Number / Reusable Pattern
var input = new List<string> { "+123 345 67 89", "+1424 345 67 89" };

var pattern = new Matcher()
                  .Match("+")
                  .ThenDigitsOfLength(3)
                  .Then(" ")
                  .ThenDigitsOfLength(3)
                  .Then(" ")
                  .ThenDigitsOfLength(2)
                  .Then(" ")
                  .ThenDigitsOfLength(2);

Assert.True(pattern.Check(input[0]).Success);
Assert.False(pattern.Check(input[1]).Success);
var result = new Matcher("Apple Pineapple")
                .Match("Apple ")
                .Then("Coconut ").IsOptional()
                .Then("Pineapple")
                .Check();

Assert.True(result.Success);

Kod, Dokumentacja API z przykładami: https://github.com/xd-loler/YetAnotherStringMatcher

Mięso: https://github.com/xd-loler/YetAnotherStringMatcher/tree/master/src/Core

Testy: /dev/null YetAnotherStringMatcher/src/Tests/BasicTests.cs

możecie pohejtować, możliwe że są bugi/dziwne zachowanie :) jak i również podrzucić jakiś pomysł nt. jakie operacje waszym zdaniem takie narzędzie powinno być w stanie realizować?

1

Na pierwszy rzut oka to wygląda na proceduralną nakładkę na wyrażenia regularne. Jeśli ma to wyglądać tylko tak, że metody generują stringi dla wyrażeń, to mi się nie chce uczyć na nowo i wolę pisać wyrażenia regularne wprost. Ale widzę w tym szaleństwie metodę. Gdyby to wyglądało jak HQL, to byłbym kontent. Tzn. chciałbym mieć interpolację obiektów. Np. gdybym używał, tobym chciał móc dopasowywać do klas

class Point {
	private int x, y;
	//gettery, settery i inna kupa
}

//Sprawdzam kolejno po polach klasy
boolean matched = Matcher.forClass(Point.class)
		.integer(123)
		.integer(5)
		.match();
//albo zapisane inaczej
boolean matched = Matcher.forClass(Point.class)
		.string("11,33")
		.match();

Czyli taka biblioteka by pozwalała rozpisanie struktury klasy na wyrażenie regularne. Wtedy ktoś, kto umie obiektowość, ale nie kuma wyrażeń, miałby zapis obiektowy.

3

A nie myślałeś, żeby w klasie Matcher zostawić tylko ThenCustom, a metody typu ThenAnythingOfLength robić jako metody rozszerzające?

0

@PerlMonk

Na pierwszy rzut oka to wygląda na proceduralną nakładkę na wyrażenia regularne. Jeśli ma to wyglądać tylko tak, że metody generują stringi dla wyrażeń, to mi się nie chce uczyć na nowo i wolę pisać wyrażenia regularne wprost.

Pod spodem nie jest generowany Regex :P

@maszrum

Brzmi jak świetny pomysł

0

Pod spodem nie jest generowany Regex :P

Czyli to ułomne wyrażenia regularne?

3

@PerlMonk

Na ten moment tak, generalnie zamiarem było utworzenie API pozwalające osiągnąć coś jak Regex, jednakże o wiele bardziej przejrzystego i wymownego.

Jest zbiór pewnych operacji "Regexowych", które się często powtarzają w kodach i chciałem je fajniej opakować.

2

Wrzuciłem to na Reddita i zgarnąłem #1 na r/csharp, 8 gwiazdek na gh i 1 forka lul

I don't like regex, so I wrote this

2

a nie łatwiej się było nauczyć regexów?

0

@obscurity:

Ale właściwie czemu?

Uważam że to Regex to dobra rzecz, ale ze słabym API, coś jak Git.

[Params
(
	"Parcel Delivery London",
	"ParcelLondon",
	"Letter Delivery London"
)]
public string text;
[Benchmark]
public bool Case4_YASM()
{
	return new Matcher(text)
			   .MatchAnyOf("Parcel", "Taxi")
			   .ThenAnything().IsOptional()
			   .ThenAnyOf("London", "Zurich")
			   .Check()
			   .Success;
}

jest ładniejsze niż Regex-like

[Benchmark]
public bool Case4_Regex()
{
	return new Regex("^(Parcel|Taxi).*?(London|Zurich)").IsMatch(text);
}

i z tego co widzę, to wiele osób powiela ta opinie (Na reddicie, aczkolwiek woleliby aby to generowało Regexa, LUL.)

0

Dodałem do tego Code Generation, niestety nie wszystko będzie się dało (łatwo?) obsłużyć ze względu na predykaty Func<char, ..., bool>

var matcher = new Matcher()
                  .Match("[2021-09-05] ")
                  .ThenAnyOf("WARNING:", "ERROR:");

var generator = new RegexGenerator(matcher.GetRequirements.ToList());
var result = generator.Emit();
Console.WriteLine(result.Code); // ([2021-09-05] )(WARNING:|ERROR:)
var matcher = new Matcher()
                  .MatchAnyOf("Apple", "Banana")
                  .Then(" ")
                  .ThenAnyOf("Giraffe", "melon", "Watermelon");

var generator = new RegexGenerator(matcher.GetRequirements.ToList());
var result = generator.Emit();
Console.WriteLine(result.Code); // (Banana|Apple)( )(Watermelon|Giraffe|melon)

Source code

Some Tests

2

Ehh to tak jakby uzywac Scratcha bo pisanie kodu za trudne :)
Myslales zeby do tego WYSWIG dolozyc? :D

A tak na powaznie to problem sie zaczyna kiedy pattern, ktorego potrzebujesz to nie jest proste \d{2}-\d{3}.
No i teraz tak... Skoro proste wyrazenia sa tez proste w formie regexow a skomplikowane wyrazenia w tym to bedzie pain in the ass no to... po co? Zupelnie jak Scratch wlasnie. Dla dzieciakow (wciaz mowa o Scratchu) moze fajna sprawa ale profesjonalista sie pochlasta jak bedzie musial cos powaznego z tym zrobic.


Mimo krytycznego podejscia i tak szacunek za prace :)

1

@stivens:

Co to znaczy "skomplikowane wyrażenia"?

bo jeżeli są to faktycznie "skomplikowane wyrażenia", to porzuciłbym oba rozwiązania: regex i to swoje i napisał/użył porządny parser.

np. html, kod w danym języku programowania, email(RFC), url (gotowy parser).

Wyjdźmy od przykładów, bo bez tego ciężko sensownie dyskutować.

2

Eh no wlasnie chyba nawet nie musi byc takie skomplikowane.
Zalozmy, ze driver do Postgresa rzuca Ci wyjatek bo unique constraint jest zlamany. I teraz chcesz takiego exceptiona - gdzie szczegoly sa niestety w error msg - przetlumaczyc na jakis bardziej domenowy wyjatek.

Przyklad co driver wypluwa:

ERROR: duplicate key value violates unique constraint "some_relation_pkey"
  Detail: Key (some_column)=(b01a0e23-da71-3a08-9893-11b8b2dfb069) already exists.

No to robisz regexa, grupujesz i sobie wyciagasz key, value. Podkreslam ze chodzi zarowno o key i value.
val duplicatedKeyRegex = """(?s).*Key (\(.+\))=(\(.+\)) already exists.*""".r
Czyli group0 = (some_column), group1 = (b01a0e23-da71-3a08-9893-11b8b2dfb069)

Jak bys to zrobil w Twojej nakladce? Chyba sie nie da... Ale dobra - moze inaczej. Jak bys chcial zeby to wygladalo?


Przypominam ze ten przyklad nawet nie jest skomplikowany. Po prostu pokazuje, ze regexy maja wieksza "moc".

1

Jako że ten mój tool nie ma jeszcze wyciągania, to nim tego nie zrobisz.

Niemniej jednak ja uważam że w/w Regex jest paskudny jak na to że robi tak prostą rzecz, bo taki jest mój point

że te szalone Regexy zastępuje się prostymi string utils, których po prostu brakuje

klepnąłem to teraz na szybko z ręki pewnie ma z 5 bugów, ale koncept jest prosty GetStringBetween (other strings)

public static (bool Success, string Value) GetStringBetween(this string original, string start, string end)
{
    var startIndex = 0;
    var firstIndex = original.IndexOf(start, startIndex);

    if (firstIndex == -1) return (false, null);

    var secondIndex = original.IndexOf(end, firstIndex + 1);

    if (secondIndex == -1) return (false, null);

    var value = original.Substring(firstIndex + start.Length, secondIndex - firstIndex - start.Length);
    return (true, value);
}

I nagle twój val duplicatedKeyRegex = """(?s).*Key (\(.+\))=(\(.+\)) already exists.*""".r zamieniam na 1 contains, get string between oraz split.

var txt = "RROR: duplicate key value violates unique constraint \"some_relation_pkey\" " +
    "  Detail: Key (some_column)=(b01a0e23-da71-3a08-9893-11b8b2dfb069) already exists.";

if (txt.Contains("duplicate key value violates unique constraint \"some_relation_pkey\""))
{
    var result = txt.GetStringBetween("Detail: Key ", " already exists.");
    var split = result.Value.Split("=");
    Console.WriteLine($"Group0: {split[0]}");
    Console.WriteLine($"Group1: {split[1]}");
}
Group0: (some_column)
Group1: (b01a0e23-da71-3a08-9893-11b8b2dfb069)

i jest to na 1 rzut oka 10 razy prostsze do zdekodowania

1

No dobra, ma sens. Ale to wciaz 1 linijka vs 25 :P

0

@stivens:

No ale implementacja Regexa to też pewnie wiele tysięcy linii kodu

różnica jest taka że Regexa już ktoś zrobił i możesz wpiąć bibliotekę, a w/w string utilsów itd. chyba nie, więc temu wypadałoby to zrobić :P

0

Jak to tam pod spodem działa, w skrócie? Gdzieś pisałeś, że to nie jest wrapper regexów, więc jak to jest zaimplementowane?

0

@Pyxis:

W skrócie: idę w pętli po każdym "requirement" (Then, ThenAnyOf)

i przekazuje do niego oryginalny string i aktualny index

i ten "requirement" ma w sobie w sobie metodę Check która zwraca czy mu się udało czy nie oraz jaki jest nowy index po jego operacji

Tutaj przykład dla Then

public class ThenRequirement : IRequirement
{
	public string Name => "Match one element";

	public string Item { get; set; }

	public CheckOptions Options { get; set; } = new CheckOptions();

	public ThenRequirement(string item)
	{
		Item = item;
	}

	public CheckResult Check(string original, int index)
	{
		if (original is null)
			return new CheckResult(false, index);

		var strComparison = Options?.IgnoreCase ?? false ?
							StringComparison.OrdinalIgnoreCase :
							StringComparison.Ordinal;

		var result = original.IndexOf(Item, index, strComparison) == index;

		var newIndex = result ? index + Item.Length : index;

		return new CheckResult(result, newIndex);
	}

	public override string ToString()
	{
		return "Then Requirement";
	}
}

Ale nie wiem czy nie będę zmieniał podejścia aby coś ugrać na wydajności.

1

kod jest okropny, ale działa ¯\_(ツ)_/¯

var str = @"RROR: duplicate key value violates unique...
            Detail: Key (some_column)=(b01a0e23-da71-3a08-9893-11b8b2dfb069) already exists.";

var check = new Matcher(str)
                .MatchAnything()
                .Then("duplicate key value violates unique")
                .ThenAnything()
                .Then("Detail: Key ")
                .ThenExtract().ExtractAs("output")
                .Then(" already exists.")
                .Check();

Assert.True(check.Success);
Assert.Equal("(some_column)=(b01a0e23-da71-3a08-9893-11b8b2dfb069)", check.ExtractedData["output"]);

Jak pozbieram wszystkie usecasy, to się przepisze

and 101 other hilarious jokes you can tell yourself

1
WeiXiao napisał(a):
[Benchmark]
public bool Case4_YASM()
{
	return new Matcher(text)
			   .MatchAnyOf("Parcel", "Taxi")
			   .ThenAnything().IsOptional()
			   .ThenAnyOf("London", "Zurich")
			   .Check()
			   .Success;
}

jest ładniejsze niż Regex-like

[Benchmark]
public bool Case4_Regex()
{
	return new Regex("^(Parcel|Taxi).*?(London|Zurich)").IsMatch(text);
}

i z tego co widzę, to wiele osób powiela ta opinie (Na reddicie, aczkolwiek woleliby aby to generowało Regexa, LUL.)

jest ładniejsze bo tak to napisałeś, regexa możesz też napisać w ten sposób i z komentarzami:

public bool Case4_Regex()
{
	return new Regex(@"^
                    (Parcel|Taxi)   # any of
                    .*?             # optionally followed by anything
                    (London|Zurich) # then any of these
                ", RegexOptions.IgnorePatternWhitespace).IsMatch("Parcel something London"));
}

i w IDE masz ładne kolorowanie składni, komentarze są w innym kolorze. Jeśli chcesz zrobić jednolinijkowca to możesz, ale da się też napisać czytelnie.
Rozumiem jakby Twoje rozwiązanie było szybsze, ale według benchmarków na reddicie wychodzi że jest kilkanaście razy wolniejsze, w dodatku z ograniczonymi możliwościami i zapewne z wieloma bugami - obsługuje to chociaż backtracking?
Nie mówię już o tym że już widzę niejasności w API - bo czemu "ThenAnything" domyślnie nie dopuszcza niczego?

// edit:

napisałem prosty test na backtracking - wychodzi że nie obsługuje najbardziej podstawowego scenariusza, chyba że coś źle zrobiłem:

        public void Test_Backtracking()
        {
            var result = new Matcher("a bc bd")
                   .Match("a")
                   .ThenAnything()
                   .Then("b")
                   .Then("d")
                   .Check();

            Assert.True(result.Success); // FAIL
        }

Jeśli chcesz żeby to było szybkie i w miarę pozbawione błędów to pod spodem najlepiej jak wygenerujesz i odpalisz regexa. Wtedy masz najlepsze z obu światów (prędkość + rzetelność i wymuszona większa czytelność kodu)

1

@obscurity:

public bool Case4_Regex()
{
    return new Regex(@"^
                    (Parcel|Taxi)   # any of
                    .*?             # optionally followed by anything
                    (London|Zurich) # then any of these
                ", RegexOptions.IgnorePatternWhitespace).IsMatch("Parcel something London"));
}

jest ładniejsze bo tak to napisałeś, regexa możesz też napisać w ten sposób i z komentarzami:

Zauważ tylko proszę, że w ten sposób właściwie to piszesz dokładnie to co ja (AnyOf, Optional, AnyOf) oraz Regexa, czyli taka trochę nadmiarowość

Nie mówię już o tym że już widzę niejasności w API - bo czemu "ThenAnything" domyślnie nie dopuszcza niczego?

Wydaje mi się że cokolwiek, ale jednak coś, a zatem minimum 1 znak? jest też IsOptional :P

Jeśli chcesz żeby to było szybkie i w miarę pozbawione błędów to pod spodem najlepiej jak wygenerujesz i odpalisz regexa. Wtedy masz najlepsze z obu światów (prędkość + rzetelność i wymuszona większa czytelność kodu)

Rozumiem jakby Twoje rozwiązanie było szybsze, ale według benchmarków na reddicie wychodzi że jest kilkanaście razy wolniejsze, w dodatku z ograniczonymi możliwościami i zapewne z wieloma bugami

Zastanawiam się czy gdy podniosę wersję .NET Standard do takiej, która ma już m.in Spany oraz różne perf improvements bo poleci na wyższych .NETach, oraz faktycznie pisząc to uwzględnię wydajność (bo teraz to było na pałę aby działało), to czy faktycznie będzie ciężko zbliżyć się ku Regexowi.

Regex Performance Improvements in .NET 5

1
WeiXiao napisał(a):

Zauważ tylko proszę, że w ten sposób właściwie to piszesz dokładnie to co ja (AnyOf, Optional, AnyOf) oraz Regexa, czyli taka trochę nadmiarowość

po lewej praktycznie są parametry Twoich metod, a po prawej opis, który może lepiej pasować. plus taki że każda osoba znająca regexy odczyta to jednoznacznie i nie będzie miała dodatkowych wątpliwości jak to działa.
Zauważ że nie potrzeba do tego żadnych dodatkowych bibliotek i że jest to dużo bardziej elastyczne.

Zastanawiam się czy gdy podniosę wersję .NET Standard do takiej, która ma już m.in Spany oraz różne perf improvements bo poleci na wyższych .NETach, oraz faktycznie pisząc to uwzględnię wydajność (bo teraz to było na pałę aby działało), to czy faktycznie będzie ciężko zbliżyć się ku Regexowi.

i tak ustawiłeś target framework .net 6.0 i nie da się tego skompilować bez zmian pod starsze wersje. Musiałbyś emitować bezpośrednio IL-code bo tak to robią regexy. Nie problem obsłużyć konkretną sytuację i zrobić to wydajniej niż regexy, ale jeśli chcesz dodać do tego elastyczność i parametry to bez kompilacji się nie obejdzie. Przechodzenie za każdym razem przez listę wymagań nie ma szans być szybsze.

No i nie odniosłeś się do problemu backtrackingu, bez poprawienia którego moim zdaniem biblioteka jest bezużyteczna

0

To API które zaprogramowałeś ogarnie może jakieś 2% użyć.

Powiedz, jak w Twoim API robi się grupy (łapiące i nie łapiące), Look aheady, Look behindy, zagnieżdżone wyrazenkau, przełączniki greeedy/ungreedy, meta znaki, alternatywy, pod modyfikatory i inne rzeczy do których się używa regexpów?

0

@obscurity

Musiałbyś emitować bezpośrednio IL-code bo tak to robią regexy.

Wow, nie sądziłem że aż tyle effortu w to poszło, a zatem jeżeli w żaden sposób nie uda mi się zbliżyć do tego, to pójdę wyłącznie w generowanie Regexa

Na razie przekształcę sobie testy aby sprawdzały czy to co zwraca moja implementacja jest zgodna z tym, co zwraca wygenerowany przeze mnie Regex

[Fact]
public void Test010_DigitsWithLengthBetween()
{
    var input = new List<string> { "Apple123", "Apple5" };

    var matcher = new Matcher()
                        .ThenAnyOf("Apple")
                        .ThenDigitsWithLengthBetween(2,3);

    EnsureRegexAndMatcherReturnEqualResults(matcher, input);
}

public static void EnsureRegexAndMatcherReturnEqualResults(Matcher matcher, string item)
{
    matcher.Build();
    var generator = new RegexGenerator(matcher.GetRequirements.ToList());
    var emitted = generator.Emit();

    Assert.True(emitted.Success);
    Assert.False(string.IsNullOrWhiteSpace(emitted.Code));

    var regex = new Regex(emitted.Code);
    Assert.Equal(matcher.Check(item).Success, regex.IsMatch(item));
}

No i nie odniosłeś się do problemu backtrackingu, bez poprawienia którego moim zdaniem biblioteka jest bezużyteczna
(i też w sumie do @TomRiddle)

Nie wiedziałem że jest to aż tak pożądane, te zaimplementowane rzeczy to casy z którymi najczęściej się spotykałem przy jakiejkolwiek obróbce stringów
I właściwie też po to jest ten thread, bo chciałem się dowiedzieć czego ludzie najczęściej potrzebują gdy muszą już się babrać w stringach.

Dodałem też benche z Compiled oraz NotCompiled

O ile z NotCompiled jest bardzo dobrze (szczególnie Case4), to przy Compiled jest miernie.

Compiled:

Method text Mean Error StdDev Gen 0 Allocated
Case1_YASM Apple 73.05 ns 1.140 ns 1.067 ns 0.0381 160 B
Case1_Regex Apple 53.99 ns 0.603 ns 0.564 ns - -
Case1_YASM AppleWatermelon 75.12 ns 0.572 ns 0.478 ns 0.0381 160 B
Case1_Regex AppleWatermelon 53.64 ns 0.395 ns 0.369 ns - -
Case1_YASM Watermelon 311.46 ns 1.126 ns 0.879 ns 0.0877 368 B
Case1_Regex Watermelon 46.79 ns 0.386 ns 0.342 ns - -
Method text Mean Error StdDev Gen 0 Allocated
Case2_YASM 12312(...)Apple [28] 1,029.61 ns 20.388 ns 21.815 ns 0.2575 1,080 B
Case2_Regex 12312(...)Apple [28] 46.84 ns 0.394 ns 0.308 ns - -
Case2_YASM qqqqq(...)melon [30] 594.30 ns 7.961 ns 7.447 ns 0.1812 760 B
Case2_Regex qqqqq(...)melon [30] 47.57 ns 0.567 ns 0.502 ns - -
Case2_YASM xcvxc(...)melon [44] 1,728.00 ns 16.347 ns 15.291 ns 0.5093 2,136 B
Case2_Regex xcvxc(...)melon [44] 47.03 ns 0.280 ns 0.262 ns - -
Method text Mean Error StdDev Gen 0 Allocated
Case3_YASM 12312(...)pple3 [29] 1,065.93 ns 15.903 ns 14.875 ns 0.3033 1,272 B
Case3_Regex 12312(...)pple3 [29] 63.71 ns 0.648 ns 0.606 ns - -
Case3_YASM qqqqq(...)melon [32] 720.06 ns 10.396 ns 9.724 ns 0.2270 952 B
Case3_Regex qqqqq(...)melon [32] 59.86 ns 0.697 ns 0.652 ns - -
Case3_YASM xcvxc(...)melon [44] 1,852.92 ns 21.209 ns 19.839 ns 0.5093 2,136 B
Case3_Regex xcvxc(...)melon [44] 39.03 ns 0.494 ns 0.438 ns - -
Method text Mean Error StdDev Gen 0 Allocated
Case4_YASM Lette(...)ondon [22] 348.59 ns 3.678 ns 3.260 ns 0.1106 464 B
Case4_Regex Lette(...)ondon [22] 65.44 ns 0.722 ns 0.675 ns - -
Case4_YASM Parce(...)ondon [22] 547.35 ns 8.901 ns 7.891 ns 0.1431 600 B
Case4_Regex Parce(...)ondon [22] 231.41 ns 1.582 ns 1.479 ns - -
Case4_YASM ParcelLondon 264.08 ns 3.405 ns 2.658 ns 0.1049 440 B
Case4_Regex ParcelLondon 85.63 ns 0.639 ns 0.566 ns - -

Not Compiled:


Method text Mean Error StdDev Gen 0 Allocated
Case1_YASM Apple 761.3 ns 10.47 ns 9.28 ns 0.2537 1 KB
Case1_Regex Apple 1,928.2 ns 32.31 ns 35.91 ns 0.6485 3 KB
Case1_YASM AppleWatermelon 744.4 ns 14.63 ns 21.45 ns 0.2537 1 KB
Case1_Regex AppleWatermelon 1,868.8 ns 8.65 ns 7.67 ns 0.6485 3 KB
Case1_YASM Watermelon 1,049.0 ns 9.55 ns 8.94 ns 0.3033 1 KB
Case1_Regex Watermelon 1,698.6 ns 12.28 ns 11.49 ns 0.4978 2 KB
Method text Mean Error StdDev Gen 0 Allocated
Case2_YASM 12312(...)Apple [28] 1.915 μs 0.0222 μs 0.0208 μs 0.5035 2 KB
Case2_Regex 12312(...)Apple [28] 1.483 μs 0.0122 μs 0.0108 μs 0.6046 2 KB
Case2_YASM qqqqq(...)melon [30] 1.494 μs 0.0141 μs 0.0132 μs 0.4292 2 KB
Case2_Regex qqqqq(...)melon [30] 1.533 μs 0.0100 μs 0.0094 μs 0.6046 2 KB
Case2_YASM xcvxc(...)melon [44] 2.629 μs 0.0361 μs 0.0320 μs 0.7591 3 KB
Case2_Regex xcvxc(...)melon [44] 1.307 μs 0.0125 μs 0.0111 μs 0.4539 2 KB
Method text Mean Error StdDev Gen 0 Allocated
Case3_YASM 12312(...)pple3 [29] 2.254 μs 0.0211 μs 0.0198 μs 0.5722 2 KB
Case3_Regex 12312(...)pple3 [29] 2.032 μs 0.0126 μs 0.0106 μs 0.6676 3 KB
Case3_YASM qqqqq(...)melon [32] 1.831 μs 0.0324 μs 0.0287 μs 0.4978 2 KB
Case3_Regex qqqqq(...)melon [32] 2.013 μs 0.0401 μs 0.0478 μs 0.6676 3 KB
Case3_YASM xcvxc(...)melon [44] 2.770 μs 0.0169 μs 0.0132 μs 0.7820 3 KB
Case3_Regex xcvxc(...)melon [44] 1.813 μs 0.0106 μs 0.0099 μs 0.5035 2 KB
Method text Mean Error StdDev Gen 0 Allocated
Case4_YASM Lette(...)ondon [22] 2.098 μs 0.0122 μs 0.0102 μs 0.5493 2 KB
Case4_Regex Lette(...)ondon [22] 5.163 μs 0.0573 μs 0.0536 μs 1.3046 5 KB
Case4_YASM Parce(...)ondon [22] 2.320 μs 0.0149 μs 0.0124 μs 0.5836 2 KB
Case4_Regex Parce(...)ondon [22] 6.136 μs 0.0673 μs 0.0629 μs 1.3199 5 KB
Case4_YASM ParcelLondon 2.013 μs 0.0193 μs 0.0181 μs 0.5455 2 KB
Case4_Regex ParcelLondon 5.068 μs 0.0535 μs 0.0474 μs 1.3199 5 KB
0

Czym to się różni od innych bibliotek do pisania czytelnych regexów (poza tym że jeszcze nie generuje regexu)?

0

@szatkus:

A masz jakiś przykład takiej? bo nie znalazłem żadnego C# regex builder nuget

poza tym że jeszcze nie generuje regexu

Na ten moment potrafi wygenerować Regexa, ale wewnętrznie nie korzysta z niego

Tam wyżej pisząc to pójdę w generowanie Regexa miałem na myśli że wyłącznie, że implementacje bazującą na przejściu po Requirements porzucę

1
WeiXiao napisał(a):

Nie wiedziałem że jest to aż tak pożądane, te zaimplementowane rzeczy to casy z którymi najczęściej się spotykałem przy jakiejkolwiek obróbce stringów
I właściwie też po to jest ten thread, bo chciałem się dowiedzieć czego ludzie najczęściej potrzebują gdy muszą już się babrać w stringach.

No wiesz, możesz sobie oczywiście zrobić prywatny projekt, popróbować różne idee - bo to wygląda jakbyś po prostu miał ochotę sobie napisać taki builder, i nie ma w tym nic złego - ale nie spodziewaj się że ktoś będzie tego używał, z prostego powodu.

Wziąłeś bardzo wszechstronną rzecz, wyrażenia regularne, których było już masa, ereg, bash, perl, pcre, one ewoluowały do potrzeb programistów bardzo długo, z 40 lat już będzie. Można by zaryzykować stwierdzenie że są tak dobrane pod programistów jak tylko się da. I teraz na to bardzo wszechstronne rozwiązanie, Ty nałożyłeś bardzo wąski, bardzo prymitywny wrapper.

Wynik tego może być tylkl jeden - jak już skończysz swój builder, i zaczniesz pisząc swoi inny projekt, jakąś aplikacje - myślę że skończy się tak: znajdziesz 4 potrzeby użycia regexpa, a Twoją biblioteką będzie się dało ogarnąć może jeden. Im dłużej będziesz korzystał z wyrażeń regularnych, tyn prędzej zauważysz ze jakakolwiek próba "uproszczenia" ich, skutkuje odebraniem jakiejś funkcjonalności, która została w nich nie bez powodu - bo jest użyteczna

Piszę to tylko po to żeby Ci oszczędzić rozczarowania.

Odpowiadając na pytanie, z jakich elementów najczęściej korzystacie - ja najczęściej korzystam z alternatyw, grup (łapiących i nie łapiących), przełączników greeedy/ungreedy i meta znaków

1
WeiXiao napisał(a):

No i nie odniosłeś się do problemu backtrackingu, bez poprawienia którego moim zdaniem biblioteka jest bezużyteczna
(i też w sumie do @TomRiddle)

Nie wiedziałem że jest to aż tak pożądane, te zaimplementowane rzeczy to casy z którymi najczęściej się spotykałem przy jakiejkolwiek obróbce stringów
I właściwie też po to jest ten thread, bo chciałem się dowiedzieć czego ludzie najczęściej potrzebują gdy muszą już się babrać w stringach.

To nie jest pożądane tylko wręcz jest to podstawa w tego typu rozwiązaniach o której użytkownik normalnie nie musi nawet myśleć
Obecnie jakiekolwiek rozgałęzienia typu ThenAnyOf czy ThenAnything tak naprawdę u Ciebie nie działają bo dane w środku tego "anything" mogą psuć przypasowanie.
Powiedzmy:

new Matcher("ab")
                   .MatchAnyOf("a", "ab")
                   .ThenAnyOf("b", "c")

zwraca false bo pierwsze MatchAny "zabrało" całe "ab" z wejścia i drugie ThenAnyOf już nie może przypasować "b". Nie działają w pełni nawet najbardziej podstawowe scenariusze. Ale jak już coś takiego zrobiłeś to na Twoim miejscu bym się tym pobawił dalej i próbował to rozwiązać. Zwykłe generatory regexów jak widzisz wyżej już istnieją więc nie ma sensu tworzyć kolejnego

0
obscurity napisał(a):
WeiXiao napisał(a):

No i nie odniosłeś się do problemu backtrackingu, bez poprawienia którego moim zdaniem biblioteka jest bezużyteczna
(i też w sumie do @TomRiddle)

Nie wiedziałem że jest to aż tak pożądane, te zaimplementowane rzeczy to casy z którymi najczęściej się spotykałem przy jakiejkolwiek obróbce stringów
I właściwie też po to jest ten thread, bo chciałem się dowiedzieć czego ludzie najczęściej potrzebują gdy muszą już się babrać w stringach.

To nie jest pożądane tylko wręcz jest to podstawa w tego typu rozwiązaniach o której użytkownik normalnie nie musi nawet myśleć
Obecnie jakiekolwiek rozgałęzienia typu ThenAnyOf czy ThenAnything tak naprawdę u Ciebie nie działają bo dane w środku tego "anything" mogą psuć przypasowanie.
Powiedzmy:

new Matcher("ab")
                   .MatchAnyOf("a", "ab")
                   .ThenAnyOf("b", "c")

zwraca false bo pierwsze MatchAny "zabrało" całe "ab" z wejścia i drugie ThenAnyOf już nie może przypasować "b". Nie działają w pełni nawet najbardziej podstawowe scenariusze. Ale jak już coś takiego zrobiłeś to na Twoim miejscu bym się tym pobawił dalej i próbował to rozwiązać. Zwykłe generatory regexów jak widzisz wyżej już istnieją więc nie ma sensu tworzyć kolejnego

Niech pisze co mu się podoba. może znajdzie jednego usera który tego użyje.

0

Zmieniłem trochę podejście

Od teraz zamiast bawić się w listę wymagań i przejście w pętli, to jest coś bardziej a'la linked lista pomiędzy nimi*

public interface IOperation
{
	...

	IOperation NextOperation { get; set; }
}

co pozwoliło na obsługę przykładów od @obscurity

var result = new Matcher("Apple Water_Banana Watermelon")
				 .Match("Apple")
				 .ThenAnything()
				 .Then("Water")
				 .Then("melon")
				 .Check();
					 
Assert.True(result.Success);

Generalnie "main loop" się uprościł

private EvaluationResult Evaluate()
{
    if (OriginalString is null)
        return new EvaluationResult(false, "Original string is null");

    if (Root is null)
        return new EvaluationResult(true, "");

    var result = Root.Check(OriginalString, 0);

    if (result.Success)
    {
        return new EvaluationResult(true, "", result.ExtractedData);
    }
    else
    {
        // TODO: Improve error diagnostics
        return new EvaluationResult(false, $"Some requirement wasn't fulfilled.");
    }
}

O ile wszystkie testy hulają, to jest pewnie jeszcze kilka przypadków do poprawy / sprawdzenia (TODO™).

* - wcześniej jeszcze zastanawiałem się czy aby nie wygenerować po prostu wszystkich kombinacji i byłoby najmniej roboty, ale przy 1 przypadku (ThenAnything) już się trochę skomplikowało.

1 użytkowników online, w tym zalogowanych: 0, gości: 1