Package detail

universal-common

ong.andrew198CC0-1.02.2.0

Library that provides useful missing base class library functionality.

universal, common, date, dateonly

readme

universal-common

A JavaScript utility library that provides useful base class functionality missing from standard JavaScript libraries.

Installation

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

// 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// 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

// 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