Date & Time #
Tanggal dan waktu adalah salah satu domain yang terlihat sederhana tapi menyimpan kompleksitas yang mengejutkan — timezone, daylight saving time, leap year, format regional yang berbeda, dan representasi yang tidak konsisten antar sistem. Ruby menyediakan tiga kelas utama untuk bekerja dengan waktu: Time (bawaan, untuk waktu dengan presisi hingga nanodetik), Date (dari standard library, untuk tanggal saja), dan DateTime (gabungan keduanya, tapi sudah sebagian besar digantikan oleh Time modern). Memahami kapan menggunakan masing-masing kelas, bagaimana menangani timezone dengan benar, dan cara format waktu untuk berbagai konteks adalah keterampilan penting yang tidak boleh dilewatkan.
Tiga Kelas Waktu di Ruby #
Sebelum masuk ke contoh, penting untuk memahami perbedaan ketiga kelas ini:
flowchart TD
A[Kebutuhan] --> B{"Perlu info waktu\n(jam, menit, detik)?"}
B -- Tidak --> C["Date\nrequire 'date'\nHanya tanggal: 2024-08-15"]
B -- Ya --> D{"Perlu timezone\ndan presisi tinggi?"}
D -- Ya --> E["Time\nBawaan Ruby\nPaling direkomendasikan"]
D -- Ringan saja --> F["DateTime\nrequire 'date'\nLegacy — lebih lambat dari Time"]
E --> G["Time.now\nTime.new\nTime.at\nTime.parse"]
C --> H["Date.today\nDate.new\nDate.parse"]
F --> I["Hindari untuk kode baru\nGunakan Time"]require 'date'
# Time — bawaan Ruby, paling umum dan paling direkomendasikan
t = Time.now
puts t.class # => Time
# Date — hanya tanggal, tanpa jam/menit/detik
d = Date.today
puts d.class # => Date
# DateTime — legacy, sebaiknya gunakan Time untuk kode baru
dt = DateTime.now
puts dt.class # => DateTime
# Perbandingan kemampuan
puts Time.now.nsec # => nanosecond — presisi tertinggi
puts Date.today.wday # => 0=Minggu, 1=Senin, ..., 6=Sabtu
Membuat Objek Waktu #
Time #
# Waktu saat ini
sekarang = Time.now # waktu lokal sistem
utc_kini = Time.now.utc # waktu saat ini dalam UTC
# Waktu spesifik — Time.new(tahun, bulan, hari, jam, menit, detik, timezone)
kemerdekaan = Time.new(1945, 8, 17, 10, 0, 0, "+07:00")
puts kemerdekaan # => 1945-08-17 10:00:00 +0700
# Time.mktime — gunakan timezone lokal sistem
waktu_lokal = Time.mktime(2024, 12, 25, 8, 30, 0)
# Time.gm / Time.utc — selalu UTC
waktu_utc = Time.gm(2024, 12, 25, 1, 30, 0) # 08:30 WIB = 01:30 UTC
# Time.at — dari Unix timestamp (detik sejak 1 Januari 1970 UTC)
dari_unix = Time.at(0) # => 1970-01-01 07:00:00 +0700
dari_unix = Time.at(1_700_000_000) # sekitar November 2023
puts dari_unix.to_i # kembali ke Unix timestamp
# Time.parse — dari string (membutuhkan require 'time')
require 'time'
dari_string = Time.parse("2024-08-15 08:30:00 +0700")
dari_iso = Time.parse("2024-08-15T08:30:00+07:00") # format ISO 8601
puts dari_string
Date #
require 'date'
# Hari ini
hari_ini = Date.today
puts hari_ini # => 2024-08-15
# Tanggal spesifik
kemerdekaan = Date.new(1945, 8, 17)
natal = Date.new(2024, 12, 25)
# Date.parse — dari string
dari_string = Date.parse("2024-08-15")
dari_string = Date.parse("15 Agustus 2024") # format yang lebih bebas
# Date.strptime — parsing dengan format eksplisit (lebih aman dari parse)
dari_format = Date.strptime("15/08/2024", "%d/%m/%Y")
puts dari_format # => 2024-08-15
# Tanggal julian — hari ke-n dalam setahun
hari_ke = Date.ordinal(2024, 100) # hari ke-100 tahun 2024
puts hari_ke # => 2024-04-09
Mengakses Komponen Waktu #
t = Time.new(2024, 8, 15, 14, 30, 45, "+07:00")
# Komponen tanggal
puts t.year # => 2024
puts t.month # => 8 (1=Januari, 12=Desember)
puts t.day # => 15
puts t.yday # => 228 (hari ke-228 dalam tahun)
# Komponen waktu
puts t.hour # => 14
puts t.min # => 30
puts t.sec # => 45
puts t.nsec # => 0 (nanosecond)
puts t.usec # => 0 (microsecond)
# Hari dalam seminggu
puts t.wday # => 4 (0=Minggu, 1=Senin, ..., 6=Sabtu)
puts t.monday? # => false
puts t.thursday? # => true
# Informasi timezone
puts t.zone # => "+07:00" atau "WIB"
puts t.utc_offset # => 25200 (detik = 7 jam)
puts t.utc? # => false
puts t.gmt? # => false
# Unix timestamp
puts t.to_i # => detik sejak epoch (1 Jan 1970 UTC)
puts t.to_f # => dengan desimal untuk sub-detik
# Komponen Date
d = Date.new(2024, 8, 15)
puts d.year # => 2024
puts d.month # => 8
puts d.day # => 15
puts d.wday # => 4 (Kamis)
puts d.yday # => 228
puts d.cweek # => 33 (minggu ke-33 dalam tahun ISO)
puts d.cwday # => 4 (1=Senin, 7=Minggu dalam standar ISO)
puts d.leap? # => true (2024 adalah tahun kabisat)
Format dengan strftime #
strftime adalah method utama untuk mengubah objek waktu menjadi string dengan format tertentu. Nama method berasal dari C standard library dan formatnya konsisten di hampir semua bahasa pemrograman.
t = Time.new(2024, 8, 15, 14, 30, 5, "+07:00")
# Format tanggal
puts t.strftime("%Y") # => "2024" (tahun 4 digit)
puts t.strftime("%y") # => "24" (tahun 2 digit)
puts t.strftime("%m") # => "08" (bulan 2 digit)
puts t.strftime("%-m") # => "8" (bulan tanpa leading zero)
puts t.strftime("%d") # => "15" (hari 2 digit)
puts t.strftime("%-d") # => "15" (hari tanpa leading zero)
puts t.strftime("%j") # => "228" (hari dalam setahun)
# Format waktu
puts t.strftime("%H") # => "14" (jam 24-hour)
puts t.strftime("%I") # => "02" (jam 12-hour)
puts t.strftime("%M") # => "30" (menit)
puts t.strftime("%S") # => "05" (detik)
puts t.strftime("%p") # => "PM"
puts t.strftime("%P") # => "pm"
# Nama hari dan bulan
puts t.strftime("%A") # => "Thursday" (nama hari lengkap)
puts t.strftime("%a") # => "Thu" (nama hari pendek)
puts t.strftime("%B") # => "August" (nama bulan lengkap)
puts t.strftime("%b") # => "Aug" (nama bulan pendek)
# Timezone
puts t.strftime("%Z") # => "+07:00" atau "WIB"
puts t.strftime("%z") # => "+0700"
# Format gabungan umum
puts t.strftime("%Y-%m-%d") # => "2024-08-15"
puts t.strftime("%d/%m/%Y") # => "15/08/2024"
puts t.strftime("%H:%M:%S") # => "14:30:05"
puts t.strftime("%Y-%m-%d %H:%M:%S") # => "2024-08-15 14:30:05"
puts t.strftime("%Y-%m-%dT%H:%M:%S%z") # => ISO 8601: "2024-08-15T14:30:05+0700"
puts t.strftime("%d %B %Y, pukul %H.%M") # => "15 August 2024, pukul 14.30"
Format Tanggal Indonesia #
Karena nama hari dan bulan dari strftime dalam bahasa Inggris, untuk tampilan berbahasa Indonesia kamu perlu mapping sendiri:
NAMA_HARI = %w[Minggu Senin Selasa Rabu Kamis Jumat Sabtu].freeze
NAMA_BULAN = %w[
Januari Februari Maret April Mei Juni
Juli Agustus September Oktober November Desember
].freeze
def format_indonesia(waktu)
hari = NAMA_HARI[waktu.wday]
bulan = NAMA_BULAN[waktu.month - 1]
"#{hari}, #{waktu.day} #{bulan} #{waktu.year}"
end
t = Time.new(2024, 8, 15)
puts format_indonesia(t) # => "Kamis, 15 Agustus 2024"
Aritmatika Waktu #
Time menyimpan waktu sebagai jumlah detik sejak epoch, sehingga aritmatika dilakukan dalam satuan detik:
sekarang = Time.now
# Menambah dan mengurangi waktu (dalam detik)
satu_jam_lagi = sekarang + 3_600 # + 60*60 detik
satu_hari_lagi = sekarang + 86_400 # + 24*60*60 detik
satu_minggu_lagi = sekarang + 7 * 86_400
satu_jam_lalu = sekarang - 3_600
kemarin = sekarang - 86_400
# Konstanta untuk keterbacaan
DETIK_PER_MENIT = 60
DETIK_PER_JAM = 60 * DETIK_PER_MENIT # 3_600
DETIK_PER_HARI = 24 * DETIK_PER_JAM # 86_400
DETIK_PER_MINGGU = 7 * DETIK_PER_HARI # 604_800
besok = sekarang + DETIK_PER_HARI
minggu_depan = sekarang + DETIK_PER_MINGGU
Untuk menambah bulan atau tahun, jangan kalikan dengan 30 hari atau 365 hari karena hasilnya akan tidak tepat (bulan punya 28-31 hari, tahun kabisat punya 366 hari). Gunakan kelasDateyang punya method>>(maju N bulan) dan<<(mundur N bulan), atau gunakan ActiveSupport di Rails yang menyediakan1.month.from_nowdan sejenisnya.
require 'date'
# Date mendukung aritmatika hari secara akurat
hari_ini = Date.today
besok = hari_ini + 1
kemarin = hari_ini - 1
minggu_lalu = hari_ini - 7
# Maju/mundur bulan dengan >> dan << (akurat!)
bulan_depan = Date.today >> 1 # maju 1 bulan
dua_bulan_lalu = Date.today << 2 # mundur 2 bulan
tahun_depan = Date.today >> 12
# Bandingkan dengan penghitungan manual yang tidak akurat:
# Date.today + 30 ← tidak selalu "bulan depan"
# Date.today + 365 ← tidak selalu "tahun depan" (tahun kabisat!)
# Iterasi tanggal dalam range
(Date.new(2024, 1, 1)..Date.new(2024, 1, 7)).each do |tanggal|
puts tanggal.strftime("%A, %d %B %Y")
end
# => Monday, 01 January 2024
# => Tuesday, 02 January 2024
# => ...
Menghitung Selisih Waktu #
mulai = Time.new(2024, 1, 1, 0, 0, 0)
akhir = Time.new(2024, 8, 15, 14, 30, 0)
# Selisih Time — hasilnya dalam DETIK (Float)
selisih_detik = akhir - mulai
puts selisih_detik # => 19940200.0 (detik)
# Konversi manual ke satuan yang lebih bermakna
def format_durasi(detik)
hari = (detik / 86_400).floor
sisa = detik % 86_400
jam = (sisa / 3_600).floor
sisa = sisa % 3_600
menit = (sisa / 60).floor
detik = (sisa % 60).floor
"#{hari} hari, #{jam} jam, #{menit} menit, #{detik} detik"
end
puts format_durasi(selisih_detik)
# => "227 hari, 14 jam, 30 menit, 0 detik"
# Selisih Date — hasilnya dalam HARI (Rational)
d1 = Date.new(2024, 1, 1)
d2 = Date.new(2024, 8, 15)
puts (d2 - d1).to_i # => 227 hari
# Selisih hari, bulan, tahun secara akurat
def selisih_detail(tanggal_awal, tanggal_akhir)
tahun = tanggal_akhir.year - tanggal_awal.year
bulan = tanggal_akhir.month - tanggal_awal.month
hari = tanggal_akhir.day - tanggal_awal.day
if hari < 0
bulan -= 1
hari += Date.new(tanggal_akhir.year, tanggal_akhir.month, 1).prev_month.next_month.prev_day.day
end
if bulan < 0
tahun -= 1
bulan += 12
end
"#{tahun} tahun, #{bulan} bulan, #{hari} hari"
end
lahir = Date.new(1990, 3, 15)
kini = Date.new(2024, 8, 15)
puts selisih_detail(lahir, kini) # => "34 tahun, 5 bulan, 0 hari"
Zona Waktu #
Penanganan timezone adalah salah satu bagian tersulit dari bekerja dengan waktu. Ruby sendiri (tanpa Rails) punya dukungan timezone yang terbatas:
# UTC — Coordinated Universal Time
utc = Time.now.utc
puts utc # => 2024-08-15 07:30:00 UTC
puts utc.utc? # => true
# Konversi dari UTC ke lokal dan sebaliknya
lokal = Time.now
puts lokal.utc_offset # => 25200 (detik = +07:00)
puts lokal.utc # konversi ke UTC
puts lokal.localtime # konversi ke timezone lokal sistem
# getlocal — ubah ke offset tertentu tanpa mengubah waktu absolut
t = Time.now
puts t.getlocal("+07:00") # WIB
puts t.getlocal("+08:00") # WITA
puts t.getlocal("+09:00") # WIT
# Time.now vs Time.now.utc — perbedaan penting!
lokal_sekarang = Time.now # menggunakan timezone sistem
utc_sekarang = Time.now.utc # selalu UTC
puts lokal_sekarang.to_i == utc_sekarang.to_i # => true (Unix timestamp sama)
# Membuat Time dengan timezone eksplisit
# Selalu sertakan offset saat membuat waktu dari data eksternal
dari_db = Time.new(2024, 8, 15, 14, 30, 0, "+07:00") # WIB
dari_api = Time.parse("2024-08-15T07:30:00Z") # Z = UTC
Praktik terbaik timezone:
✓ Simpan waktu di database selalu dalam UTC
✓ Konversi ke timezone lokal hanya saat ditampilkan ke user
✓ Gunakan Time.now.utc bukan Time.now untuk timestamp server
✓ Selalu sertakan offset/timezone saat parse string dari sumber eksternal
✓ Gunakan Time.parse (require 'time') bukan DateTime.parse untuk kode baru
✗ Jangan asumsikan timezone sistem sama di semua environment
✗ Jangan simpan waktu sebagai string tanpa info timezone
Perbandingan Waktu #
Time dan Date mendukung semua operator perbandingan standar:
t1 = Time.new(2024, 1, 1)
t2 = Time.new(2024, 8, 15)
t3 = Time.new(2024, 1, 1)
puts t1 < t2 # => true
puts t1 > t2 # => false
puts t1 == t3 # => true
puts t1 != t2 # => true
puts t1 <= t3 # => true
# between? — cek apakah waktu berada dalam rentang
sekarang = Time.now
awal = Time.new(2024, 1, 1)
akhir = Time.new(2024, 12, 31)
puts sekarang.between?(awal, akhir) # => true/false tergantung saat dijalankan
# Urutkan array waktu
waktu = [Time.new(2024, 3, 1), Time.new(2024, 1, 15), Time.new(2024, 6, 30)]
puts waktu.sort.map { |t| t.strftime("%d %b") }.inspect
# => ["15 Jan", "01 Mar", "30 Jun"]
waktu.min.strftime("%d %B %Y") # => "15 January 2024"
waktu.max.strftime("%d %B %Y") # => "30 June 2024"
# Range waktu — cek apakah tanggal dalam range
periode_promo = Date.new(2024, 8, 1)..Date.new(2024, 8, 31)
puts periode_promo.include?(Date.new(2024, 8, 15)) # => true
puts periode_promo.include?(Date.new(2024, 9, 1)) # => false
# Iterasi tanggal dalam range
periode_promo.each do |tanggal|
puts tanggal if tanggal.wday == 0 # hanya hari Minggu
end
Mengukur Durasi Eksekusi #
Untuk mengukur performa kode, gunakan Process.clock_gettime (lebih presisi dari Time.now):
# Cara yang tepat — Process.clock_gettime untuk benchmarking
mulai = Process.clock_gettime(Process::CLOCK_MONOTONIC)
# Kode yang diukur
1_000_000.times { Math.sqrt(rand) }
selesai = Process.clock_gettime(Process::CLOCK_MONOTONIC)
puts "Durasi: #{((selesai - mulai) * 1000).round(2)}ms"
# Abstraksi yang bisa dipakai ulang
def ukur_waktu(label = "Operasi")
mulai = Process.clock_gettime(Process::CLOCK_MONOTONIC)
hasil = yield
durasi = Process.clock_gettime(Process::CLOCK_MONOTONIC) - mulai
puts "#{label}: #{(durasi * 1000).round(3)}ms"
hasil
end
ukur_waktu("Sorting 100k items") do
Array.new(100_000) { rand }.sort
end
# Benchmark dari standard library — lebih lengkap
require 'benchmark'
Benchmark.bm(20) do |x|
x.report("Array#sort:") { Array.new(10_000) { rand }.sort }
x.report("Array#sort_by:") { Array.new(10_000) { rand }.sort_by { |n| n } }
end
Parsing yang Aman #
require 'time'
require 'date'
# Time.parse — toleran tapi bisa menghasilkan hasil mengejutkan
Time.parse("15 Agustus") # => mungkin memakai tahun saat ini
Time.parse("08:30") # => memakai tanggal hari ini
# Date.strptime — lebih ketat, format harus cocok persis
def parse_tanggal_aman(str, format = "%Y-%m-%d")
Date.strptime(str, format)
rescue ArgumentError, TypeError => e
puts "Format tanggal tidak valid: #{e.message}"
nil
end
puts parse_tanggal_aman("2024-08-15") # => 2024-08-15
puts parse_tanggal_aman("15/08/2024", "%d/%m/%Y") # => 2024-08-15
puts parse_tanggal_aman("bukan tanggal").inspect # => nil (aman)
# Validasi tanggal yang lebih robust
def tanggal_valid?(tahun, bulan, hari)
Date.valid_date?(tahun, bulan, hari)
end
puts tanggal_valid?(2024, 2, 29) # => true (2024 tahun kabisat)
puts tanggal_valid?(2023, 2, 29) # => false (2023 bukan tahun kabisat)
puts tanggal_valid?(2024, 13, 1) # => false (tidak ada bulan 13)
Pola Umum di Aplikasi #
Timestamp Audit #
module Timestampable
def self.included(base)
base.instance_eval do
attr_reader :dibuat_pada, :diperbarui_pada
end
end
def sentuh_dibuat
@dibuat_pada = Time.now.utc
@diperbarui_pada = Time.now.utc
end
def sentuh_diperbarui
@diperbarui_pada = Time.now.utc
end
def umur_dalam_hari
((Time.now.utc - @dibuat_pada) / 86_400).floor
end
end
class Artikel
include Timestampable
attr_reader :judul
def initialize(judul)
@judul = judul
sentuh_dibuat
end
def perbarui(judul_baru)
@judul = judul_baru
sentuh_diperbarui
end
end
artikel = Artikel.new("Ruby Dasar")
puts artikel.dibuat_pada.strftime("%Y-%m-%d %H:%M:%S UTC")
sleep 0.01
artikel.perbarui("Panduan Ruby Lengkap")
puts artikel.diperbarui_pada > artikel.dibuat_pada # => true
Pengecekan Waktu yang Sering Dibutuhkan #
def hari_kerja?(tanggal = Date.today)
!tanggal.saturday? && !tanggal.sunday?
end
def akhir_bulan?(tanggal = Date.today)
tanggal.next_day.month != tanggal.month
end
def awal_bulan?(tanggal = Date.today)
tanggal.day == 1
end
def hari_kerja_berikutnya(dari = Date.today)
tanggal = dari + 1
tanggal += 1 while !hari_kerja?(tanggal)
tanggal
end
def hari_kerja_dalam_bulan(tahun, bulan)
(Date.new(tahun, bulan, 1)..Date.new(tahun, bulan, -1))
.count { |d| hari_kerja?(d) }
end
puts hari_kerja?(Date.today)
puts hari_kerja_berikutnya.strftime("%A, %d %B %Y")
puts "Hari kerja Agustus 2024: #{hari_kerja_dalam_bulan(2024, 8)}"
# => "Hari kerja Agustus 2024: 22"
Ringkasan #
- Gunakan
Timeuntuk kode baru — lebih cepat dariDateTime, mendukung timezone, dan punya presisi nanosecond.DateTimemasih ada tapi dianggap legacy.- Gunakan
Dateuntuk data tanggal saja — ketika jam tidak relevan (ulang tahun, deadline, hari libur),Datelebih tepat dan mencegah bug timezone.- Simpan waktu selalu dalam UTC — konversi ke timezone lokal hanya saat ditampilkan ke user. Ini mencegah bug Daylight Saving Time dan perbedaan server timezone.
strftimeuntuk format ke string,strptime/parseuntuk parse dari string —strftimefleksibel,strptimelebih aman karena formatnya eksplisit.- Gunakan
>>dan<<padaDateuntuk maju/mundur bulan — jangan kalikan dengan 30 karena bulan punya 28-31 hari.- Aritmatika
Timemenghasilkan detik (Float) — bagi dengan 3_600 untuk jam, 86_400 untuk hari.- Aritmatika
Datemenghasilkan hari (Rational) — panggil.to_iuntuk mendapat Integer.Process.clock_gettimeuntuk benchmarking — lebih akurat dariTime.nowkarena tidak terpengaruh perubahan jam sistem.Date.valid_date?untuk validasi tanggal — jauh lebih andal dari mencobaDate.newdan rescue ArgumentError.- Nama bulan dan hari dari
strftimedalam bahasa Inggris — untuk tampilan bahasa Indonesia, buat mapping array sendiri.