«Этюды для программистов» (Чарльз Уэзерелл, ― «Etudes for Programmers», Charles Wetherell) գրքի չորրորդ էտյուդն առաջարկում է գրել տեքստի ֆորմատավորման ծրագիր, որի պահանջներից մեկը տողի բառերի արանքներում բացատներ ավելացնելով տեքստը ըստ էջի լայնության հավասարացնելն է։ Տեքստի ֆորմատավորման այս մասնակի խնդիրն է նկարագրված նաև, օրինակ, «100 задач по программированию» գրքի (հեղինակներ՝ Дагене В. А., Григас Г. К., Аугутис К. Ф.) 78-րդ առաջադրանքում, որ կոչվում է «Տեքստի տեխնիկական խմբագրում»։
Մի կողմից կարող է այս խնդիրը, գործնական տեսակետից, հնացած թվալ, մյուս կողմից էլ, սակայն, ժամանակակից տեքստային խմբագրիչներում և տեքստային պրոցեսորներում նույնպես կիրառվում են տեքստի հավասարեցման գործողություններ։ Օրինակ, LibreOffice Writer ծրագիրը հավասարեցումը կատարում է ոչ թե բացատների քանակն ավելացնելով, այլ դրանց չափերն ավելացնելով։ Կամ, TeX տեքստային պրոցեսորում ընդհանրապես բացակայում է բացատ նիշը, իսկ հավասարեցման համար բառերի արանքները լցվում են «սոսինձ» կոչվող ազատ տարածությամբ։
Խնդրի լուծման ընթացքը կարող է լինել այսպիսին (որ նույնպես կարդացել եմ ինչ-որ գրքում, արդեն չեմ հիշում, թե՝ որ). ա) տրված տեքստը տրոհել առավելագույնը n
երկարությամբ տողերի՝ չթույլատրելով բառի տրոհում, բ) եթե արդյունքում տողի երկարությունն ավելի կարճ է ստացվում, քան n
թիվը, ապա պատահական բառերի արանքներում ավելացնել այնքան բացատներ, որ տողի երկարությունը դառնա n
-ի հավասար։ Հավասարեցման գործողությունը պետք չէ կիրառել վերջին տողի նկատմամբ։
Այս գրառման մեջ ես պատմում եմ, թե ինչպես եմ C լեզվով ծրագրավորել տեքստն ըստ լայնության հավասարեցման գործողությունը։ Ստորև բերված ծրագրում ես դիտարկում եմ տեքստի հետ կապված մի քանի հասկացություններ. բառ, տող և պարագրաֆ։ Տվյալների կառուցվածքների տեսակետից տեքստը պարագրաֆների հաջորդականություն է։ Պարագրաֆը տողերի կապակցված ցուցակ է։ Տողը բառերի կապակցված ցուցակ է։
#include <ctype.h> #include <string.h> #include <stdlib.h> #include <stdio.h> #include <stdbool.h> #include <time.h>
Տեքստի մեկ բառը ներկայացնելու համար նախատեսել եմ word
ստրուկտուրան։ Սրա w
դաշտը բառի տեքստն է, l
դաշտը բառի երկարությունն է, s
դաշտը բառին հաջորդող բացատների քանակն է, իսկ n
դաշտը հաջորդ բառի ցուցիչն է։
/* տեքստի բառի ներկայացումը */ struct word { char* w; /* պարունակությունը */ size_t l; /* երկարությունը */ size_t s; /* հաջորդող բացատների քանակը */ struct word* n; /* հաջորդ բառի ցուցիչը */ };
Իմ առաջին խնդիրն է const char* text
ցուցիչով տրված տողի սկզբից առանձնացնել len
երկարությամբ հատված, և կազմել այդ հատվածի բառերի կապակցված ցուցակը։ split_to_words
ֆունկցիան ստանում է տողի ցուցիչը, դիտարկվող հատվածի երկարությունը և վերադարձնում է կառուցված ցուցակի առաջին տարրի ցուցիչը։ (Բայց քանի որ տողը բառերի տրոհելու ընթացքում բառերի ցուցակը կառուցվում է գլխիվայր, առաջին բառի ցուցիչը վերադարձնելուց առաջ նախ պետք է շրջել կառուցված ցուցակը։) Բոլոր լրացուցիչ բացատրությունները գրված են ֆունկցիայի մարմնում՝ C լեզվի մեկնաբանությունների տեսքով։
/* Տրոհել տեքստը և կառուցել բառերի կապակցված ցուցակ։ Վերադարձնում է առաջին բառի ցուցիչը։ */ struct word* split_to_words( const char* text, size_t len ) { /* բառերի ցուցակը */ struct word* res = NULL; /* տրոհել տողը */ const char* end = text + len; /* դիտարկվող հատվածի վերջը */ /* քանի դեռ text ցուցիչը չի հասել հատվածի վերջին */ while( text != end ) { /* անցնել բացատների վրայով */ while( isspace(*text) ) ++text; /* text-ը ցույց է տալիս բառի սկիզբը, p ցուցիչը որոնում է բառի վերջը */ const char* p = text; /* քանի դեռ բացատ չէ և տողն ավարտող 0 նիշը չէ, առաջ տանել p-ն */ while( !isspace(*p) && *p != '\0' ) ++p; /* բառի ստրուկտուրայի նմուշի համար վերցնել հիշողություն */ struct word* wo = malloc(sizeof(struct word)); /* բառի չափը նրա վերջն ու սկիզբը ցույց տվող ցուցիչների դիրքերի տարբերությունն է */ wo->l = p - text; /* հիշղություն վերցնել բառի պարունակության համար */ wo->w = malloc(1 + wo->l); /* պատճենել գտված բառը ստրուկտուրայի մեջ */ strncpy(wo->w, text, wo->l); /* բառի վերջում ավելացնել 0 նիշը */ wo->w[wo->l] = '\0'; /* բառին հաջորդում է 1 բացատ */ wo->s = 1; /* նոր ստեղծվող բառը կցել ցուցակի սկզբից */ wo->n = res; /* ցուցակի սկիզբ համարել նոր ավելացրած բառը */ res = wo; /* անցնել հաջորդ բառի որոնմանը */ text = p; } /* վերջին բառին հաջորդող բացատների քանակը նշել զրո */ res->s = 0; /* շրջել բառերի միակապ ցուցակը */ struct word* pp = NULL; struct word* nn = res->n; while( nn != NULL ) { res->n = pp; pp = res; res = nn; nn = nn->n; } res->n = pp; /* վերադարձնել կառուցված ցուցակը */ return res; }
Տեքստի մեկ տողը ներկայացնելու համար սահմանել եմ line
ստրուկտուրան։ Սրա w
դաշտը կապված է տվյալ տողի բառերի ցուցակի առաջին տարրին, c
դաշտում պահվում է տողի բառերի քանակը, l
դաշտը ցույց է տալիս տողի երկարությունը, իսկ n
դաշտը հաջորդ տողի ցուցիչն է։
/* տեքստի տողի ներկայացումը */ struct line { struct word* w; /* բառերի ցուցակ */ size_t c; /* բառերի քանակ */ size_t l; /* տողի երկարություն */ struct line* n; /* հաջորդ տողի ցուցիչ */ };
Տեքստի տողը, ինչպես վերն ասացի՝ բառերի կապակցված ցուցակ է, կառուցվում է create_line
ֆունկցիայով։ Սա ստանում է տեքստի ցուցիչը և մեկ տողի առավելագույն երկարությունը։
/* Կառուցել տրված երկորությամբ տող */ struct line* create_line( const char* str, size_t len ) { /* կառուցել բառերի ցուցակը */ struct line* res = malloc(sizeof(struct line)); res->w = split_to_words( str, len ); /* բառերի քանակն ու տողի երկարությունը դեռ զրո են */ res->c = 0; res->l = 0; /* սկսել առաջին բառից */ struct word* i = res->w; /* քանի դեռ բառերը չեն վերջացել */ while( i != NULL ) { /* ավելացնել բառերի հաշվիչը */ ++res->c; /* հաշվել տողի ընթացիկ երկարությունը */ res->l += i->l + i->s; /* անցնել հաջորդ բառին */ i = i->n; } /* հաջորդ տողի ցուցիչը դեռ դատարկ է */ res->n = NULL; /* վերադարձնել կառուցված օբյեկտը */ return res; }
Քանի որ տողերի ցուցակի կառուցման համար էլ է օգտագործվում դինամիկ առանձնացված հիշողություն, պետք է նախատեսել նաև այդ հիշողությունը խնամքով ազատելու և համակարգին վերադարձնելու միջոցը։ Դա արվել է destroy_words
ֆունկցիայով։
/* քանդել տողերի ցուցակը */ void destroy_words( struct word* l ) { /* եթե ցուցակը դատարկ չէ */ if( l != NULL ) { /* ռեկուրսիվ կանչ ցուցակի պոչի համար */ destroy_words( l->n ); /* ազատել բառի տեեքստի տեղը */ free(l->w); /* ազատել բառի ստրուկտուրայի տեղը */ free(l); } }
Տողերի ցուցակ կազմելու համար պետք է append_line
ֆունկցիայով տրված տողի n
ցուցիչին կապել ևս մի տող։ Սա մի հասարակ ռեկուրսիվ ֆունկցիա է։
/* տողերի ցուցակի պոչից կցել ևս մի տող */ void append_line( struct line** dest, struct line* src ) { if( *dest == NULL ) *dest = src; else append_line( &((*dest)->n), src ); }
Պարագրաֆը տողերի ցուցակ է (տողն էլ իր հերթին՝ բառերի ցուցակ է)։ create_paragraph
ֆունկցիան ստանում է նախնական տողի ցուցիչը և ֆորմատավորվող տեքստի նպատակային լայնությունը։ Ինչպես վերևում՝ լրացուցիչ բացատրությունները ծրագրի տեքստում են։
/* տրված տեքստից կառուցել տրված երկարությունը չգերազանցող տողերի ցուցակ */ struct line* create_paragraph( const char* text, size_t width ) { /* արդյունքի ցուցիչը */ struct line* res = NULL; /* դիտարկվողղ տողի սկիզբն ու վերջը ցույց տվող ինդեքսներ */ size_t begin = 0, end = strlen(text); /* քանի դեռ տեքստի վերջը չէ */ while( begin < end ) { /* հաշվել հերթական հատվածի վերջը */ size_t pos = begin + width; /* ճշտվում է վերջին հատվածի եզրը */ if( pos > end ) pos = end; /* հատվածի աջ եզրից հետ գալ՝ կիսատ բառը չվերցնելու համար */ while( !isspace(text[pos]) && text[pos] != '\0' ) --pos; /* կառուցել նոր տող և կցել տողերի ցուցակի պոչից */ append_line( &res, create_line( text + begin, pos - begin ) ); /* անցնել տեքստի հաջորդ հատվածին */ begin = pos; } /* վերադարձնել պարագրաֆների ցուցակը */ return res; }
Պարագրաֆի համար առանձնացված հիշողության դինամիկ տիրույթը համակարգին է վերադարձվում destroy_lines
ֆունկցիայի օգնությամբ։
/* ազատել պարագրաֆի զբաղեցրած հիշողությունը */ void destroy_lines( struct line* p ) { /* եթե տողերի ցուցակը դատարկ չէ */ if( p != NULL ) { /* ռեկուրսիվ կանչ պոչի համար */ destroy_lines(p->n); /* քանդել բառերի ցուցակը */ destroy_words(p->w); /* ազատել տողի ստրուկտուրայի տեղը */ free(p); } }
Պարագրաֆի տողերը արտածման ստանդարտ հոսքին են դուրս բերվում print_lines
ֆունկցիայով։ Այն նախ տպում է բառը, իսկ հետո՝ դրան հաջորդող բացատաները՝ հարկավոր քանակով։ Արտածվելիք բացատների քանակը պահվում է word
ստրուկտուրայի s
դաշտում։ print_lines
ֆունկցիայում սահմանված spaces
ստատիկ հաստատունը պարունակում է բառից հետո արտածվող բացատների ենթադրվող առավելագույն երկարությամբ տողը, իսկ scount
փոփոխականը՝ այդ տողի երկարությունը։ k->w
բառն արտածելուց հետո պարզապես պետք է արտածել նաև spaces
տողի վերջին k->s
հատվածը։
/* արտածել տողերը */ void print_lines( struct line* lines ) { /* բացատների տող, և բացատների քանակ */ static const char* spaces = " "; static const size_t scount = 20; /* քանի դեռ ցուցակի վերջը չէ */ while( lines != NULL ) { /* k-ն ցուցակի աչաջին բառն է */ struct word* k = lines->w; /* քանի դեռ k-ն չի հասել բառերի ցուցակի վերջին */ while( k != NULL ) { /* արտածել բառը դրան հաջորդող բացատները */ printf( "%s%s", k->w, spaces + scount - k->s ); /* անցնել հաջորդ բառին */ k = k->n; } /* արտածել նոր տողին անցնելու նիշը */ putchar('\n'); /* անցնել պարագրաֆի հաջորդ տողին */ lines = lines->n; } }
Տողի հավասարեցման մարտավարությունը հետևյալն է․ քանի դեռ հացասարեցվող տողի երկարությունը, որը ցույց է տալիս line
ստրուկտուրայի l
դաշտը, փոքր է պահաջվածից, տողում ընտրել պատահական մի բառ (բացի վերջինից) և դրան հաջորդող բացատների քանակն ավելացնել մեկով։
Տողի n
-րդ բառի ցուցիչը վերցնելու համար է նախատեսված nth
ֆունկցիան։ Եթե տողում բառերի քանակը ավելի քիչ է, քան տրված n
թիվը, ապա, բնականաբար, վերադարձվում է NULL
։
/* Վերադարձնում է բառերի ցուցակի n-րդ տարրը։ */ struct word* nth( struct word* list, size_t n ) { struct word* res = list; while( n-- > 0 ) { res = res->n; if( res == NULL ) return NULL; } return res; }
Վերջապես ամեն ինչ պատրաստ է՝ պարագրաֆը տրված լայնությամբ հավասարեցնելու համար։ justify_paragraph
ֆունկցիան ստանում է տողերի ցուցակն ու տողի հարկավոր width
լայնությունը։ Այնուհետև, անցնելով ցուցակի բոլոր տարրերով, բառերի արանքներում բացատներ է ավելացնում այնքան ժամանակ, քանի դեռ տողի երկարությունը չի հասել width
թվին։
/* տողերը հավասարեցնել՝ բառերի արանքներում ներմուծելով լրացուցիչ բացատներ */ void justify_paragraph( struct line* par, size_t width ) { /* քանի դեռ ցուցակի նախավերջին տողը չէ */ while( par->n != NULL ) { /* քանի դեռ տողի երկարությունը փոքր է width-ից */ while( par->l < width ) { /* ընտրել պատահական ինդեքս */ int po = rand() % (par->c - 1); /* վերցնել տողի՝ այդ ինդեքսով բառը */ struct word* wd = nth( par->w, po ); /* 1-ով ավելացնել բառի բացատների քանակը */ ++wd->s; /* 1-ով ավելացնել տողի ընդհանուր երկարությունը */ ++par->l; } /* անցնել հաջորդ տողին */ par = par->n; } }
Ահա այսքանը։ Մնում է միայն կազմակերպել ծրագրի մուտքի կետը՝ main
ֆունկցիան, որպեսզի հնարավոր լինի ծրագիրն օգտագործել իր նշանակությամբ։ Նախատեսված է, որ ծրագիրը տեքստը կարդում է ներմուծման ստանդարտ հոսքից, իսկ ֆորմատավորված արդյունքը դուրս է բերում արտածման ստանդարտ հոսքին։
int main( int argc, char** argv ) { /* ծրագիրը որպես պարամետր սպասում է միայն տեքստի լայնությունը */ if( argc != 1 && argc != 3 ) return 1; /* եթե լայնությունը տրված չէ՝ այն համարել 40 */ size_t length = 40; /* հրամանային տողից կարդալ length-ի նոր արժեքը */ if( argc == 3 ) if( argv[1][0] == '-' && argv[1][1] == 'w' ) sscanf(argv[2], "%d", &length); /* արժեքավորել պատահական թվերի գեներատորը */ srand(time(0)); /* նախնական տեքստի բուֆերի չափը */ const size_t bsize = 4096; /* դինամիկ բուֆեր՝ տեքստի համար */ char* text = calloc(bsize, sizeof(char)); /* քանի դեռ stdin-ի վրա կարդալու բան կա, կարդալ այն text բուֆերի մեջ */ while( fgets(text, bsize, stdin ) != NULL ) { /* կարդացած տեքստից կառուցել տողերի ցուցակ */ struct line* u = create_paragraph( text, length ); /* հավասարեցնել պարագրաֆը տրված լայնությամբ */ justify_paragraph( u, length ); /* արտածել պարագրաֆի տողերը */ print_lines( u ); /* ազատել պարագրաֆի զբաղեցրած հիշողությունը */ destroy_lines( u ); } /* ազատել բուֆերի զբաղեցրած հիշողությունը */ free(text); return 0; }
Ծրագիրը կոմպիլյացնելու համար կարելի է օտագործել gcc
կամ clang
կոմպիլյատորները․
$ gcc -std=c11 -o splitext splitext.c
Իսկ test1.txt
ֆայլում պարունակվող տեքստը, օրինակ, 30 նիշ լայնությամբ տողերով ֆորմատավորելու համար պետք է գրել։
$ ./splittext -w 30 < text1.txt
Նույն արդյունքը կարելի է ստանալ նաև հետևյալ հրամանով․
$ cat test1.txt | ./splittext -w 30
No comments:
Post a Comment