UNPKG

universal-common

Version:

Library that provides useful missing base class library functionality.

695 lines (538 loc) 23.1 kB
# universal-common A JavaScript utility library that provides useful base class functionality missing from standard JavaScript libraries. ## Installation ```bash npm install universal-common ``` ## Overview This library provides a comprehensive set of utility classes that implement common patterns and functionality often needed in JavaScript applications. It's designed to be used in both browser and Node.js environments, with automatic environment detection. ## Features - **Date and Time Handling** - Complete date/time manipulation with DateOnly, DateTime, DateTimeOffset, and TimeOnly - **Time Spans** - Powerful duration and time interval calculations - **Environment Detection** - Automatically detects browser or Node.js environment - **Error Types** - Specialized error classes for common error scenarios - **GUID Generation** - RFC4122 version 4 compliant UUID implementation - **Promise Utilities** - Enhanced Promise functionality with external control - **URI Building** - Fluent API for constructing and manipulating URIs - **String Building** - Efficient string concatenation with StringBuilder - **Media Type Handling** - Tools for working with MIME types and file extensions - **Task Abstraction** - Higher-level abstraction over Promises for async operations - **Path Utilities** - Functions for working with file paths - **Day of Week Enumeration** - Constants for day of week operations - **DateTime Kind Enumeration** - Constants for UTC, Local, and Unspecified time contexts ## Usage ### Importing ```javascript // Import everything import * as Universal from 'universal-common'; // Import specific utilities import { Guid, StringBuilder, MediaType, DateTime, DateOnly, TimeOnly, TimeSpan, DateTimeOffset } from 'universal-common'; ``` ### Date and Time Operations #### Working with DateOnly ```javascript import { DateOnly } from 'universal-common'; // Create a date const date = new DateOnly(2024, 12, 25); // Christmas 2024 console.log(date.toString()); // "2024-12-25" // Get date components console.log(date.year); // 2024 console.log(date.month); // 12 console.log(date.day); // 25 console.log(date.dayOfWeek); // 3 (Wednesday) console.log(date.dayOfYear); // 360 // Date arithmetic const nextWeek = date.addDays(7); const nextMonth = date.addMonths(1); const nextYear = date.addYears(1); // Parse from string const parsed = DateOnly.parse("2024-12-25"); const tryResult = DateOnly.tryParse("2024-12-25"); if (tryResult.success) { console.log("Parsed successfully:", tryResult.value); } // Convert to/from JavaScript Date const jsDate = new Date(2024, 11, 25); // Note: month is 0-based in JS Date const dateOnly = DateOnly.fromDate(jsDate); const backToJs = dateOnly.toDate(15, 30, 45); // With time components // Leap year checking console.log(DateOnly.isLeapYear(2024)); // true ``` #### Working with TimeOnly ```javascript import { TimeOnly, TimeSpan } from 'universal-common'; // Create a time const time = new TimeOnly(14, 30, 45, 123); // 2:30:45.123 PM console.log(time.toString()); // "14:30:45.123" // Get time components console.log(time.hour); // 14 console.log(time.minute); // 30 console.log(time.second); // 45 console.log(time.millisecond); // 123 // Time arithmetic (wraps around 24-hour clock) const later = time.addHours(6); const muchLater = time.addHours(12); // Wraps to next day // With wrapped days tracking const result = time.addHoursWithWrappedDays(30); console.log(result.time.hour); // 20 (8 PM) console.log(result.wrappedDays); // 1 // Check if time is between two other times const workStart = new TimeOnly(9, 0, 0); const workEnd = new TimeOnly(17, 0, 0); const lunchTime = new TimeOnly(12, 30, 0); console.log(lunchTime.isBetween(workStart, workEnd)); // true // Handle overnight ranges const nightStart = new TimeOnly(22, 0, 0); // 10 PM const nightEnd = new TimeOnly(6, 0, 0); // 6 AM const lateNight = new TimeOnly(2, 0, 0); // 2 AM console.log(lateNight.isBetween(nightStart, nightEnd)); // true // Parse from string const parsed = TimeOnly.parse("14:30:45.123"); const tryResult = TimeOnly.tryParse("14:30:45"); // Convert to/from TimeSpan and DateTime const timeSpan = time.toTimeSpan(); const fromSpan = TimeOnly.fromTimeSpan(timeSpan); const dateTime = new DateTime(2024, 12, 25, 14, 30, 45); const timeFromDateTime = TimeOnly.fromDateTime(dateTime); ``` #### Working with DateTime ```javascript import { DateTime, DateTimeKind, TimeSpan } from 'universal-common'; // Create DateTime instances const dt1 = new DateTime(2024, 12, 25, 14, 30, 45); // Local time const dt2 = new DateTime(2024, 12, 25, 14, 30, 45, 0, DateTimeKind.UTC); const dt3 = new DateTime(638000000000000000n); // From ticks // Get current time const now = DateTime.now; // Local time const utcNow = DateTime.utcNow; // UTC time const today = DateTime.today; // Today at midnight (local) // DateTime components console.log(dt1.year); // 2024 console.log(dt1.month); // 12 console.log(dt1.day); // 25 console.log(dt1.hour); // 14 console.log(dt1.minute); // 30 console.log(dt1.second); // 45 console.log(dt1.millisecond); // 0 console.log(dt1.dayOfWeek); // 3 (Wednesday) console.log(dt1.dayOfYear); // 360 console.log(dt1.kind); // DateTimeKind.UNSPECIFIED // DateTime arithmetic const nextWeek = dt1.addDays(7); const nextHour = dt1.addHours(1); const nextMonth = dt1.addMonths(1); // Handles month-end edge cases const nextYear = dt1.addYears(1); // Handles leap year edge cases // Add TimeSpan const duration = TimeSpan.fromHours(2.5); const later = dt1.add(duration); // Subtraction const earlier = dt1.subtract(duration); const timeDiff = dt2.subtract(dt1); // Returns TimeSpan // Comparison console.log(dt1.equals(dt2)); console.log(dt1.compareTo(dt2)); // -1, 0, or 1 console.log(DateTime.compare(dt1, dt2)); // Properties const dateOnly = dt1.date; // DateOnly portion const timeOfDay = dt1.timeOfDay; // TimeSpan portion // Convert to/from JavaScript Date const jsDate = new Date(2024, 11, 25, 14, 30, 45, 123); const dateTime = DateTime.fromDate(jsDate, DateTimeKind.LOCAL); const backToJs = dateTime.toDate(); // String formatting const formatted = dt1.toString("yyyy-MM-dd HH:mm:ss"); // Leap year utilities console.log(DateTime.isLeapYear(2024)); console.log(DateTime.daysInMonth(2024, 2)); // 29 ``` #### Working with DateTimeOffset ```javascript import { DateTimeOffset, DateTime, DateOnly, TimeOnly, TimeSpan } from 'universal-common'; // Create DateTimeOffset instances const dto1 = new DateTimeOffset(2024, 12, 25, 14, 30, 0, TimeSpan.fromHours(-5)); // EST const dto2 = new DateTimeOffset(DateTime.now, TimeSpan.fromHours(-8)); // PST const dto3 = new DateTimeOffset( new DateOnly(2024, 12, 25), new TimeOnly(14, 30, 0), TimeSpan.fromHours(2) ); // CET // Get current time with offset const now = DateTimeOffset.now; // Current local time with local offset const utcNow = DateTimeOffset.utcNow; // Current UTC time with zero offset // Access components console.log(dto1.year); // 2024 (local time) console.log(dto1.month); // 12 console.log(dto1.hour); // 14 console.log(dto1.offset.totalHours); // -5 console.log(dto1.totalOffsetMinutes); // -300 // Get different representations const localDateTime = dto1.dateTime; // DateTime without offset info const utcDateTime = dto1.utcDateTime; // Converted to UTC DateTime const utcTicks = dto1.utcTicks; // UTC time as ticks // Time zone conversion const pacificTime = dto1.toOffset(TimeSpan.fromHours(-8)); // Convert to PST const utcTime = dto1.toUniversalTime(); // Convert to UTC (zero offset) const localTime = dto1.toLocalTime(); // Convert to system local time // Arithmetic (preserves offset) const later = dto1.addHours(3); const nextDay = dto1.addDays(1); const withDuration = dto1.add(TimeSpan.fromMinutes(30)); // Comparison (based on UTC time) const dto4 = new DateTimeOffset(2024, 12, 25, 19, 30, 0, TimeSpan.zero); // Same UTC time as dto1 console.log(dto1.equals(dto4)); // true (same UTC instant) console.log(dto1.equalsExact(dto4)); // false (different offsets) // Unix time conversion const unixSeconds = dto1.toUnixTimeSeconds(); const unixMs = dto1.toUnixTimeMilliseconds(); const fromUnix = DateTimeOffset.fromUnixTimeSeconds(unixSeconds); // Parsing const parsed = DateTimeOffset.parse("2024-12-25T14:30:45-05:00"); const tryResult = DateTimeOffset.tryParse("2024-12-25T14:30:45+08:00"); // Convert to JavaScript Date const jsDate = dto1.toDate(); // Represents the same UTC instant // String representation const isoString = dto1.toString("yyyy-MM-dd HH:mm:ss zzz"); // Create from JavaScript Date const jsDateNow = new Date(); const dtoFromJs = DateTimeOffset.fromDate(jsDateNow); ``` #### Working with TimeSpan ```javascript import { TimeSpan } from 'universal-common'; // Create TimeSpan instances const ts1 = new TimeSpan(1, 2, 30, 45); // 1 day, 2 hours, 30 minutes, 45 seconds const ts2 = new TimeSpan(1, 2, 30, 45, 123); // Include milliseconds const ts3 = new TimeSpan(1, 2, 30, 45, 123, 456); // Include microseconds const ts4 = new TimeSpan(864000000000); // From ticks (1 day) // Factory methods const days = TimeSpan.fromDays(1.5); // 1.5 days const hours = TimeSpan.fromHours(2.5); // 2.5 hours const minutes = TimeSpan.fromMinutes(90); // 90 minutes const seconds = TimeSpan.fromSeconds(90); // 90 seconds const milliseconds = TimeSpan.fromMilliseconds(1500); // 1500 ms const microseconds = TimeSpan.fromMicroseconds(1500000); // 1500000 μs const ticks = TimeSpan.fromTicks(864000000000); // From ticks // Component properties console.log(ts1.days); // 1 console.log(ts1.hours); // 2 console.log(ts1.minutes); // 30 console.log(ts1.seconds); // 45 console.log(ts1.milliseconds); // 0 console.log(ts1.microseconds); // 0 console.log(ts1.nanoseconds); // 0 // Total properties console.log(ts1.totalDays); // ~1.1046 console.log(ts1.totalHours); // ~26.5125 console.log(ts1.totalMinutes); // ~1590.75 console.log(ts1.totalSeconds); // ~95445 console.log(ts1.totalMilliseconds); // ~95445000 // Arithmetic operations const sum = ts1.add(ts2); const difference = ts1.subtract(ts2); const doubled = ts1.multiply(2); const halved = ts1.divide(2); const ratio = ts1.divideBy(ts2); // Returns number // Absolute value and negation const negative = TimeSpan.fromHours(-2); const positive = negative.duration(); // Absolute value const negated = ts1.negate(); // -ts1 // Comparison console.log(ts1.equals(ts2)); console.log(ts1.compareTo(ts2)); // -1, 0, or 1 console.log(TimeSpan.compare(ts1, ts2)); console.log(TimeSpan.equals(ts1, ts2)); // Static values const zero = TimeSpan.zero; // Zero duration const max = TimeSpan.maxValue; // Maximum TimeSpan const min = TimeSpan.minValue; // Minimum TimeSpan // String representation and parsing console.log(ts1.toString()); // "1.02:30:45" const parsed = TimeSpan.parse("1.02:30:45"); const parseResult = TimeSpan.tryParse("02:30:45"); // Handle fractional seconds const withFraction = TimeSpan.parse("00:00:01.5"); // 1.5 seconds console.log(withFraction.milliseconds); // 500 // Negative TimeSpans const negativeParsed = TimeSpan.parse("-01:30:00"); // -1.5 hours console.log(negativeParsed.totalHours); // -1.5 ``` ### Day of Week Constants ```javascript import { DayOfWeek } from 'universal-common'; console.log(DayOfWeek.SUNDAY); // 0 console.log(DayOfWeek.MONDAY); // 1 console.log(DayOfWeek.TUESDAY); // 2 console.log(DayOfWeek.WEDNESDAY); // 3 console.log(DayOfWeek.THURSDAY); // 4 console.log(DayOfWeek.FRIDAY); // 5 console.log(DayOfWeek.SATURDAY); // 6 // Use with DateTime const christmas = new DateTime(2024, 12, 25); if (christmas.dayOfWeek === DayOfWeek.WEDNESDAY) { console.log("Christmas 2024 is on a Wednesday"); } ``` ### DateTime Kind Constants ```javascript import { DateTimeKind } from 'universal-common'; console.log(DateTimeKind.UNSPECIFIED); // 0 console.log(DateTimeKind.UTC); // 1 console.log(DateTimeKind.LOCAL); // 2 // Use when creating DateTime const utcTime = new DateTime(2024, 12, 25, 14, 30, 0, 0, DateTimeKind.UTC); const localTime = new DateTime(2024, 12, 25, 14, 30, 0, 0, DateTimeKind.LOCAL); ``` ### GUID Generation ```javascript import { Guid } from 'universal-common'; // Generate a new random GUID const guid = Guid.newGuid(); console.log(guid.toString()); // e.g. "f47ac10b-58cc-4372-a567-0e02b2c3d479" // Parse a GUID from string const parsed = Guid.parse("f47ac10b-58cc-4372-a567-0e02b2c3d479"); const withBraces = Guid.parse("{f47ac10b-58cc-4372-a567-0e02b2c3d479}"); // Try parse (returns null if invalid) const tryResult = Guid.tryParse("f47ac10b-58cc-4372-a567-0e02b2c3d479"); if (tryResult !== null) { console.log("Successfully parsed GUID"); } // Check if two GUIDs are equal if (guid.equals(parsed)) { console.log("The GUIDs are equal"); } // Check if a GUID is empty const empty = new Guid(); // Creates empty GUID if (empty.isEmpty()) { console.log("This GUID is empty"); } // Work with bytes const bytes = new Uint8Array(16); // ... fill bytes ... const fromBytes = Guid.fromBytes(bytes); const backToBytes = fromBytes.toBytes(); ``` ### Environment Detection ```javascript import { Environment } from 'universal-common'; // Check execution environment if (Environment.isBrowser) { console.log("Running in a browser"); } else if (Environment.isNode) { console.log("Running in Node.js"); } // Get platform-specific newline const newline = Environment.newLine; // "\n" on Unix, "\r\n" on Windows // Get operating system platform (when available) const platform = Environment.platform; // "win32", "darwin", "linux", etc. ``` ### Working with Promises and Tasks ```javascript import { PromiseCompletionSource, Task } from 'universal-common'; // Create a promise you can manually control const pcs = new PromiseCompletionSource(); // Get the promise to await or chain const promise = pcs.promise; promise.then(value => console.log(`Resolved with: ${value}`)); // Later, resolve or reject the promise externally pcs.resolve("Operation completed"); // or pcs.reject(new Error("Operation failed")); // Using Task for more control over asynchronous operations const task = new Task(async () => { const response = await fetch('https://api.example.com/data'); return response.json(); }); // Tasks can be started explicitly task.start(); // Or implicitly when using then/catch task.then(data => { console.log(data); }).catch(error => { console.error(error); }); // Check task state console.log(task.state); // "Pending", "Fulfilled", or "Rejected" // Create a delay await Task.delay(1000); // Waits for 1 second // Task states console.log(Task.STATE_PENDING); // "Pending" console.log(Task.STATE_FULFILLED); // "Fulfilled" console.log(Task.STATE_REJECTED); // "Rejected" ``` ### URI Building ```javascript import { UriBuilder, RelativeUriBuilder } from 'universal-common'; // Create an absolute URI const uriBuilder = new UriBuilder("https", "example.com"); uriBuilder.addSegment("api") .addSegments("v1", "users") // Add multiple segments .addQuery("page", "1") .addQuery("limit", "10"); // Add authentication uriBuilder.username = "user"; uriBuilder.password = "pass"; uriBuilder.port = 8080; const uri = uriBuilder.uri; console.log(uri); // https://user:pass@example.com:8080/api/v1/users?page=1&limit=10 // Add queries from object uriBuilder.addQueries({ sort: "name", filter: "active" }); // Add queries from Map const queryMap = new Map([["search", "keyword"], ["page", "2"]]); uriBuilder.addQueries(queryMap); // Parse existing URI const parsed = new UriBuilder("https://user:pass@example.com:899/path/to/resource?q=test"); console.log(parsed.scheme); // https console.log(parsed.host); // example.com console.log(parsed.username); // user console.log(parsed.password); // pass console.log(parsed.port); // 899 console.log(parsed.segments); // ["path", "to", "resource"] // Create a relative URI const relativeBuilder = new RelativeUriBuilder(RelativeUriBuilder.TYPE_ROOT); relativeBuilder.addSegment("products") .addQuery("category", "electronics"); console.log(relativeBuilder.uri); // /products?category=electronics // Relative URI types console.log(RelativeUriBuilder.TYPE_SCHEME); // "//" console.log(RelativeUriBuilder.TYPE_ROOT); // "/" console.log(RelativeUriBuilder.TYPE_CURRENT); // "" // Scheme-relative URI (//example.com/path) const schemeRelative = new RelativeUriBuilder(RelativeUriBuilder.TYPE_SCHEME); schemeRelative.addSegments("example.com", "api"); // Current-relative URI (relative/path) const currentRelative = new RelativeUriBuilder(RelativeUriBuilder.TYPE_CURRENT); currentRelative.addSegments("relative", "path"); ``` ### String Building ```javascript import { StringBuilder, Environment } from 'universal-common'; // Create a string builder const sb = new StringBuilder(); sb.append("Hello"); sb.append(" "); sb.append("World"); sb.appendLine("!"); // Adds platform-specific newline // Initialize with content const sb2 = new StringBuilder("Initial content"); // Append different types sb.append(42); // Numbers sb.append(true); // Booleans sb.append(null); // null/undefined // Append lines sb.appendLine("Line 1"); sb.appendLine("Line 2"); sb.appendLine(); // Just a newline // Get properties console.log(sb.length); // Current length console.log(sb.toString()); // Final string // Clear content sb.clear(); console.log(sb.length); // 0 ``` ### Media Type Handling ```javascript import { MediaType, Path } from 'universal-common'; // Get MIME type from file extension const extension = Path.getExtension("document.pdf"); const mediaType = MediaType.fromExtension(extension); console.log(mediaType.toString()); // "application/pdf" // Common extensions console.log(MediaType.fromExtension(".html")); // text/html console.log(MediaType.fromExtension(".jpg")); // image/jpeg console.log(MediaType.fromExtension(".json")); // application/json console.log(MediaType.fromExtension(".xml")); // application/xml console.log(MediaType.fromExtension(".css")); // text/css console.log(MediaType.fromExtension(".js")); // application/javascript // Case insensitive console.log(MediaType.fromExtension(".PDF")); // application/pdf // Unknown extensions return null console.log(MediaType.fromExtension(".unknown")); // null // Create media type directly const htmlType = new MediaType("text", "html"); console.log(htmlType.type); // "text" console.log(htmlType.subtype); // "html" console.log(htmlType.toString()); // "text/html" // From string format const jsonType = new MediaType("application/json"); console.log(jsonType.type); // "application" console.log(jsonType.subtype); // "json" ``` ### Path Utilities ```javascript import { Path } from 'universal-common'; // Get file extensions console.log(Path.getExtension("file.txt")); // ".txt" console.log(Path.getExtension("path/to/file.html")); // ".html" console.log(Path.getExtension("noextension")); // "" console.log(Path.getExtension("path/to/dir/")); // "" console.log(Path.getExtension(".gitignore")); // ".gitignore" console.log(Path.getExtension("file.tar.gz")); // ".gz" console.log(Path.getExtension("")); // "" ``` ### Error Handling ```javascript import { ArgumentError, InvalidOperationError } from 'universal-common'; function divide(a, b) { if (typeof a !== 'number' || typeof b !== 'number') { throw new ArgumentError("Both arguments must be numbers"); } if (b === 0) { throw new InvalidOperationError("Division by zero is not allowed"); } return a / b; } try { divide("5", 2); } catch (error) { if (error instanceof ArgumentError) { console.log("Invalid argument:", error.message); } else if (error instanceof InvalidOperationError) { console.log("Invalid operation:", error.message); } } ``` ## Date/Time Best Practices ### Choosing the Right Type - **DateOnly**: Use for dates without time (birthdays, holidays, etc.) - **TimeOnly**: Use for times without dates (daily schedules, recurring times) - **DateTime**: Use for local date/time values or when timezone is not important - **DateTimeOffset**: Use when working with multiple timezones or when precise timezone information is required - **TimeSpan**: Use for durations, intervals, or time differences ### Working with Timezones ```javascript // Always be explicit about timezone context const utcTime = new DateTime(2024, 12, 25, 14, 30, 0, 0, DateTimeKind.UTC); const localTime = new DateTime(2024, 12, 25, 14, 30, 0, 0, DateTimeKind.LOCAL); // Use DateTimeOffset for timezone-aware operations const eastCoast = new DateTimeOffset(2024, 12, 25, 14, 30, 0, TimeSpan.fromHours(-5)); const westCoast = eastCoast.toOffset(TimeSpan.fromHours(-8)); // Always compare in UTC when dealing with different timezones console.log(eastCoast.utcDateTime.equals(westCoast.utcDateTime)); ``` ### Parsing and Validation ```javascript // Always use tryParse for user input const dateInput = "2024-12-25"; const parseResult = DateOnly.tryParse(dateInput); if (parseResult.success) { console.log("Valid date:", parseResult.value); } else { console.log("Invalid date format"); } // Use parse only when you're certain the input is valid const knownGoodDate = DateOnly.parse("2024-12-25"); ``` ## API Reference All classes include comprehensive comparison methods (`equals`, `compareTo`), arithmetic operations where applicable, string parsing (`parse`, `tryParse`), and string formatting (`toString`). Refer to the test suite for complete usage examples and edge cases. ## Environment Compatibility This library works in both browser and Node.js environments. Some features may behave differently based on the environment: - **Environment.platform**: Available in Node.js, may be null or simplified in browsers - **Environment.newLine**: Uses platform-appropriate line endings - Date/time operations use the system's timezone information when working with local times