tl; dr. Դուք կարող եք օգտագործել Parsimmon-ը (կամ ցանկացած վերլուծիչի կոմբինատոր գրադարան)՝ տեքստային մուտքագրումներ ստեղծելու համար, ինչպես օրինակ այս ամսաթվերի ընտրիչում, որը կարող եք փորձել այստեղ: Նաև մենք աշխատանքի ենք ընդունում։ Գրեք մեզ [email protected] հասցեով

Երբ մեր արտադրանքի թիմը սկսեց նախագծել ամսաթվերի ընտրիչ՝ պատմական գործարքները որոնելու համար, նրանք հասկացան, որ գոյություն ունեցող ամսաթիվ ընտրողները այնքան էլ չեն համապատասխանում օրինագծին: Ամսաթվեր ընտրողների մեծամասնությունը նախագծված է ընտրելու մեկ ամսաթիվ կամ ճշգրիտ ամսաթվերի միջակայք (սովորաբար ապագայում, օրինակ՝ ճանապարհորդության համար), մինչդեռ մեր ամսաթվերը ընտրողի համար սովորական օգտագործման դեպքը կլինի ժամանակի ընդհանուր տիրույթ ընտրելը: Վերջնական դիզայնը ներառում էր ամսաթվի մուտքագրման երեք ոճ՝ բացվող մենյու սովորական հարցումների համար («անցյալ ամիս», «այս տարի»), ամսական օրացուցային վիդջեթ և տեքստային դաշտեր՝ ստեղնաշարի վրա հիմնված մուտքագրման համար:

Տեքստային դաշտի վրա հիմնված մուտքերը, որոնք մենք տեսանք այլ կայքերում, օգտագործում էին կոշտ ձևաչափեր, ինչպիսիք են MM/DD/YYYY, որոնք մեզ համար անհարմար էր օգտագործել: Ձեր ծննդյան տարեդարձն այդ ձևաչափով մուտքագրելն այնքան էլ դժվար չէ, բայց ամսաթվերի տիրույթի հարցումները, ինչպիսիք են հուլիսի 12-ից մինչև նոյեմբերի վերջը (նոյեմբերը քանի՞ օր է կրկին) ավելի դժվար է մուտքագրել: MM/DD/YYYY ոճը նաև պահանջում է ավելի շատ տեղեկատվություն, քան դուք բնականաբար կմտածեիք. մտավոր կամ բանավոր, դուք հավանաբար կմտածեք «հունվար-մարտ» ընդգրկող ամսաթվերի մասին՝ ընթացիկ տարին և կոնկրետ օրերը՝ 01/01-ի փոխարեն: /2018–03/31/2018.

Այսպիսով, փոխարենը մենք որոշեցինք կատարել ամսաթվի մուտքագրում, որը կարող է կարգավորել գրեթե բոլոր հնարավոր մուտքերը †: Դուք կարող եք փորձել ամսաթիվը մուտքագրել այստեղ:

Ահա մեր ընդունած ձևաչափերի նմուշը՝ վերցված մեր միավորի թեստերից.

1.1.2018
1–1–2018
1/1/2018
1,1,2018
1 1 2018
1 1 // January 1st of the current year
Jan 1
January 1 2018
January 1st 2018
January 2nd 2018
January 3rd 2018
January 4th 2018
March 1 18
December 1, 18
December 1, 19
December 1, 00
Jan // January of the current year (Either first or last day, depending on which date input is being used)
Dec // If December of the current year is in the future, then December of last year, otherwise December of this year.
Jan 17 // January 17, current year
Jan 2017 // January of 2017
today
yesterday
TODAY

Պարզ վերլուծողների համար դուք կարող եք դիմել կանոնավոր արտահայտության, բայց պատկերացնու՞մ եք գրել կանոնավոր արտահայտություն այդ ամենի համար: Կամ ավելի վատ՝ խմբագրե՞լ մեկը, որը գրել է ուրիշը: Մինչ ես կսովորեի վերլուծական կոմբինատորների մասին, ես կարող էի այդպես մոտենալ այս առաջադրանքին, եթե այն ընդհանրապես դուրս չգրեի: Նույնիսկ գրադարանային ծածկագրի ամսաթվերի վերլուծումը չափազանց բարդ կլիներ: Բայց վերլուծական կոմբինատորների միջոցով հեշտ է սկսել փոքր ծավալներով և կատարել այն բարդ ֆունկցիոնալությունը, որն անհրաժեշտ է մեծ օգտագործողի փորձի համար:

Շատ լեզուներ ունեն վերլուծող կոմբինատոր գրադարաններ, սակայն Javascript/Typescript-ի համար մենք կօգտագործենք Parsimmon գրադարանը: Սկսելու համար եկեք նայենք վերլուծական կոմբինատորների վերլուծիչ կեսին:

Վերլուծիչներ

Այս համատեքստում «վերլուծիչը» մի բան է, որը կարող է տողը վերցնել որպես մուտքագրում և կամ վերադարձնել ելք (օրինակ՝ թիվ) կամ ձախողվել: Այսպիսով, օրինակ, եթե մենք ցանկանայինք տողի մի ամբողջ թիվը վերլուծել Javascript համարի մեջ, մենք կցանկանայինք հետևյալ վարքագիծը.

"abc" -> failure
"123" -> 123
"123!" -> 123 (remaining input: "!")
"!123" -> failure
"123.12" -> 123 (remaining input: ".12")

Ահա թե ինչպես մենք կգրեինք դրա համար վերլուծիչ՝ օգտագործելով Parsimmon՝ Typescript-ում.

import * as P from 'parsimmon'
const numberParser: P.Parser<number> =
  P.regexp(/[0–9]+/)
   .map(s => Number(s))

1. Ստեղծեք փոփոխական՝ numberParser, որը Parser է Javascript numbers-ի համար

2. Ընդունեք մուտքագրումը, որը համապատասխանում է կանոնավոր արտահայտությանը, տալով մեզ համապատասխան տողի հատվածը: Այս վերադարձված հատվածը տող է, ուստի դրա տեսակը Parser<string> է:

3. Ի վերջո, մենք կօգտագործենք Parser-ի map ֆունկցիան՝ Javascript-ի Number կոնստրուկտորը կիրառելու համար մինչ այժմ մեր կողմից համապատասխանեցված տողի վրա՝ տալով մեզ Parser<number>: Այսպիսով, եթե վերլուծիչը հետագայում կիրառվի 8_ տողի վրա, այն կթողարկի 123: Նշում. սա տարբերվում է Javascript-ի սովորական քարտեզի ֆունկցիայից, որը մի զանգվածը քարտեզագրում է մյուսին:

Վերլուծիչն իրականում օգտագործելու համար օգտագործեք parse կամ tryParse ֆունկցիաները.

numberParser.parse("123") // {status: true, value: 123}
numberParser.tryParse("123") // 123
numberParser.tryParse("!123") // Exception thrown

Դա արդեն բավականին օգտակար վերլուծիչ է: Բայց մեկ ամիս վերլուծելու համար եկեք սահմանափակենք այն վերլուծելով 1-ից 12-ը.

const numberMonthParser: P.Parser<number> =
  P.regexp(/[0–9]+/)
   .map(s => Number(s))
   .chain(n => {
     if (n >= 1 && n <= 12) {
       return P.succeed(n)
     } else {
       return P.fail(‘Month must be between 1 and 12’)
     }
   })

numberParser-ով մենք պարզապես վերադարձրեցինք մեր ստացած ցանկացած համար: Այս վերլուծիչի միջոցով մենք վերցնում ենք այն արժեքը, որը մինչ այժմ վերլուծել ենք (ամբողջ թիվը n) և դրա վրա կիրառում ենք որոշակի հատուկ տրամաբանություն՝ այս դեպքում սահմանափակելով այն 1-ից մինչև 12-ը:

Դա անելու համար մենք օգտագործում ենք նոր ֆունկցիա՝ chain, որը մի փոքր տարբերվում է mapից:

  • Ի տարբերություն map-ի, chain-ը վերադարձնում է նոր Parser, ոչ թե սովորական արժեք
  • Քանի որ այն կարող է վերադարձնել նոր Parser, շղթան կարող է ձախողվել՝ օգտագործելով P.fail

Այստեղ մենք օգտագործում ենք P.succeed՝ վերադարձնելու նույն արժեքը, որը մտել է, բայց մենք կարող ենք վերադարձնել ցանկացած արժեք, որը ցանկանում ենք: P.fail գործը տալիս է գեղեցիկ սխալի հաղորդագրություն, եթե մենք վերլուծել ենք վատ տվյալները:

Այդ վերլուծիչը մեզ թույլ է տալիս ամիսները վերլուծել իրենց թվային տեսքով: Կատարենք նաև ամիսների լրիվ անվանումների վերլուծիչ.

const monthNames = {
  january: 1,
  february: 2,
  march: 3,
  april: 4,
  may: 5,
  june: 6,
  july: 7,
  august: 8,
  september: 9,
  october: 10,
  november: 11,
  december: 12
};
const namedMonthParser: P.Parser<number> = P.letters.chain(s => {
  const n = monthNames[s.toLowerCase()];
  if (n) {
    return P.succeed(n);
  } else {
    return P.fail(`${s} is not a valid month`);
  }
});

Այսպիսով, սա մեզ թույլ է տալիս վերլուծել ամիսները, որոնք ձևաչափված են 1–12 ձևաչափով մեկ վերլուծիչով, և ամիսները՝ իրենց լրիվ անվանումով մեկ այլ վերլուծիչով: Բայց ի՞նչ, եթե մենք ցանկանանք փորձել կամ վերլուծիչը տողի վրա: Դրա համար մեզ կոմբինատորներ են պետք։

Կոմբինատորներ

Կոմբինատորները օգտակար գործառույթներ են, որոնք ընդունում են մի քանի վերլուծիչներ որպես մուտքագրում և վերադարձնում նոր վերլուծիչ; նրանք «համատեղում են» վերլուծիչները: Պարզ կոմբինատորը alt է, որը վերցնում է վերլուծողների ցուցակը և փորձում է նրանցից յուրաքանչյուրը, մինչև հաջողվի: Օրինակ, ահա թե ինչպես մենք կփորձենք վերլուծել ամիսը որպես թիվ (1–12) կամ անուն (սեպտեմբեր).

const monthParser: P.Parser<number> = P.alt(numberMonth, namedMonth)

Երբ մենք միավորեցինք երկու Parser<number>ներ՝ օգտագործելով alt, ստացանք Parser<number> դուրս: Այս «վերլուծիչները մինչև վերջ» մոդելը շատ հեշտ է դարձնում կոդ կազմելը և նորից օգտագործելը:

Parsimmon-ի մեկ այլ օգտակար կոմբինատոր է seq («sequence» բառի կրճատում): Այն վերցնում է վերլուծողների ցուցակը և գործարկում է բոլորը հերթականությամբ՝ արդյունքները վերադարձնելով զանգվածով: Ահա, թե ինչպես մենք կարող ենք վերլուծել գծիկով բաժանված երկու ամիսը.

const monthRangeParser: P.Parser<[number]> = 
  P.seq(monthParser, P.string("-"), monthParser).map(
    ([firstMonth, _hyphen, secondMonth]) => {
      return [firstMonth, secondMonth];
    }
  );

Այս հիմքով դուք գրեթե պատրաստ եք դուրս գալ և սկսել գրել ձեր սեփական վերլուծական կոմբինատորները. դուք կարող եք շատ հեռուն գնալ վերը նշված պարզ կոմբինատորներով, և այն, ինչ առաջարկում է Parsimmon-ը, վերը նշված գործիքների միջոցով կառուցված հարմար գործառույթներն են: Բայց որպեսզի շարունակենք մեր պատմությունը, ահա որոշ ավելի հետաքրքիր վերլուծիչներ, որոնք անհրաժեշտ են մեր ամսաթվերի վերլուծիչի համար.

Ամսվա օրվա վերլուծիչ

Այս վերլուծիչը վերլուծում է ամսվա թվային օրը (1–31)՝ 1-ին, 2-րդ, 3-րդ և այլնի կամընտիր վերջածանցով:

// Parse a day of the month (1–31)
const dayOfMonthParser: P.Parser<number> = P.regexp(/[0–9]+/)
  .map(s => Number(s))
  .chain(n => {
    return numberDaySuffixParser // See next function
      .fallback("") // Falling back to a value and not using it makes the suffix parser optional
      .chain(() => {
        if (n > 0 && n <= 31) {
          return P.succeed(n);
        } else {
          return P.fail("Day must be between 1 and 31");
        }
      });
  });
// Accept suffixes like 1st, 2nd, etc.
// Note: For our own convenience we'll accept invalid names like 1nd  or 4st
// (If you were implementing something like a programming language,
// you'd want to be more strict)
const numberDaySuffixParser: P.Parser<string> = P.alt(
  P.string("st"),
  P.string("nd"),
  P.string("rd"),
  P.string("th")
);

2 կամ 4 նիշ տարվա վերլուծիչ

// Parse a 2 or 4 digit year, using custom logic to convert digits
// like "14" to 2015, and digits like "70" to 1970:
const yearParser: P.Parser<number> = P.regexp(/[0–9]+/)
  .map(s => Number(s))
  .chain(n => {
    if (n > 999 && n <= 9999) {
      return P.succeed(n);
    } else if (n > 30 && n < 99) {
      return P.succeed(1900 + n);
    } else if (n >= 0 && n <= 30) {
      return P.succeed(2000 + n);
    } else {
      return P.fail("Invalid year");
    }
  });

Տարանջատող

// Parses 0 or more separator characters between words
// Again, we're fine with 
// (and even want to accept, for greatest possible compatibility)
// inputs like "1 , 2" or "1//3", so we accept 
// arbitrarily many separators using `many`
const separatorParser: P.Parser<string[]> = P.oneOf(",-/ .").many();

Երբ մենք ստեղծենք վերլուծիչներ, ինչպիսիք են month-ը և year-ը, մենք կարող ենք դրանք միավորել միասին՝ ամբողջական ամսաթվերը վերլուծելու համար: Օրինակ՝ այսպես է մեր կոդերի բազան ընդունում «ամսվա տարի» ձևաչափը (օրինակ՝ հունվարի 2018, 1 18, սեպտեմբերի 19 և այլն):

// We pass an argument to the parser to tell it to return
// the first or last day of a month,
// so a text field for the starting day in a date range
// can ask for the first day of the month,
// and the text field for the ending day can ask for the last day
export enum DayDefault {
  Start,
  End
}
const monthYear = (dayDefault: DayDefault): P.Parser<Day> => {
  return P.seq(monthParser, separatorParser, yearParser)
          .map(([aMonth, _s, aYear]) => {
            const firstDay = new Day(aYear, aMonth, 1);
            return dayDefault === DayDefault.Start
              ? firstDay
              : firstDay.toMonth().toLastDay();
          });
};

Օգտագործելով այդ օրինաչափությունը, մենք կարող ենք հեշտությամբ կառուցել վերլուծիչներ բոլոր տարբեր ձևաչափերի համար, որոնք մենք ցանկանում ենք ընդունել, այնուհետև փորձել դրանք բոլորը alt-ով:

export const megaParser = (dayDefault: DayDefault): P.Parser<Day> => {
  return P.alt(
    wordDayParser, // e.g. "today", "yesterday", etc.
    monthDayYearParser, // e.g. "March 19th, 1992", "1 1 01", etc.
    monthDayParser, // e.g. "Dec 12", "1 1", etc.
    monthYearParser(dayDefault), // e.g. "March 1992", "1 01", etc.
    justMonthParser(dayDefault) // e.g. "March", "2", etc.
  );
};

(Նկատի ունեցեք, որ կարգը կարևոր է, քանի որ monthDay-ի նման ձևաչափերը monthDayYear-ի նման ձևաչափերի ենթաբազմություն են:)

Parser Combinators-ի առավելությունները

Այսպիսով, այժմ դուք գիտեք, թե ինչպես կարելի է օգտագործել վերլուծական կոմբինատորներ: Բայց թույլ տվեք մի փոքր ավելին ներկայացնել այս լուծման առավելությունները.

1. Վերլուծման յուրաքանչյուր միավոր փորձարկելի է: Դուք կարող եք ստուգել ձեր վերլուծիչը կարճ ամիսների համար («փետրվար») ձեր վերլուծիչից առանձին թվային ամիսների համար («2»), ձեր վերլուծիչից առանձին բոլոր ամսվա ձևաչափերի համար («սեպտեմբեր» կամ «սեպտեմբեր» կամ «9»), առանձին ձեր վերլուծիչը ամբողջական տարբերակների համար, ինչպիսիք են «Ամիս/օր/տարի»: Նույնիսկ եթե դուք միավորի փորձարկում չեք, կարող եք փորձել այս բոլոր գործառույթները REPL-ում, որը չափազանց օգտակար է մշակման և վրիպազերծման համար:

2. Վերլուծման յուրաքանչյուր միավոր բաղադրելի է: Հետագայում դրանք կարող եք համատեղել տարբեր ձևերով։ Օրինակ, մեր monthParser-ն օգտագործվում է 4 տարբեր բարձր մակարդակի ձևաչափերով՝ ամիս/օր/տարի, ամիս/օր, ամիս/տարի և որպես առանձին ամիս:

3. Վերլուծիչներ կոկիկ վերացական. Ես կարող եմ օգտագործել P.seq(monthParser, P.string(‘-’), yearParser)-ի նման վերլուծիչ՝ չմտածելով ամսվա բոլոր տարբեր ձևաչափերի ընդունման մասին: Ես կարող եմ նաև ավելացնել նոր ընդունված ամսվա ձևաչափեր ավելի ուշ (օրինակ՝ երբեմն օգտագործվող «Sept» հապավումը), և կոդերի վերլուծման բոլոր ամիսները կթարմացվեն:

4. Parser կոմբինատորներն ունեն ձեր ծրագրավորման լեզվի ողջ հզորությունը: Դրանցում ինչ տրամաբանություն ուզես՝ կարող ես անել։ Օրինակ, մեր ամսաթվերի վերլուծիչը ստուգում է ընթացիկ ամիսը՝ պարզելու, թե Դեկտեմբեր-ի նման մուտքագրումը նշանակում է այս կամ անցյալ տարվա դեկտեմբեր: Կարող եք նաև վերլուծել ավելի բարդ ձևաչափեր, ինչպիսիք են HTML-ը, որոնք սովորական արտահայտությունները չեն կարող:

5. Կոդն, իմ աչքերով, շատ ընթեռնելի է: Ահա մի արագ հիշեցում, թե ինչ տեսք ունեն նույնիսկ հիմնական կանոնավոր արտահայտությունները.

[1–9]|1[0–2](,-\/ \.)*[1–9]|1[0–2]

Ուր գնալ այստեղից

Հետաքրքրվա՞ծ եք նման ծրագրավորման տեխնիկայի կիրառմամբ՝ օգտատերերի հնարավոր լավագույն փորձը ստեղծելու համար: Միացե՛ք մեզ Mercury-ում; մենք ծրագրավորողներ ենք վարձում Typescript-ի, React-ի, Haskell-ի և Nix-ի համար (փորձառության բոլոր մակարդակները ողջունելի են, տեղում Սան Ֆրանցիսկոյում): Գրեք մեզ [email protected] հասցեով

† Մեր արտադրանքը բացառիկ է Միացյալ Նահանգների համար, ինչը թույլ է տալիս մեզ խուսափել միջազգայնորեն օգտագործվող ձևաչափերից: