Dátumok csoportosítása

dátumintervallumok logó

dátumintervallumok logóEbben a Java projektben dátumok csoportosítását oldjuk meg, többféleképpen is. Mikor van erre szükség? Jelentés, kimutatás, riport, lista készítése során.

Példaként tekintsünk egy blogot. A blogban rendszeresen jelenik meg új tartalom (bejegyzés, poszt). Azért, hogy a blog hosszabb távon, sok bejegyzéssel is könnyen kereshető, átlátható, böngészhető legyen/maradjon a felhasználók, látogatók számára, célszerű:

  • taxonómia kialakítása. Ez kategóriákat és címkéket jelent. Ebből címkefelhő vagy szófelhő is készíthető, ahogyan erről blogoltunk már: Címkefelhő generálása.
  • marketing analitika használata. Ezek általában toplisták valamilyen könnyen hozzáférhető adat alapján. Például: látogatottság, népszerűség, eltöltött idő, hozzászólások száma, megosztások száma, egérmutató mozgása alapján hőtérkép. Ezek általában toplisták, amelyek eleje listázódik csökkenő sorrendben.
  • dátum szerint is csoportosítani a blog bejegyzéseit. Érdemes megjeleníteni a legújabbtól a régebbi felé haladó (retrospektív) listát, hierarchikus fa struktúrát, lenyíló panelt. Mindez kombinálható toplistával. A csoportosítás elvégezhető igény szerint tetszőlegesen, például évente, negyedévente, havonta.

Lássuk, hogyan lehet megvalósítani a dátumok csoportosítását Java programozás nyelven!

Milyen adatokra van szükség?

Egy megadott zárt dátumtartományban véletlenszerűen előállítunk néhány dátumot. Nem számít, hogy különböznek-e. A dátumokat tároló listát érdemes csökkenő sorrendben tárolni. Minden dátum múltbeli, így ez a sorrend a jelenhez legközelebbitől halad a legtávolabbi felé. Például a Java program ezekkel a dátumokkal dolgozik (lapozható):

Milyen eredményeket kaphatunk?

Az évenkénti csoportosítás így jelenik meg:

A havonkénti csoportosítás így jelenik meg (lapozható):

Természetesen blog esetén gyűjtőoldalra mutató hivatkozást kell tenni a megjelenő elemekre. Azok az évek és hónapok nem jelennek meg, ahol nincs dátum (blog bejegyzés).

Hogyan kapjuk meg az eredményeket?

Természetesen Java nyelven programozva készítünk megoldást, sőt többféle megoldást. Ezek szépen összevethetők és mindenki kiválaszthatja azt, amit szívesen használna. A dátumobjektumok tárolása generikus listában történik, aminek típusa LocalDate. A dátumok formátuma: DateTimeFormatter.ofPattern("yyyy.MM.dd.").

1. megoldás

Ez a hagyományosnak tekinthető megoldás. Végigjárja a dátumobjektumokat tartalmazó dateList dátumlista adatszerkezetet. Két egymásba ágyazott ciklussal csoportváltást valósít meg. Feltételezi – nem ellenőrzi -, hogy az adatok sorrendje megegyezik az eredmény kiírásának megfelelő sorrenddel. Amíg két egymást követő dátum GROUP_BY_FORMAT formátuma azonos, addig ugyanabba a csoportba tartoznak. A csoportváltáskor az eredmény TYPE_FORMAT formátumú. Közben a beépített megszámolás programozási tétel is működik.

A groupByDate1() függvény képes az évente és havonta csoportosítás megvalósítására. Mindez a paraméterezésén múlik. Évente csoportosít, ha így hívja meg a vezérlés:

Évenkénti csoportosítás során például a 2024.02.26. és a 2024.01.30. (dátumként, nem szövegként értelmezve) azért tartozik egy csoportba, mert a dátumobjektumoktól elkért év "2024" szövegként mindkettő esetében megegyezik.

A groupByDate1() függvény havonta csoportosít, ha így hívja meg a vezérlés:

Havonkénti csoportosítás során például a  2023.06.14. és a 2023.06.08. (szintén dátumként értelmezve) azért tartozik egy csoportba, mert mindkettő illeszkedik a "202306" szövegmintára.

2. megoldás

Ez a Stream API-t és funkcionális programozást használó, újabb megoldás. Ciklus helyett beépített műveletek vannak. A groupByDate2() függvény a dátumok évenkénti csoportosítását képes megoldani:

A groupByDate3() függvény a dátumok havonkénti csoportosítására készült. A YearMonth osztály beépített ( java.time csomag). A DateCount saját POJO. Konstruktora 4 paramétert kap: YearMonth key, Long value, DateTimeFormatter format és String groupText, valamint van két hasznos metódusa. Az egyik az örökölt és felüldefiniált toString() a formázott kiíráshoz, a másik pedig a Comparable interfésztől implementált compareTo() a sorrend kialakításához szükséges összehasonlításhoz.

A funkcionális programozáshoz kötődő lambda műveletekről többször is blogoltunk már, így azokat most nem részletezem. Helyette ajánlom a szakmai blog lambda kifejezés címkéjét.

Továbbfejlesztés

Érdemes átgondolni az 1. és 2. megoldás markáns különbözőségeit, illetve egymást kiegészítő gondolatmenetét. Zárjuk két ötlettel a továbbfejlesztésre vonatkozóan:

  • A 2. megoldás két függvénye megoldható egyetlen függvénnyel, amely hasonlóan paraméterezhető, mint az 1. megoldás függvénye. Ezáltal univerzális(abb)nak tekinthető megoldás is készülhetne. Aki kellően motivált és végiggyakorolja a fentieket, biztosan meg tudja oldani. Várjuk hozzászólásban, vagy az ILIAS-ban a megoldást!
  • A csoportosítás egyben hierarchiát jelent, amiből fa szerkezet építhető. A fa vizuális komponensen is megjeleníthető, ahogyan blogoltunk is róla: Fát építünk.

A bejegyzéshez tartozó teljes forráskódot ILIAS e-learning tananyagban tesszük elérhetővé tanfolyamaink résztvevői számára.

A feladat a Java SE szoftverfejlesztő tanfolyam, a Java EE szoftverfejlesztő tanfolyam és a Java adatbázis-kezelő tanfolyam szakmai moduljának több alkalmához és az orientáló moduljának 1-4. óra: Programozási tételek alkalmához is kötődik. A Stream API-val és a lambda kifejezésekkel sokszor foglalkozunk.

Rómeó és Júlia

Vajon hogyan kerül elő a Rómeó és Júlia az it-tanfolyam.hu szakmai blogban témaként? Hiszen mégiscsak egy Shakespeare színműről/tragédiáról van szó. Vajon mit programozhatunk Java nyelven ehhez kötődően épp Valentin-napon? Mindjárt kiderül.

Tegyünk fel egy kérdést és próbáljunk rá válaszolni! Vajon ki szereti jobban a másikat? Rómeó vagy Júlia?

Induljunk el az adatforrásból, amihez alkalmazkodnunk kell. A színmű angol nyelven publikusan elérhető XML formátumban: The Tragedy of Romeo and Juliet. Az XML fájlok könnyen feldolgozhatók Java nyelven. Részletek a fájlból (görgethető):

Az XML fájl felépítését tanulmányozva (1-5 alapján) megállapíthatóak az alábbiak:

  • A színmű öt felvonásból áll, ezeket <ACT></ACT> csomópontok jelölik.
  • Egy „adagnyi” beszédet a <SPEECH></SPEECH> csomópont fog össze.
  • A csomópontban található, hogy ki beszél: ez a <SPEAKER></SPEAKER> elem. A mesélő, kar esetén ez az elem üres, és a null-t nem szabad feldolgozni.
  • A csomópontban találhatók a szabadvers kimondott sorai: ezek a <LINE></LINE> elemek. Legalább egy sor minden beszédben van, és nem tudjuk előre a számukat.
  • Nem következetes helyen a DOM-ban, többféleképpen beágyazva és önállóan is előfordulhatnak <STAGEDIR></STAGEDIR> elemek. Ezek a színmű Kosztolányi-féle magyar fordításában dőlt betűvel megjelenő – cselekvésre utaló – színpadi utasítások. Van köztük csók is, amit az XML-ből nem szabad feldolgozni, bár erősen ráutaló magatartás. 🙂
  • Nem tudjuk előre, hogy hány csomópont található a fájlban.

A Java program készítése, tesztelése közben – mintegy mellékesen – megtudhatjuk, hogy Rómeó 612 sorban 24075 betűnyi, Júlia 544 sorban 21855 betűnyi szöveget mond. Persze nem mindet egymásnak mondják. Eközben vajon hányszor mondják ki a szeret, szeretem, szeretlek szavakat? A ragoktól, toldalékoktól, kis- és nagybetűket nem megkülönböztetve és attól is eltekintve, hogy éppen kinek/kiknek mondják amit éppen mondanak, egy becsléshez elegendő, ha a love szóra fókuszálunk (számíthatna a loving alak is).

Az alábbi Java forráskód betölti az XML fájlt a memóriába. Ezután kiválogatja a beszédeket. Ha a beszélő élő ember (szereplő), akkor érdekes, hogy mit/miket mond. Ha ROMEO vagy JULIET mondja az adott sort, akkor azt a program kiválogatja két generikus listába ( romeoLineList és julietLineList) beszédnyi adagokban. Ez nem szétválogatás programozási tétel, mert nem minden beszéd minden sora kerül valahová. A kivételkezelés nem kidolgozott.

Könnyen megkaphatjuk, hogy Rómeó hány darab olyan sort mond, amely tartalmazza a love szót. Például ennek a lambda kifejezésnek kiíratva az eredményét a konzolra:

Könnyen megkaphatjuk Rómeótól a 53 sornyi szöveget is így:

Íme Rómeó kiválogatott sorai (az 5. sorban kétszer is előfordul a love, de ez most nem számít):

Hasonlóan megkaphatjuk Júlia 38 kiválogatott sorát is:

Próbáljunk válaszolni a fentiek alapján a feltett kérdésre! Következtethetünk arra, hogy Rómeó jobban szereti Júliát. Legalábbis többször említi. 53>38. Persze tudjuk, hogy mindez nem ilyen egyszerű. 🙂

A bejegyzéshez tartozó teljes forráskódot ILIAS e-learning tananyagban tesszük elérhetővé tanfolyamaink résztvevői számára.

A feladat a Java SE szoftverfejlesztő tanfolyam szakmai moduljának 21-24. óra: Objektumorientált programozás 2. rész, 25-28. óra: Objektumorientált programozás 3. rész, valamint a Java EE szoftverfejlesztő tanfolyam szakmai moduljának 9-12. óra: XML feldolgozás alkalmaihoz kötődik.

Nagyon különböző megoldásokat készíthetünk és szerteágazóan gyakorolhatunk, ha:

  • az XML fájlt kézzel mentjük a webről és utána a helyi fájlrendszerből dolgozzuk fel,
  • az XML fájlt közvetlenül a webről, dinamikusan olvassuk,
  • csak beépített XML-feldolgozást használunk,
  • külső XML API-t használunk,
  • DOM, SAX, XSL, van-e DTD,
  • XPath kifejezésekkel adunk választ a kérdésre,
  • a fenti didaktikusan egyszerű megoldás helyett haladóbb eszközöket (például: Stream API-t) használunk.

Címkefelhő generálása

szófelhő logó

szófelhő logóA címkefelhők/szófelhők népszerűek, sok weboldalon megtalálhatóak. A CMS rendszerekben beépített szolgáltatás is lehet, vagy külön bővítmény/plugin is megvalósíthatja. Egy szövegben előforduló szavakból a gyakrabban előfordulókat nagyobb betűmérettel emeli ki. Eredménye lehet listás, táblázatos, esetleg képpé generált is. Kétféleképpen is megközelíthető, erre utal a Word Cloud és a Tag Cloud elnevezés. Utóbbi inkább egy blog taxonomiájához kapcsolódik és kategóriákra/címkékre érvényesül. A szakmai blogunkhoz is tartozik egy táblázatos címkefelhő. A szófelhő a szöveg betűméretén túl megjelenítheti a szavak előfordulását, például Java forráskód (69), címkefelhő (2).

Példánkban tetszőleges szöveget dolgozunk fel. Ebből felépítünk egy előfordulást is mutató listás szófelhőt, amely rendezett, és a szavak betűmérete 32-16-ig változik. Azok a szavak kerülnek a szófelhőbe, amelyek legalább 5-ször előfordulnak. Kezelünk kivételeket is, például olyan szavakat, amiket nem érdemes szófelhőbe tenni. Lépésenként haladva ismertetjük a megvalósító forráskódot, és külön megjeleníthetők az egyes lépések részeredményei.

A Java programozási nyelv csomagjait, osztályait, interfészeit, metódusait, műveleteit használjuk. Különböző adatszerkezetek kerülnek elő: tömb, generikus lista, generikus map, generikus folyam. Építünk a Stream API szolgáltatásaira és a lambda kifejezésekre. A megvalósítás könnyen testre szabható, kezeli a tipikusan előforduló igényeket.

1. Szövegforrás előkészítése

Generálunk egy 10 bekezdésből álló szöveget a Lorem Ipsum – All the facts – Lipsum generator weboldalon és a későbbi feldolgozáshoz mentjük a Java projekt files mappájába  lorem.txt néven. A fájl mérete: 5781 bájt. Szövegfájl:

2. Szöveges tartalom előkészítése

A megadott útvonalról a java.nio csomag metódusaival betöltjük a szövegfájl tartalmát byte[]-be, majd az s szövegbe. A replace() metódus hívásaival eltávolítjuk a szövegből a sor és bekezdés végét jelző soremelés ( LF="\n") és kocsi vissza ( CR="\r") vezérlőkaraktereket, a vessző és a pont írásjeleket (mindet külön-külön cseréljük a semmire), végül kisbetűssé alakítjuk ( toLowerCase()) a szöveget. A szöveg 5563 db karakterből áll. Előkészített szöveg:

3. Szólista elkészítése

A szóközök mentén darabolva ( split()) a szöveget elkészül belőle egy névtelen szövegtömb ( String[]), amit rögtön átalakítunk ( Arrays.asList()) szöveg típusú generikus listává ( List<String>). A lista 826 db elemből áll. Generikus lista:

4. Csoportosítás és megszámolás

A szólistát csoportosítjuk és megszámoljuk, hogy az egyes szavak hányszor fordulnak elő (másképpen: egy-egy csoport hány elemű). Elkészül a wordCountMap generikus map, amely kulcs-érték párok halmaza (leképezés). A kulcs a szó ( String), az érték a darabszáma ( Long). Alkalmazkodunk ahhoz, hogy a csoportosítás során használt counting() megszámoló művelet Long típusú értéket ad vissza. 188 db kulcs-érték párt kapunk. Generikus map:

5. Szűrés és rendezés

A generikus map-et kétszer szűrjük ( filter() művelet) úgy, hogy a kivételeket tartalmazó exceptList-ben ne szerepeljen a szó, valamint csak a legalább 5-ször előforduló szavakat hagyjuk meg. 71 db elemből álló folyam marad. Ebből a maradékból készítünk rendezett generikus folyamot ( sortedWordCountStream). A sorted() művelet két kulcs-érték párt hasonlít össze. A rendezés érték/darabszám szerint ( getValue()) csökkenő, azon belül kulcs/szavak szerint ( getKey()) növekvő sorrendet biztosít. Másképpen: ha az értékek megegyeznek, akkor a növekvő sorrendet a szavak ábécé sorrendje határozza meg, egyébként a darabszámok csökkenő sorrendje dönti el. Most már könnyen látható, hogy a leggyakrabban előforduló kevés szóból 15 van, 14 előfordulás nincs… Rendezett generikus folyam:

6. Saját típusú listává konvertálás

Definiálunk egy WordCount POJO-t, String típusú word nevű, Long típusú count nevű, int típusú fontSize nevű tulajdonságokkal, getter/setter metódusokkal, és toString() függvénnyel.

A map() intermediate művelettel a rendezett generikus folyamot bejárva, előállítjuk a POJO/ WordCount  típusú kimeneti objektumok rendezett generikus listáját. Továbbra is 71 elemmel dolgozunk. Rendezett generikus lista:

7. Darabszámok összegyűjtése

A POJO típusú rendezett generikus listában lévő objektumoktól elkért darabszámok ( getCount() POJO függvény) közül a különbözőeket ( distinct() művelet) összegyűjtjük egy Long típusú generikus listába ( distinctCountList). Az egyediesítő művelet nincs hatással az adatok sorrendjére. Tízféle előfordulást kapunk. Generikus lista:

8. Betűméret lépésköze

A szófelhőben a szavak gyakorisága alapján határozzuk meg a betűméretet. A betűméret 32-ről indul és fokozatosan csökken 16-ig. A betűméret léptetéséhez a tízféle gyakoriság/előfordulás meghatározza a stepFontSize  lépésközt. Lépésköz:

9. Betűméret kiszámítása

Csoportváltást alkalmazunk és a csoportot gi-vel indexeljük. Egy csoportba azok a POJO objektumok tartoznak, amelyeknél a szavak előfordulása megegyezik. Az algoritmus 2. lépésében az aktuális csoportra érvényesen kiszámítjuk a betűméretet ( fontSize), ami az algoritmus 3. lépésében a csoportba tartozó minden POJO objektumnál beállításra kerül a setFontSize() POJO eljárással. Az algoritmus 4. lépésében léptetjük a csoport gi indexét. A POJO-k esetén először csak a word és count tulajdonságok kerültek beállításra, de most már a fontSize tulajdonság is értéket kapott. Generikus lista:

10. HTML tartalom előállítása

A generikus lista POJO objektumain végighaladva, a forEach() záró művelettel összeállítható a weboldal szófelhőt tartalmazó része ( sbHTML). A 71 db szóból álló szófelhő HTML forráskódjának mérete 3409 bájt. HTML forráskód:

Eredmény

Szöveges formában:

lorem ipsum szófelhő

Képként (a 3. lépés részeredményéből a WordClouds.com weboldalon generálva):

lorem ipsum szófelhő eredmény

A bejegyzéshez tartozó teljes forráskódot ILIAS e-learning tananyagban tesszük elérhetővé tanfolyamaink résztvevői számára.

A feladat a Java SE szoftverfejlesztő tanfolyam szakmai moduljának több alkalmához is kötődik. A Stream API-val és a lambda kifejezésekkel sokszor foglalkozunk.