«Этюды для программистов» (Чарльз Уэзерелл, ― «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