Sunday, August 16, 2015

Ծրագրերի սկզբնային տեքստերի գեղեցկության մասին

Կարելի՞ է արդյոք կոմպյուտերային ծրագրի կոդի մասին խոսելիս ասել, որ այն գեղեցիկ է։ Արդյո՞ք ծրագրի տեքստը կարելի է բնութագրել գեղագիտական հատկանիշներով։ Այս հարցին ես առաջին անգամ առնչվեցի, որբ բակալավրիատում սովորելու առաջին կուրսում, ծրագրավորման գործնական պարապմունքի ժամին դասախոսը՝ Արմեն Անդրեասյանը, ցույց տվեց գրատախտակին գրված ծրագիրը ու ասաց. «Գեղեցիկ է, չէ՞»։ Ինչքան հիշում եմ, թեև հնարավոր է, որ սխալվեմ, այդ ծրագիրը Էվկլիդեսի ալգորիթմն էր՝ գրված C++ լեզվով.

int gcd( int a, int b )
{
  while( a != b )
    if( a > b )
      a -= b;
    else
      b -= a;
  return a;
}

Այն ժամանակ մենք նույնիսկ քմծիծաղեցինք, թե ծրագրի գեղեցիկը ո՞րն է, սովորական ծրագիր է։ Հետագայում, սովորելու և աշխատելու տարիներին, երբ գրեցի շատ ու շատ ծրագրեր, երբ նաև կարդացի ուրիշների գրած շատ ու շատ ծրագրեր, համարյա միշտ ինքս ինձ հարցնում էի, թե գեղեցի՞կ է արդյոք իմ գրածն ու կարդացածը։

Էվկլիդեսի ալգորիթմին վերադառնալով ուզում եմ ասել, որ այն իսկապես գեղեցիկե։ Առավել գեղեցիկ է նրա ռեկուրսիվ իրականացումը, որ ահա գրել եմ Oberon լեզվով․

PROCEDURE Gcd( u, v : INTEGER ) : INTEGER;
VAR
  re : INTEGER;
BEGIN
  IF v = 0 THEN
    re := u
  ELSE
    re := Gcd(v, u MOD v)
  END;
RETURN re END Gcd;

Չէ, իսկապես գեղեցիկ է։ Այս ալգորիթմի համար նույնիսկ կարևոր չէ, թե այն ինչ լեզվով է գրված։ Նրա հիմքում գեղեցիկ մաթեմատիկական միտքն է, այն գեղեցիկ է ստացվելու, երևի թե ցանկացած ծրագրավորման լեզվով գրելիս։ Ճիշտ ինչպես Շիլլերի տողերը, որ միատեսակ գեղեցիկ են ինձ ծանոթ բոլոր լեզուներով։ Բնագիրը.

    Ihr Matten lebt wohl,
    Ihr sonnigen Weiden!
    Der Senn muss scheiden,
    Der Sommer ist hin.
Wir fahren zu Berg, wir kommen wieder,
Wenn der Kuckuck ruft, wenn erwachen die Lieder,
Wenn mit Blumen die Erde sich kleidet neu,
Wenn die Brünnlein fliessen im lieblichen Mai
    Ihr Matten lebt wohl,
    Ihr sonnigen Weiden!
    Der Senne muss scheiden,
    Der Sommer ist hin.

Հայերեն՝ Հ. Թումանյանի թարգմանությամբ.

Մնաք բարով, դո՛ւք, արոտնե՛ր սիրուն,
Ամառն անց կացավ, հոտն իջնում է տուն։

Մենք ետ կըգանք ձեզ նորեկ գարունքին,
Երբ զարթնեն ուրախ երգերը կըրկին,
Երբ որ սարերը զուգվեն կանաչով,
Երբ որ ջըրերը վազեն կարկաչով։

Մնաք բարով, դո՛ւք, արոտնե՛ր սիրուն,
Ամառն անց կացավ, հոտն իջնում է տուն։ 

Ռուսերեն՝ Ն. Սլավյատինսկու թարգմանությամբ.

    Прощайте, луга,
    Багряные зори!
    Разлука нам — горе.
    Ах, лето прошло!
Пора нам в долины... Увидимся снова,
Когда все очнется от сна ледяного
И голос кукушки в лесу зазвучит,
Цветы запестреют, родник зажурчит.
    Прощайте, луга,
    Багряные зори!
    Разлука нам — горе.
    Ах, лето прошло!

Անգլերեն՝ Թ. Մարտինի թարգմանությամբ.

    Farewell, ye green meadows,
    Farewell, sunny shore,
    The herdsman must leave you,
    The summer is o'er.
We go to the hills, but you'll see us again,
When the cuckoo is calling, and wood-notes are gay,
When flowerets are blooming in dingle and plain,
And the brooks sparkle up in the sunshine of May.
    Farewell, ye green meadows,
    Farewell, sunny shore,
    The herdsman must leave you,
    The summer is o'er.

Հիմա արդեն, ծրագրային կոդերի հետ աշխատանքի ավելի քան տաս տարիների փորձն ամփոփելով, կարող եմ ինքս ինձ համար պնդել, որ այո՛, կոմպյուտերային ծրագրի տեքստը (սկզբնային տեքստ, source code) նույնպես կարող է գեղեցիկ լինել։ Ավելին, հուսալի ու արդյունավետ կարող են աշխատել միայն այնպիսի ծրագրերը, որոնք գեղեցիկ տեքստ ունեն։ Համարյա ինչպես ավիացիայում. ասում են, որ լավ է թռչում միայն գեղեցիկ ինքնաթիռը։ Օրինակ, МиГ-29 կործանիչը։ Ես կարող եմ ժամերով նայել այս ինքնաթիռի թռիչքին։ Այն թռչում է ինչպես բալետի պարուհին է պարում բեմի վրա, ինչպես գեղասահորդը սահում է սառույցին։ Եվ երբ նայում եմ այդ ինքնաթիռի գծապատկերին, տեսնում եմ նույն պարուհուն կամ գեղասահորդին:

Նույնպիսի համեմատություն կարող եմ անել Դ. Կնուտի (D. Knuth) TeX և METAFONT ծրագրերի համար։ Այդ երկու ծրագրերն էլ գրված են Literate Programming մեթոդով. կարծես գեղարվեստական ստեղծագործություն լինեն։ Դե, իսկ ո՞ւմ չէ հայտնի TeX-ով պատրաստված տեքստերի և METAFONT-ով պատրաստված տպատառերի գեղեցկությունը։

Կան նաև տգեղ ծրագրեր։ Բառացիորեն վերջերս իմ աշխատանքում պետք եղավ C++ լեզվով գրել ծրագրի մի հատված, որտեղ օբյեկտները բնութագրող տողերի զույգերից պետք էր կառուցել օբյեկտների բառարան (map): Տողերի զույգի առաջին տարրը բառարանի բանալին է, իսկ երկրորդ տարրից պետք է կառուցել բանալուն համապատասխանեցված արժեքը։ Ծրագրի բնույթն այնպիսին է, որ տողերի զույգերը ժամանակի ընթացքում ավելանալու են։ Սկզբում, երբ զույգերը դեռ քիչ էին, գրել էի մի այսպիսի տեքստ (պարզեցված տարբերակով, իհարկե).

std::vector keys;
std::map dict;
keys.push_back("k0");
dict["k0"] = new Descriptor("v0");
keys.push_back("k1");
dict["k1"] = new Descriptor("v1");
keys.push_back("k2");
dict["k2"] = new Descriptor("v2");

Չնայած, որ բնութագրող տողերի փոքր քանակի համար սա ընդունելի է, այնուամենայնիվ, ես համարում եմ, որ այս կոդը տգեղ է։ Հենց թեկուզ այն պատճառով, որ տվյալները «կորել» են լեզվի արտահայտությունների մեջ։

Հետո, երբ բնութագրիչների քանակներն ավելանան, ես keys և dict կոնտեյներները լրացնելու համար կոդը ձևափոխեցի հետևյալ տեսքին.

using pair_t = std::pair;
std::list cpairs = {
    { "k0", "v0" },
    { "k1", "v1" },
    { "k2", "v2" },
    // ....
    { "k12", "v12" }
};
auto maker_f = [&]( pair_t e ) { 
                   keys.push_back(e.first);
                   dict[e.first] = new descriptor(e.second);
               };
std::vector keys;
std::map dict;
std::for_each( cpairs.begin(), cpairs.end(), maker_f );

Մի քիչ երկար է, բայց այս դեպքում, եթե նոր տվյալներ ավելացնեմ, ապա դրանք ավելացնելու եմ միայն cpairs ցուցակում, իսկ տվյալների մշակման մասն արդեն անփոփոխ է մնալու։ Պետք է նշել, որ այս գեղեցիկ տեքստը ես ստացել եմ C++11 ստանդարտում ավելացված հնարավորությունների շնորհիվ։ Եթե ես այս կոդը գրեի, C++98 ստանդարտի հնարավորություններով, ապա ստանալու էի մի կոդ, որին ես գեղեցիկ ասել չեմ կարող.

typedef std::pair pair_t;
std::list cpairs;
cpairs.push_back( std::make_pair( "k0", "v0" ) );
cpairs.push_back( std::make_pair( "k1", "v1" ) );
cpairs.push_back( std::make_pair( "k2", "v2" ) );
// ....
cpairs.push_back( std::make_pair( "k12", "v12" ) );

std::vector names;
std::map dict;
for( std::list::iterator it = cpairs.begin(); it != cpairs.end(); ++it ) {
    names.push_back( it->first );
    dict[it->first] = new Descriptor(it->second);
}

Ցանկացած ծրագրում կարելի է գտնել այսպիսի օրինակներ։ Ցանկացած այսպիսի օրինակի գեղեցկության կամ տգեղության մասին կարելի է վիճել։ Բայց ինձ համար մի բան հաստատ է. ծրագրավորումը կարելի է համարել կիրառական արվեստի մի ճյուղ։ Եվ տեղին է այդ արվեստի նմուշների համար օգտագործել գեղեցիկ և տգեղ բնութագրումները։ Իսկ ծրագրավորողներին ու թեսթավորողներին, այն մարդկանց, ովքեր իրենք առօրյա աշխատանքում գրում ու կարդում են հարյուրավոր ու հազարավոր տողերով ծրագրային տեքստ, մնում է այդ գործում գտնել ու գնահատել գեղեցիկը։

Saturday, August 1, 2015

Տեքստի հավասարեցում ըստ էջի լայնության

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