123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008(* This file is free software, part of containers. See file "license" for more details. *)(** {1 Basic String Utils} *)type'aiter=('a->unit)->unittype'agen=unit->'aoption(* standard implementations *)includeStringletcompare_int(a:int)b=Stdlib.compareabletcompare=String.comparelethashs=Hashtbl.hashsletlength=String.lengthletis_emptys=equals""letrevs=letn=lengthsininitn(funi->s.[n-i-1])letrec_to_listsaccilen=iflen=0thenList.revaccelse_to_lists(s.[i]::acc)(i+1)(len-1)let_is_sub~subisj~len=letreccheckk=ifk=lenthentrueelseCCChar.equalsub.[i+k]s.[j+k]&&check(k+1)inj+len<=String.lengths&&check0letis_sub~subisj~len=ifi+len>String.lengthsubtheninvalid_arg"CCString.is_sub";_is_sub~subisj~lentype_direction=|Direct:[`Direct]direction|Reverse:[`Reverse]direction(* we follow https://en.wikipedia.org/wiki/Knuth–Morris–Pratt_algorithm *)moduleFind=structtype'akmp_pattern={failure:intarray;str:string;}(* invariant: [length failure = length str].
We use a phantom type to avoid mixing the directions. *)letkmp_pattern_lengthp=String.lengthp.str(* access the [i]-th element of [s] according to direction [dir] *)letget_:typea.dir:adirection->string->int->char=fun~dir->matchdirwith|Direct->String.get|Reverse->funsi->s.[String.lengths-i-1]letkmp_compile_:typea.dir:adirection->string->akmp_pattern=fun~dirstr->letlen=lengthstrinletget=get_~dirin(* how to read elements of the string *)matchlenwith|0->{failure=[||];str}|1->{failure=[|-1|];str}|_->(* at least 2 elements, the algorithm can work *)letfailure=Array.makelen0infailure.(0)<--1;(* i: current index in str *)leti=ref2in(* j: index of candidate substring *)letj=ref0inwhile!i<lendomatch!jwith|_whenCCChar.equal(getstr(!i-1))(getstr!j)->(* substring starting at !j continues matching current char *)incrj;failure.(!i)<-!j;incri|0->(* back to the beginning *)failure.(!i)<-0;incri|_->(* fallback for the prefix string *)assert(!j>0);j:=failure.(!j)done;(* Format.printf "{@[failure:%a, str:%s@]}@." CCFormat.(array int) failure str; *){failure;str}letkmp_compiles=kmp_compile_~dir:Directsletkmp_rcompiles=kmp_compile_~dir:Reverses(* proper search function.
[i] index in [s]
[j] index in [pattern]
[len] length of [s] *)letkmp_find~patternsidx=letlen=lengthsinleti=refidxinletj=ref0inletpat_len=kmp_pattern_lengthpatterninwhile!j<pat_len&&!i+!j<lendoletc=String.gets(!i+!j)inletexpected=String.getpattern.str!jinifCCChar.equalcexpectedthen(* char matches *)incrjelse(letfail_offset=pattern.failure.(!j)iniffail_offset>=0then(assert(fail_offset<!j);(* follow the failure link *)i:=!i+!j-fail_offset;j:=fail_offset)else((* beginning of pattern *)j:=0;incri))done;if!j=pat_lenthen!ielse-1(* proper search function, from the right.
[i] index in [s]
[j] index in [pattern]
[len] length of [s] *)letkmp_rfind~patternsidx=letlen=lengthsinleti=ref(len-idx-1)inletj=ref0inletpat_len=kmp_pattern_lengthpatterninwhile!j<pat_len&&!i+!j<lendoletc=String.gets(len-!i-!j-1)inletexpected=String.getpattern.str(String.lengthpattern.str-!j-1)inifCCChar.equalcexpectedthen(* char matches *)incrjelse(letfail_offset=pattern.failure.(!j)iniffail_offset>=0then(assert(fail_offset<!j);(* follow the failure link *)i:=!i+!j-fail_offset;j:=fail_offset)else((* beginning of pattern *)j:=0;incri))done;(* adjust result: first, [res = string.length s - res -1] to convert
back to real indices; then, what we got is actually the position
of the end of the pattern, so we subtract the [length of the pattern -1]
to obtain the real result. *)if!j=pat_lenthenlen-!i-kmp_pattern_lengthpatternelse-1type'apattern=|P_charofchar|P_KMPof'akmp_patternletpattern_length=function|P_char_->1|P_KMPp->kmp_pattern_lengthpletcompilesub:[`Direct]pattern=iflengthsub=1thenP_charsub.[0]elseP_KMP(kmp_compilesub)letrcompilesub:[`Reverse]pattern=iflengthsub=1thenP_charsub.[0]elseP_KMP(kmp_rcompilesub)letfind?(start=0)~(pattern:[`Direct]pattern)s=matchpatternwith|P_charc->(tryString.index_fromsstartcwithNot_found->-1)|P_KMPpattern->kmp_find~patternsstartletrfind?start~(pattern:[`Reverse]pattern)s=letstart=matchstartwith|Somen->n|None->String.lengths-1inmatchpatternwith|P_charc->(tryString.rindex_fromsstartcwithNot_found->-1)|P_KMPpattern->kmp_rfind~patternsstartendletfind?(start=0)~sub=letpattern=Find.compilesubinfuns->Find.find~start~patternsletfind_all?(start=0)~sub=letpattern=Find.compilesubinfuns->leti=refstartinfun()->letres=Find.find~start:!i~patternsinifres=~-1thenNoneelse(i:=res+1;(* possible overlap *)Someres)letfind_all_l?start~subs=letrecauxaccg=matchg()with|None->List.revacc|Somei->aux(i::acc)ginaux[](find_all?start~subs)letmem?start~subs=find?start~subs>=0letrfind~sub=letpattern=Find.rcompilesubinfuns->Find.rfind~start:(String.lengths-1)~patterns(* Replace substring [s.[pos] … s.[pos+len-1]] by [by] in [s] *)letreplace_at_~pos~len~bys=letb=Buffer.create(lengths+lengthby-len)inBuffer.add_substringbs0pos;Buffer.add_stringbby;Buffer.add_substringbs(pos+len)(String.lengths-pos-len);Buffer.contentsbletreplace?(which=`All)~sub~bys=ifis_emptysubtheninvalid_arg"CCString.replace";matchwhichwith|`Left->leti=find~start:0~subsinifi>=0thenreplace_at_~pos:i~len:(String.lengthsub)~byselses|`Right->leti=rfind~subsinifi>=0thenreplace_at_~pos:i~len:(String.lengthsub)~byselses|`All->(* compile search pattern only once *)letpattern=Find.compilesubinletb=Buffer.create(String.lengths)inletstart=ref0inwhile!start<String.lengthsdoleti=Find.find~start:!start~patternsinifi>=0then((* between last and cur occurrences *)Buffer.add_substringbs!start(i-!start);Buffer.add_stringbby;start:=i+String.lengthsub)else((* add remainder *)Buffer.add_substringbs!start(String.lengths-!start);start:=String.lengths(* stop *))done;Buffer.contentsbmoduleSplit=structtypedrop_if_empty={first:bool;last:bool;}letno_drop={first=false;last=false}letdefault_drop=no_droptypesplit_state=|SplitStop|SplitAtofint(* previous *)letrec_split~bysstate=matchstatewith|SplitStop->None|SplitAtprev->_split_search~bysprevand_split_search~bysprev=letj=Find.find~start:prev~pattern:bysinifj<0thenSome(SplitStop,prev,String.lengths-prev)elseSome(SplitAt(j+Find.pattern_lengthby),prev,j-prev)let_tuple3xyz=x,y,zlet_mkgen~drop~bysk=letstate=ref(SplitAt0)inletby=Find.compilebyinletrecnext()=match_split~bys!statewith|None->None|Some(state',0,0)whendrop.first->state:=state';next()|Some(_,i,0)whendrop.last&&i=lengths->None|Some(state',i,len)->state:=state';Some(ksilen)innextletgen?(drop=default_drop)~bys=_mkgen~drop~bys_tuple3letgen_cpy?(drop=default_drop)~bys=_mkgen~drop~bysString.sublet_mklist~drop~bysk=letby=Find.compilebyinletrecbuildaccstate=match_split~bysstatewith|None->List.revacc|Some(state',0,0)whendrop.first->buildaccstate'|Some(_,i,0)whendrop.last&&i=lengths->List.revacc|Some(state',i,len)->build(ksilen::acc)state'inbuild[](SplitAt0)letlist_?(drop=default_drop)~bys=_mklist~drop~bys_tuple3letlist_cpy?(drop=default_drop)~bys=_mklist~drop~bysString.sublet_mkseq~drop~bysk=letby=Find.compilebyinletrecmakestate()=match_split~bysstatewith|None->Seq.Nil|Some(state',0,0)whendrop.first->makestate'()|Some(_,i,0)whendrop.last&&i=lengths->Seq.Nil|Some(state',i,len)->Seq.Cons(ksilen,makestate')inmake(SplitAt0)letseq?(drop=default_drop)~bys=_mkseq~drop~bys_tuple3letseq_cpy?(drop=default_drop)~bys=_mkseq~drop~bysString.sublet_mk_iter~drop~bysfk=letby=Find.compilebyinletrecauxstate=match_split~bysstatewith|None->()|Some(state',0,0)whendrop.first->auxstate'|Some(_,i,0)whendrop.last&&i=lengths->()|Some(state',i,len)->k(fsilen);auxstate'inaux(SplitAt0)letiter?(drop=default_drop)~bys=_mk_iter~drop~bys_tuple3letiter_cpy?(drop=default_drop)~bys=_mk_iter~drop~bysString.subletleft_exn~bys=leti=find~sub:bysinifi=~-1thenraiseNot_foundelse(letright=i+String.lengthbyinString.subs0i,String.subsright(String.lengths-right))letleft~bys=trySome(left_exn~bys)withNot_found->Noneletright_exn~bys=leti=rfind~sub:bysinifi=~-1thenraiseNot_foundelse(letright=i+String.lengthbyinString.subs0i,String.subsright(String.lengths-right))letright~bys=trySome(right_exn~bys)withNot_found->Noneendletsplit~bys=Split.list_cpy~bysletcompare_versionsab=letof_ints=trySome(int_of_strings)withFailure_->Noneinletreccmp_recab=matcha(),b()with|None,None->0|Some_,None->1|None,Some_->-1|Somex,Somey->(matchof_intx,of_intywith|None,None->letc=String.comparexyinifc<>0thencelsecmp_recab|Some_,None->1|None,Some_->-1|Somex,Somey->letc=compare_intxyinifc<>0thencelsecmp_recab)incmp_rec(Split.gen_cpy~by:"."a)(Split.gen_cpy~by:"."b)typenat_chunk=|NC_charofchar|NC_intofintletcompare_naturalab=(* stream of chunks *)letchunkss:unit->nat_chunkoption=leti=ref0inletrecnext()=if!i=lengthsthenNoneelse(matchString.gets!iwith|'0'..'9'asc->incri;read_int(Char.codec-Char.code'0')|c->incri;Some(NC_charc))andread_intn=if!i=lengthsthenSome(NC_intn)else(matchString.gets!iwith|'0'..'9'asc->incri;read_int((10*n)+Char.codec-Char.code'0')|_->Some(NC_intn))innextinletreccmp_recab=matcha(),b()with|None,None->0|Some_,None->1|None,Some_->-1|Somex,Somey->(matchx,ywith|NC_charx,NC_chary->letc=Char.comparexyinifc<>0thencelsecmp_recab|NC_int_,NC_char_->1|NC_char_,NC_int_->-1|NC_intx,NC_inty->letc=compare_intxyinifc<>0thencelsecmp_recab)incmp_rec(chunksa)(chunksb)letedit_distance?(cutoff=max_int)s1s2=letn1=lengths1inletn2=lengths2inifn1=0thenmincutoffn2elseifn2=0thenmincutoffn1elseifequals1s2then0elseifn1-n2>=cutoff||n2-n1>=cutoffthencutoff(* at least cutoff inserts *)else(try(* distance vectors (v0=previous, v1=current) *)letv0=Array.make(lengths2+1)0inletv1=Array.make(lengths2+1)0in(* initialize v0: v0(i) = A(0)(i) = delete i chars from t *)letlower_bound=refmax_intinfori=0tolengths2dov0.(i)<-idone;(* main loop for the bottom up dynamic algorithm *)fori=0tolengths1-1do(* first edit distance is the deletion of i+1 elements from s *)v1.(0)<-i+1;(* try add/delete/replace operations *)forj=0tolengths2-1doletcost=ifChar.equal(String.gets1i)(String.gets2j)then0else1inv1.(j+1)<-min(v1.(j)+1)(min(v0.(j+1)+1)(v0.(j)+cost))done;ifcutoff<Array.lengthv1&&i<=2*cutoff&&(2*cutoff)-i<String.lengths2thenlower_bound:=min!lower_boundv1.((2*cutoff)-i);(* did we compute up to the diagonal 2*cutoff+1? *)ifcutoff<Array.lengthv1&&i=cutoff*2&&!lower_bound>=cutoffthenraise_notraceExit;(* copy v1 into v0 for next iteration *)Array.blitv10v00(lengths2+1)done;v1.(lengths2)withExit->cutoff)letrepeatsn=assert(n>=0);letlen=String.lengthsinassert(len>0);init(len*n)(funi->s.[imodlen])letprefix~pres=letlen=String.lengthpreiniflen>String.lengthsthenfalseelse(letrecchecki=ifi=lenthentrueelseifStdlib.(<>)(String.unsafe_getsi)(String.unsafe_getprei)thenfalseelsecheck(i+1)incheck0)letsuffix~sufs=letlen=String.lengthsufiniflen>String.lengthsthenfalseelse(letoff=String.lengths-leninletrecchecki=ifi=lenthentrueelseifStdlib.(<>)(String.unsafe_gets(off+i))(String.unsafe_getsufi)thenfalseelsecheck(i+1)incheck0)lettakens=ifn<String.lengthsthenString.subs0nelseslettake_whilefs=leti=ref0inwhile!i<String.lengths&&f(String.unsafe_gets!i)doincridone;String.subs0!iletrtake_whilefs=lets_len_pred=String.lengths-1inleti=refs_len_predinwhile!i>=0&&f(String.unsafe_gets!i)dodecridone;if!i<s_len_predthenString.subs(!i+1)(s_len_pred-!i)else""letdropns=ifn<String.lengthsthenString.subsn(String.lengths-n)else""lettake_dropns=takens,dropnsletchop_suffix~sufs=ifsuffix~sufsthenSome(String.subs0(String.lengths-String.lengthsuf))elseNoneletchop_prefix~pres=ifprefix~presthenSome(String.subs(String.lengthpre)(String.lengths-String.lengthpre))elseNoneletblit=String.blitletfoldfaccs=letrecfold_recfaccsi=ifi=String.lengthsthenaccelsefold_recf(faccs.[i])s(i+1)infold_recfaccs0letfoldifaccs=letrecfold_recfaccsi=ifi=String.lengthsthenaccelsefold_recf(faccis.[i])s(i+1)infold_recfaccs0letpad?(side=`Left)?(c=' ')ns=letlen_s=String.lengthsiniflen_s>=nthenselse(letpad_len=n-len_sinmatchsidewith|`Left->initn(funi->ifi<pad_lenthencelses.[i-pad_len])|`Right->initn(funi->ifi<len_sthens.[i]elsec))let_to_gensi0len=leti=refi0infun()->if!i=i0+lenthenNoneelse(letc=String.unsafe_gets!iinincri;Somec)letto_gens=_to_gens0(String.lengths)letof_charc=String.make1cletof_geng=letb=Buffer.create32inletrecaux()=matchg()with|None->Buffer.contentsb|Somec->Buffer.add_charbc;aux()inaux()letto_itersk=String.iterksletrec_to_seqsilen()=iflen=0thenSeq.NilelseSeq.Cons(s.[i],_to_seqs(i+1)(len-1))letto_seqs=_to_seqs0(String.lengths)letof_iteri=letb=Buffer.create32ini(Buffer.add_charb);Buffer.contentsbletof_seqseq=letb=Buffer.create32inSeq.iter(Buffer.add_charb)seq;Buffer.contentsbletto_lists=_to_lists[]0(String.lengths)letof_listl=letbuf=Buffer.create(List.lengthl)inList.iter(Buffer.add_charbuf)l;Buffer.contentsbufletof_arraya=init(Array.lengtha)(funi->a.(i))letto_arrays=Array.init(String.lengths)(funi->s.[i])letlines_gens=Split.gen_cpy~drop:{Split.first=false;last=true}~by:"\n"sletlines_iters=Split.iter_cpy~drop:{Split.first=false;last=true}~by:"\n"sletlines_seqs=Split.seq_cpy~drop:{Split.first=false;last=true}~by:"\n"sletliness=Split.list_cpy~drop:{Split.first=false;last=true}~by:"\n"sletconcat_gen_buf~sepg:Buffer.t=letb=Buffer.create256inletrecaux~first()=matchg()with|None->b|Somes->ifnotfirstthenBuffer.add_stringbsep;Buffer.add_stringbs;aux~first:false()inaux~first:true()letconcat_gen~sepg=letbuf=concat_gen_buf~sepginBuffer.contentsbufletconcat_iter_buf~sepi:Buffer.t=letbuf=Buffer.create256inletfirst=reftrueini(funs->if!firstthenfirst:=falseelseBuffer.add_stringbufsep;Buffer.add_stringbufs);bufletconcat_iter~sepi=letbuf=concat_iter_buf~sepiinBuffer.contentsbufletconcat_seq_buf~sepseq:Buffer.t=letbuf=Buffer.create256inletfirst=reftrueinSeq.iter(funs->if!firstthenfirst:=falseelseBuffer.add_stringbufsep;Buffer.add_stringbufs)seq;bufletconcat_seq~sepseq=letbuf=concat_seq_buf~sepseqinBuffer.contentsbufletunlinesl=letlen=List.fold_left(funns->n+1+String.lengths)0linletbuf=Bytes.createleninletrecaux_blitil=matchlwith|[]->assert(i=len);Bytes.to_stringbuf|s::tail->letlen_s=String.lengthsinBytes.blit_strings0bufilen_s;Bytes.setbuf(i+len_s)'\n';aux_blit(i+len_s+1)tailinaux_blit0lletunlines_geng=letbuf=concat_gen_buf~sep:"\n"ginBuffer.add_charbuf'\n';Buffer.contentsbufletunlines_iteri=letbuf=concat_iter_buf~sep:"\n"iinBuffer.add_charbuf'\n';Buffer.contentsbufletunlines_seqseq=letbuf=concat_seq_buf~sep:"\n"seqinBuffer.add_charbuf'\n';Buffer.contentsbufletsetsic=ifi<0||i>=String.lengthstheninvalid_arg"CCString.set";init(String.lengths)(funj->ifi=jthencelses.[j])letiter=String.iterletfilter_mapfs=letbuf=Buffer.create(String.lengths)initer(func->matchfcwith|None->()|Somec'->Buffer.add_charbufc')s;Buffer.contentsbufletfilterfs=letbuf=Buffer.create(String.lengths)initer(func->iffcthenBuffer.add_charbufc)s;Buffer.contentsbufletuniqeqs=ifString.lengths=0thenselse(letbuf=Buffer.create(String.lengths)inBuffer.add_charbufs.[0];let_=fold(funprevious_cc->ifnot(eqprevious_cc)thenBuffer.add_charbufc;c)s.[0]sinBuffer.contentsbuf)letflat_map?sepfs=letbuf=Buffer.create(String.lengths)initeri(funic->(matchsepwith|Some_wheni=0->()|None->()|Somesep->Buffer.add_stringbufsep);Buffer.add_stringbuf(fc))s;Buffer.contentsbufexceptionMyExitletfor_allps=tryiter(func->ifnot(pc)thenraiseMyExit)s;truewithMyExit->falseletexistsps=tryiter(func->ifpcthenraiseMyExit)s;falsewithMyExit->trueletdrop_whilefs=leti=ref0inwhile!i<lengths&&f(unsafe_gets!i)doincridone;if!i>0thensubs!i(lengths-!i)elsesletrdrop_whilefs=leti=ref(lengths-1)inwhile!i>=0&&f(unsafe_gets!i)dodecridone;if!i<lengths-1thensubs0(!i+1)elses(* notion of whitespace for trim *)letis_space_=function|' '|'\012'|'\n'|'\r'|'\t'->true|_->falseletltrims=drop_whileis_space_sletrtrims=rdrop_whileis_space_sletmap2fs1s2=iflengths1<>lengths2theninvalid_arg"CCString.map2";init(String.lengths1)(funi->fs1.[i]s2.[i])letiter2fs1s2=iflengths1<>lengths2theninvalid_arg"CCString.iter2";fori=0toString.lengths1-1dofs1.[i]s2.[i]doneletiteri2fs1s2=iflengths1<>lengths2theninvalid_arg"CCString.iteri2";fori=0toString.lengths1-1dofis1.[i]s2.[i]doneletfold2faccs1s2=iflengths1<>lengths2theninvalid_arg"CCString.fold2";letrecfold'accs1s2i=ifi=String.lengths1thenaccelsefold'(faccs1.[i]s2.[i])s1s2(i+1)infold'accs1s20letfor_all2ps1s2=tryiter2(func1c2->ifnot(pc1c2)thenraiseMyExit)s1s2;truewithMyExit->falseletexists2ps1s2=tryiter2(func1c2->ifpc1c2thenraiseMyExit)s1s2;falsewithMyExit->true(** {2 Ascii functions} *)letequal_caselesss1s2:bool=String.lengths1=String.lengths2&&for_all2(func1c2->CCChar.equal(CCChar.lowercase_asciic1)(CCChar.lowercase_asciic2))s1s2letto_hex(s:string):string=leti_to_hex(i:int)=ifi<10thenChar.chr(i+Char.code'0')elseChar.chr(i-10+Char.code'a')inletres=Bytes.create(2*lengths)infori=0tolengths-1doletn=Char.code(getsi)inBytes.setres(2*i)(i_to_hex((nland0xf0)lsr4));Bytes.setres((2*i)+1)(i_to_hex(nland0x0f))done;Bytes.unsafe_to_stringresletof_hex_exn(s:string):string=letn_of_c=function|'0'..'9'asc->Char.codec-Char.code'0'|'a'..'f'asc->10+Char.codec-Char.code'a'|'A'..'F'asc->10+Char.codec-Char.code'A'|_->invalid_arg"string: invalid hex"inifString.lengthsmod2<>0theninvalid_arg"string: hex sequence must be of even length";letres=Bytes.make(String.lengths/2)'\x00'infori=0to(String.lengths/2)-1doletn1=n_of_c(String.gets(2*i))inletn2=n_of_c(String.gets((2*i)+1))inletn=(n1lsl4)lorn2inBytes.setresi(Char.chrn)done;Bytes.unsafe_to_stringresletof_hexs=trySome(of_hex_exns)withInvalid_argument_->Noneletpp_bufbufs=Buffer.add_charbuf'"';Buffer.add_stringbufs;Buffer.add_charbuf'"'letppfmts=Format.fprintffmt"\"%s\""smoduleInfix=structlet(=)=equallet(<>)ab=not(equalab)let(>):t->t->bool=Stdlib.(>)let(>=):t->t->bool=Stdlib.(>=)let(<):t->t->bool=Stdlib.(<)let(<=):t->t->bool=Stdlib.(<=)endincludeInfix