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:
Firman Ramdhani
2026-01-28 18:18:29 +07:00
parent 97f48ce1b9
commit a1db9b93e1
15 changed files with 269 additions and 268 deletions
@@ -0,0 +1,168 @@
import { describe, it, expect } from 'vitest';
import { StringUtils } from './string.utils';
describe('StringUtils', () => {
// ----------------------------------------------------------------
// Instantiation & Static Methods
// ----------------------------------------------------------------
describe('Initialization', () => {
it('should handle string input correctly', () => {
const svc = new StringUtils('Hello');
expect(svc.value()).toBe('Hello');
});
it('should handle number input by converting to string', () => {
const svc = new StringUtils(123);
expect(svc.value()).toBe('123');
});
it('should handle null input gracefully (default to empty string)', () => {
const svc = new StringUtils(null);
expect(svc.value()).toBe('');
});
it('should handle undefined input gracefully', () => {
const svc = new StringUtils(undefined);
expect(svc.value()).toBe('');
});
it('should support static factory method .of()', () => {
const svc = StringUtils.of('Factory');
expect(svc.value()).toBe('Factory');
});
});
describe('random()', () => {
it('should generate string with specified length', () => {
const random = StringUtils.random(15);
expect(random.value()).toHaveLength(15);
});
it('should generate alphanumeric characters only', () => {
const random = StringUtils.random(100);
expect(random.value()).toMatch(/^[A-Za-z0-9]+$/);
});
});
// ----------------------------------------------------------------
// Core Manipulations
// ----------------------------------------------------------------
describe('Transformations', () => {
it('should convert to upperCase', () => {
expect(StringUtils.of('hello').upperCase().value()).toBe('HELLO');
});
it('should convert to lowerCase', () => {
expect(StringUtils.of('HELLO').lowerCase().value()).toBe('hello');
});
it('should trim whitespace', () => {
expect(StringUtils.of(' hello ').trim().value()).toBe('hello');
});
it('should support method chaining (Immutable)', () => {
const original = StringUtils.of(' hello ');
const modified = original.trim().upperCase();
expect(original.value()).toBe(' hello '); // Original untouched
expect(modified.value()).toBe('HELLO'); // New instance modified
});
});
describe('Capitalization', () => {
it('should capitalize first letter only', () => {
expect(StringUtils.of('hELLO world').capitalizeFirst().value()).toBe('Hello world');
});
it('should capitalize each word', () => {
expect(StringUtils.of('hello world').capitalizeEachWord().value()).toBe('Hello World');
});
it('should handle double spaces in capitalizeEachWord', () => {
// Test regex logic for splitting words
expect(StringUtils.of('hello world').capitalizeEachWord().value()).toBe('Hello World');
});
});
// ----------------------------------------------------------------
// Utilities (Slug, Limit, Mask)
// ----------------------------------------------------------------
describe('slugify()', () => {
it('should create valid slugs', () => {
expect(StringUtils.of('Hello World!').slugify().value()).toBe('hello-world');
});
it('should handle complex characters', () => {
expect(StringUtils.of('C# & .NET Core').slugify().value()).toBe('c-net-core');
});
it('should remove leading/trailing separators', () => {
expect(StringUtils.of('---Hello---').slugify().value()).toBe('hello');
});
});
describe('limit()', () => {
it('should truncate string if longer than max', () => {
expect(StringUtils.of('Hello World').limit(5).value()).toBe('Hello...');
});
it('should not truncate if shorter than max', () => {
expect(StringUtils.of('Hi').limit(5).value()).toBe('Hi');
});
it('should support custom suffix', () => {
expect(StringUtils.of('Hello World').limit(5, '!!!').value()).toBe('Hello!!!');
});
});
describe('mask()', () => {
it('should mask middle characters', () => {
// 0812 3456 7890 (Length 12)
// Start 4: 0812
// End 3: 890
// Middle masked: *****
expect(StringUtils.of('081234567890').mask(4, 3).value()).toBe('0812*****890');
});
it('should handle custom mask char', () => {
expect(StringUtils.of('123456').mask(2, 2, 'X').value()).toBe('12XX56');
});
it('should return original if string is shorter than visible parts', () => {
expect(StringUtils.of('123').mask(5, 5).value()).toBe('123');
});
});
// ----------------------------------------------------------------
// Inspection & Outputs
// ----------------------------------------------------------------
describe('Inspection & Outputs', () => {
it('should detect empty strings correctly', () => {
expect(StringUtils.of('').isEmpty()).toBe(true);
expect(StringUtils.of(' ').isEmpty()).toBe(true); // Trim check
expect(StringUtils.of(null).isEmpty()).toBe(true);
expect(StringUtils.of('a').isEmpty()).toBe(false);
});
it('should return default value with orElse', () => {
expect(StringUtils.of(null).orElse('Default')).toBe('Default');
expect(StringUtils.of('Valid').orElse('Default')).toBe('Valid');
});
it('should support native string interpolation (toString)', () => {
const name = StringUtils.of('World');
expect(`Hello ${name}`).toBe('Hello World');
});
it('should support JSON serialization (toJSON)', () => {
const data = {
id: 1,
name: StringUtils.of('Product A'),
};
// JSON.stringify automatically calls .toJSON()
const json = JSON.stringify(data);
expect(json).toBe('{"id":1,"name":"Product A"}');
});
});
});
+275
View File
@@ -0,0 +1,275 @@
/**
* ------------------------------------------------------------
* StringUtils
* ------------------------------------------------------------
* Centralized string manipulation utility.
*
* Features:
* - 🛡️ Null-safe (handles null/undefined gracefully)
* - ⛓️ Fluent and immutable API (chainable methods)
* - 🛠️ Common formatting utilities (Capitalization, Truncation)
* - 🎲 Generator utilities (Random string, Slugs)
*
* @example
* // Basic Chaining
* StringUtils.of(" hello world ")
* .trim()
* .capitalizeEachWord()
* .value(); // "Hello World"
* ------------------------------------------------------------
*/
/* ------------------------------------------------------------------
* Types
* ------------------------------------------------------------------ */
export type StringInput = string | number | null | undefined;
/* ------------------------------------------------------------------
* Interfaces
* ------------------------------------------------------------------ */
interface IStringUtils {
// --- Output & Conversion ---
value(): string;
toString(): string;
toJSON(): string;
// --- Manipulations (Chainable) ---
upperCase(): IStringUtils;
lowerCase(): IStringUtils;
capitalizeFirst(): IStringUtils;
capitalizeEachWord(): IStringUtils;
limit(maxLength: number, suffix?: string): IStringUtils;
trim(): IStringUtils;
slugify(): IStringUtils;
mask(visibleStart: number, visibleEnd: number, maskChar?: string): IStringUtils;
// --- Inspection & Fallback ---
isEmpty(): boolean;
orElse(defaultValue: string): string;
}
/* ------------------------------------------------------------------
* StringUtils
* ------------------------------------------------------------------ */
export class StringUtils implements IStringUtils {
private readonly _value: string;
/* ----------------------------------------------------------------
* Constructor & Static Initializers
* ---------------------------------------------------------------- */
constructor(value?: StringInput) {
// Defensive coding: Convert null/undefined/number to string immediately
if (value === null || value === undefined) {
this._value = '';
} else {
this._value = String(value);
}
}
/**
* Static factory method for cleaner chaining.
*
* @param value - The input string, number, or null/undefined.
* @example
* StringUtils.of("hello").upperCase().value()
*/
static of(value?: StringInput): StringUtils {
return new StringUtils(value);
}
/**
* Generates a random alphanumeric string.
* Useful for IDs, tokens, or temporary passwords.
*
* @param length - Length of the generated string (default: 10).
* @example
* StringUtils.random(8).value() // "aB9x2Z1m"
*/
static random(length: number = 10): StringUtils {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return new StringUtils(result);
}
/* ----------------------------------------------------------------
* Core Manipulations
* ---------------------------------------------------------------- */
/**
* Converts string to UPPERCASE.
*
* @example
* StringUtils.of("hello").upperCase().value() // "HELLO"
*/
upperCase(): StringUtils {
return new StringUtils(this._value.toUpperCase());
}
/**
* Converts string to lowercase.
*
* @example
* StringUtils.of("HELLO").lowerCase().value() // "hello"
*/
lowerCase(): StringUtils {
return new StringUtils(this._value.toLowerCase());
}
/**
* Capitalizes only the first letter of the entire string.
* Remainder is forced to lowercase (Sentence case).
*
* @example
* StringUtils.of("HELLO world").capitalizeFirst().value() // "Hello world"
*/
capitalizeFirst(): StringUtils {
if (!this._value) return this;
const lower = this._value.toLowerCase();
return new StringUtils(lower.charAt(0).toUpperCase() + lower.slice(1));
}
/**
* Capitalizes the first letter of every word (Title Case).
* Automatically handles multiple spaces.
*
* @example
* StringUtils.of("hello world").capitalizeEachWord().value() // "Hello World"
* StringUtils.of("hello world").capitalizeEachWord().value() // "Hello World"
*/
capitalizeEachWord(): StringUtils {
if (!this._value) return this;
const transformed = this._value
.toLowerCase()
.split(/\s+/) // Split by any whitespace regex to handle double spaces
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return new StringUtils(transformed);
}
/**
* Truncates string to a specific length and adds a suffix.
*
* @param maxLength - The character limit.
* @param suffix - The string to append if truncated (default: "...").
* @example
* StringUtils.of("Lorem Ipsum").limit(5).value() // "Lorem..."
* StringUtils.of("Lorem Ipsum").limit(5, "").value() // "Lorem"
*/
limit(maxLength: number, suffix: string = '...'): StringUtils {
if (this._value.length <= maxLength) return this;
return new StringUtils(this._value.substring(0, maxLength) + suffix);
}
/* ----------------------------------------------------------------
* Utility Extensions
* ---------------------------------------------------------------- */
/**
* Removes whitespace from both ends of the string.
*
* @example
* StringUtils.of(" data ").trim().value() // "data"
*/
trim(): StringUtils {
return new StringUtils(this._value.trim());
}
/**
* Converts string to URL-friendly slug.
* Removes special characters and replaces spaces with dashes.
*
* @example
* StringUtils.of("Hello World!").slugify().value() // "hello-world"
* StringUtils.of("C# & .NET").slugify().value() // "c-net"
*/
slugify(): StringUtils {
const slug = this._value
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '') // Remove non-word chars
.replace(/[\s_-]+/g, '-') // Replace spaces and underscores with -
.replace(/^-+|-+$/g, ''); // Remove leading/trailing -
return new StringUtils(slug);
}
/**
* Masks part of the string. Useful for PII (emails/phones).
*
* @param visibleStart - Number of characters to show at the start.
* @param visibleEnd - Number of characters to show at the end.
* @param maskChar - The character to use for masking (default: "*").
* @example
* StringUtils.of("08123456789").mask(4, 3).value() // "0812****789"
*/
mask(visibleStart: number = 0, visibleEnd: number = 0, maskChar: string = '*'): StringUtils {
if (this._value.length <= visibleStart + visibleEnd) return this;
const start = this._value.slice(0, visibleStart);
const end = this._value.slice(-visibleEnd);
const middle = maskChar.repeat(this._value.length - visibleStart - visibleEnd);
return new StringUtils(start + middle + end);
}
/* ----------------------------------------------------------------
* Inspection & Output
* ---------------------------------------------------------------- */
/**
* Checks if the string is empty or contains only whitespace.
*
* @returns true if string is empty or whitespace-only.
* @example
* StringUtils.of("").isEmpty() // true
* StringUtils.of(" ").isEmpty() // true
*/
isEmpty(): boolean {
return this._value.trim().length === 0;
}
/**
* Returns the raw string value.
*/
value(): string {
return this._value;
}
/**
* Returns default value if current string is empty.
*
* @param defaultValue - The fallback string.
* @example
* StringUtils.of(null).orElse("N/A") // "N/A"
*/
orElse(defaultValue: string): string {
return this.isEmpty() ? defaultValue : this._value;
}
/**
* Allows default JS string interpolation to work.
*
* @example
* const name = StringUtils.of("John");
* console.log(`Hello ${name}`); // "Hello John"
*/
toString(): string {
return this._value;
}
/**
* Allows JSON.stringify to serialize just the string, not the object wrapper.
*
* @example
* JSON.stringify({ name: StringUtils.of("John") }) // '{"name":"John"}'
*/
toJSON(): string {
return this._value;
}
}