Programming

Kemaren waktu nyari info tentang “map”nya Java di stackoverflow.com, di samping kanan websitenya ada bagian “Hot Network Questions” yg isinya adalah pertanyaan2 dari berbagai bidang (dan dari berbagai website), ada yg dari superuser.com, math.stackexchange.com, codegolf.stackexchange.com, english.stackexchange.com, codereview.stackexchange.com, dll (detailnya liat langsung aja di link ‘Hot Network Questions’ di atas).

Nah, dari code review, ada yg nanya tentang Conway’s Game of Life. Penasaran, akhirnya malah bikin sendiri deh, pake javascript aja yg gampang. Informasinya saya dapet dari wikipedia. Dunia dari game of life adalah sel kotak yang hidup dalam grid dua dimensi yang tak berhingga luasnya. Tiap sel dari grid cuma punya salah satu status, yaitu hidup atau mati. Tiap kali perubahan memiliki hukum seperti ini:

  1. Sel hidup yang punya tetangga hidup kurang dari 2 akan menjadi mati.
  2. Sel hidup yang punya tetangga hidup 2 atau 3 tetap akan hidup.
  3. Sel hidup yang punya tetangga hidup lebih dari 3 akan mati.
  4. Sel yang mati tapi punya 3 tetangga hidup akan menjadi sel hidup.

Langsung aja, ini source-code nya:

<html>
    <head>
        <title>Conway's Game of Life</title>
        <style type="text/css">
            table.cgol { border-collapse:collapse; border: 1px solid #333; border-spacing: 0px; }
            table.cgol td { text-align:center; width:10px; height:10px; border: 1px solid #333; overflow: hidden; }
        </style>
        <script type="text/javascript" src="conways.js"></script>
        <script type="text/javascript">
            var gol;
            var lop;
            function $(id) { return document.getElementById(id); }
            function getRandomInt(min, max) { return Math.floor(Math.random()*(max-min+1)+min); }
            function isOdd(num) { return (num % 2) == 1; }

            function draw()        { $('output').innerHTML = gol.draw(); $('stp').innerHTML = gol.steps; }
            function doCreate()    {
                if (typeof lop != "undefined") clearInterval(lop);
                clearInterval(lop);
                gol = new CGOL($('we').value,$('ha').value,"gol");
                draw();
            }
            function doRandom()    { gol.initRandom(); draw(); }
            function doStep()      { gol.step(); draw(); }
            function doStepClick() { if (typeof lop != "undefined") clearInterval(lop); clearInterval(lop); doStep(); }
            function doLoop()      { if (typeof lop != "undefined") clearInterval(lop); lop = setInterval(doStep, 150); }
        </script>
    </head>
    <body>
        <h2>Conway's Game of Life</h2>
        <div>
            Width <input type="text" id="we" value="50">
            Height <input type="text" id="ha" value="50">
            <input type="button" value="Create" onclick="doCreate()">
            <input type="button" value="Random" onclick="doRandom()">
            <input type="button" value="Step" onclick="doStepClick()">
            <input type="button" value="Auto" onclick="doLoop()">
        </div>
        <br>
        Steps : <span id="stp"></span>
        <br>
        <div id="output"></div>
    </body>
</html>
function CGOL (wid, hig, nam) {
    this.width = wid;
    this.height = hig;
    this.cells = [];
    this.varname = nam; // little hack for board designer
    this.steps = 0;

    this.init();
}

CGOL.prototype.alive = function(x,y) { return this.cells[(this.width*y)+x]; }
CGOL.prototype.toggle = function(x,y) { this.cells[(this.width*y)+x] = !this.alive(x,y); }

CGOL.prototype.countLiveNeighboursSimple = function(x,y) {
    /*
     * The simplest strategy is simply to assume that every cell outside the array
     * is dead. This is easy to program, but leads to inaccurate results when the
     * active area crosses the boundary.
     */
    var live = 0;
    var atas = false;
    var bawa = false;
    var kiri = false;
    var kana = false;
    if (x>0) kiri = true;
    if (x<this.width-1) kana = true;
    if (y>0) atas = true;
    if (y<this.height-1) bawa = true;

    if (kiri) {
        if (this.alive(x-1,y)) live++;
        if (atas) { if (this.alive(x-1,y-1)) live++; }
        if (bawa) { if (this.alive(x-1,y+1)) live++; }
    }
    if (kana) {
        if (this.alive(x+1,y)) live++;
        if (atas) { if (this.alive(x+1,y-1)) live++; }
        if (bawa) { if (this.alive(x+1,y+1)) live++; }
    }
    if (atas) { if (this.alive(x,y-1)) live++; }
    if (bawa) { if (this.alive(x,y+1)) live++; }
    return live;
}

CGOL.prototype.countLiveNeighbours = function(x,y) {
    /*
     * A more sophisticated trick is to consider the left and right edges of the
     * field to be stitched together, and the top and bottom edges also, yielding
     * a toroidal array. The result is that active areas that move across a field
     * edge reappear at the opposite edge. Inaccuracy can still result if the
     * pattern grows too large, but at least there are no pathological edge
     * effects.
     */
    var live = 0;
    var atas = false;
    var bawa = false;
    var kiri = false;
    var kana = false;
    if (x>0) kiri = true;
    if (x<this.width-1) kana = true;
    if (y>0) atas = true;
    if (y<this.height-1) bawa = true;

    if (kiri) {
        if (this.alive(x-1,y)) live++;
        if (atas) { if (this.alive(x-1,y-1)) live++; } else { if (this.alive(x-1,this.height-1)) live++; }
        if (bawa) { if (this.alive(x-1,y+1)) live++; } else { if (this.alive(x-1,0)) live++; }
    } else {
        if (this.alive(this.width-1,y)) live++;
        if (atas) { if (this.alive(this.width-1,y-1)) live++; } else { if (this.alive(this.width-1,this.height-1)) live++; }
        if (bawa) { if (this.alive(this.width-1,y+1)) live++; } else { if (this.alive(this.width-1,0)) live++; }
    }
    if (kana) {
        if (this.alive(x+1,y)) live++;
        if (atas) { if (this.alive(x+1,y-1)) live++; } else { if (this.alive(x+1,this.height-1)) live++; }
        if (bawa) { if (this.alive(x+1,y+1)) live++; } else { if (this.alive(x+1,0)) live++; }
    } else {
        if (this.alive(0,y)) live++;
        if (atas) { if (this.alive(0,y-1)) live++; } else { if (this.alive(0,this.height-1)) live++; }
        if (bawa) { if (this.alive(0,y+1)) live++; } else { if (this.alive(0,0)) live++; }
    }
    if (atas) { if (this.alive(x,y-1)) live++; } else { if (this.alive(x,this.height-1)) live++; }
    if (bawa) { if (this.alive(x,y+1)) live++; } else { if (this.alive(x,0)) live++; }
    return live;
}

CGOL.prototype.step = function() {
    var newcell = [];
    for (var y=0; y<this.height; y++) {
        for (var x=0; x<this.width; x++) {
            var t = this.countLiveNeighbours(x,y);
            newcell.push(this.alive(x,y));
            var i = newcell.length-1;
            //Any live cell with fewer than two live neighbours dies, as if caused by under-population.
            if (this.alive(x,y) && (t<2)) newcell[i]=false;
            //Any live cell with two or three live neighbours lives on to the next generation.
            if (this.alive(x,y) && t>=2 && t<=3) newcell[i]=true;
            //Any live cell with more than three live neighbours dies, as if by overcrowding.
            if (t>3) newcell[i]=false;
            //Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
            if (!this.alive(x,y) && t==3) newcell[i]=true;
        }
    }
    this.cells = [];
    for (var z=0; z<newcell.length; z++) {
        this.cells.push(newcell[z]);
    }
    this.steps++;
}

CGOL.prototype.init = function() {
    this.cells = [];
    for (var x=0; x<this.width*this.height; x++) {
        this.cells.push(false);
    }
    this.steps = 0;
}

CGOL.prototype.initRandom = function() {
    var min = 0;
    var max = 9;
    this.cells = [];
    for (var x=0; x<this.width*this.height; x++) {
        var c = (((Math.floor(Math.random() * (max - min + 1) + min)) % 4) == 1);
        this.cells.push(c);
    }
    this.steps = 0;
}

CGOL.prototype.draw = function() {
    var tbl = "<table class='cgol'>";
    for (var y=0; y<this.height; y++) {
        tbl += "<tr>";
        for (var x=0; x<this.width; x++) {
            var warna = "#000";
            if (this.alive(x,y)) warna = "#0f0";
            tbl += "<td style='background-color:" + warna + "; ' "+
                   "onclick='"+this.varname+".toggle("+x+","+y+"); draw();'></td>";
        }
        tbl += "</tr>";
    }
    tbl += "</table>";
    return tbl;
}

Seperti keterangan pada wikipedia, ada metode paling simple untuk menghitung jumlah “tetangga hidup” yaitu dengan menganggap sel diluar array adalah sel mati. Metode ini dianggap sangat tidak akurat. Di conways.js, saya sertakan metode ini dengan nama “countLiveNeighboursSimple”.

Lalu ada metode lainnya yaitu yang menganggap bahwa bagian kiri dan kanan grid adalah menyatu, begitu pula bagian atas dan bawah. Kalau di visualisasikan, akan terbentuk torodial. Ini yang jadi default cara menghitung “tetangga hidup” dalam conways.js.

Beberapa hari lalu saya diminta untuk membuat ulang program penghitung harga rata-rata pada tiap akhir hari perdagangan di back-office. Program sebelumnya selalu menghitung pembelian terlebih dahulu baru mengurangi balance saat penjualan, tentu saja hasilnya akan berbeda jika perhitungan dilakukan menurut waktu terjadinya transaksi.

Yang pertama kali saya lakukan adalah membuat Stored Procedure yang kerjanya mengambil data transaksi, lalu mengupdate/insert (data disimpan per tanggal) tabel saldo yg berisi Saldo, AvgPrice, dll, terurut menurut waktu terjadinya transaksi. Stored procedure yang saya buat berjalan lambat, karena pada tiap transaksi stored procedure akan menulis ke table yang berarti akan ada aktivitas harddisk untuk menulis, dan dalam 1 hari bisa ada ribuan (antara 2 sampai 4 ribu) frekuensi transaksi.

Stored procedure saya ubah dengan menambahkan variabel dengan tipe “table”, saya melakukan semua insert ke variabel tabel, lalu di akhir proses saya lakukan bulk insert ke tabel aslinya. Hasilnya stored procedure berjalan 10x lebih cepat.

Table temporary di memory memang cepat, tapi ada kasus lain yang ternyata kebalikannya. Saya punya 1 table yang akan saya bulk update berdasarkan sebuah multi-statement table-valued function (fungsi yang outputnya berupa table). Begitu saya coba jalankan, query update itu berjalan lebih dari 13 menit. Googling sebentar dapet informasi dari MSDN :

Joining to a multistatement table valued function in a FROM clause is possible, but can give poor performance. SQL Server is unable to use all the optimized techniques against some statements that can be included in a multistatement function, resulting in a suboptimal query plan. To obtain the best possible performance, whenever possible use joins between base tables instead of functions.

Saya ubah lagi querynya dengan menyimpan terlebih dahulu hasil fungsi ke tabel fisik dengan menggunakan select into, lalu melakukan bulk-update dan terakhir menghapus kembali tabel penyimpanan sementara itu. Hasilnya query berjalan 39 detik untuk mengupdate 54.827 row data.

Selalu cari jalan tercepat, jangan puas hanya dengan hasil yang benar.

Sebagai seorang pencinta buku, sudah tentu buku yang saya punya cukup banyak, dan pencatatan menjadi cukup penting. Selain mengoleksi buku, saya juga punya koleksi koin dan koleksi film. Sudah lama saya mencari program yang tugasnya hanya mencatat benda koleksi seseorang, tapi tidak pernah dapat yang benar2 pamungkas. Karena saya memprioritaskan satu program dengan banyak tipe koleksi, berarti program itu harus bisa mengelola data koleksi buku dan film, padahal field yang digunakan dalam dua koleksi itu sangat jauh berbeda (apalagi kalau koleksi koin).

Sesudah istri saya meminta saya untuk mendokumentasikan seluruh tas miliknya (istri saya senang mengoleksi tas), saya jadi berpikir untuk bikin sendiri program pengelola koleksi itu. Akhirnya tercetus sebuah ide dalam benak saya untuk membuat koleksi dengan field dinamis, user bisa mendefinisikan sendiri field2 apa saja yang mau disimpan. Atau dalam istilah OOP, si user bisa mendefinisikan property apa saja yang terdapat dalam sebuah objek koleksi. Dan untuk mengakomodir user2 malas seperti saya, bisa disiapkan template untuk berbagai macam koleksi yang bisa diedit.

Karena koleksi semacem ini biasanya cuma dipake oleh 1 orang, maka yang paling gampang pake SQLite.

Ini dia struktur table nya:

Tabel Koleksi

IDAuto Number
NamaNama Koleksi
CatatanCatatan koleksi (tanggal dimulai, dll)
BarisAkhirNomor baris yang terakhir dipakai (pada table BARIS)

Tabel Kolom

IDAuto Number
KOLEKSI_IDNomor ID dari table KOLEKSI
NAMANama field
TIPETipe data field (string, numeric, date)
URUTANUrutan posisi field
REFERENSIReferensi nilai. Untuk tipe field yang isinya sudah ditentukan dari daftar

Tabel Baris

IDAuto Number
KOLEKSI_IDNomor ID dari table KOLEKSI
KOLOM_IDNomor ID dari table KOLOM
NILAIIsi field
NO_BARISNomor urut data (tidak boleh dobel, sekuen bisa terputus apabila ada penghapusan data)

Tabel Lampiran

IDAuto Number
KOLEKSI_IDNomor ID dari table KOLEKSI
NO_BARISNomor urut data. Satu nomor urut bisa mempunyai lebih dari satu lampiran
NAMA_FILENama file lampiran
BERKASIsi file lampiran

Contoh datanya kira2 seperti ini:

Isi Table Koleksi

IDNamaCatatanBarisAkhir
1Koleksi Tas-2

Isi Tabel Kolom

IDKOLEKSI_IDNAMATIPEURUTANREFERENSI
11MerkSTRING1Charles & Keith|Gucci|Channel
21WarnaSTRING2
31HargaNUMERIC3
41Tanggal BeliDATE4

Isi Tabel Baris

IDKOLEKSI_IDKOLOM_IDNILAINO_BARIS
111Charles & Keith1
212Hijau Pupus1
3136300001
4142011-03-241
511Gucci2
612Merah2
71321000002
8142012-01-192

Isi Tabel Lampiran

IDKOLEKSI_IDNO_BARISNAMA_FILEBERKAS
111foto1.jpg--blob--
212tas.png--blob--

Nantinya akan ditampilkan seperti ini:

NoMerkWarnaHargaTanggal Beli
1Charles & KeithHijau Pupus630.0002011-03-24
2GucciMerah2.100.0002012-01-19

Jadi deh program pencatat koleksi yang dinamis cuma pake 4 table. Tinggal pinter2 aja nge-query. Kalau perlu bikin view untuk setiap koleksi yang ada, misal untuk koleksi ID=1 viewnya “view_koleksi_1”, dst.