Tavallaan on järkevää pyrkiä eroon kielestä jossa on mielettömästi liikaa käskyjä - ja BASH:issa on. Sillä se ohjelmointi-ympäristö josta yleensä puhutaan on vain yksi sadoista. Senkin käskyt ovat tarkoitettu tiedostojen käsittelyyn ja ovat melkein täysin sopimattomia 'numeronmurskaamiseen' - eikä näin ole alkuunsa ollut joten senkin takia käskyjä on kaksinkertainen määrä.
Aikoinaan luin että BASH:issa on tuhatkunta käskyä ja nän näennäisesti olikin - ja se tuntui ihan hyvältä määrältä. Mutta nopeasti löysin toisista ympäristöistä lisää käskyjä eikä lisäyksille tullut koskaan loppua. Esimerkiksi löysin 'vanhat käskyt' merkkijonojen käsittelyyn ja niitä oli ainakin satoja - mutta niistä ei missään ole minkäänlaista yhteenvetoa, ei käsitystä niiden lukumäärästä, ei tietoa niiden suunnittelu-säännöistä (kyllä joitain perus-sääntöjä on selvinnyt joten niitä voi hyvin rajoitetusti kasata itsekin) eikä niiden olemassaolostakaan yleensä kerrota - tosin jopa BASH-raamatussa on pari mainittuna sivumennen - mutta näiden vanhojen käskyjen hyöty ei valkene ennenkuin niitä on skriptistä suurin osa. Vanhat käskyt toimivat aina samoin vaikka data vaihtuisi paljonkin - mutta uudet käskyt saattavat muuttaa käytöstään kun data muuttuu paljon. Ja vanha käskyt osaavat tehdä asiat samallatavoin muissakin kielissä - esimerkiksi toimitaan kokonaisilla sanoilla eikä merkeillä. Lisäksi ne osaavat hyödyntää yksinkertaisia regex:iä ja ne ovat yli kymmenenkertaa nopeampia kuin uudet käskyt - ja vanhojen käskyjen ryhmät ovat vielä nopeampia mikäli skripti muodostuu yksinomaan noista vanhoista käskyistä.
Esimerkiksi hyvin pieni otos tekstijonojen käsittely-käskyistä:
merkkijono=123; echo "${merkkijono::-${montakolopustapoistetaan:-1}}" # tulostaa: 12 (tai:echo "${merkkijono%?}" tai:echo "${merkkijono%3}"
merkkijono=12343; echo "${merkkijono%?43}" # tulostaa: 12 (tai:echo "${merkkijono%[0-9]43}")
merkkijono=123; echo "${merkkijono: -montakolopusta tulostetaan}"
merkkijono=123; echo "${merkkijono:0:montakoalusta tulostetaan}" # eli: "${merkkijono:monennestako aloitetaan:kuinka monta}"
merkkijono=123; echo "${merkkijono:montakoalusta poistetaan}"
merkkijono=xabyabz; echo ${merkkijono/ab/acc} # tulostaa: xaccyabz -> korvaa vain kerran alusta - korvaava ja korvattava voivat olla eripitkiä
merkkijono=xabyabz; echo ${merkkijono//ab/acc} # tulostaa: xaccyaccz -> korvaa kaikki
merkkijono='9 8 7 5 4'; echo ${merkkijono//8*5/} # tulostaa: 9 4
merkkijono='9 8 7 5 4'; echo ${merkkijono%% *} # tulostaa: 9
merkkijono='9 8 7 5 4'; echo ${merkkijono% *} # tulostaa: 9 8 7 5
merkkijono='9 8 7 5 4'; echo ${merkkijono##* } # tulostaa: 4
merkkijono='9 8 7 5 4'; echo ${merkkijono#* } # tulostaa: 8 7 5 4
- jakopiste voi olla myös sana, muuttuja, erottavien merkkien lista tai regex - tai olisikohan taas niin että vain mielikuvitus on rajana.
- erottavien merkkien lista esimerkiksi - .:/ ->
merkkijono='9:8:7:5:4'; echo ${merkkijono%%[- .:/]*}
merkkijono='9 8 7 5 4'; apu=${merkkijono% * *}; echo ${apu##* } # tulostaa: 7 . Tämmöisiä kannattaa muodostaa ohjelmallisesti.
- näiden käskyjen suurin heikkous on se, että mikäli erottavaa merkkiä ei edes ole tulostetaankin koko sana josta etsitään - eikä tulosteta tyhjää niinkuin pitäisi. Mutta tuohon oikeaan käyttäytymiseen pääsee kun haun eteen kirjoittaa: [[ "$merkkijono" =~ "$jakavasana" ]] && -> esimerkiksi:
jakavasana=next; merkkijono='9next8next7next5next4'; [[ "$merkkijono" =~ "$jakavasana" ]] && echo ${merkkijono%%"$jakavasana"*} # tulostaa: 9
Ja vaikka merkintä on pitkä on se edelleen nopea; eikä sitä edes kirjoiteta koodiin vaan kopioidaan-liimataan dokumentista.
Joten tästä ryhmästä voidaan muodostaa seuraavat funktiot (siis määritellä uudet käskyt):
otaeka () { merkkijono=$1; [[ $2 ]] && erottaja=$2 || erottaja='[- .:/]'; echo ${merkkijono%%$erottaja*} ;}
# kutsuesimerkki: otaeka '9 8 7 5 4' tai: otaeka '9 8 7 5 4' erottavasana
#
# siis mikähyvänsä muuttujan:erota merkeistä voi toimi erottajana - ei koko joukko.
# muuttuja:erottavasana sensijaan voi olla tosiaan sana. Sitä ei tarvise kutsussa olla mutta jos se on niin sitä käytetään muutta jos ei ole niin sitten käytetään merkkejä.
# rakenne: if-then-else vanhentui - vastaava merkintä nykyään on tuo tässäkin oleva: [[ $2 ]] && 'joko hypi' || 'tai tärise'. Ja koko ehto on aina samalla rivillä.
## on kommentin lisä-selvennys. Joten ### on seli-seli-seli
## [[ $2 ]] on se if - elikä looginen ehto: onko-olemassa - mikähyvänsä teksti on jotakin joten nollakin on jotakin - ainoastaan '' on ei-mitään
## (( $2 )) olisi matemaattinen if: onko matemaatista arvoa vai onko se nolla. Vain numeroita voi testata näin.
## && on then ja || on else
otavika () { merkkijono=$1; [[ $2 ]] && erottaja=$2 || erottaja='[- .:/]'; echo ${merkkijono##*$erottaja} ;}
poistaeka () { merkkijono=$1; [[ $2 ]] && erottaja=$2 || erottaja='[- .:/]'; echo ${merkkijono#*$erottaja} ;}
poistavika () { merkkijono=$1; [[ $2 ]] && erottaja=$2 || erottaja='[- .:/]'; echo ${merkkijono%$erottaja*} ;}
# kutsuesimerkki ja muut kommentit näissä kolmessa ovat samat kuin otaeka:ssa. Nämä yksittäiset käskyt ovat tosi-nopeita.
# Ja näistä saa kasattua käskyn poimijoku:
otaeka () { merkkijono=$1; [[ $2 ]] && erottaja=$2 || erottaja='[- .:/]'; echo ${merkkijono%%$erottaja*} ;}
poistaeka () { merkkijono=$1; [[ $2 ]] && erottaja=$2 || erottaja='[- .:/]'; echo ${merkkijono#*$erottaja} ;}
poimijoku () { merkkijono=$1; apu=$1; for (( n=1; n<=$(($2-1)); n++ )); do apu=$(poistaeka "$apu"); done; otaeka "$apu" ;}
# esimerkkikutsu:
poimijoku '987654321 87654321 7654321 54321 4321' 3 # tulee siis 7654321
Hidashan tämmöinen poimijoku on ja turhan monimutkainenkin mutta ajankuluahan sen kokoaminen oli - ja saihan sen tekemisestä paljon oppiakin. Kymmenenkertaa nopeampi ja yksinkertaisempi on:
poimijoku () { IFS="- .:/&¤"; apu=($1); unset IFS; echo ${apu[$2-1]} ;} # IFS (->sananväli joka oletuksena on välilyönti) on aina yksi merkki kerrallaan annetusta listasta - siis tavallaan kokeillaan kunnes joku kelpaa.
time poimijoku '987654321-87654321 7654321.54321:4321/321&21¤1' 8 # siis erottava merkki ovat yleensä sama mutta voivat ne vaihdella etsittävässä vaikka jokakerta.
# mutta IFS on sikäli huono ettei se voi olla sana - ja toisekseenkin leikit IFS:n kanssa menevät joskus pahasti pieleen. Seuraava hyväksyy erotusmerkiksi sanan - mutta se on
# hidas:
poimijoku () { apu=$(echo ${1//$2/' '}); apu=($apu); echo ${apu[$3-1]} ;}
# esimerkkikutsu:
poimijoku '987654321next87654321next7654321next54321next4321' next 3
# apu=($apu) ei ole väkivaltaa sillä jokainen BASH:in tavallinen muuttuja on samalla myös matriisi.
# kohdassa: echo ${apu[$3-1]} lasku: $3-1 on merkinnältään normaali eikä $(($3-1)) niinkui BASH:issa muuten -> sulut tämän aiheuttavat
***
Kirjoitin tämän alkulukujen etsinnän uusiksi saatuani skriptin toiminnasta kokemusta - skripti on sama kuin ennenkin mutta teksti on täysin uutta. Toiminta perustuu merkityksellisimmiltä osiltaan iänikuisen vanhaan C-kieliseen ohjelmaan joten:
1. Tämänpäivän koneille ja ohjelmille tämä on aikalailla vaatimatonta mutta silloin kolmekymmentävuotta sitten ja varsinkin huonoissa laitteessa tämä olisi ollut täysin ihmeellistä - nimittäin tämän kaikki osat ovat noilta ajoilta ja skriptin olisi voinut kasata silloinkin.
2. mikä on BASH:in merkitys sillä se pistää tuon C-kielisen ohjelman toimimaan aikalailla toisin kuin ohjelman tekijät aikoinaan tarkoittivat?
Jättimäisten alkulukujen etsimisessä peruslaskutoimituksetkin vaativat rajoittamatonta tarkkuutta ja usein erittäin edistynyttä matematiikkaa - meidän kannaltamme silkkaa henkimaailman toimintaa ja siihen täytyy löytää valmisohjelma.
Tekijöihin jakavan ohjelman kaltaistakaan ei kukaan pysty tekemään yksin. Ja maailmassa on vain muutama ukko joka saa kasattua toisten tekosia yhteen tarpeeksi monta - siis yksi äänekäs päälläpäsmäri ja useita hiljaisia neroja.
BASH:illakin on merkityksellisiä tehtäviä: se poistaa haettavien joukosta kaikki pienillä alkuluvuilla jaolliset jolloin etsittävien joukko kutistuu kymmenenteen osaansa mikä nopeuttaa toimintaa lähes vastaavasti - nämäkin laskut edellytävät että käytettävissä on rajaton tarkkuus ja laskujen tekeminen kestää silloin kauemmin. BASH myös päättää kuinka kauan yhtä lukua selvitellään sillä tässä käytetty valmisohjelma ei itse tajua sitä tehdä.
Jättikokoisia alkulukuja etsitään tässä ohjelmassa aivan samalla tavalla kuin pieniäkin: luku jaetaan alkutekijöihinsä ja alkulukuja ovat ne joilla luku itse on luvun ainoa tekijä. Normaalisti tämmöinen menetelmä on täysin kahjo etsittäessä tekijöitä jättikokoisista luvuista sillä melkein aina yritys epäonnistuu surkeasti koska useimpien tosisuurten lukujen tekijöihin jakaminen kestää iäisyyksiä.
Mutta tuon ohjelman avulla kannattaaa tehdä tällätavoin sillä ohjelma muodostaa alkuluvun ainoan tekijän erittäin nopeasti.
Joten ei tarvita muuta kuin koettaa onnistuuko jako ja jos se ei onnistu nopeasti niin kyseessä ei ole alkuluku ja voidaan siirtyä tutkimaan seuraavaa lukua. Tosin muutamien muidenkin lukujen tekijät selviävät nopeasti joten kyllä aina joutuu laskemaan myös tekijöiden lukumäärän.
Huonossa läppärissäni tuo tekijöihin jakava factor-ohjelma osaa melkein aina muodostaa alkuluvun ainoan tekijän 0.2 sekunnissa mikäli luku on alle 64 numeroinen ja kolmessa sekunnissa mikäli se on alle 192 numeroinen - ja kunnon koneessa toiminta on kymmeniäkertoja nopeampaa.
Hakuskripti on sama riippumatta siitä kuinka suurista luvuista etsitään, mutta muutamia muutoksia sen parametreihin kannattaa tehdä:
timeout:it ovat: .2, .7 ja 3 ja etsintäalueet 5000, 50000 tai 500000 kun etsittävä alkuluku on alle 64 numeroa, 65-128 numeroa ja 129-193 numeroa. Skripti toiminee suuremmillakin numeromäärillä mutta se on toistaiseksi kokeilematta sillä laskenta-aika kasvaa suunnattomasti - esimerkiksi 173 numeroa kestää huonolla koneella päivän. Muuten myös koneen nopeus kannattaa ottaa huomioon: koneen parantuessa timeout pienenee ja hakualue kasvaa.
Skripti ja sen esimerkki_kutsu:
function alkuluvun_haku () { echo "Etsin alkulukuja luvun:$1 perästä."; echo -e "\n\nlöytösekunti alkuluku" > /tmp/delme; pienet_alkuluvut=$( seq 2 200 | factor | awk {' if ($3 =="") {print $1} '} | sed 's/://'); alkuaika=$(awk 'BEGIN {printf "%s\n", systime()}'); time for n in $( seq $1 $(bc<<<$1+5000 | tr -d '\\\n')); do for apu in ${pienet_alkuluvut[*]}; do (( $(bc<<<$n%$apu)==0 )) && n=0 && break ; done; (( $n )) && echo -ne '\rTutkittavana: '$n && timeout .2 factor $n | awk -v alkuaika=$alkuaika {' if ($3 =="") {printf "\r%s\n", systime()-asta luvuitalkuaika" "$1}'} | sed 's/://' >> /tmp/delme; done ; cat /tmp/delme | column -t ;}
read -e -p "editoipa lukua josta etsintä aloitetaan: " -i 1234567890123456789012345678901234567890123456789012345678901234567891234567811 luku; alkuluvun_haku $luku
Skriptin toimiessa näytölle kirjoitetaan kokoajan uusia rivejä. Noilla annetuilla arvoilla toiminta kestää noin 3 minuuttia jonka jälkeen näytölle kirjoitetaan loppuyhteenveto:
löytösekunti alkuluku
28 1234567890123456789012345678901234567890123456789012345678901234567891234568623
105 1234567890123456789012345678901234567890123456789012345678901234567891234570811
168 1234567890123456789012345678901234567890123456789012345678901234567891234572647
- skriptiä on helppo kokeilla: senkun leikkaat-liimaat skriptin koko koodin yhdelläkertaa päätteeseesi.
ja painat return. Mitään muuta ei saa tehdä paitsi editoida kun pyydetään.
- niitä suuria alkulukuja ei tässä näytetä sillä ne eivät mahdu mihinkään kovinkaan siististi.
- alkulukujen testaamiseksi on verkossa serveri-ohjelma nimeltään:
https://www.dcode.fr/primality-test. Sillä kuluu samankokoisen alkuluvun testaamiseen sekunteja - mistäs minä tiedän vaikka se kuluttaisi tuon ajan odotus-loopissa jotta serveriä ei käytettäisi alkulukujen etsintään mutta silti se osoittaa että homma on vaikea isoillekin koneille ja siihen tarkoitetuille ohjelmille.
- ja nyt näppini syyhyävät päästä kunnon koneen näppäimille sillä nopeus vähentää suoritusaikaa tunneista minuutteihin. Saavutukset alkavat olla ikivanhalta leikkikalulta kunnioitettavia?