Using Kotlin kscript for Preprocessing Data

Using Kotlin kscript for Preprocessing Data

DroidCon Boston 2018’s backend this year was Firebase Realtime DB so it could be easily updated and shared with the iOS and Android conference apps. Unfortunately, not all the data was. As is the case with most projects, all the bits needed were in different formats. The main web site was done using Slidesup (https://slidesup.com/). The volunteer list was done using a Google Slides spreadsheet. The FAQ from last year was stored in a Sqlite DB.

To those who haven’t used Firebase RTDB before, it’s basically a nested hashmap; there are no arrays, so they’re done by defining key/value collections. For added complexity, the client APIs for Firebase RTDB is fairly crude; you can’t do complex compound queries or even do inner joins easily (inner joins are done by using nested queries). Firebase Firestore DB addressed the complex compound query API shortcoming, but it was still in Beta when development had to be done for the conference, so we couldn’t risk using it.

The short of it is, we needed the data preprocessed into a Firebase RTDB schema that matched closely to what we needed in the app. The other data also needed to be imported into Firebase. I took this as an opportunity to try using Kotlin kscript (https://github.com/holgerbrandl/kscript) to see if it was up to the task.

For input data formats, we had:
– CSV data exported from Sqlite for the FAQ
– CSV data exported from Google Slides for the Volunteers list
– JSON from Slidesup’s Firebase export, but it was normalized

To simplify the queries on the client, I wrote scripts to denormalize the data so it could be easily consumed by the apps.

First up was the schedule data exported from Slidesup’s database. It looked something like (some fields removed to shorten snippet):

{
"events" : {
"-L4b_dz4i6Wh94qh1kkr": {
"description" : "",
"duration" : "PT45M",
"name" : "",<br> "roomIds": {<br> "-L5_PsZvWSAv4Lrf-Yo9" : true<br> },<br> "speakerIds" : {<br> "-L4NL8eGr2K_XjDFHiTJ" : true<br> },<br> "startTime" : "2018-03-26T15:00:00.000Z",<br> "trackId" : "-L5___NDDDtAjf0dEO7W"<br> },<br> "tracks" : {<br> "-L4bbBGSqk7ZVv5lfKNV" : {<br> "description" : "<p><br></p>",<br> "name" : "Workshops",<br> "sortOrder" : 2<br> },<br> ...<br> },<br> "speakers" : {<br> "-L4CAY7WXpAXJm0PYTwH" : {<br> "bio" : "<p>Lora Kulm is a designer turned developer who has been working with Android for a little over 5 years. She is mostly self-taught with a supplementary Comp Sci 101 class in college, and has worked everywhere from a children’s education app startup to city government. She currently works as an Android developer and is passionate about graphics, animations, and her two dogs.</p>",<br> "name" : "Lora Kulm",<br> "org" : "Phunware",<br> "pictureUrl" : "https://firebasestorage.googleapis.com/v0/b/slidesup-8b9d6.appspot.com/o/confs%2Fdetail%2Fdroidcon-boston-2018%2Fspeakers%2Ff32b0416-59b3-451b-8bd2-879cd8cce082.png?alt=media&token=bb202012-8824-427e-a13f-967fa3f5dbd8",<br> "socialProfiles" : {<br> "github" : "senojl",<br> "linkedIn" : "lora-kulm-a4614569",<br> "twitter" : "loraj_k"<br> },<br> "title" : "Software Engineer"<br> },<br> ...<br> },<br> "rooms" : {<br> "-L5_PsZvWSAv4Lrf-Yo9" : {<br> "name" : "Virginia Wimberly Theatre",<br> "updatedAt" : 1518898744018,<br> "updatedBy" : "y556KVJSEqPQ4s1isGkLnEc9HlD3"<br> },<br> ...<br> }<br>}</pre> <p>Comparing this to what we need for the main agenda screen:</p> <figure><img alt="" src="https://i0.wp.com/www.pgtfb.com/wp-content/uploads/2018/04/12AmDn8Gc2B-2GXI_MEHtJ0Kg.png?w=1200" data-recalc-dims="1"></figure> <p>As you can see, the first issue was the events didn’t include the room names or speaker names. The ID is used instead. Ditto speaker names. The Firebase solution for this is to use a query inside a query (see <a href="https://firebase.googleblog.com/2013/10/queries-part-1-common-sql-queries.html">https://firebase.googleblog.com/2013/10/queries-part-1-common-sql-queries.html</a> if you’re curious, but it’s very tedious to do nested queries in Java or Kotlin which is why the examples are in Javascript :-).</p> <p>Conceptually, it makes sense, but it makes the app code more complicated than it needed to be, and it compounds because it has to be done on multiple platforms. But we can just preprocess the JSON before importing into Firebase and denormalize everything to make it simpler for the apps to process!</p> <p>First we need a data model for the Slidesup export, which is as you’d expect from this nested HashMap structure:</p> <pre><strong>data class </strong>ConferenceDataModel(<br><strong>val events</strong>: Map<String, EventModel>,<br><strong>val rooms</strong>: Map<String, RoomModel>,<br><strong>val speakers</strong>: Map<String, SpeakerModel>,<br><strong>val tracks</strong>: Map<String, TrackModel><br>)<br><strong>data class </strong>EventModel(<br><strong>val name</strong>: String,<br><strong>val description</strong>: String,<br><strong>val duration</strong>: Duration,<br><strong>val startTime</strong>: Date,<br><strong>var endTime</strong>: Date?,<br><strong>val roomIds</strong>: Map<String, Boolean>?,<br><strong>val speakerIds</strong>: Map<String, Boolean>?,<br><strong>val trackId</strong>: String?,<br><strong>var roomNames</strong>: Map<String, Boolean>?,<br><strong>var primarySpeakerName</strong>: String?,<br><strong>var speakerNames</strong>: Map<String, Boolean>?,<br><strong>var speakerNameToPhotoUrl</strong>: Map<String, String>?,<br><strong>var speakerNameToOrg</strong>: Map<String, String>?,<br><strong>var trackName</strong>: String?,<br><strong>var trackSortOrder</strong>: Int?<br>)<br>...</pre> <p>We just need to enhance some of Slideups’ Firebase data so we can import it into our own Firebase RTDB. The “val” fields are the fields that come directly from Slidesup. The “var” fields are the ones we’ll be adding. The processing looks like this:</p> <pre>confData?.<strong>events</strong>?.<em>forEach </em><strong>{<br></strong><em>// denormalize speakers<br></em><strong>val </strong>speakerNames = HashMap<String, Boolean>()<br><strong>val </strong>speakerNameToPhotoUrl = HashMap<String, String>()<br><strong>val </strong>speakerNameToOrg = HashMap<String, String>()<br><strong>it</strong>.<strong>value</strong>.<strong>speakerIds</strong>?.<em>forEach </em><strong>{<br></strong>confData.<strong>speakers</strong>.get(<strong>it</strong>.<strong>key</strong>)?.<em>let </em><strong>{<br></strong>speakerNames.put(<strong>it</strong>.<strong>name</strong>, <strong>true</strong>)<br><strong>if </strong>(<strong>it</strong>.<strong>pictureUrl </strong>!= <strong>null</strong>) {<br> speakerNameToPhotoUrl.put(<strong>it</strong>.<strong>name</strong>, <strong>it</strong>.<strong>pictureUrl</strong>)<br> }<br><strong>if </strong>(<strong>it</strong>.<strong>org </strong>!= <strong>null</strong>) {<br> speakerNameToOrg.put(<strong>it</strong>.<strong>name</strong>, <strong>it</strong>.<strong>org</strong>)<br> }<br><strong>}<br> }<br> if </strong>(speakerNames.<strong>size </strong>> 0) {<br><strong>it</strong>.<strong>value</strong>.<strong>speakerNames </strong>= speakerNames<br><strong>it</strong>.<strong>value</strong>.<strong>speakerNameToPhotoUrl </strong>= speakerNameToPhotoUrl<br><strong>it</strong>.<strong>value</strong>.<strong>speakerNameToOrg </strong>= speakerNameToOrg<br>...<br> }<br><em>// denormalize rooms<br></em><strong>val </strong>roomNames = HashMap<String, Boolean>()<br><strong>it</strong>.<strong>value</strong>.<strong>roomIds</strong>?.<em>forEach </em><strong>{<br></strong>confData.<strong>rooms</strong>.get(<strong>it</strong>.<strong>key</strong>)?.<em>let </em><strong>{<br></strong>roomNames.put(<strong>it</strong>.<strong>name</strong>, <strong>true</strong>)<br><strong>}<br> }<br> if </strong>(roomNames.<strong>size </strong>> 0) {<br><strong>it</strong>.<strong>value</strong>.<strong>roomNames </strong>= roomNames<br> }<br><em>// look up track name<br></em><strong>it</strong>.<strong>value</strong>.<strong>trackId</strong>?.<em>apply </em><strong>{<br> val </strong>trackInfo = confData.<strong>tracks</strong>.get(<strong>it</strong>.<strong>value</strong>.<strong>trackId</strong>!!)<br> trackInfo?.<em>let </em><strong>{ </strong>track <strong>-><br> it</strong>.<strong>value</strong>.<strong>trackName </strong>= track.<strong>name<br> it</strong>.<strong>value</strong>.<strong>trackSortOrder </strong>= track.<strong>sortOrder<br> }<br> }<br></strong><em>// calculate the end time<br></em><strong>it</strong>.<strong>value</strong>.<strong>endTime </strong>= Date.from(<strong>it</strong>.<strong>value</strong>.<strong>startTime</strong>.toInstant().plus(<strong>it</strong>.<strong>value</strong>.<strong>duration</strong>))<br><strong>}</strong></pre> <p>Besides denormalizing the ID fields, we also had to calculate the end time (Firebase’s API also doesn’t support deserializing ISO8601 datetime fields).</p> <p>Next up is the CSV data. Luckily, there are CSV to JSON converters online like <a href="https://www.csvjson.com/csv2json">https://www.csvjson.com/csv2json</a>. From this, we just get json like this:</p> <pre>[<br> {<br> "HS": "X",<br> "First Name": "Stephen",<br> "Last Name": "Vance",<br> "Position": "CTO / Innovation Lead (advising)",<br> "Twitter": "StephenRVance",<br> "PhotoUrl": "http://www.droidcon-boston.com/wp-content/uploads/2018/02/stephen-vance-520x324.jpg"<br> },<br>...</pre> <p>Unfortunately, if you remember, Firebase is only a nested Hashmap (aka a key/value store), so you can’t have arrays. Arrays have to be transposed into hashmaps. Because we’re converting between different data types instead of just enhancing a data model, we’ll need different data models:</p> <pre><strong>data class </strong>CsvVolunteerModel(<br> @Json(name = <strong>"First Name"</strong>)<br><strong>val firstName</strong>: String,<br> @Json(name = <strong>"Last Name"</strong>)<br><strong>val lastName</strong>: String,<br> @Json(name = <strong>"Position"</strong>)<br><strong>val position</strong>: String,<br> @Json(name = <strong>"Email"</strong>)<br><strong>val email</strong>: String,<br> @Json(name = <strong>"Twitter"</strong>)<br><strong>val twitter</strong>: String,<br> @Json(name = <strong>"PhotoUrl"</strong>)<br><strong>val photoUrl</strong>: String?<br>)</pre> <pre><strong>data class </strong>VolunteerModel(<br><strong>val firstName</strong>: String,<br><strong>val lastName</strong>: String,<br><strong>val position</strong>: String,<br><strong>val email</strong>: String,<br><strong>val twitter</strong>: String?,<br><strong>val pictureUrl</strong>: String?<br>) {<br><strong>constructor</strong>(csvModel: CsvVolunteerModel): <strong>this</strong>(<br> csvModel.<strong>firstName</strong>,<br> csvModel.<strong>lastName</strong>,<br> csvModel.<strong>position</strong>,<br> csvModel.<strong>email</strong>,<br><strong>if </strong>(<strong>""</strong>.equals(csvModel.<strong>twitter</strong>) || <strong>"---"</strong>.equals(csvModel.<strong>twitter</strong>)) <strong>null else </strong>csvModel.<strong>twitter</strong>,<br> csvModel.<strong>photoUrl<br></strong>)<br>}</pre> <p>Using a constructor lets us generate the model we want to put into our Firebase DB. Processing this is simple because we can just use a numerical index for the key:</p> <pre><strong>fun </strong>processData(data: List<CsvVolunteerModel>?): Map<Integer, VolunteerModel> {<br><strong>val </strong>volunteerModels = HashMap<Integer, VolunteerModel>()<br><strong>var </strong>volunteerIndex = 0<br> data?.<em>forEach </em><strong>{<br> val </strong>volunteerModel = VolunteerModel(<strong>it</strong>)<br> volunteerModels.put(Integer(volunteerIndex++), volunteerModel)<br><strong>}</strong></pre> <pre><strong> return </strong>volunteerModels<br>}</pre> <p>One other useful tip is to write the processing methods/classes in separate files and write unit tests for them in Android Studio. Then you can easily test the import/export code. Your final kscript will just be a bunch of includes and code to reading a file into your data models:</p> <pre>#!/usr/bin/env kscript<br>//DEPS com.squareup.moshi:moshi:1.5.0,com.squareup.moshi:moshi-adapters:1.5.0,com.squareup.moshi:moshi-kotlin:1.5.0<br>//INCLUDE VolunteerDataModels.kt<br>//INCLUDE VolunteerDataUtils.kt</pre> <pre>import okio.Okio<br>import java.io.FileInputStream<br>import com.squareup.moshi.Types</pre> <pre>if (args.size != 1) {<br> System.err.println("Usage: processVolunteers <jsonfile>")<br> kotlin.system.exitProcess(-1)<br>}<br>val inputStream = FileInputStream(args.get(0))</pre> <pre>val csvJsonAdapter = VolunteerDataUtils.getCsvVolunteerAdapter()<br>val csvData = csvJsonAdapter.fromJson(Okio.buffer(Okio.source(inputStream)))</pre> <pre>val data = VolunteerDataUtils.processData(csvData)</pre> <pre>val jsonAdapter = VolunteerDataUtils.getVolunteerAdapter()<br>val fixedJson = jsonAdapter.toJson(data)<br>println(fixedJson)</pre> <p>Overall, kscript did it’s job and made writing typesafe scripts easily and you can use Android Studio to help you debug your script instead of using print statements in a normal scripting language.</p> <p>I ran into two bugs while using kscript because it’s still in its infancy. One was that the INCLUDE of your files had to include the full path but that was fixed. The other is that the “@file” notation doesn’t work yet, but I’ve reported it: <a href="https://github.com/holgerbrandl/kscript/issues/107">https://github.com/holgerbrandl/kscript/issues/107</a></p> <p>Code for preprocessing the Droidcon data is in this branch if you need to do anything similar or want to see how it works: <a href="https://github.com/Droidcon-Boston/conference-app-android/blob/feature/ken/denormalizeSlidesUp/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/preprocess/processVolunteers">https://github.com/Droidcon-Boston/conference-app-android/blob/feature/ken/denormalizeSlidesUp/Droidcon-Boston/app/src/test/java/com/mentalmachines/droidcon_boston/preprocess/processVolunteers</a></p> <p>kscript does hold promise for being a useful scripting tool if you want to stick with doing everything in Kotlin 🙂</p> <p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1dbff4eae292" width="1" height="1"></div> <div class="sharedaddy sd-sharing-enabled"><div class="robots-nocontent sd-block sd-social sd-social-icon sd-sharing"><h3 class="sd-title">Share this:</h3><div class="sd-content"><ul><li class="share-twitter"><a rel="nofollow noopener noreferrer" data-shared="sharing-twitter-3696" class="share-twitter sd-button share-icon no-text" href="https://www.pgtfb.com/using-kotlin-kscript-for-preprocessing-data/?share=twitter" target="_blank" title="Click to share on Twitter"><span></span><span class="sharing-screen-reader-text">Click to share on Twitter (Opens in new window)</span></a></li><li class="share-facebook"><a rel="nofollow noopener noreferrer" data-shared="sharing-facebook-3696" class="share-facebook sd-button share-icon no-text" href="https://www.pgtfb.com/using-kotlin-kscript-for-preprocessing-data/?share=facebook" target="_blank" title="Click to share on Facebook"><span></span><span class="sharing-screen-reader-text">Click to share on Facebook (Opens in new window)</span></a></li><li class="share-end"></li></ul></div></div></div><div class='sharedaddy sd-block sd-like jetpack-likes-widget-wrapper jetpack-likes-widget-unloaded' id='like-post-wrapper-141932352-3696-5d639ee6323c1' data-src='https://widgets.wp.com/likes/#blog_id=141932352&post_id=3696&origin=www.pgtfb.com&obj_id=141932352-3696-5d639ee6323c1' data-name='like-post-frame-141932352-3696-5d639ee6323c1'><h3 class="sd-title">Like this:</h3><div class='likes-widget-placeholder post-likes-widget-placeholder' style='height: 55px;'><span class='button'><span>Like</span></span> <span class="loading">Loading...</span></div><span class='sd-text-color'></span><a class='sd-link-color'></a></div> <div id='jp-relatedposts' class='jp-relatedposts' > <h3 class="jp-relatedposts-headline"><em>Related</em></h3> </div><style>.guerrillawrap { background: #ECECEC; -webkit-box-sizing:border-box; -moz-box-sizing:border-box; -ms-box-sizing:border-box; box-sizing:border-box; border: 1px solid #d0d0d0; float: left; padding: 2%; width: 100%; } .guerrillagravatar { float: left; margin: 0 10px 0 0; width: 10%; } .guerrillagravatar img { border-radius:50%; border:1 px solid #eee; } .guerrillatext { float: left; width: 84%; } .guerrillatext h4 { font-size: 20px; line-height: 20px; margin: 0 0 0 0; padding: 0; } .guerrillatext p { margin: 10px 0 15px 0; font-style: italic; } .guerrillasocial { float: left; width: 100%; } .guerrillasocial a { border: 0; margin-right: 10px; }</style> <div class="aab_wrap"> <div class="aab_text"> <div class="aab_gravatar"> <img alt='' src='//www.gravatar.com/avatar/7b311685dd78e4db8945ca02afcc1a59?s=100&r=g&d=mm' srcset='//www.gravatar.com/avatar/7b311685dd78e4db8945ca02afcc1a59?s=100&r=g&d=mm 2x' class='avatar avatar-100 photo' height='100' width='100' /> </div> <h4>Author: <span><a href="" title="Visit Pawan Kumar’s website" rel="author external ">Pawan Kumar</a></span></h4><p></p> </div> <div class="aab_social"> </div><div class="clear"></div></div></div><!-- .entry --> <div class="post-tags clr"> </div> <section id="related-posts" class="clr"> <h3 class="theme-heading related-posts-title"> <span class="text">You Might Also Like</span> </h3> <div class="oceanwp-row clr"> <article class="related-post clr col span_1_of_3 col-1 post-28119 post type-post status-publish format-standard hentry category-events category-kotlin category-newsletter entry"> <h3 class="related-post-title"> <a href="https://www.pgtfb.com/watch-kotlinconf-2018-live-now/" title="Watch KotlinConf 2018 Live Now" rel="bookmark">Watch KotlinConf 2018 Live Now</a> </h3><!-- .related-post-title --> <time class="published" datetime="2018-10-04T06:53:31+00:00"><i class="icon-clock"></i>October 4, 2018</time> </article><!-- .related-post --> <article class="related-post clr col span_1_of_3 col-2 post-8514 post type-post status-publish format-standard has-post-thumbnail hentry category-kotlin entry has-media"> <figure class="related-post-media clr"> <a href="https://www.pgtfb.com/kotlin-should-i-define-function-or-property/" class="related-thumb"> <img width="300" height="192" src="https://i1.wp.com/www.pgtfb.com/wp-content/uploads/2018/04/12AOYb1hkNkuwIeH-q2gGjpjw.png?fit=300%2C192&ssl=1" class="attachment-medium size-medium wp-post-image" alt="Kotlin: should I define Function or Property?" itemprop="image" srcset="https://i1.wp.com/www.pgtfb.com/wp-content/uploads/2018/04/12AOYb1hkNkuwIeH-q2gGjpjw.png?w=1024&ssl=1 1024w, https://i1.wp.com/www.pgtfb.com/wp-content/uploads/2018/04/12AOYb1hkNkuwIeH-q2gGjpjw.png?resize=300%2C192&ssl=1 300w, https://i1.wp.com/www.pgtfb.com/wp-content/uploads/2018/04/12AOYb1hkNkuwIeH-q2gGjpjw.png?resize=768%2C491&ssl=1 768w" sizes="(max-width: 300px) 100vw, 300px" data-attachment-id="8515" data-permalink="https://www.pgtfb.com/kotlin-should-i-define-function-or-property/12aoyb1hknkuwieh-q2ggjpjw/" data-orig-file="https://i1.wp.com/www.pgtfb.com/wp-content/uploads/2018/04/12AOYb1hkNkuwIeH-q2gGjpjw.png?fit=1024%2C655&ssl=1" data-orig-size="1024,655" data-comments-opened="1" data-image-meta="{"aperture":"0","credit":"","camera":"","caption":"","created_timestamp":"0","copyright":"","focal_length":"0","iso":"0","shutter_speed":"0","title":"","orientation":"0"}" data-image-title="12AOYb1hkNkuwIeH-q2gGjpjw" data-image-description="" data-medium-file="https://i1.wp.com/www.pgtfb.com/wp-content/uploads/2018/04/12AOYb1hkNkuwIeH-q2gGjpjw.png?fit=300%2C192&ssl=1" data-large-file="https://i1.wp.com/www.pgtfb.com/wp-content/uploads/2018/04/12AOYb1hkNkuwIeH-q2gGjpjw.png?fit=1024%2C655&ssl=1" /> </a> </figure> <h3 class="related-post-title"> <a href="https://www.pgtfb.com/kotlin-should-i-define-function-or-property/" title="Kotlin: should I define Function or Property?" rel="bookmark">Kotlin: should I define Function or Property?</a> </h3><!-- .related-post-title --> <time class="published" datetime="2018-04-11T06:22:18+00:00"><i class="icon-clock"></i>April 11, 2018</time> </article><!-- .related-post --> <article class="related-post clr col span_1_of_3 col-3 post-10156 post type-post status-publish format-standard hentry category-android category-android-app-development category-androiddev category-kotlin category-kotlin-beginners entry"> <h3 class="related-post-title"> <a href="https://www.pgtfb.com/good-onei-want-to-add-that-types-coming-from-java-are-platform-types-2/" title="Good one! I want to add that types coming from Java are platform types." rel="bookmark">Good one! I want to add that types coming from Java are platform types.</a> </h3><!-- .related-post-title --> <time class="published" datetime="2018-04-12T13:03:36+00:00"><i class="icon-clock"></i>April 12, 2018</time> </article><!-- .related-post --> </div><!-- .oceanwp-row --> </section><!-- .related-posts --> <section id="comments" class="comments-area clr"> <div id="respond" class="comment-respond"> <h3 id="reply-title" class="comment-reply-title">Leave a Reply <small><a rel="nofollow" id="cancel-comment-reply-link" href="/using-kotlin-kscript-for-preprocessing-data/#respond" style="display:none;">Cancel reply</a></small></h3> <form action="https://www.pgtfb.com/wp-comments-post.php" method="post" id="commentform" class="comment-form" novalidate> <div class="comment-textarea"><textarea name="comment" id="comment" cols="39" rows="4" tabindex="100" class="textarea-comment" placeholder="Your Comment Here..."></textarea></div><div class="comment-form-author"><input type="text" name="author" id="author" value="" placeholder="Name (required)" size="22" tabindex="101" aria-required="true" class="input-name" /></div> <div class="comment-form-email"><input type="text" name="email" id="email" value="" placeholder="Email (required)" size="22" tabindex="102" aria-required="true" class="input-email" /></div> <div class="comment-form-url"><input type="text" name="url" id="url" value="" placeholder="Website" size="22" tabindex="103" class="input-website" /></div> <div class="gglcptch gglcptch_v2"><div id="gglcptch_recaptcha_1942245138" class="gglcptch_recaptcha"></div> <noscript> <div style="width: 302px;"> <div style="width: 302px; height: 422px; position: relative;"> <div style="width: 302px; height: 422px; position: absolute;"> <iframe src="https://www.google.com/recaptcha/api/fallback?k=6LefgakUAAAAADnNkx-f_THJsKhuqH4H-u4TlgGw" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe> </div> </div> <div style="border-style: none; bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px; background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px; height: 60px; width: 300px;"> <textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px !important; height: 40px !important; border: 1px solid #c1c1c1 !important; margin: 10px 25px !important; padding: 0px !important; resize: none !important;"></textarea> </div> </div> </noscript></div><p class="wpgdprc-checkbox"><label><input type="checkbox" name="wpgdprc" id="wpgdprc" value="1" /> By using this form you agree with the storage and handling of your data by this website. <abbr class="wpgdprc-required" title="You need to accept this checkbox.">*</abbr></label></p><p class="comment-subscription-form"><input type="checkbox" name="subscribe_blog" id="subscribe_blog" value="subscribe" style="width: auto; -moz-appearance: checkbox; -webkit-appearance: checkbox;" /> <label class="subscribe-label" id="subscribe-blog-label" for="subscribe_blog">Notify me of new posts by email.</label></p><p class="form-submit"><input name="submit" type="submit" id="comment-submit" class="submit" value="Post Comment" /> <input type='hidden' name='comment_post_ID' value='3696' id='comment_post_ID' /> <input type='hidden' name='comment_parent' id='comment_parent' value='0' /> </p><p style="display: none;"><input type="hidden" id="akismet_comment_nonce" name="akismet_comment_nonce" value="6975a263d5" /></p><p style="display: none;"><input type="hidden" id="ak_js" name="ak_js" value="82"/></p> </form> </div><!-- #respond --> </section><!-- #comments --> </article> </div><!-- #content --> </div><!-- #primary --> <aside id="right-sidebar" class="sidebar-container widget-area sidebar-primary" itemscope="itemscope" itemtype="http://schema.org/WPSideBar"> <div id="right-sidebar-inner" class="clr"> <div id="bp_core_login_widget-3" class="sidebar-box widget_bp_core_login_widget buddypress widget clr"><h4 class="widget-title">Login</h4> <form name="bp-login-form" id="bp-login-widget-form" class="standard-form" action="https://www.pgtfb.com/login/" method="post"> <label for="bp-login-widget-user-login">Username</label> <input type="text" name="log" id="bp-login-widget-user-login" class="input" value="" /> <label for="bp-login-widget-user-pass">Password</label> <input type="password" name="pwd" id="bp-login-widget-user-pass" class="input" value="" spellcheck="false" autocomplete="off" /> <div class="forgetmenot"><label for="bp-login-widget-rememberme"><input name="rememberme" type="checkbox" id="bp-login-widget-rememberme" value="forever" /> Remember Me</label></div> <input type="submit" name="wp-submit" id="bp-login-widget-submit" value="Log In" /> <span class="bp-login-widget-register-link"><a href="https://www.pgtfb.com/signup/">Register</a></span> </form> </div><div id="search-3" class="sidebar-box widget_search clr"> <form method="get" class="searchform" id="searchform" action="https://www.pgtfb.com/"> <input type="text" class="field" name="s" id="s" placeholder="Search"> </form></div><div id="blog_subscription-3" class="sidebar-box widget_blog_subscription jetpack_subscription_widget clr"><h4 class="widget-title">Subscribe to Blog via Email</h4> <form action="#" method="post" accept-charset="utf-8" id="subscribe-blog-blog_subscription-3"> <div id="subscribe-text"><p>Enter your email address to subscribe to this blog and receive notifications of new posts by email.</p> </div> <p id="subscribe-email"> <label id="jetpack-subscribe-label" class="screen-reader-text" for="subscribe-field-blog_subscription-3"> Email Address </label> <input type="email" name="email" required="required" class="required" value="" id="subscribe-field-blog_subscription-3" placeholder="Email Address"/> </p> <p id="subscribe-submit"> <input type="hidden" name="action" value="subscribe"/> <input type="hidden" name="source" value="https://www.pgtfb.com/using-kotlin-kscript-for-preprocessing-data/"/> <input type="hidden" name="sub-type" value="widget"/> <input type="hidden" name="redirect_fragment" value="blog_subscription-3"/> <button type="submit" name="jetpack_subscriptions_widget" > Subscribe </button> </p> </form> </div><div id="top-posts-3" class="sidebar-box widget_top-posts clr"><h4 class="widget-title">Top Posts & Pages</h4><ul> <li> <a href="https://www.pgtfb.com/open-machine-learning-course-topic-9-time-series-analysis-in-python/" class="bump-view" data-bump-view="tp"> Open Machine Learning Course. Topic 9. Time series analysis in Python </a> </li> <li> <a href="https://www.pgtfb.com/looking-back-to-understand-the-impact-of-the-driverless-car-revolution/" class="bump-view" data-bump-view="tp"> Looking Back to Understand the Impact of the Driverless Car Revolution </a> </li> <li> <a href="https://www.pgtfb.com/learn-about-cyber-security-from-the-master-at-just-12/" class="bump-view" data-bump-view="tp"> Learn about cyber security from the master, at just $12! </a> </li> <li> <a href="https://www.pgtfb.com/next-js-ssr-vs-create-react-app-csr/" class="bump-view" data-bump-view="tp"> Next.js (SSR) vs. Create React App (CSR) </a> </li> <li> <a href="https://www.pgtfb.com/best-10-app-development-companies-in-india/" class="bump-view" data-bump-view="tp"> Best 10+ app development companies in India </a> </li> <li> <a href="https://www.pgtfb.com/create-svg-line-chart-in-react/" class="bump-view" data-bump-view="tp"> Create SVG line chart in React </a> </li> <li> <a href="https://www.pgtfb.com/swift-for-data-science/" class="bump-view" data-bump-view="tp"> Swift for Data Science! </a> </li> <li> <a href="https://www.pgtfb.com/pyppeteer-the-snake-charmer/" class="bump-view" data-bump-view="tp"> pyppeteer, the snake-charmer </a> </li> </ul></div> </div><!-- #sidebar-inner --> </aside><!-- #right-sidebar --> </div><!-- #content-wrap --> </main><!-- #main --> <footer id="footer" class="site-footer" itemscope="itemscope" itemtype="http://schema.org/WPFooter"> <div id="footer-inner" class="clr"> <div id="footer-widgets" class="oceanwp-row clr"> <div class="footer-widgets-inner container"> <div class="footer-box span_1_of_4 col col-1"> </div><!-- .footer-one-box --> <div class="footer-box span_1_of_4 col col-2"> </div><!-- .footer-one-box --> <div class="footer-box span_1_of_4 col col-3 "> </div><!-- .footer-one-box --> <div class="footer-box span_1_of_4 col col-4"> </div><!-- .footer-box --> </div><!-- .container --> </div><!-- #footer-widgets --> <div id="footer-bottom" class="clr"> <div id="footer-bottom-inner" class="container clr"> <div id="footer-bottom-menu" class="navigation clr"> </div><!-- #footer-bottom-menu --> <div id="copyright" class="clr" role="contentinfo"> Copyright [oceanwp_date] - PGTFB. All content and Image are owned by their respective Owner. </div><!-- #copyright --> </div><!-- #footer-bottom-inner --> </div><!-- #footer-bottom --> </div><!-- #footer-inner --> </footer><!-- #footer --> </div><!-- #wrap --> </div><!-- #outer-wrap --> <a id="scroll-top" class="scroll-top-right" href="#"><span class="fa fa-angle-up"></span></a> <div id="sidr-close"> <a href="#" class="toggle-sidr-close"> <i class="icon icon-close"></i><span class="close-text">Close Menu</span> </a> </div> <div id="mobile-nav" class="navigation clr"> </div> <div id="mobile-menu-search" class="clr"> <form method="get" action="https://www.pgtfb.com/" class="mobile-searchform"> <input type="search" name="s" autocomplete="off" placeholder="Search" /> <button type="submit" class="searchform-submit"> <i class="icon icon-magnifier"></i> </button> </form> </div><!-- .mobile-menu-search --> <link href="<? echo plugins_url(); ?>/advanced-author-box/font-awesome/css/font-awesome.min.css" rel="stylesheet" /> <div style="display:none"> <div class="grofile-hash-map-7b311685dd78e4db8945ca02afcc1a59"> </div> </div> <script type="text/javascript"> window.WPCOM_sharing_counts = {"https:\/\/www.pgtfb.com\/using-kotlin-kscript-for-preprocessing-data\/":3696}; </script> <link rel='stylesheet' id='gglcptch-css' href='https://www.pgtfb.com/wp-content/plugins/google-captcha/css/gglcptch.css?ver=1.50' type='text/css' media='all' /> <script type='text/javascript' src='https://www.pgtfb.com/wp-includes/js/admin-bar.min.js?ver=5.2.2'></script> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/plugins/jetpack/_inc/build/photon/photon.min.js?ver=20190201'></script> <script type='text/javascript' src='https://www.pgtfb.com/wp-includes/js/comment-reply.min.js?ver=5.2.2'></script> <script type='text/javascript' src='https://s0.wp.com/wp-content/js/devicepx-jetpack.js?ver=201935'></script> <script type='text/javascript'> /* <![CDATA[ */ var themeMyLogin = {"action":"","errors":[]}; /* ]]> */ </script> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/plugins/theme-my-login/assets/scripts/theme-my-login.min.js?ver=7.0.14'></script> <script type='text/javascript' src='https://www.pgtfb.com/wp-includes/js/imagesloaded.min.js?ver=3.2.0'></script> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/themes/oceanwp/assets/js/third/magnific-popup.min.js?ver=1.0'></script> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/themes/oceanwp/assets/js/third/lightbox.min.js?ver=1.0'></script> <script type='text/javascript'> /* <![CDATA[ */ var oceanwpLocalize = {"isRTL":"","menuSearchStyle":"header_replace","sidrSource":"#sidr-close, #mobile-nav, #mobile-menu-search","sidrDisplace":"1","sidrSide":"left","sidrDropdownTarget":"icon","verticalHeaderTarget":"icon","customSelects":".woocommerce-ordering .orderby, #dropdown_product_cat, .widget_categories select, .widget_archive select, .single-product .variations_form .variations select"}; /* ]]> */ </script> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/themes/oceanwp/assets/js/main.min.js?ver=1.0'></script> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/plugins/ank-prism-for-wp/out/prism-js.min.js?ver=1520745099'></script> <script type='text/javascript'> /* <![CDATA[ */ var wpgdprcData = {"ajaxURL":"https:\/\/www.pgtfb.com\/wp-admin\/admin-ajax.php","ajaxSecurity":"33079b854f","isMultisite":"","path":"\/","blogId":""}; /* ]]> */ </script> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/plugins/wp-gdpr-compliance/assets/js/front.js?ver=1559404056'></script> <script type='text/javascript' src='https://www.pgtfb.com/wp-includes/js/wp-embed.min.js?ver=5.2.2'></script> <!--[if lt IE 9]> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/themes/oceanwp/assets/js//third/html5.min.js?ver=1.0'></script> <![endif]--> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/plugins/jetpack/_inc/build/postmessage.min.js?ver=7.5.3'></script> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/plugins/jetpack/_inc/build/jquery.jetpack-resize.min.js?ver=7.5.3'></script> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/plugins/jetpack/_inc/build/likes/queuehandler.min.js?ver=7.5.3'></script> <script async="async" type='text/javascript' src='https://www.pgtfb.com/wp-content/plugins/akismet/_inc/form.js?ver=4.1.2'></script> <script type='text/javascript'> /* <![CDATA[ */ var sharing_js_options = {"lang":"en","counts":"1","is_stats_active":"1"}; /* ]]> */ </script> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/plugins/jetpack/_inc/build/sharedaddy/sharing.min.js?ver=7.5.3'></script> <script type='text/javascript'> var windowOpen; jQuery( document.body ).on( 'click', 'a.share-twitter', function() { // If there's another sharing window open, close it. if ( 'undefined' !== typeof windowOpen ) { windowOpen.close(); } windowOpen = window.open( jQuery( this ).attr( 'href' ), 'wpcomtwitter', 'menubar=1,resizable=1,width=600,height=350' ); return false; }); var windowOpen; jQuery( document.body ).on( 'click', 'a.share-facebook', function() { // If there's another sharing window open, close it. if ( 'undefined' !== typeof windowOpen ) { windowOpen.close(); } windowOpen = window.open( jQuery( this ).attr( 'href' ), 'wpcomfacebook', 'menubar=1,resizable=1,width=600,height=400' ); return false; }); </script> <script type='text/javascript' data-cfasync="false" async="async" defer="defer" src='https://www.google.com/recaptcha/api.js?render=explicit&ver=1.50'></script> <script type='text/javascript'> /* <![CDATA[ */ var gglcptch = {"options":{"version":"v2","sitekey":"6LefgakUAAAAADnNkx-f_THJsKhuqH4H-u4TlgGw","theme":"light","error":"<strong>Warning<\/strong>: More than one reCAPTCHA has been found in the current form. Please remove all unnecessary reCAPTCHA fields to make it work properly.","disable":0},"vars":{"visibility":false}}; /* ]]> */ </script> <script type='text/javascript' src='https://www.pgtfb.com/wp-content/plugins/google-captcha/js/script.js?ver=1.50'></script> <iframe src='https://widgets.wp.com/likes/master.html?ver=201935#ver=201935' scrolling='no' id='likes-master' name='likes-master' style='display:none;'></iframe> <div id='likes-other-gravatars'><div class="likes-text"><span>%d</span> bloggers like this:</div><ul class="wpl-avatars sd-like-gravatars"></ul></div> <script type='text/javascript' src='https://stats.wp.com/e-201935.js' async='async' defer='defer'></script> <script type='text/javascript'> _stq = window._stq || []; _stq.push([ 'view', {v:'ext',j:'1:7.5.3',blog:'141932352',post:'3696',tz:'0',srv:'www.pgtfb.com'} ]); _stq.push([ 'clickTrackerInit', '141932352', '3696' ]); </script> <div id="wpadminbar" class="nojq nojs"> <a class="screen-reader-shortcut" href="#wp-toolbar" tabindex="1">Skip to toolbar</a> <div class="quicklinks" id="wp-toolbar" role="navigation" aria-label="Toolbar"> <ul id='wp-admin-bar-root-default' class="ab-top-menu"><li id='wp-admin-bar-wp-logo' class="menupop"><div class="ab-item ab-empty-item" tabindex="0" aria-haspopup="true"><span class="ab-icon"></span><span class="screen-reader-text">About WordPress</span></div><div class="ab-sub-wrapper"><ul id='wp-admin-bar-wp-logo-external' class="ab-sub-secondary ab-submenu"><li id='wp-admin-bar-wporg'><a class='ab-item' href='https://wordpress.org/'>WordPress.org</a></li><li id='wp-admin-bar-documentation'><a class='ab-item' href='https://codex.wordpress.org/'>Documentation</a></li><li id='wp-admin-bar-support-forums'><a class='ab-item' href='https://wordpress.org/support/'>Support</a></li><li id='wp-admin-bar-feedback'><a class='ab-item' href='https://wordpress.org/support/forum/requests-and-feedback'>Feedback</a></li></ul></div></li><li id='wp-admin-bar-bp-login'><a class='ab-item' href='https://www.pgtfb.com/login/?redirect_to=https%3A%2F%2Fwww.pgtfb.com%2Fusing-kotlin-kscript-for-preprocessing-data%2F'>Log In</a></li><li id='wp-admin-bar-bp-register'><a class='ab-item' href='https://www.pgtfb.com/signup/'>Register</a></li><li id='wp-admin-bar-admin-bar-likes-widget'><div class="ab-item ab-empty-item"></div><iframe class='admin-bar-likes-widget jetpack-likes-widget' scrolling='no' frameBorder='0' name='admin-bar-likes-widget' src='https://widgets.wp.com/likes/#blog_id=141932352&post_id=3696&origin=https://www.pgtfb.com'></iframe></li></ul><ul id='wp-admin-bar-top-secondary' class="ab-top-secondary ab-top-menu"><li id='wp-admin-bar-search' class="admin-bar-search"><div class="ab-item ab-empty-item" tabindex="-1"><form action="https://www.pgtfb.com/" method="get" id="adminbarsearch"><input class="adminbar-input" name="s" id="adminbar-search" type="text" value="" maxlength="150" /><label for="adminbar-search" class="screen-reader-text">Search</label><input type="submit" class="adminbar-button" value="Search"/></form></div></li></ul> </div> </div> </body> </html>