indonesia-formatter-dev
Version:
A comprehensive TypeScript library for formatting Indonesian-specific data
373 lines (368 loc) • 10.9 kB
JavaScript
// src/formatters/date.ts
var DateFormatter = class {
/**
* Format date to Indonesian format
* @param date - Date to format
* @param format - Format type
*/
static format(date, format = "long") {
const day = date.getDate();
const month = this.months[date.getMonth()];
const year = date.getFullYear();
const dayName = this.days[date.getDay()];
switch (format) {
case "long":
return `${dayName}, ${day} ${month} ${year}`;
case "medium":
return `${day} ${month} ${year}`;
case "short":
return `${day.toString().padStart(2, "0")}/${(date.getMonth() + 1).toString().padStart(2, "0")}/${year}`;
case "monthYear":
return `${month} ${year}`;
default:
return `${day} ${month} ${year}`;
}
}
/**
* Format relative time in Indonesian
* @param date - Date to compare
* @param baseDate - Base date (default: now)
*/
static relative(date, baseDate = /* @__PURE__ */ new Date()) {
const diff = baseDate.getTime() - date.getTime();
const seconds = Math.floor(diff / 1e3);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);
if (seconds < 0) {
const future = this.relative(baseDate, date);
return `dalam ${future}`;
}
if (seconds < 60)
return "baru saja";
if (minutes === 1)
return "1 menit yang lalu";
if (minutes < 60)
return `${minutes} menit yang lalu`;
if (hours === 1)
return "1 jam yang lalu";
if (hours < 24)
return `${hours} jam yang lalu`;
if (days === 1)
return "kemarin";
if (days < 7)
return `${days} hari yang lalu`;
if (days < 14)
return "1 minggu yang lalu";
if (days < 30)
return `${Math.floor(days / 7)} minggu yang lalu`;
if (months === 1)
return "1 bulan yang lalu";
if (months < 12)
return `${months} bulan yang lalu`;
if (years === 1)
return "1 tahun yang lalu";
return `${years} tahun yang lalu`;
}
};
DateFormatter.months = [
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
];
DateFormatter.days = [
"Minggu",
"Senin",
"Selasa",
"Rabu",
"Kamis",
"Jumat",
"Sabtu"
];
// src/formatters/calendar.ts
var CalendarFormatter = class {
/**
* Convert Gregorian date to Javanese calendar
*/
static toJavanese(date) {
const baseDate = new Date(1633, 6, 8);
const diff = Math.floor((date.getTime() - baseDate.getTime()) / (1e3 * 60 * 60 * 24));
let year = 1555;
let days = diff;
while (days > 354) {
const yearLength = year % 8 === 2 || year % 8 === 5 || year % 8 === 0 ? 355 : 354;
if (days >= yearLength) {
days -= yearLength;
year++;
} else {
break;
}
}
const months = [
{ name: "Sura", days: 30 },
{ name: "Sapar", days: 29 },
{ name: "Mulud", days: 30 },
{ name: "Bakda Mulud", days: 29 },
{ name: "Jumadil Awal", days: 30 },
{ name: "Jumadil Akhir", days: 29 },
{ name: "Rejeb", days: 30 },
{ name: "Ruwah", days: 29 },
{ name: "Pasa", days: 30 },
{ name: "Sawal", days: 29 },
{ name: "Sela", days: 30 },
{ name: "Besar", days: 29 }
];
let month = 0;
let dayOfMonth = days;
for (let i = 0; i < months.length; i++) {
if (dayOfMonth > months[i].days) {
dayOfMonth -= months[i].days;
month++;
} else {
break;
}
}
const pasaranNames = ["Legi", "Pahing", "Pon", "Wage", "Kliwon"];
const pasaranIndex = diff % 5;
return {
year,
month: months[month].name,
day: dayOfMonth + 1,
pasaran: pasaranNames[pasaranIndex]
};
}
/**
* Convert Gregorian date to Hijri calendar
*/
static toHijri(date) {
const gregorianYear = date.getFullYear();
const gregorianMonth = date.getMonth() + 1;
const gregorianDay = date.getDate();
let jd = Math.floor(1461 * (gregorianYear + 4800 + Math.floor((gregorianMonth - 14) / 12)) / 4) + Math.floor(367 * (gregorianMonth - 2 - 12 * Math.floor((gregorianMonth - 14) / 12)) / 12) - Math.floor(3 * Math.floor((gregorianYear + 4900 + Math.floor((gregorianMonth - 14) / 12)) / 100) / 4) + gregorianDay - 32075;
let l = jd - 1948440 + 10632;
let n = Math.floor((l - 1) / 10631);
l = l - 10631 * n + 354;
let j = Math.floor((10985 - l) / 5316) * Math.floor(50 * l / 17719) + Math.floor(l / 5670) * Math.floor(43 * l / 15238);
l = l - Math.floor((30 - j) / 15) * Math.floor(17719 * j / 50) - Math.floor(j / 16) * Math.floor(15238 * j / 43) + 29;
let month = Math.floor(24 * l / 709);
let day = l - Math.floor(709 * month / 24);
let year = 30 * n + j - 30;
const monthNames = [
"Muharram",
"Safar",
"Rabiul Awal",
"Rabiul Akhir",
"Jumadil Awal",
"Jumadil Akhir",
"Rajab",
"Syaban",
"Ramadhan",
"Syawal",
"Dzulqaidah",
"Dzulhijjah"
];
return {
year,
month: monthNames[month - 1],
day,
monthNumber: month
};
}
/**
* Format Javanese date to string
*/
static formatJavanese(date, includeGregorian = true) {
const jav = this.toJavanese(date);
const formatted = `${jav.pasaran}, ${jav.day} ${jav.month} ${jav.year}`;
if (includeGregorian) {
return `${formatted} (${DateFormatter.format(date, "medium")})`;
}
return formatted;
}
/**
* Format Hijri date to string
*/
static formatHijri(date, includeGregorian = true) {
const hijri = this.toHijri(date);
const formatted = `${hijri.day} ${hijri.month} ${hijri.year} H`;
if (includeGregorian) {
return `${formatted} (${DateFormatter.format(date, "medium")})`;
}
return formatted;
}
};
// src/formatters/nik.ts
var NIKFormatter = class {
/**
* Format NIK (Nomor Induk Kependudukan)
* @param nik - NIK number
* @returns Object with parsed NIK information
*/
static parse(nik) {
const cleaned = nik.replace(/\D/g, "");
if (cleaned.length !== 16) {
throw new Error("NIK must be 16 digits");
}
const provinceCode = cleaned.slice(0, 2);
const cityCode = cleaned.slice(2, 4);
const districtCode = cleaned.slice(4, 6);
const birthDate = cleaned.slice(6, 12);
const uniqueCode = cleaned.slice(12, 16);
let day = parseInt(birthDate.slice(0, 2));
const month = parseInt(birthDate.slice(2, 4));
const year = parseInt(birthDate.slice(4, 6));
const isFemale = day > 40;
if (isFemale) {
day -= 40;
}
const fullYear = year > 50 ? 1900 + year : 2e3 + year;
return {
nik: cleaned,
provinceCode,
cityCode,
districtCode,
birthDate: new Date(fullYear, month - 1, day),
gender: isFemale ? "female" : "male",
uniqueCode
};
}
/**
* Validate NIK
*/
static validate(nik) {
const cleaned = nik.replace(/\D/g, "");
if (cleaned.length !== 16 || !/^\d{16}$/.test(cleaned)) {
return false;
}
try {
const birthDateStr = cleaned.slice(6, 12);
let day = parseInt(birthDateStr.slice(0, 2));
const month = parseInt(birthDateStr.slice(2, 4));
const year = parseInt(birthDateStr.slice(4, 6));
if (day > 40) {
day -= 40;
}
if (month < 1 || month > 12) {
return false;
}
if (day < 1 || day > 31) {
return false;
}
const fullYear = year > 50 ? 1900 + year : 2e3 + year;
const testDate = new Date(fullYear, month - 1, day);
if (testDate.getFullYear() !== fullYear || testDate.getMonth() !== month - 1 || testDate.getDate() !== day) {
return false;
}
return true;
} catch {
return false;
}
}
/**
* Format NIK with spaces for readability
*/
static format(nik) {
const cleaned = nik.replace(/\D/g, "");
if (cleaned.length !== 16) {
throw new Error("NIK must be 16 digits");
}
return `${cleaned.slice(0, 4)} ${cleaned.slice(4, 8)} ${cleaned.slice(8, 12)} ${cleaned.slice(12, 16)}`;
}
};
// src/formatters/npwp.ts
var NPWPFormatter = class {
/**
* Format NPWP (Nomor Pokok Wajib Pajak)
* @param npwp - NPWP number
* @returns Formatted NPWP (XX.XXX.XXX.X-XXX.XXX)
*/
static format(npwp) {
const cleaned = npwp.replace(/\D/g, "");
if (cleaned.length !== 15) {
throw new Error("NPWP must be 15 digits");
}
return `${cleaned.slice(0, 2)}.${cleaned.slice(2, 5)}.${cleaned.slice(5, 8)}.${cleaned.slice(8, 9)}-${cleaned.slice(9, 12)}.${cleaned.slice(12, 15)}`;
}
/**
* Validate NPWP
*/
static validate(npwp) {
const cleaned = npwp.replace(/\D/g, "");
return cleaned.length === 15 && /^\d{15}$/.test(cleaned);
}
};
// src/formatters/phone.ts
var PhoneFormatter = class {
/**
* Format nomor telepon Indonesia
* @param phone - Nomor telepon
* @param options - Opsi formatting
* @returns Formatted phone number
*/
static format(phone, options) {
const cleaned = phone.replace(/\D/g, "");
if (cleaned.startsWith("62")) {
return this.formatInternational(cleaned, options);
} else if (cleaned.startsWith("0")) {
return this.formatDomestic(cleaned, options);
}
throw new Error("Invalid Indonesian phone number");
}
static formatInternational(phone, options) {
const format = options?.format || "standard";
switch (format) {
case "standard":
return `+${phone.slice(0, 2)} ${phone.slice(2, 5)}-${phone.slice(5, 9)}-${phone.slice(9)}`;
case "dots":
return `+${phone.slice(0, 2)}.${phone.slice(2, 5)}.${phone.slice(5, 9)}.${phone.slice(9)}`;
case "spaces":
return `+${phone.slice(0, 2)} ${phone.slice(2, 5)} ${phone.slice(5, 9)} ${phone.slice(9)}`;
default:
return `+${phone}`;
}
}
static formatDomestic(phone, options) {
const format = options?.format || "standard";
switch (format) {
case "standard":
return `${phone.slice(0, 4)}-${phone.slice(4, 8)}-${phone.slice(8)}`;
case "dots":
return `${phone.slice(0, 4)}.${phone.slice(4, 8)}.${phone.slice(8)}`;
case "spaces":
return `${phone.slice(0, 4)} ${phone.slice(4, 8)} ${phone.slice(8)}`;
default:
return phone;
}
}
/**
* Validate nomor telepon Indonesia
*/
static validate(phone) {
const cleaned = phone.replace(/\D/g, "");
const patterns = [
/^62[2-9]\d{7,11}$/,
// International format
/^0[2-9]\d{7,11}$/
// Domestic format
];
return patterns.some((pattern) => pattern.test(cleaned));
}
};
export {
CalendarFormatter,
DateFormatter,
NIKFormatter,
NPWPFormatter,
PhoneFormatter
};