feat: add DateUtils and StringUtils for date and string manipulation
- Implement DateUtils for handling date operations with timezone support. - Add unit tests for DateUtils to ensure functionality and edge cases. - Create StringUtils for string manipulation with a fluent API. - Include unit tests for StringUtils covering various string operations. - Introduce EncryptionUtils for secure data encryption and decryption. - Add tests for EncryptionUtils to validate encryption and decryption processes. - Update index.ts to export new utility modules. - Introduce encryption key management with a dedicated key file.
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
// import { DateService } from '@repo/utils';
|
// import { DateUtils } from '@repo/utils';
|
||||||
// import { CurrencyService } from '@repo/utils';
|
// import { CurrencyUtils } from '@repo/utils';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
import { lazy, StrictMode } from 'react';
|
import { lazy, StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
const App = lazy(() => import('./apps'));
|
const App = lazy(() => import('./apps'));
|
||||||
|
|
||||||
// DateService.setGlobalConfig('Asia/Jakarta');
|
// DateUtils.setGlobalConfig('Asia/Jakarta');
|
||||||
// DateService.setGlobalConfig('Asia/Makassar');
|
// DateUtils.setGlobalConfig('Asia/Makassar');
|
||||||
|
|
||||||
// CurrencyService.setGlobalPrefix('IDR ');
|
// CurrencyUtils.setGlobalPrefix('IDR ');
|
||||||
|
// CurrencyUtils.setGlobalDecimalSeparator(',');
|
||||||
createRoot(document.getElementById('app')!).render(
|
createRoot(document.getElementById('app')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { DateService } from '@repo/utils';
|
import { DateUtils } from '@repo/utils';
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
export const DateServiceExample: React.FC = () => {
|
export const DateServiceExample: React.FC = () => {
|
||||||
// --- 0. TIMEZONE CONFIGURATION (ROOT CONTROL) ---
|
// --- 0. TIMEZONE CONFIGURATION (ROOT CONTROL) ---
|
||||||
const [currentZone, setCurrentZone] = useState<string>(DateService.getGlobalTimezone());
|
const [currentZone, setCurrentZone] = useState<string>(DateUtils.getGlobalTimezone());
|
||||||
|
|
||||||
// Ambil list timezone dari helper static yang kita buat sebelumnya
|
// Ambil list timezone dari helper static yang kita buat sebelumnya
|
||||||
const timezoneList = useMemo(() => DateService.getSupportedTimezones(), []);
|
const timezoneList = useMemo(() => DateUtils.getSupportedTimezones(), []);
|
||||||
|
|
||||||
// --- 1. STATE & DATA ---
|
// --- 1. STATE & DATA ---
|
||||||
|
|
||||||
@@ -17,40 +17,40 @@ export const DateServiceExample: React.FC = () => {
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: 'Bayar Server (Lusa)',
|
title: 'Bayar Server (Lusa)',
|
||||||
dueDate: DateService.now().add(2, 'day').toISOString(),
|
dueDate: DateUtils.now().add(2, 'day').toISOString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'Laporan Bulanan (Lewat 5 Hari)',
|
title: 'Laporan Bulanan (Lewat 5 Hari)',
|
||||||
dueDate: DateService.now().subtract(5, 'day').toISOString(),
|
dueDate: DateUtils.now().subtract(5, 'day').toISOString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: 'Meeting Tahunan (Bulan Depan)',
|
title: 'Meeting Tahunan (Bulan Depan)',
|
||||||
dueDate: DateService.now().add(1, 'month').toISOString(),
|
dueDate: DateUtils.now().add(1, 'month').toISOString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: 'Deadline Besok Pagi',
|
title: 'Deadline Besok Pagi',
|
||||||
dueDate: DateService.now().add(1, 'day').startOf('day').toISOString(),
|
dueDate: DateUtils.now().add(1, 'day').startOf('day').toISOString(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [currentZone]); // Dependency: currentZone
|
}, [currentZone]); // Dependency: currentZone
|
||||||
|
|
||||||
const [currentTime, setCurrentTime] = useState<DateService>(DateService.now());
|
const [currentTime, setCurrentTime] = useState<DateUtils>(DateUtils.now());
|
||||||
const [selectedDate, setSelectedDate] = useState<DateService>(DateService.now());
|
const [selectedDate, setSelectedDate] = useState<DateUtils>(DateUtils.now());
|
||||||
|
|
||||||
// State demo logic calendar
|
// State demo logic calendar
|
||||||
const [calendarTarget, setCalendarTarget] = useState(() => DateService.now().add(1, 'day').startOf('day'));
|
const [calendarTarget, setCalendarTarget] = useState(() => DateUtils.now().add(1, 'day').startOf('day'));
|
||||||
|
|
||||||
// Static date (Memo kosong) -> Akan tetap di Timezone saat pertama kali load (kecuali di-refresh)
|
// Static date (Memo kosong) -> Akan tetap di Timezone saat pertama kali load (kecuali di-refresh)
|
||||||
// Ini bagus untuk demo bahwa object lama tidak berubah (immutable) kecuali dibuat ulang.
|
// Ini bagus untuk demo bahwa object lama tidak berubah (immutable) kecuali dibuat ulang.
|
||||||
const staticDate = useMemo(() => new DateService(), []);
|
const staticDate = useMemo(() => new DateUtils(), []);
|
||||||
|
|
||||||
// Effect update jam tiap detik
|
// Effect update jam tiap detik
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setCurrentTime(DateService.now());
|
setCurrentTime(DateUtils.now());
|
||||||
}, 1000);
|
}, 1000);
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -61,17 +61,17 @@ export const DateServiceExample: React.FC = () => {
|
|||||||
setCurrentZone(newZone);
|
setCurrentZone(newZone);
|
||||||
|
|
||||||
// 1. Set Global Config (Inti Perubahan)
|
// 1. Set Global Config (Inti Perubahan)
|
||||||
DateService.setGlobalConfig(newZone);
|
DateUtils.setGlobalConfig(newZone);
|
||||||
|
|
||||||
// 2. Force Refresh State agar UI langsung berubah
|
// 2. Force Refresh State agar UI langsung berubah
|
||||||
// Kita buat instance baru agar mengambil config timezone terbaru
|
// Kita buat instance baru agar mengambil config timezone terbaru
|
||||||
setCurrentTime(DateService.now());
|
setCurrentTime(DateUtils.now());
|
||||||
|
|
||||||
// Kita recreate selectedDate dengan nilai raw yang sama, tapi context timezone baru
|
// Kita recreate selectedDate dengan nilai raw yang sama, tapi context timezone baru
|
||||||
setSelectedDate((prev) => new DateService(prev.getRaw()));
|
setSelectedDate((prev) => new DateUtils(prev.getRaw()));
|
||||||
|
|
||||||
// Recreate target demo calendar
|
// Recreate target demo calendar
|
||||||
setCalendarTarget(DateService.now().add(1, 'day').startOf('day'));
|
setCalendarTarget(DateUtils.now().add(1, 'day').startOf('day'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddDay = (days: number) => {
|
const handleAddDay = (days: number) => {
|
||||||
@@ -82,7 +82,7 @@ export const DateServiceExample: React.FC = () => {
|
|||||||
<div className="p-8 max-w-5xl mx-auto bg-gray-50 min-h-screen font-sans text-gray-800">
|
<div className="p-8 max-w-5xl mx-auto bg-gray-50 min-h-screen font-sans text-gray-800">
|
||||||
{/* HEADER & TIMEZONE CONTROLLER */}
|
{/* HEADER & TIMEZONE CONTROLLER */}
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4 border-b border-gray-200 pb-6">
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4 border-b border-gray-200 pb-6">
|
||||||
<h1 className="text-3xl font-bold text-blue-900">DateService Feature Showcase</h1>
|
<h1 className="text-3xl font-bold text-blue-900">DateUtils Feature Showcase</h1>
|
||||||
|
|
||||||
<div className="bg-white p-3 rounded-lg shadow-sm border border-blue-100 flex items-center gap-3">
|
<div className="bg-white p-3 rounded-lg shadow-sm border border-blue-100 flex items-center gap-3">
|
||||||
<label htmlFor="tz-switcher" className="text-xs font-bold text-gray-500 uppercase tracking-wide">
|
<label htmlFor="tz-switcher" className="text-xs font-bold text-gray-500 uppercase tracking-wide">
|
||||||
@@ -162,11 +162,11 @@ export const DateServiceExample: React.FC = () => {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[10px] uppercase text-gray-500">Epoch Millis</div>
|
<div className="text-[10px] uppercase text-gray-500">Epoch Millis</div>
|
||||||
<div className="font-mono font-bold text-gray-700">{new DateService().epochMillis}</div>
|
<div className="font-mono font-bold text-gray-700">{new DateUtils().epochMillis}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[10px] uppercase text-gray-500">Epoch Seconds</div>
|
<div className="text-[10px] uppercase text-gray-500">Epoch Seconds</div>
|
||||||
<div className="font-mono font-bold text-gray-700">{new DateService().epochSeconds}</div>
|
<div className="font-mono font-bold text-gray-700">{new DateUtils().epochSeconds}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,8 +278,8 @@ export const DateServiceExample: React.FC = () => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{tasks.map((task) => {
|
{tasks.map((task) => {
|
||||||
const dueDate = new DateService(task.dueDate);
|
const dueDate = new DateUtils(task.dueDate);
|
||||||
const now = DateService.now();
|
const now = DateUtils.now();
|
||||||
|
|
||||||
const isOverdue = now.isAfter(dueDate);
|
const isOverdue = now.isAfter(dueDate);
|
||||||
const daysDiff = dueDate.diffCalendarDay(now);
|
const daysDiff = dueDate.diffCalendarDay(now);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { EncryptionService } from '@repo/utils';
|
import { EncryptionUtils } from '@repo/utils';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
export const EncryptionExample: React.FC = () => {
|
export const EncryptionExample: React.FC = () => {
|
||||||
// --- 1. SETUP INSTANCE ---
|
// --- 1. SETUP INSTANCE ---
|
||||||
// Kita gunakan Singleton agar hemat memory
|
// Kita gunakan Singleton agar hemat memory
|
||||||
const cryptoService = EncryptionService.getInstance();
|
const cryptoService = EncryptionUtils.getInstance();
|
||||||
|
|
||||||
// --- 2. STATE ---
|
// --- 2. STATE ---
|
||||||
const [plainText, setPlainText] = useState<string>('');
|
const [plainText, setPlainText] = useState<string>('');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
// 1. ATOMIC COMPONENTS (Berdasar Config 13px & Burgundy)
|
// 1. ATOMIC COMPONENTS (Berdasar Config 13px & Burgundy)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CurrencyService } from '@repo/utils';
|
import { CurrencyUtils } from '@repo/utils';
|
||||||
import { InputCurrencyProps } from '../types';
|
import { InputCurrencyProps } from '../types';
|
||||||
import { InputNumber } from './input-number.component';
|
import { InputNumber } from './input-number.component';
|
||||||
|
|
||||||
@@ -6,13 +6,13 @@ import { InputNumber } from './input-number.component';
|
|||||||
* InputCurrency
|
* InputCurrency
|
||||||
*
|
*
|
||||||
* A controlled InputNumber component with currency formatting.
|
* A controlled InputNumber component with currency formatting.
|
||||||
* Delegates all formatting/parsing to CurrencyService.
|
* Delegates all formatting/parsing to CurrencyUtils.
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Display currency prefix (e.g., Rp, $)
|
* - Display currency prefix (e.g., Rp, $)
|
||||||
* - Thousand separator formatting
|
* - Thousand separator formatting
|
||||||
* - Optional decimal rounding
|
* - Optional decimal rounding
|
||||||
* - Global prefix support via CurrencyService
|
* - Global prefix support via CurrencyUtils
|
||||||
*/
|
*/
|
||||||
export const InputCurrency = (props: InputCurrencyProps) => {
|
export const InputCurrency = (props: InputCurrencyProps) => {
|
||||||
const {
|
const {
|
||||||
@@ -22,8 +22,8 @@ export const InputCurrency = (props: InputCurrencyProps) => {
|
|||||||
...restProps
|
...restProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// Create a CurrencyService instance for this input
|
// Create a CurrencyUtils instance for this input
|
||||||
const currencyService = new CurrencyService({
|
const currencyService = new CurrencyUtils({
|
||||||
prefix,
|
prefix,
|
||||||
decimalSeparator,
|
decimalSeparator,
|
||||||
decimalScale,
|
decimalScale,
|
||||||
|
|||||||
+18
-18
@@ -1,20 +1,20 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
import { CurrencyService } from './currency-service';
|
import { CurrencyUtils } from './currency.utils';
|
||||||
|
|
||||||
describe('CurrencyService', () => {
|
describe('CurrencyUtils', () => {
|
||||||
let currency: CurrencyService;
|
let currency: CurrencyUtils;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset global prefix and decimal separator before each test
|
// Reset global prefix and decimal separator before each test
|
||||||
CurrencyService.setGlobalPrefix('Rp ');
|
CurrencyUtils.setGlobalPrefix('Rp ');
|
||||||
CurrencyService.setGlobalDecimalSeparator(',');
|
CurrencyUtils.setGlobalDecimalSeparator(',');
|
||||||
|
|
||||||
// Mock console to keep tests clean
|
// Mock console to keep tests clean
|
||||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
// Default instance for testing
|
// Default instance for testing
|
||||||
currency = new CurrencyService();
|
currency = new CurrencyUtils();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -23,37 +23,37 @@ describe('CurrencyService', () => {
|
|||||||
|
|
||||||
describe('Global Prefix', () => {
|
describe('Global Prefix', () => {
|
||||||
it('should set and get global prefix', () => {
|
it('should set and get global prefix', () => {
|
||||||
CurrencyService.setGlobalPrefix('USD ');
|
CurrencyUtils.setGlobalPrefix('USD ');
|
||||||
expect(CurrencyService.getGlobalPrefix()).toBe('USD ');
|
expect(CurrencyUtils.getGlobalPrefix()).toBe('USD ');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use global prefix if instance prefix is not provided', () => {
|
it('should use global prefix if instance prefix is not provided', () => {
|
||||||
CurrencyService.setGlobalPrefix('USD ');
|
CurrencyUtils.setGlobalPrefix('USD ');
|
||||||
const c = new CurrencyService();
|
const c = new CurrencyUtils();
|
||||||
expect(c.format(1000)).toBe('USD 1.000');
|
expect(c.format(1000)).toBe('USD 1.000');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should override instance prefix', () => {
|
it('should override instance prefix', () => {
|
||||||
const c = new CurrencyService({ prefix: '€ ' });
|
const c = new CurrencyUtils({ prefix: '€ ' });
|
||||||
expect(c.format(1000)).toBe('€ 1.000');
|
expect(c.format(1000)).toBe('€ 1.000');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Global Decimal Separator', () => {
|
describe('Global Decimal Separator', () => {
|
||||||
it('should set and get global decimal separator', () => {
|
it('should set and get global decimal separator', () => {
|
||||||
CurrencyService.setGlobalDecimalSeparator('.');
|
CurrencyUtils.setGlobalDecimalSeparator('.');
|
||||||
expect(CurrencyService.getGlobalDecimalSeparator()).toBe('.');
|
expect(CurrencyUtils.getGlobalDecimalSeparator()).toBe('.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use global decimal separator if instance separator is not provided', () => {
|
it('should use global decimal separator if instance separator is not provided', () => {
|
||||||
CurrencyService.setGlobalDecimalSeparator('.');
|
CurrencyUtils.setGlobalDecimalSeparator('.');
|
||||||
const c = new CurrencyService();
|
const c = new CurrencyUtils();
|
||||||
expect(c.format(1234.56)).toBe('Rp 1,234.56');
|
expect(c.format(1234.56)).toBe('Rp 1,234.56');
|
||||||
expect(c.parseToRaw('Rp 1,234.56')).toBe('1234.56');
|
expect(c.parseToRaw('Rp 1,234.56')).toBe('1234.56');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should override instance decimal separator', () => {
|
it('should override instance decimal separator', () => {
|
||||||
const c = new CurrencyService({ decimalSeparator: '.' });
|
const c = new CurrencyUtils({ decimalSeparator: '.' });
|
||||||
expect(c.format(1234.56)).toBe('Rp 1,234.56');
|
expect(c.format(1234.56)).toBe('Rp 1,234.56');
|
||||||
expect(c.parseToRaw('Rp 1,234.56')).toBe('1234.56');
|
expect(c.parseToRaw('Rp 1,234.56')).toBe('1234.56');
|
||||||
});
|
});
|
||||||
@@ -69,7 +69,7 @@ describe('CurrencyService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should round decimal when decimalScale is set', () => {
|
it('should round decimal when decimalScale is set', () => {
|
||||||
const c = new CurrencyService({ decimalScale: 2 });
|
const c = new CurrencyUtils({ decimalScale: 2 });
|
||||||
expect(c.format(1234.5678)).toBe('Rp 1.234,56');
|
expect(c.format(1234.5678)).toBe('Rp 1.234,56');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ describe('CurrencyService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should format numbers with multiple decimals correctly when decimalScale is set', () => {
|
it('should format numbers with multiple decimals correctly when decimalScale is set', () => {
|
||||||
const c = new CurrencyService({ decimalScale: 3 });
|
const c = new CurrencyUtils({ decimalScale: 3 });
|
||||||
expect(c.format(1234.56789)).toBe('Rp 1.234,567');
|
expect(c.format(1234.56789)).toBe('Rp 1.234,567');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+9
-9
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* ------------------------------------------------------------
|
* ------------------------------------------------------------
|
||||||
* CurrencyService
|
* CurrencyUtils
|
||||||
* ------------------------------------------------------------
|
* ------------------------------------------------------------
|
||||||
* Centralized currency utility for formatting and parsing numeric values.
|
* Centralized currency utility for formatting and parsing numeric values.
|
||||||
*
|
*
|
||||||
@@ -32,7 +32,7 @@ export interface CurrencyOptions {
|
|||||||
decimalScale?: number;
|
decimalScale?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CurrencyService {
|
export class CurrencyUtils {
|
||||||
/** ----------------------------------------------------------------
|
/** ----------------------------------------------------------------
|
||||||
* Global default prefix shared across all instances
|
* Global default prefix shared across all instances
|
||||||
* ---------------------------------------------------------------- */
|
* ---------------------------------------------------------------- */
|
||||||
@@ -54,14 +54,14 @@ export class CurrencyService {
|
|||||||
private readonly thousandSeparator: string;
|
private readonly thousandSeparator: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for CurrencyService instance.
|
* Constructor for CurrencyUtils instance.
|
||||||
* Allows overriding prefix, decimal separator, and rounding per instance.
|
* Allows overriding prefix, decimal separator, and rounding per instance.
|
||||||
*
|
*
|
||||||
* @param options CurrencyOptions for prefix, separator, and rounding
|
* @param options CurrencyOptions for prefix, separator, and rounding
|
||||||
*/
|
*/
|
||||||
constructor(options?: CurrencyOptions) {
|
constructor(options?: CurrencyOptions) {
|
||||||
this.prefix = options?.prefix ?? CurrencyService._globalPrefix;
|
this.prefix = options?.prefix ?? CurrencyUtils._globalPrefix;
|
||||||
this.decimalSeparator = options?.decimalSeparator ?? CurrencyService._globalDecimalSeparator;
|
this.decimalSeparator = options?.decimalSeparator ?? CurrencyUtils._globalDecimalSeparator;
|
||||||
this.decimalScale = options?.decimalScale;
|
this.decimalScale = options?.decimalScale;
|
||||||
this.thousandSeparator = this.decimalSeparator === ',' ? '.' : ',';
|
this.thousandSeparator = this.decimalSeparator === ',' ? '.' : ',';
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ export class CurrencyService {
|
|||||||
* @param prefix New global prefix string
|
* @param prefix New global prefix string
|
||||||
*/
|
*/
|
||||||
static setGlobalPrefix(prefix: string) {
|
static setGlobalPrefix(prefix: string) {
|
||||||
CurrencyService._globalPrefix = prefix;
|
CurrencyUtils._globalPrefix = prefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,7 +86,7 @@ export class CurrencyService {
|
|||||||
* @returns Current global prefix string
|
* @returns Current global prefix string
|
||||||
*/
|
*/
|
||||||
static getGlobalPrefix(): string {
|
static getGlobalPrefix(): string {
|
||||||
return CurrencyService._globalPrefix;
|
return CurrencyUtils._globalPrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------
|
/** ------------------------------------------------------------
|
||||||
@@ -94,11 +94,11 @@ export class CurrencyService {
|
|||||||
* ------------------------------------------------------------ */
|
* ------------------------------------------------------------ */
|
||||||
|
|
||||||
static setGlobalDecimalSeparator(separator: ',' | '.') {
|
static setGlobalDecimalSeparator(separator: ',' | '.') {
|
||||||
CurrencyService._globalDecimalSeparator = separator;
|
CurrencyUtils._globalDecimalSeparator = separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getGlobalDecimalSeparator(): ',' | '.' {
|
static getGlobalDecimalSeparator(): ',' | '.' {
|
||||||
return CurrencyService._globalDecimalSeparator;
|
return CurrencyUtils._globalDecimalSeparator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ------------------------------------------------------------
|
/** ------------------------------------------------------------
|
||||||
+48
-48
@@ -1,11 +1,11 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
import { DateService } from './date-service';
|
import { DateUtils } from './date.utils';
|
||||||
|
|
||||||
describe('DateService', () => {
|
describe('DateUtils', () => {
|
||||||
// Setup standard environment before each test
|
// Setup standard environment before each test
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// 1. Reset Global Timezone to UTC for deterministic results across environments
|
// 1. Reset Global Timezone to UTC for deterministic results across environments
|
||||||
DateService.setGlobalConfig('UTC');
|
DateUtils.setGlobalConfig('UTC');
|
||||||
|
|
||||||
// 2. Mock console warn/error to keep the terminal clean during error handling tests
|
// 2. Mock console warn/error to keep the terminal clean during error handling tests
|
||||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
@@ -20,21 +20,21 @@ describe('DateService', () => {
|
|||||||
|
|
||||||
describe('Initialization', () => {
|
describe('Initialization', () => {
|
||||||
it('should create an instance with the current time (now) using static method', () => {
|
it('should create an instance with the current time (now) using static method', () => {
|
||||||
const date = DateService.now();
|
const date = DateUtils.now();
|
||||||
expect(date).toBeInstanceOf(DateService);
|
expect(date).toBeInstanceOf(DateUtils);
|
||||||
expect(date.toDate()).toBeInstanceOf(Date);
|
expect(date.toDate()).toBeInstanceOf(Date);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create an instance from a specific ISO string', () => {
|
it('should create an instance from a specific ISO string', () => {
|
||||||
// Use full ISO format (.000Z) for precision
|
// Use full ISO format (.000Z) for precision
|
||||||
const input = '2025-01-01T10:00:00.000Z';
|
const input = '2025-01-01T10:00:00.000Z';
|
||||||
const date = new DateService(input);
|
const date = new DateUtils(input);
|
||||||
expect(date.toISOString()).toBe(input);
|
expect(date.toISOString()).toBe(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a clone from another DateService instance', () => {
|
it('should create a clone from another DateUtils instance', () => {
|
||||||
const original = new DateService('2025-01-01T00:00:00.000Z');
|
const original = new DateUtils('2025-01-01T00:00:00.000Z');
|
||||||
const clone = new DateService(original);
|
const clone = new DateUtils(original);
|
||||||
|
|
||||||
expect(clone.toISOString()).toBe(original.toISOString());
|
expect(clone.toISOString()).toBe(original.toISOString());
|
||||||
expect(clone).not.toBe(original); // Ensure references are different (Memory Address check)
|
expect(clone).not.toBe(original); // Ensure references are different (Memory Address check)
|
||||||
@@ -46,7 +46,7 @@ describe('DateService', () => {
|
|||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
vi.setSystemTime(fixedTime);
|
vi.setSystemTime(fixedTime);
|
||||||
|
|
||||||
const invalidDate = new DateService('invalid-date-string-xyz');
|
const invalidDate = new DateUtils('invalid-date-string-xyz');
|
||||||
|
|
||||||
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Invalid date input'));
|
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Invalid date input'));
|
||||||
expect(invalidDate.toISOString()).toBe(fixedTime.toISOString());
|
expect(invalidDate.toISOString()).toBe(fixedTime.toISOString());
|
||||||
@@ -55,29 +55,29 @@ describe('DateService', () => {
|
|||||||
|
|
||||||
describe('Global Configuration', () => {
|
describe('Global Configuration', () => {
|
||||||
it('should update global timezone correctly', () => {
|
it('should update global timezone correctly', () => {
|
||||||
DateService.setGlobalConfig('Asia/Jakarta');
|
DateUtils.setGlobalConfig('Asia/Jakarta');
|
||||||
expect(DateService.getGlobalTimezone()).toBe('Asia/Jakarta');
|
expect(DateUtils.getGlobalTimezone()).toBe('Asia/Jakarta');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle invalid timezone gracefully', () => {
|
it('should handle invalid timezone gracefully', () => {
|
||||||
const initialTz = DateService.getGlobalTimezone();
|
const initialTz = DateUtils.getGlobalTimezone();
|
||||||
DateService.setGlobalConfig('Mars/Alien_City');
|
DateUtils.setGlobalConfig('Mars/Alien_City');
|
||||||
|
|
||||||
expect(console.error).toHaveBeenCalled();
|
expect(console.error).toHaveBeenCalled();
|
||||||
expect(DateService.getGlobalTimezone()).toBe(initialTz);
|
expect(DateUtils.getGlobalTimezone()).toBe(initialTz);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply timezone to new instances', () => {
|
it('should apply timezone to new instances', () => {
|
||||||
const utcString = '2025-01-01T00:00:00Z';
|
const utcString = '2025-01-01T00:00:00Z';
|
||||||
|
|
||||||
// Test UTC
|
// Test UTC
|
||||||
DateService.setGlobalConfig('UTC');
|
DateUtils.setGlobalConfig('UTC');
|
||||||
const dateUtc = new DateService(utcString);
|
const dateUtc = new DateUtils(utcString);
|
||||||
expect(dateUtc.format('HH:mm')).toBe('00:00');
|
expect(dateUtc.format('HH:mm')).toBe('00:00');
|
||||||
|
|
||||||
// Test Jakarta (UTC+7)
|
// Test Jakarta (UTC+7)
|
||||||
DateService.setGlobalConfig('Asia/Jakarta');
|
DateUtils.setGlobalConfig('Asia/Jakarta');
|
||||||
const dateJkt = new DateService(utcString);
|
const dateJkt = new DateUtils(utcString);
|
||||||
expect(dateJkt.format('HH:mm')).toBe('07:00');
|
expect(dateJkt.format('HH:mm')).toBe('07:00');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -85,7 +85,7 @@ describe('DateService', () => {
|
|||||||
describe('Immutability & Manipulation', () => {
|
describe('Immutability & Manipulation', () => {
|
||||||
// This verifies that .clone() is working effectively
|
// This verifies that .clone() is working effectively
|
||||||
it('should be immutable on "add"', () => {
|
it('should be immutable on "add"', () => {
|
||||||
const start = new DateService('2025-01-01T00:00:00.000Z');
|
const start = new DateUtils('2025-01-01T00:00:00.000Z');
|
||||||
const nextDay = start.add(1, 'day');
|
const nextDay = start.add(1, 'day');
|
||||||
|
|
||||||
expect(start.format('DD')).toBe('01'); // Original instance MUST NOT change
|
expect(start.format('DD')).toBe('01'); // Original instance MUST NOT change
|
||||||
@@ -94,7 +94,7 @@ describe('DateService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be immutable on "subtract"', () => {
|
it('should be immutable on "subtract"', () => {
|
||||||
const start = new DateService('2025-01-02T00:00:00.000Z');
|
const start = new DateUtils('2025-01-02T00:00:00.000Z');
|
||||||
const prevDay = start.subtract(1, 'day');
|
const prevDay = start.subtract(1, 'day');
|
||||||
|
|
||||||
expect(start.format('DD')).toBe('02'); // Original instance MUST NOT change
|
expect(start.format('DD')).toBe('02'); // Original instance MUST NOT change
|
||||||
@@ -103,7 +103,7 @@ describe('DateService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be immutable on "startOf"', () => {
|
it('should be immutable on "startOf"', () => {
|
||||||
const date = new DateService('2025-01-15T12:00:00.000Z');
|
const date = new DateUtils('2025-01-15T12:00:00.000Z');
|
||||||
const startOfMonth = date.startOf('month');
|
const startOfMonth = date.startOf('month');
|
||||||
|
|
||||||
expect(date.format('DD')).toBe('15'); // Original remains 15th
|
expect(date.format('DD')).toBe('15'); // Original remains 15th
|
||||||
@@ -112,7 +112,7 @@ describe('DateService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be immutable on "endOf"', () => {
|
it('should be immutable on "endOf"', () => {
|
||||||
const date = new DateService('2025-01-01T00:00:00.000Z');
|
const date = new DateUtils('2025-01-01T00:00:00.000Z');
|
||||||
const endOfDay = date.endOf('day');
|
const endOfDay = date.endOf('day');
|
||||||
|
|
||||||
// Format HH:mm:ss depends on global timezone (currently UTC)
|
// Format HH:mm:ss depends on global timezone (currently UTC)
|
||||||
@@ -123,20 +123,20 @@ describe('DateService', () => {
|
|||||||
|
|
||||||
describe('Formatting & Comparison', () => {
|
describe('Formatting & Comparison', () => {
|
||||||
it('should format date string correctly', () => {
|
it('should format date string correctly', () => {
|
||||||
const date = new DateService('2025-12-25T00:00:00.000Z');
|
const date = new DateUtils('2025-12-25T00:00:00.000Z');
|
||||||
expect(date.format('DD/MM/YYYY')).toBe('25/12/2025');
|
expect(date.format('DD/MM/YYYY')).toBe('25/12/2025');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return strict ISO 8601 string (always UTC)', () => {
|
it('should return strict ISO 8601 string (always UTC)', () => {
|
||||||
DateService.setGlobalConfig('Asia/Jakarta');
|
DateUtils.setGlobalConfig('Asia/Jakarta');
|
||||||
// Input Jakarta local time 7 AM = 0 AM UTC
|
// Input Jakarta local time 7 AM = 0 AM UTC
|
||||||
const date = new DateService('2025-01-01T07:00:00+07:00');
|
const date = new DateUtils('2025-01-01T07:00:00+07:00');
|
||||||
expect(date.toISOString()).toBe('2025-01-01T00:00:00.000Z');
|
expect(date.toISOString()).toBe('2025-01-01T00:00:00.000Z');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compare dates correctly', () => {
|
it('should compare dates correctly', () => {
|
||||||
const d1 = new DateService('2025-01-01T00:00:00.000Z');
|
const d1 = new DateUtils('2025-01-01T00:00:00.000Z');
|
||||||
const d2 = new DateService('2025-01-02T00:00:00.000Z');
|
const d2 = new DateUtils('2025-01-02T00:00:00.000Z');
|
||||||
|
|
||||||
expect(d1.isBefore(d2)).toBe(true);
|
expect(d1.isBefore(d2)).toBe(true);
|
||||||
expect(d2.isAfter(d1)).toBe(true);
|
expect(d2.isAfter(d1)).toBe(true);
|
||||||
@@ -144,16 +144,16 @@ describe('DateService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should calculate precise diff', () => {
|
it('should calculate precise diff', () => {
|
||||||
const d1 = new DateService('2025-01-01T00:00:00.000Z');
|
const d1 = new DateUtils('2025-01-01T00:00:00.000Z');
|
||||||
const d2 = new DateService('2025-01-03T00:00:00.000Z');
|
const d2 = new DateUtils('2025-01-03T00:00:00.000Z');
|
||||||
expect(d2.diff(d1, 'day')).toBe(2);
|
expect(d2.diff(d1, 'day')).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should calculate calendar day diff (ignoring time)', () => {
|
it('should calculate calendar day diff (ignoring time)', () => {
|
||||||
// Case: 23:00 vs 01:00 the next day
|
// Case: 23:00 vs 01:00 the next day
|
||||||
// Technically only 2 hours difference (0 full days), but 1 calendar day difference.
|
// Technically only 2 hours difference (0 full days), but 1 calendar day difference.
|
||||||
const d1 = new DateService('2025-01-01T23:00:00.000Z');
|
const d1 = new DateUtils('2025-01-01T23:00:00.000Z');
|
||||||
const d2 = new DateService('2025-01-02T01:00:00.000Z');
|
const d2 = new DateUtils('2025-01-02T01:00:00.000Z');
|
||||||
|
|
||||||
expect(d2.diff(d1, 'day')).toBe(0); // Standard diff (less than 24h)
|
expect(d2.diff(d1, 'day')).toBe(0); // Standard diff (less than 24h)
|
||||||
expect(d2.diffCalendarDay(d1)).toBe(1); // Calendar diff
|
expect(d2.diffCalendarDay(d1)).toBe(1); // Calendar diff
|
||||||
@@ -162,26 +162,26 @@ describe('DateService', () => {
|
|||||||
|
|
||||||
describe('Timezone Utilities (Indonesian & Offset)', () => {
|
describe('Timezone Utilities (Indonesian & Offset)', () => {
|
||||||
it('should return custom mapping for Indonesia (WIB)', () => {
|
it('should return custom mapping for Indonesia (WIB)', () => {
|
||||||
DateService.setGlobalConfig('Asia/Jakarta');
|
DateUtils.setGlobalConfig('Asia/Jakarta');
|
||||||
const date = new DateService();
|
const date = new DateUtils();
|
||||||
expect(date.timezoneAbbr).toBe('WIB');
|
expect(date.timezoneAbbr).toBe('WIB');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return custom mapping for Indonesia (WITA)', () => {
|
it('should return custom mapping for Indonesia (WITA)', () => {
|
||||||
DateService.setGlobalConfig('Asia/Makassar');
|
DateUtils.setGlobalConfig('Asia/Makassar');
|
||||||
const date = new DateService();
|
const date = new DateUtils();
|
||||||
expect(date.timezoneAbbr).toBe('WITA');
|
expect(date.timezoneAbbr).toBe('WITA');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return custom mapping for Indonesia (WIT)', () => {
|
it('should return custom mapping for Indonesia (WIT)', () => {
|
||||||
DateService.setGlobalConfig('Asia/Jayapura');
|
DateUtils.setGlobalConfig('Asia/Jayapura');
|
||||||
const date = new DateService();
|
const date = new DateUtils();
|
||||||
expect(date.timezoneAbbr).toBe('WIT');
|
expect(date.timezoneAbbr).toBe('WIT');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fallback to standard abbr for other zones', () => {
|
it('should fallback to standard abbr for other zones', () => {
|
||||||
DateService.setGlobalConfig('UTC');
|
DateUtils.setGlobalConfig('UTC');
|
||||||
const date = new DateService();
|
const date = new DateUtils();
|
||||||
expect(date.timezoneAbbr).toBe('UTC');
|
expect(date.timezoneAbbr).toBe('UTC');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,20 +190,20 @@ describe('DateService', () => {
|
|||||||
const isoInput = '2025-01-01T12:00:00Z'; // Input in UTC
|
const isoInput = '2025-01-01T12:00:00Z'; // Input in UTC
|
||||||
|
|
||||||
// Case 1: Jakarta
|
// Case 1: Jakarta
|
||||||
DateService.setGlobalConfig('Asia/Jakarta');
|
DateUtils.setGlobalConfig('Asia/Jakarta');
|
||||||
const dateJkt = new DateService(isoInput);
|
const dateJkt = new DateUtils(isoInput);
|
||||||
// Jakarta = UTC+7
|
// Jakarta = UTC+7
|
||||||
expect(dateJkt.timezoneWithOffset).toBe('WIB (+07:00)');
|
expect(dateJkt.timezoneWithOffset).toBe('WIB (+07:00)');
|
||||||
|
|
||||||
// Case 2: Makassar
|
// Case 2: Makassar
|
||||||
DateService.setGlobalConfig('Asia/Makassar');
|
DateUtils.setGlobalConfig('Asia/Makassar');
|
||||||
const dateMks = new DateService(isoInput);
|
const dateMks = new DateUtils(isoInput);
|
||||||
// Makassar = UTC+8
|
// Makassar = UTC+8
|
||||||
expect(dateMks.timezoneWithOffset).toBe('WITA (+08:00)');
|
expect(dateMks.timezoneWithOffset).toBe('WITA (+08:00)');
|
||||||
|
|
||||||
// Case 3: UTC
|
// Case 3: UTC
|
||||||
DateService.setGlobalConfig('UTC');
|
DateUtils.setGlobalConfig('UTC');
|
||||||
const dateUtc = new DateService(isoInput);
|
const dateUtc = new DateUtils(isoInput);
|
||||||
expect(dateUtc.timezoneWithOffset).toBe('UTC (+00:00)');
|
expect(dateUtc.timezoneWithOffset).toBe('UTC (+00:00)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -211,7 +211,7 @@ describe('DateService', () => {
|
|||||||
describe('Epoch & Helpers', () => {
|
describe('Epoch & Helpers', () => {
|
||||||
it('should return correct epoch values', () => {
|
it('should return correct epoch values', () => {
|
||||||
const input = 1704067200000; // 2024-01-01 00:00:00 UTC
|
const input = 1704067200000; // 2024-01-01 00:00:00 UTC
|
||||||
const date = new DateService(input);
|
const date = new DateUtils(input);
|
||||||
|
|
||||||
expect(date.timestamp).toBe(input);
|
expect(date.timestamp).toBe(input);
|
||||||
expect(date.epochMillis).toBe(input);
|
expect(date.epochMillis).toBe(input);
|
||||||
@@ -219,7 +219,7 @@ describe('DateService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return supported timezones list', () => {
|
it('should return supported timezones list', () => {
|
||||||
const timezones = DateService.getSupportedTimezones();
|
const timezones = DateUtils.getSupportedTimezones();
|
||||||
expect(Array.isArray(timezones)).toBe(true);
|
expect(Array.isArray(timezones)).toBe(true);
|
||||||
expect(timezones).toContain('Asia/Jakarta');
|
expect(timezones).toContain('Asia/Jakarta');
|
||||||
});
|
});
|
||||||
+46
-46
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* ------------------------------------------------------------
|
* ------------------------------------------------------------
|
||||||
* DateService
|
* DateUtils
|
||||||
* ------------------------------------------------------------
|
* ------------------------------------------------------------
|
||||||
* Centralized date and time utility built on top of Day.js.
|
* Centralized date and time utility built on top of Day.js.
|
||||||
*
|
*
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
* - ISO 8601 compliant output
|
* - ISO 8601 compliant output
|
||||||
* - Explicit Indonesian timezone abbreviation support
|
* - Explicit Indonesian timezone abbreviation support
|
||||||
*
|
*
|
||||||
* ⚠️ Day.js plugins MUST be initialized before using DateService.
|
* ⚠️ Day.js plugins MUST be initialized before using DateUtils.
|
||||||
* ------------------------------------------------------------
|
* ------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ dayjs.extend(advancedFormat);
|
|||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accepted input formats for DateService.
|
* Accepted input formats for DateUtils.
|
||||||
*/
|
*/
|
||||||
export type DateInput = string | number | Date | Dayjs | null | undefined;
|
export type DateInput = string | number | Date | Dayjs | null | undefined;
|
||||||
|
|
||||||
@@ -67,23 +67,23 @@ const INDONESIA_TZ_MAP: Record<string, string> = {
|
|||||||
* Fluent date manipulation interface.
|
* Fluent date manipulation interface.
|
||||||
* All methods are immutable and return a new instance.
|
* All methods are immutable and return a new instance.
|
||||||
*/
|
*/
|
||||||
interface IDateService {
|
interface IDateUtils {
|
||||||
format(format?: string): string;
|
format(format?: string): string;
|
||||||
add(value: number, unit: TimeUnit): IDateService;
|
add(value: number, unit: TimeUnit): IDateUtils;
|
||||||
subtract(value: number, unit: TimeUnit): IDateService;
|
subtract(value: number, unit: TimeUnit): IDateUtils;
|
||||||
isBefore(date: DateInput | DateService): boolean;
|
isBefore(date: DateInput | DateUtils): boolean;
|
||||||
isAfter(date: DateInput | DateService): boolean;
|
isAfter(date: DateInput | DateUtils): boolean;
|
||||||
isSame(date: DateInput | DateService, unit?: TimeUnit): boolean;
|
isSame(date: DateInput | DateUtils, unit?: TimeUnit): boolean;
|
||||||
diff(date: DateInput | DateService, unit: TimeUnit, precise?: boolean): number;
|
diff(date: DateInput | DateUtils, unit: TimeUnit, precise?: boolean): number;
|
||||||
diffCalendarDay(date: DateInput | DateService): number;
|
diffCalendarDay(date: DateInput | DateUtils): number;
|
||||||
startOf(unit: TimeUnit): IDateService;
|
startOf(unit: TimeUnit): IDateUtils;
|
||||||
endOf(unit: TimeUnit): IDateService;
|
endOf(unit: TimeUnit): IDateUtils;
|
||||||
toISOString(): string;
|
toISOString(): string;
|
||||||
toDate(): Date;
|
toDate(): Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* DateService
|
* DateUtils
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,12 +92,12 @@ interface IDateService {
|
|||||||
* All instances are automatically normalized
|
* All instances are automatically normalized
|
||||||
* to a single global timezone.
|
* to a single global timezone.
|
||||||
*/
|
*/
|
||||||
export class DateService implements IDateService {
|
export class DateUtils implements IDateUtils {
|
||||||
private readonly _date: Dayjs;
|
private readonly _date: Dayjs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global default timezone.
|
* Global default timezone.
|
||||||
* Used by all DateService instances.
|
* Used by all DateUtils instances.
|
||||||
*/
|
*/
|
||||||
private static _defaultTimezone: string = dayjs.tz.guess();
|
private static _defaultTimezone: string = dayjs.tz.guess();
|
||||||
|
|
||||||
@@ -109,14 +109,14 @@ export class DateService implements IDateService {
|
|||||||
* Set the global timezone for the application.
|
* Set the global timezone for the application.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* DateService.setGlobalConfig('Asia/Jakarta');
|
* DateUtils.setGlobalConfig('Asia/Jakarta');
|
||||||
*/
|
*/
|
||||||
static setGlobalConfig(timezone: string): void {
|
static setGlobalConfig(timezone: string): void {
|
||||||
try {
|
try {
|
||||||
dayjs().tz(timezone);
|
dayjs().tz(timezone);
|
||||||
DateService._defaultTimezone = timezone;
|
DateUtils._defaultTimezone = timezone;
|
||||||
} catch {
|
} catch {
|
||||||
console.error(`[DateService] Invalid timezone "${timezone}". Using previous value.`);
|
console.error(`[DateUtils] Invalid timezone "${timezone}". Using previous value.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,30 +124,30 @@ export class DateService implements IDateService {
|
|||||||
* Get the currently active global timezone.
|
* Get the currently active global timezone.
|
||||||
*/
|
*/
|
||||||
static getGlobalTimezone(): string {
|
static getGlobalTimezone(): string {
|
||||||
return DateService._defaultTimezone;
|
return DateUtils._defaultTimezone;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a DateService instance representing the current moment.
|
* Create a DateUtils instance representing the current moment.
|
||||||
*/
|
*/
|
||||||
static now(): DateService {
|
static now(): DateUtils {
|
||||||
return new DateService();
|
return new DateUtils();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
* Constructor
|
* Constructor
|
||||||
* ---------------------------------------------------------------- */
|
* ---------------------------------------------------------------- */
|
||||||
|
|
||||||
constructor(date?: DateInput | DateService) {
|
constructor(date?: DateInput | DateUtils) {
|
||||||
if (date instanceof DateService) {
|
if (date instanceof DateUtils) {
|
||||||
this._date = date.getRaw().tz(DateService._defaultTimezone);
|
this._date = date.getRaw().tz(DateUtils._defaultTimezone);
|
||||||
} else {
|
} else {
|
||||||
this._date = dayjs(date).tz(DateService._defaultTimezone);
|
this._date = dayjs(date).tz(DateUtils._defaultTimezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._date.isValid()) {
|
if (!this._date.isValid()) {
|
||||||
console.warn('[DateService] Invalid date input. Falling back to now().');
|
console.warn('[DateUtils] Invalid date input. Falling back to now().');
|
||||||
this._date = dayjs().tz(DateService._defaultTimezone);
|
this._date = dayjs().tz(DateUtils._defaultTimezone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,8 +166,8 @@ export class DateService implements IDateService {
|
|||||||
/**
|
/**
|
||||||
* Normalize input into Day.js using the global timezone.
|
* Normalize input into Day.js using the global timezone.
|
||||||
*/
|
*/
|
||||||
private toDayjs(date: DateInput | DateService): Dayjs {
|
private toDayjs(date: DateInput | DateUtils): Dayjs {
|
||||||
return date instanceof DateService ? date.getRaw() : dayjs(date).tz(DateService._defaultTimezone);
|
return date instanceof DateUtils ? date.getRaw() : dayjs(date).tz(DateUtils._defaultTimezone);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
@@ -191,46 +191,46 @@ export class DateService implements IDateService {
|
|||||||
* Manipulation
|
* Manipulation
|
||||||
* ---------------------------------------------------------------- */
|
* ---------------------------------------------------------------- */
|
||||||
|
|
||||||
add(value: number, unit: TimeUnit): DateService {
|
add(value: number, unit: TimeUnit): DateUtils {
|
||||||
return new DateService(this._date.clone().add(value, unit as ManipulateType));
|
return new DateUtils(this._date.clone().add(value, unit as ManipulateType));
|
||||||
}
|
}
|
||||||
|
|
||||||
subtract(value: number, unit: TimeUnit): DateService {
|
subtract(value: number, unit: TimeUnit): DateUtils {
|
||||||
return new DateService(this._date.clone().subtract(value, unit as ManipulateType));
|
return new DateUtils(this._date.clone().subtract(value, unit as ManipulateType));
|
||||||
}
|
}
|
||||||
|
|
||||||
startOf(unit: TimeUnit): DateService {
|
startOf(unit: TimeUnit): DateUtils {
|
||||||
return new DateService(this._date.clone().startOf(unit as OpUnitType));
|
return new DateUtils(this._date.clone().startOf(unit as OpUnitType));
|
||||||
}
|
}
|
||||||
|
|
||||||
endOf(unit: TimeUnit): DateService {
|
endOf(unit: TimeUnit): DateUtils {
|
||||||
return new DateService(this._date.clone().endOf(unit as OpUnitType));
|
return new DateUtils(this._date.clone().endOf(unit as OpUnitType));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
* Comparison
|
* Comparison
|
||||||
* ---------------------------------------------------------------- */
|
* ---------------------------------------------------------------- */
|
||||||
|
|
||||||
isBefore(date: DateInput | DateService): boolean {
|
isBefore(date: DateInput | DateUtils): boolean {
|
||||||
return this._date.isBefore(this.toDayjs(date));
|
return this._date.isBefore(this.toDayjs(date));
|
||||||
}
|
}
|
||||||
|
|
||||||
isAfter(date: DateInput | DateService): boolean {
|
isAfter(date: DateInput | DateUtils): boolean {
|
||||||
return this._date.isAfter(this.toDayjs(date));
|
return this._date.isAfter(this.toDayjs(date));
|
||||||
}
|
}
|
||||||
|
|
||||||
isSame(date: DateInput | DateService, unit?: TimeUnit): boolean {
|
isSame(date: DateInput | DateUtils, unit?: TimeUnit): boolean {
|
||||||
return this._date.isSame(this.toDayjs(date), unit as OpUnitType);
|
return this._date.isSame(this.toDayjs(date), unit as OpUnitType);
|
||||||
}
|
}
|
||||||
|
|
||||||
diff(date: DateInput | DateService, unit: TimeUnit, precise: boolean = false): number {
|
diff(date: DateInput | DateUtils, unit: TimeUnit, precise: boolean = false): number {
|
||||||
return this._date.diff(this.toDayjs(date), unit as OpUnitType, precise);
|
return this._date.diff(this.toDayjs(date), unit as OpUnitType, precise);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calendar-day difference ignoring time components.
|
* Calendar-day difference ignoring time components.
|
||||||
*/
|
*/
|
||||||
diffCalendarDay(date: DateInput | DateService): number {
|
diffCalendarDay(date: DateInput | DateUtils): number {
|
||||||
const target = this.toDayjs(date);
|
const target = this.toDayjs(date);
|
||||||
return this._date.startOf('day').diff(target.startOf('day'), 'day');
|
return this._date.startOf('day').diff(target.startOf('day'), 'day');
|
||||||
}
|
}
|
||||||
@@ -263,7 +263,7 @@ export class DateService implements IDateService {
|
|||||||
try {
|
try {
|
||||||
return Intl.supportedValuesOf('timeZone');
|
return Intl.supportedValuesOf('timeZone');
|
||||||
} catch {
|
} catch {
|
||||||
console.warn('[DateService] Failed to retrieve timezones via Intl.');
|
console.warn('[DateUtils] Failed to retrieve timezones via Intl.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ export class DateService implements IDateService {
|
|||||||
* 2. Day.js dynamic abbreviation (DST-safe)
|
* 2. Day.js dynamic abbreviation (DST-safe)
|
||||||
*/
|
*/
|
||||||
get timezoneAbbr(): string {
|
get timezoneAbbr(): string {
|
||||||
const tz = DateService._defaultTimezone;
|
const tz = DateUtils._defaultTimezone;
|
||||||
return INDONESIA_TZ_MAP[tz] ?? this._date.format('z');
|
return INDONESIA_TZ_MAP[tz] ?? this._date.format('z');
|
||||||
}
|
}
|
||||||
|
|
||||||
+13
-13
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { EncryptionService } from './encryption-service';
|
import { EncryptionUtils } from './encryption.utils';
|
||||||
import { AES } from 'crypto-js';
|
import { AES } from 'crypto-js';
|
||||||
|
|
||||||
// 1. Mock the key module to ensure test consistency
|
// 1. Mock the key module to ensure test consistency
|
||||||
@@ -7,15 +7,15 @@ vi.mock('./encryption-key', () => ({
|
|||||||
ENC_STORAGE_KEY: 'default-test-key-123',
|
ENC_STORAGE_KEY: 'default-test-key-123',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('EncryptionService', () => {
|
describe('EncryptionUtils', () => {
|
||||||
const TEST_KEY = 'secret-key-xyz';
|
const TEST_KEY = 'secret-key-xyz';
|
||||||
let service: EncryptionService;
|
let service: EncryptionUtils;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset the Singleton instance to ensure clean state for every test
|
// Reset the Singleton instance to ensure clean state for every test
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(EncryptionService as any).instance = undefined;
|
(EncryptionUtils as any).instance = undefined;
|
||||||
service = new EncryptionService(TEST_KEY);
|
service = new EncryptionUtils(TEST_KEY);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -24,12 +24,12 @@ describe('EncryptionService', () => {
|
|||||||
|
|
||||||
describe('Constructor', () => {
|
describe('Constructor', () => {
|
||||||
it('should create an instance with the provided key', () => {
|
it('should create an instance with the provided key', () => {
|
||||||
const instance = new EncryptionService('custom-key');
|
const instance = new EncryptionUtils('custom-key');
|
||||||
expect(instance).toBeInstanceOf(EncryptionService);
|
expect(instance).toBeInstanceOf(EncryptionUtils);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use the default ENC_STORAGE_KEY if no key is provided', () => {
|
it('should use the default ENC_STORAGE_KEY if no key is provided', () => {
|
||||||
const instance = new EncryptionService();
|
const instance = new EncryptionUtils();
|
||||||
// Verify functionality with default key
|
// Verify functionality with default key
|
||||||
const data = 'test-default';
|
const data = 'test-default';
|
||||||
const encrypted = instance.encrypt(data);
|
const encrypted = instance.encrypt(data);
|
||||||
@@ -37,7 +37,7 @@ describe('EncryptionService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an Error if the provided key is empty', () => {
|
it('should throw an Error if the provided key is empty', () => {
|
||||||
expect(() => new EncryptionService('')).toThrow('[EncryptionService] Encryption key is missing/empty.');
|
expect(() => new EncryptionUtils('')).toThrow('[EncryptionUtils] Encryption key is missing/empty.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ describe('EncryptionService', () => {
|
|||||||
const encrypted = service.encrypt(plainText);
|
const encrypted = service.encrypt(plainText);
|
||||||
|
|
||||||
// Decrypt with Service B (WRONG_KEY)
|
// Decrypt with Service B (WRONG_KEY)
|
||||||
const wrongService = new EncryptionService('wrong-key-999');
|
const wrongService = new EncryptionUtils('wrong-key-999');
|
||||||
const result = wrongService.decrypt(encrypted);
|
const result = wrongService.decrypt(encrypted);
|
||||||
|
|
||||||
expect(result).toBe('');
|
expect(result).toBe('');
|
||||||
@@ -110,14 +110,14 @@ describe('EncryptionService', () => {
|
|||||||
|
|
||||||
expect(result).toBe('');
|
expect(result).toBe('');
|
||||||
// Now we expect the catch block to be executed
|
// Now we expect the catch block to be executed
|
||||||
expect(consoleSpy).toHaveBeenCalledWith('[EncryptionService] Decryption failed:', expect.any(Error));
|
expect(consoleSpy).toHaveBeenCalledWith('[EncryptionUtils] Decryption failed:', expect.any(Error));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getInstance() (Singleton)', () => {
|
describe('getInstance() (Singleton)', () => {
|
||||||
it('should return the same instance reference', () => {
|
it('should return the same instance reference', () => {
|
||||||
const instance1 = EncryptionService.getInstance();
|
const instance1 = EncryptionUtils.getInstance();
|
||||||
const instance2 = EncryptionService.getInstance();
|
const instance2 = EncryptionUtils.getInstance();
|
||||||
|
|
||||||
expect(instance1).toBe(instance2);
|
expect(instance1).toBe(instance2);
|
||||||
});
|
});
|
||||||
+11
-11
@@ -4,12 +4,12 @@ import { ENC_STORAGE_KEY } from './encryption-key';
|
|||||||
/**
|
/**
|
||||||
* Interface defining encryption service methods.
|
* Interface defining encryption service methods.
|
||||||
*/
|
*/
|
||||||
interface IEncryptionService {
|
interface IEncryptionUtils {
|
||||||
encrypt(data: string): string;
|
encrypt(data: string): string;
|
||||||
decrypt(encryptedData: string): string;
|
decrypt(encryptedData: string): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EncryptionService implements IEncryptionService {
|
export class EncryptionUtils implements IEncryptionUtils {
|
||||||
private readonly _key: string;
|
private readonly _key: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,7 +18,7 @@ export class EncryptionService implements IEncryptionService {
|
|||||||
*/
|
*/
|
||||||
constructor(key: string = ENC_STORAGE_KEY) {
|
constructor(key: string = ENC_STORAGE_KEY) {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw new Error('[EncryptionService] Encryption key is missing/empty.');
|
throw new Error('[EncryptionUtils] Encryption key is missing/empty.');
|
||||||
}
|
}
|
||||||
this._key = key;
|
this._key = key;
|
||||||
}
|
}
|
||||||
@@ -54,23 +54,23 @@ export class EncryptionService implements IEncryptionService {
|
|||||||
|
|
||||||
return originalText;
|
return originalText;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[EncryptionService] Decryption failed:', error);
|
console.error('[EncryptionUtils] Decryption failed:', error);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the singleton instance of EncryptionService.
|
* Get the singleton instance of EncryptionUtils.
|
||||||
* Uses the default ENC_STORAGE_KEY.
|
* Uses the default ENC_STORAGE_KEY.
|
||||||
* @return EncryptionService instance
|
* @return EncryptionUtils instance
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private static instance: EncryptionService;
|
private static instance: EncryptionUtils;
|
||||||
|
|
||||||
public static getInstance(): EncryptionService {
|
public static getInstance(): EncryptionUtils {
|
||||||
if (!EncryptionService.instance) {
|
if (!EncryptionUtils.instance) {
|
||||||
EncryptionService.instance = new EncryptionService();
|
EncryptionUtils.instance = new EncryptionUtils();
|
||||||
}
|
}
|
||||||
return EncryptionService.instance;
|
return EncryptionUtils.instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export * from './encryption-service/encryption-key';
|
export * from './encryption/encryption-key';
|
||||||
export * from './encryption-service/encryption-service';
|
export * from './encryption/encryption.utils';
|
||||||
|
|
||||||
export * from './date-service/date-service';
|
export * from './date/date.utils';
|
||||||
export * from './currency-service/currency-service';
|
export * from './currency/currency.utils';
|
||||||
export * from './string-service/string-service';
|
export * from './string/string.utils';
|
||||||
|
|||||||
+33
-33
@@ -1,45 +1,45 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { StringService } from './string-service';
|
import { StringUtils } from './string.utils';
|
||||||
|
|
||||||
describe('StringService', () => {
|
describe('StringUtils', () => {
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Instantiation & Static Methods
|
// Instantiation & Static Methods
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
describe('Initialization', () => {
|
describe('Initialization', () => {
|
||||||
it('should handle string input correctly', () => {
|
it('should handle string input correctly', () => {
|
||||||
const svc = new StringService('Hello');
|
const svc = new StringUtils('Hello');
|
||||||
expect(svc.value()).toBe('Hello');
|
expect(svc.value()).toBe('Hello');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle number input by converting to string', () => {
|
it('should handle number input by converting to string', () => {
|
||||||
const svc = new StringService(123);
|
const svc = new StringUtils(123);
|
||||||
expect(svc.value()).toBe('123');
|
expect(svc.value()).toBe('123');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle null input gracefully (default to empty string)', () => {
|
it('should handle null input gracefully (default to empty string)', () => {
|
||||||
const svc = new StringService(null);
|
const svc = new StringUtils(null);
|
||||||
expect(svc.value()).toBe('');
|
expect(svc.value()).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle undefined input gracefully', () => {
|
it('should handle undefined input gracefully', () => {
|
||||||
const svc = new StringService(undefined);
|
const svc = new StringUtils(undefined);
|
||||||
expect(svc.value()).toBe('');
|
expect(svc.value()).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support static factory method .of()', () => {
|
it('should support static factory method .of()', () => {
|
||||||
const svc = StringService.of('Factory');
|
const svc = StringUtils.of('Factory');
|
||||||
expect(svc.value()).toBe('Factory');
|
expect(svc.value()).toBe('Factory');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('random()', () => {
|
describe('random()', () => {
|
||||||
it('should generate string with specified length', () => {
|
it('should generate string with specified length', () => {
|
||||||
const random = StringService.random(15);
|
const random = StringUtils.random(15);
|
||||||
expect(random.value()).toHaveLength(15);
|
expect(random.value()).toHaveLength(15);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate alphanumeric characters only', () => {
|
it('should generate alphanumeric characters only', () => {
|
||||||
const random = StringService.random(100);
|
const random = StringUtils.random(100);
|
||||||
expect(random.value()).toMatch(/^[A-Za-z0-9]+$/);
|
expect(random.value()).toMatch(/^[A-Za-z0-9]+$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -49,19 +49,19 @@ describe('StringService', () => {
|
|||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
describe('Transformations', () => {
|
describe('Transformations', () => {
|
||||||
it('should convert to upperCase', () => {
|
it('should convert to upperCase', () => {
|
||||||
expect(StringService.of('hello').upperCase().value()).toBe('HELLO');
|
expect(StringUtils.of('hello').upperCase().value()).toBe('HELLO');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should convert to lowerCase', () => {
|
it('should convert to lowerCase', () => {
|
||||||
expect(StringService.of('HELLO').lowerCase().value()).toBe('hello');
|
expect(StringUtils.of('HELLO').lowerCase().value()).toBe('hello');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should trim whitespace', () => {
|
it('should trim whitespace', () => {
|
||||||
expect(StringService.of(' hello ').trim().value()).toBe('hello');
|
expect(StringUtils.of(' hello ').trim().value()).toBe('hello');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support method chaining (Immutable)', () => {
|
it('should support method chaining (Immutable)', () => {
|
||||||
const original = StringService.of(' hello ');
|
const original = StringUtils.of(' hello ');
|
||||||
const modified = original.trim().upperCase();
|
const modified = original.trim().upperCase();
|
||||||
|
|
||||||
expect(original.value()).toBe(' hello '); // Original untouched
|
expect(original.value()).toBe(' hello '); // Original untouched
|
||||||
@@ -71,16 +71,16 @@ describe('StringService', () => {
|
|||||||
|
|
||||||
describe('Capitalization', () => {
|
describe('Capitalization', () => {
|
||||||
it('should capitalize first letter only', () => {
|
it('should capitalize first letter only', () => {
|
||||||
expect(StringService.of('hELLO world').capitalizeFirst().value()).toBe('Hello world');
|
expect(StringUtils.of('hELLO world').capitalizeFirst().value()).toBe('Hello world');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should capitalize each word', () => {
|
it('should capitalize each word', () => {
|
||||||
expect(StringService.of('hello world').capitalizeEachWord().value()).toBe('Hello World');
|
expect(StringUtils.of('hello world').capitalizeEachWord().value()).toBe('Hello World');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle double spaces in capitalizeEachWord', () => {
|
it('should handle double spaces in capitalizeEachWord', () => {
|
||||||
// Test regex logic for splitting words
|
// Test regex logic for splitting words
|
||||||
expect(StringService.of('hello world').capitalizeEachWord().value()).toBe('Hello World');
|
expect(StringUtils.of('hello world').capitalizeEachWord().value()).toBe('Hello World');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,29 +89,29 @@ describe('StringService', () => {
|
|||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
describe('slugify()', () => {
|
describe('slugify()', () => {
|
||||||
it('should create valid slugs', () => {
|
it('should create valid slugs', () => {
|
||||||
expect(StringService.of('Hello World!').slugify().value()).toBe('hello-world');
|
expect(StringUtils.of('Hello World!').slugify().value()).toBe('hello-world');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle complex characters', () => {
|
it('should handle complex characters', () => {
|
||||||
expect(StringService.of('C# & .NET Core').slugify().value()).toBe('c-net-core');
|
expect(StringUtils.of('C# & .NET Core').slugify().value()).toBe('c-net-core');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove leading/trailing separators', () => {
|
it('should remove leading/trailing separators', () => {
|
||||||
expect(StringService.of('---Hello---').slugify().value()).toBe('hello');
|
expect(StringUtils.of('---Hello---').slugify().value()).toBe('hello');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('limit()', () => {
|
describe('limit()', () => {
|
||||||
it('should truncate string if longer than max', () => {
|
it('should truncate string if longer than max', () => {
|
||||||
expect(StringService.of('Hello World').limit(5).value()).toBe('Hello...');
|
expect(StringUtils.of('Hello World').limit(5).value()).toBe('Hello...');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not truncate if shorter than max', () => {
|
it('should not truncate if shorter than max', () => {
|
||||||
expect(StringService.of('Hi').limit(5).value()).toBe('Hi');
|
expect(StringUtils.of('Hi').limit(5).value()).toBe('Hi');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support custom suffix', () => {
|
it('should support custom suffix', () => {
|
||||||
expect(StringService.of('Hello World').limit(5, '!!!').value()).toBe('Hello!!!');
|
expect(StringUtils.of('Hello World').limit(5, '!!!').value()).toBe('Hello!!!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,15 +121,15 @@ describe('StringService', () => {
|
|||||||
// Start 4: 0812
|
// Start 4: 0812
|
||||||
// End 3: 890
|
// End 3: 890
|
||||||
// Middle masked: *****
|
// Middle masked: *****
|
||||||
expect(StringService.of('081234567890').mask(4, 3).value()).toBe('0812*****890');
|
expect(StringUtils.of('081234567890').mask(4, 3).value()).toBe('0812*****890');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle custom mask char', () => {
|
it('should handle custom mask char', () => {
|
||||||
expect(StringService.of('123456').mask(2, 2, 'X').value()).toBe('12XX56');
|
expect(StringUtils.of('123456').mask(2, 2, 'X').value()).toBe('12XX56');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return original if string is shorter than visible parts', () => {
|
it('should return original if string is shorter than visible parts', () => {
|
||||||
expect(StringService.of('123').mask(5, 5).value()).toBe('123');
|
expect(StringUtils.of('123').mask(5, 5).value()).toBe('123');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,26 +138,26 @@ describe('StringService', () => {
|
|||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
describe('Inspection & Outputs', () => {
|
describe('Inspection & Outputs', () => {
|
||||||
it('should detect empty strings correctly', () => {
|
it('should detect empty strings correctly', () => {
|
||||||
expect(StringService.of('').isEmpty()).toBe(true);
|
expect(StringUtils.of('').isEmpty()).toBe(true);
|
||||||
expect(StringService.of(' ').isEmpty()).toBe(true); // Trim check
|
expect(StringUtils.of(' ').isEmpty()).toBe(true); // Trim check
|
||||||
expect(StringService.of(null).isEmpty()).toBe(true);
|
expect(StringUtils.of(null).isEmpty()).toBe(true);
|
||||||
expect(StringService.of('a').isEmpty()).toBe(false);
|
expect(StringUtils.of('a').isEmpty()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return default value with orElse', () => {
|
it('should return default value with orElse', () => {
|
||||||
expect(StringService.of(null).orElse('Default')).toBe('Default');
|
expect(StringUtils.of(null).orElse('Default')).toBe('Default');
|
||||||
expect(StringService.of('Valid').orElse('Default')).toBe('Valid');
|
expect(StringUtils.of('Valid').orElse('Default')).toBe('Valid');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support native string interpolation (toString)', () => {
|
it('should support native string interpolation (toString)', () => {
|
||||||
const name = StringService.of('World');
|
const name = StringUtils.of('World');
|
||||||
expect(`Hello ${name}`).toBe('Hello World');
|
expect(`Hello ${name}`).toBe('Hello World');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support JSON serialization (toJSON)', () => {
|
it('should support JSON serialization (toJSON)', () => {
|
||||||
const data = {
|
const data = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: StringService.of('Product A'),
|
name: StringUtils.of('Product A'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// JSON.stringify automatically calls .toJSON()
|
// JSON.stringify automatically calls .toJSON()
|
||||||
+51
-51
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* ------------------------------------------------------------
|
* ------------------------------------------------------------
|
||||||
* StringService
|
* StringUtils
|
||||||
* ------------------------------------------------------------
|
* ------------------------------------------------------------
|
||||||
* Centralized string manipulation utility.
|
* Centralized string manipulation utility.
|
||||||
*
|
*
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // Basic Chaining
|
* // Basic Chaining
|
||||||
* StringService.of(" hello world ")
|
* StringUtils.of(" hello world ")
|
||||||
* .trim()
|
* .trim()
|
||||||
* .capitalizeEachWord()
|
* .capitalizeEachWord()
|
||||||
* .value(); // "Hello World"
|
* .value(); // "Hello World"
|
||||||
@@ -29,21 +29,21 @@ export type StringInput = string | number | null | undefined;
|
|||||||
* Interfaces
|
* Interfaces
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
interface IStringService {
|
interface IStringUtils {
|
||||||
// --- Output & Conversion ---
|
// --- Output & Conversion ---
|
||||||
value(): string;
|
value(): string;
|
||||||
toString(): string;
|
toString(): string;
|
||||||
toJSON(): string;
|
toJSON(): string;
|
||||||
|
|
||||||
// --- Manipulations (Chainable) ---
|
// --- Manipulations (Chainable) ---
|
||||||
upperCase(): IStringService;
|
upperCase(): IStringUtils;
|
||||||
lowerCase(): IStringService;
|
lowerCase(): IStringUtils;
|
||||||
capitalizeFirst(): IStringService;
|
capitalizeFirst(): IStringUtils;
|
||||||
capitalizeEachWord(): IStringService;
|
capitalizeEachWord(): IStringUtils;
|
||||||
limit(maxLength: number, suffix?: string): IStringService;
|
limit(maxLength: number, suffix?: string): IStringUtils;
|
||||||
trim(): IStringService;
|
trim(): IStringUtils;
|
||||||
slugify(): IStringService;
|
slugify(): IStringUtils;
|
||||||
mask(visibleStart: number, visibleEnd: number, maskChar?: string): IStringService;
|
mask(visibleStart: number, visibleEnd: number, maskChar?: string): IStringUtils;
|
||||||
|
|
||||||
// --- Inspection & Fallback ---
|
// --- Inspection & Fallback ---
|
||||||
isEmpty(): boolean;
|
isEmpty(): boolean;
|
||||||
@@ -51,10 +51,10 @@ interface IStringService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* StringService
|
* StringUtils
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
export class StringService implements IStringService {
|
export class StringUtils implements IStringUtils {
|
||||||
private readonly _value: string;
|
private readonly _value: string;
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
@@ -75,10 +75,10 @@ export class StringService implements IStringService {
|
|||||||
*
|
*
|
||||||
* @param value - The input string, number, or null/undefined.
|
* @param value - The input string, number, or null/undefined.
|
||||||
* @example
|
* @example
|
||||||
* StringService.of("hello").upperCase().value()
|
* StringUtils.of("hello").upperCase().value()
|
||||||
*/
|
*/
|
||||||
static of(value?: StringInput): StringService {
|
static of(value?: StringInput): StringUtils {
|
||||||
return new StringService(value);
|
return new StringUtils(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,15 +87,15 @@ export class StringService implements IStringService {
|
|||||||
*
|
*
|
||||||
* @param length - Length of the generated string (default: 10).
|
* @param length - Length of the generated string (default: 10).
|
||||||
* @example
|
* @example
|
||||||
* StringService.random(8).value() // "aB9x2Z1m"
|
* StringUtils.random(8).value() // "aB9x2Z1m"
|
||||||
*/
|
*/
|
||||||
static random(length: number = 10): StringService {
|
static random(length: number = 10): StringUtils {
|
||||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
let result = '';
|
let result = '';
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
}
|
}
|
||||||
return new StringService(result);
|
return new StringUtils(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
@@ -106,20 +106,20 @@ export class StringService implements IStringService {
|
|||||||
* Converts string to UPPERCASE.
|
* Converts string to UPPERCASE.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* StringService.of("hello").upperCase().value() // "HELLO"
|
* StringUtils.of("hello").upperCase().value() // "HELLO"
|
||||||
*/
|
*/
|
||||||
upperCase(): StringService {
|
upperCase(): StringUtils {
|
||||||
return new StringService(this._value.toUpperCase());
|
return new StringUtils(this._value.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts string to lowercase.
|
* Converts string to lowercase.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* StringService.of("HELLO").lowerCase().value() // "hello"
|
* StringUtils.of("HELLO").lowerCase().value() // "hello"
|
||||||
*/
|
*/
|
||||||
lowerCase(): StringService {
|
lowerCase(): StringUtils {
|
||||||
return new StringService(this._value.toLowerCase());
|
return new StringUtils(this._value.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,12 +127,12 @@ export class StringService implements IStringService {
|
|||||||
* Remainder is forced to lowercase (Sentence case).
|
* Remainder is forced to lowercase (Sentence case).
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* StringService.of("HELLO world").capitalizeFirst().value() // "Hello world"
|
* StringUtils.of("HELLO world").capitalizeFirst().value() // "Hello world"
|
||||||
*/
|
*/
|
||||||
capitalizeFirst(): StringService {
|
capitalizeFirst(): StringUtils {
|
||||||
if (!this._value) return this;
|
if (!this._value) return this;
|
||||||
const lower = this._value.toLowerCase();
|
const lower = this._value.toLowerCase();
|
||||||
return new StringService(lower.charAt(0).toUpperCase() + lower.slice(1));
|
return new StringUtils(lower.charAt(0).toUpperCase() + lower.slice(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,17 +140,17 @@ export class StringService implements IStringService {
|
|||||||
* Automatically handles multiple spaces.
|
* Automatically handles multiple spaces.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* StringService.of("hello world").capitalizeEachWord().value() // "Hello World"
|
* StringUtils.of("hello world").capitalizeEachWord().value() // "Hello World"
|
||||||
* StringService.of("hello world").capitalizeEachWord().value() // "Hello World"
|
* StringUtils.of("hello world").capitalizeEachWord().value() // "Hello World"
|
||||||
*/
|
*/
|
||||||
capitalizeEachWord(): StringService {
|
capitalizeEachWord(): StringUtils {
|
||||||
if (!this._value) return this;
|
if (!this._value) return this;
|
||||||
const transformed = this._value
|
const transformed = this._value
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(/\s+/) // Split by any whitespace regex to handle double spaces
|
.split(/\s+/) // Split by any whitespace regex to handle double spaces
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
return new StringService(transformed);
|
return new StringUtils(transformed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,12 +159,12 @@ export class StringService implements IStringService {
|
|||||||
* @param maxLength - The character limit.
|
* @param maxLength - The character limit.
|
||||||
* @param suffix - The string to append if truncated (default: "...").
|
* @param suffix - The string to append if truncated (default: "...").
|
||||||
* @example
|
* @example
|
||||||
* StringService.of("Lorem Ipsum").limit(5).value() // "Lorem..."
|
* StringUtils.of("Lorem Ipsum").limit(5).value() // "Lorem..."
|
||||||
* StringService.of("Lorem Ipsum").limit(5, "").value() // "Lorem"
|
* StringUtils.of("Lorem Ipsum").limit(5, "").value() // "Lorem"
|
||||||
*/
|
*/
|
||||||
limit(maxLength: number, suffix: string = '...'): StringService {
|
limit(maxLength: number, suffix: string = '...'): StringUtils {
|
||||||
if (this._value.length <= maxLength) return this;
|
if (this._value.length <= maxLength) return this;
|
||||||
return new StringService(this._value.substring(0, maxLength) + suffix);
|
return new StringUtils(this._value.substring(0, maxLength) + suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
@@ -175,10 +175,10 @@ export class StringService implements IStringService {
|
|||||||
* Removes whitespace from both ends of the string.
|
* Removes whitespace from both ends of the string.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* StringService.of(" data ").trim().value() // "data"
|
* StringUtils.of(" data ").trim().value() // "data"
|
||||||
*/
|
*/
|
||||||
trim(): StringService {
|
trim(): StringUtils {
|
||||||
return new StringService(this._value.trim());
|
return new StringUtils(this._value.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,17 +186,17 @@ export class StringService implements IStringService {
|
|||||||
* Removes special characters and replaces spaces with dashes.
|
* Removes special characters and replaces spaces with dashes.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* StringService.of("Hello World!").slugify().value() // "hello-world"
|
* StringUtils.of("Hello World!").slugify().value() // "hello-world"
|
||||||
* StringService.of("C# & .NET").slugify().value() // "c-net"
|
* StringUtils.of("C# & .NET").slugify().value() // "c-net"
|
||||||
*/
|
*/
|
||||||
slugify(): StringService {
|
slugify(): StringUtils {
|
||||||
const slug = this._value
|
const slug = this._value
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/[^\w\s-]/g, '') // Remove non-word chars
|
.replace(/[^\w\s-]/g, '') // Remove non-word chars
|
||||||
.replace(/[\s_-]+/g, '-') // Replace spaces and underscores with -
|
.replace(/[\s_-]+/g, '-') // Replace spaces and underscores with -
|
||||||
.replace(/^-+|-+$/g, ''); // Remove leading/trailing -
|
.replace(/^-+|-+$/g, ''); // Remove leading/trailing -
|
||||||
return new StringService(slug);
|
return new StringUtils(slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,16 +206,16 @@ export class StringService implements IStringService {
|
|||||||
* @param visibleEnd - Number of characters to show at the end.
|
* @param visibleEnd - Number of characters to show at the end.
|
||||||
* @param maskChar - The character to use for masking (default: "*").
|
* @param maskChar - The character to use for masking (default: "*").
|
||||||
* @example
|
* @example
|
||||||
* StringService.of("08123456789").mask(4, 3).value() // "0812****789"
|
* StringUtils.of("08123456789").mask(4, 3).value() // "0812****789"
|
||||||
*/
|
*/
|
||||||
mask(visibleStart: number = 0, visibleEnd: number = 0, maskChar: string = '*'): StringService {
|
mask(visibleStart: number = 0, visibleEnd: number = 0, maskChar: string = '*'): StringUtils {
|
||||||
if (this._value.length <= visibleStart + visibleEnd) return this;
|
if (this._value.length <= visibleStart + visibleEnd) return this;
|
||||||
|
|
||||||
const start = this._value.slice(0, visibleStart);
|
const start = this._value.slice(0, visibleStart);
|
||||||
const end = this._value.slice(-visibleEnd);
|
const end = this._value.slice(-visibleEnd);
|
||||||
const middle = maskChar.repeat(this._value.length - visibleStart - visibleEnd);
|
const middle = maskChar.repeat(this._value.length - visibleStart - visibleEnd);
|
||||||
|
|
||||||
return new StringService(start + middle + end);
|
return new StringUtils(start + middle + end);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
@@ -227,8 +227,8 @@ export class StringService implements IStringService {
|
|||||||
*
|
*
|
||||||
* @returns true if string is empty or whitespace-only.
|
* @returns true if string is empty or whitespace-only.
|
||||||
* @example
|
* @example
|
||||||
* StringService.of("").isEmpty() // true
|
* StringUtils.of("").isEmpty() // true
|
||||||
* StringService.of(" ").isEmpty() // true
|
* StringUtils.of(" ").isEmpty() // true
|
||||||
*/
|
*/
|
||||||
isEmpty(): boolean {
|
isEmpty(): boolean {
|
||||||
return this._value.trim().length === 0;
|
return this._value.trim().length === 0;
|
||||||
@@ -246,7 +246,7 @@ export class StringService implements IStringService {
|
|||||||
*
|
*
|
||||||
* @param defaultValue - The fallback string.
|
* @param defaultValue - The fallback string.
|
||||||
* @example
|
* @example
|
||||||
* StringService.of(null).orElse("N/A") // "N/A"
|
* StringUtils.of(null).orElse("N/A") // "N/A"
|
||||||
*/
|
*/
|
||||||
orElse(defaultValue: string): string {
|
orElse(defaultValue: string): string {
|
||||||
return this.isEmpty() ? defaultValue : this._value;
|
return this.isEmpty() ? defaultValue : this._value;
|
||||||
@@ -256,7 +256,7 @@ export class StringService implements IStringService {
|
|||||||
* Allows default JS string interpolation to work.
|
* Allows default JS string interpolation to work.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const name = StringService.of("John");
|
* const name = StringUtils.of("John");
|
||||||
* console.log(`Hello ${name}`); // "Hello John"
|
* console.log(`Hello ${name}`); // "Hello John"
|
||||||
*/
|
*/
|
||||||
toString(): string {
|
toString(): string {
|
||||||
@@ -267,7 +267,7 @@ export class StringService implements IStringService {
|
|||||||
* Allows JSON.stringify to serialize just the string, not the object wrapper.
|
* Allows JSON.stringify to serialize just the string, not the object wrapper.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* JSON.stringify({ name: StringService.of("John") }) // '{"name":"John"}'
|
* JSON.stringify({ name: StringUtils.of("John") }) // '{"name":"John"}'
|
||||||
*/
|
*/
|
||||||
toJSON(): string {
|
toJSON(): string {
|
||||||
return this._value;
|
return this._value;
|
||||||
Reference in New Issue
Block a user