Date & Time

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 kelas Date yang punya method >> (maju N bulan) dan << (mundur N bulan), atau gunakan ActiveSupport di Rails yang menyediakan 1.month.from_now dan 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 Time untuk kode baru — lebih cepat dari DateTime, mendukung timezone, dan punya presisi nanosecond. DateTime masih ada tapi dianggap legacy.
  • Gunakan Date untuk data tanggal saja — ketika jam tidak relevan (ulang tahun, deadline, hari libur), Date lebih 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.
  • strftime untuk format ke string, strptime/parse untuk parse dari stringstrftime fleksibel, strptime lebih aman karena formatnya eksplisit.
  • Gunakan >> dan << pada Date untuk maju/mundur bulan — jangan kalikan dengan 30 karena bulan punya 28-31 hari.
  • Aritmatika Time menghasilkan detik (Float) — bagi dengan 3_600 untuk jam, 86_400 untuk hari.
  • Aritmatika Date menghasilkan hari (Rational) — panggil .to_i untuk mendapat Integer.
  • Process.clock_gettime untuk benchmarking — lebih akurat dari Time.now karena tidak terpengaruh perubahan jam sistem.
  • Date.valid_date? untuk validasi tanggal — jauh lebih andal dari mencoba Date.new dan rescue ArgumentError.
  • Nama bulan dan hari dari strftime dalam bahasa Inggris — untuk tampilan bahasa Indonesia, buat mapping array sendiri.

← Sebelumnya: Map   Berikutnya: Regex →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact