Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
5f66339
initial commit
MaverikMinett Sep 20, 2025
a83da7a
import names classes
MaverikMinett Sep 20, 2025
edddc53
remove files
MaverikMinett Sep 20, 2025
3772995
update constructors
MaverikMinett Sep 20, 2025
9ae6427
update names implementations
MaverikMinett Sep 20, 2025
b3bb7a6
add unit tests
MaverikMinett Sep 20, 2025
43cad4a
refactor
MaverikMinett Sep 21, 2025
4c0cdf9
continue refactor (now instantiating)
MaverikMinett Sep 21, 2025
0278331
update datetime pattern tests
MaverikMinett Sep 21, 2025
c0a735d
updates to unit tests
MaverikMinett Sep 22, 2025
aa26ffe
add test stubs
MaverikMinett Sep 22, 2025
fbe79bd
update date time pattern tests
MaverikMinett Sep 22, 2025
b8b6c9c
update unit tests
MaverikMinett Sep 22, 2025
848c04a
update unit tests for era short
MaverikMinett Sep 22, 2025
9a21090
update unit tests for eraNarrow
MaverikMinett Sep 23, 2025
e6aa88b
add unit tests for common era
MaverikMinett Sep 23, 2025
3c625bf
update test for isoYear and variations
MaverikMinett Sep 23, 2025
64d3b2d
add month/month padded tests
MaverikMinett Sep 23, 2025
d7d4d46
update unit tests for month names
MaverikMinett Sep 23, 2025
b489497
add tests
MaverikMinett Sep 23, 2025
b3472df
correct japanese tests
MaverikMinett Sep 23, 2025
3da5f06
add unit tests for day
MaverikMinett Sep 23, 2025
68e9b34
add tests for weekday names
MaverikMinett Sep 23, 2025
7fc98de
update weekday tests
MaverikMinett Sep 23, 2025
b012f01
move tests into individual files
MaverikMinett Sep 23, 2025
b220d59
fix spacing
MaverikMinett Sep 23, 2025
bc7f7c3
update day period tests
MaverikMinett Sep 24, 2025
af3c247
break out tests
MaverikMinett Sep 24, 2025
1881f4c
add hour minute second tests
MaverikMinett Sep 24, 2025
c628de8
add timezone tests
MaverikMinett Sep 24, 2025
71cc96f
add unit tests
MaverikMinett Sep 24, 2025
c7c06e4
move files
MaverikMinett Sep 24, 2025
f3958ff
fix era long tests
MaverikMinett Sep 24, 2025
b8ebc5e
update unit tests
MaverikMinett Sep 24, 2025
bb041ed
add unicode token tests
MaverikMinett Sep 24, 2025
84b9dc1
update tests for weekday narrow
MaverikMinett Sep 25, 2025
5b75c24
add unicode token tests
MaverikMinett Sep 25, 2025
21c1f04
forming public api
MaverikMinett Sep 25, 2025
e1d6e07
rename timeZoneId timeZone
MaverikMinett Sep 25, 2025
1e05396
create DateTimeValue
MaverikMinett Sep 28, 2025
b0365d0
fix enumerability settings on DateTimeValue
MaverikMinett Sep 28, 2025
4732302
add toDatePart to DateTimeValue
MaverikMinett Sep 28, 2025
35aad33
add toTemporal() methods to DateTimeValue
MaverikMinett Sep 28, 2025
b23ccf0
remove dead code
MaverikMinett Sep 28, 2025
200d8d1
add date and time value
MaverikMinett Sep 28, 2025
158fd58
update fractionalSeconds to nanoseconds
MaverikMinett Sep 28, 2025
348d838
update unit tests
MaverikMinett Sep 28, 2025
eb8c073
update unit tests
MaverikMinett Sep 28, 2025
ebaed59
add datetime intl parser
MaverikMinett Sep 28, 2025
dceb4db
rename nanoseconds to nanosecond
MaverikMinett Sep 28, 2025
62edef1
add timezonename shortGeneric, longGeneric
MaverikMinett Sep 28, 2025
6f08eaf
add Intl.DateTimeFormat parser and object parser
MaverikMinett Sep 29, 2025
770a6fc
update *Value implementations
MaverikMinett Sep 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
MIT License

Copyright (c) 2025 Maverik Minett

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,36 @@
# agape-datetime
Date and time parsing and formatting
# @agape/datetime

Date and time utilities for TypeScript applications.

## ✨ Features

- Date manipulation and formatting
- Time zone handling
- Date arithmetic operations
- Date parsing and validation
- Relative time calculations

---

## 🚀 Example

```ts
import { formatDate, addDays, isWeekend } from '@agape/datetime';

const today = new Date();
const tomorrow = addDays(today, 1);
const formatted = formatDate(today, 'YYYY-MM-DD');

console.log(`Today: ${formatted}`);
console.log(`Is weekend: ${isWeekend(today)}`);
```

---

## 📚 Documentation

See the full API documentation at [agape.dev/api](https://agape.dev/api).

## 📦 Agape Toolkit

This package is part of the [Agape Toolkit](https://github.com/AgapeToolkit/AgapeToolkit) - a comprehensive collection of TypeScript utilities and libraries for modern web development.
16 changes: 16 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable */
export default {
displayName: 'agape-datetime',
preset: '../../jest.preset.js',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
},
},
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/datetime',
};
39 changes: 39 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@agape/datetime",
"version": "0.1.0",
"description": "Date and time utilities",
"main": "./cjs/index.js",
"module": "./es2022/index.js",
"author": {
"name": "Maverik Minett",
"email": "maverik.minett@gmail.com"
},
"license": "MIT",
"publishConfig": {
"access": "public"
},
"homepage": "https://agape.dev",
"repository": {
"type": "git",
"url": "https://github.com/AgapeToolkit/AgapeToolkit"
},
"keywords": [
"agape",
"datetime",
"date",
"time"
],
"es2022": "./es2022/index.js",
"exports": {
"./package.json": {
"default": "./package.json"
},
".": {
"es2022": "./es2022/index.js",
"node": "./cjs/index.js",
"default": "./es2022/index.js",
"require": "./cjs/index.js",
"import": "./es2022/index.js"
}
}
}
47 changes: 47 additions & 0 deletions project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "datetime",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/datetime/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/datetime",
"main": "libs/datetime/src/index.ts",
"tsConfig": "libs/datetime/tsconfig.lib.json",
"assets": ["libs/datetime/*.md"]
}
},
"publish": {
"executor": "nx:run-commands",
"options": {
"command": "node tools/scripts/publish.mjs datetime {args.ver} {args.tag}"
},
"dependsOn": ["build"]
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/datetime/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/datetime/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
},
"tags": []
}
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @agape/datetime
// Date and time utilities for TypeScript applications

export * from './lib/names';
export * from './lib/values';
export * from './lib/types';
159 changes: 159 additions & 0 deletions src/lib/names/common-era-names.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { CommonEraNames } from './common-era-names';

describe('CommonEraNames', () => {
const testLocales = ['en-US', 'es-US', 'ru-RU', 'ja-JP', 'de-DE', 'fr-FR', 'en-GB'];
const testCases = ['default', 'uppercase', 'lowercase'] as const;

describe('get() method', () => {
test('should return same instance for same parameters', () => {
const instance1 = CommonEraNames.get({ locale: 'en-US', case: 'default' });
const instance2 = CommonEraNames.get({ locale: 'en-US', case: 'default' });
expect(instance1).toBe(instance2);
});

test('should return different instances for different parameters', () => {
const instance1 = CommonEraNames.get({ locale: 'en-US', case: 'default' });
const instance2 = CommonEraNames.get({ locale: 'en-US', case: 'uppercase' });
const instance3 = CommonEraNames.get({ locale: 'es-US', case: 'default' });

expect(instance1).not.toBe(instance2);
expect(instance1).not.toBe(instance3);
expect(instance2).not.toBe(instance3);
});

test('should use default locale when not provided', () => {
const instance = CommonEraNames.get();
expect(instance.locale).toBeDefined();
});

test('should use default case when not provided', () => {
const instance = CommonEraNames.get({ locale: 'en-US' });
expect(instance.case).toBe('default');
});
});

describe('long property', () => {
test.each(testLocales)('should return correct long names for locale %s', (locale) => {
const instance = CommonEraNames.get({ locale, case: 'default' });
const long = instance.long;

expect(long).toHaveLength(2);
expect(long[0]).toContain('Before');
expect(long[1]).toContain('Common');
});

test.each(testCases)('should handle case %s correctly', (caseType) => {
const instance = CommonEraNames.get({ locale: 'en-US', case: caseType });
const long = instance.long;

expect(long).toHaveLength(2);

if (caseType === 'uppercase') {
expect(long[0]).toBe(long[0].toUpperCase());
expect(long[1]).toBe(long[1].toUpperCase());
} else if (caseType === 'lowercase') {
expect(long[0]).toBe(long[0].toLowerCase());
expect(long[1]).toBe(long[1].toLowerCase());
}
});

test('should use locale-aware case transformation', () => {
const instance = CommonEraNames.get({ locale: 'tr-TR', case: 'uppercase' });
const long = instance.long;

// Turkish has special case rules (İ vs I)
expect(long[0]).toContain('BEFORE');
expect(long[1]).toContain('COMMON');
});
});

describe('short property', () => {
test.each(testLocales)('should return correct short names for locale %s', (locale) => {
const instance = CommonEraNames.get({ locale, case: 'default' });
const short = instance.short;

expect(short).toHaveLength(2);
expect(short[0]).toBe('BCE');
expect(short[1]).toBe('CE');
});

test.each(testCases)('should handle case %s correctly', (caseType) => {
const instance = CommonEraNames.get({ locale: 'en-US', case: caseType });
const short = instance.short;

expect(short).toHaveLength(2);

if (caseType === 'uppercase') {
expect(short[0]).toBe('BCE');
expect(short[1]).toBe('CE');
} else if (caseType === 'lowercase') {
expect(short[0]).toBe('bce');
expect(short[1]).toBe('ce');
} else {
expect(short[0]).toBe('BCE');
expect(short[1]).toBe('CE');
}
});
});

describe('narrow property', () => {
test.each(testLocales)('should return correct narrow names for locale %s', (locale) => {
const instance = CommonEraNames.get({ locale, case: 'default' });
const narrow = instance.narrow;

expect(narrow).toHaveLength(2);
expect(narrow[0]).toBe('B');
expect(narrow[1]).toBe('C');
});

test.each(testCases)('should handle case %s correctly', (caseType) => {
const instance = CommonEraNames.get({ locale: 'en-US', case: caseType });
const narrow = instance.narrow;

expect(narrow).toHaveLength(2);

if (caseType === 'uppercase') {
expect(narrow[0]).toBe('B');
expect(narrow[1]).toBe('C');
} else if (caseType === 'lowercase') {
expect(narrow[0]).toBe('b');
expect(narrow[1]).toBe('c');
} else {
expect(narrow[0]).toBe('B');
expect(narrow[1]).toBe('C');
}
});
});

describe('caching behavior', () => {
test('should cache instances correctly', () => {
const instance1 = CommonEraNames.get({ locale: 'en-US', case: 'default' });
const instance2 = CommonEraNames.get({ locale: 'en-US', case: 'default' });
const instance3 = CommonEraNames.get({ locale: 'en-US', case: 'uppercase' });

expect(instance1).toBe(instance2);
expect(instance1).not.toBe(instance3);
});

test('should cache different locales separately', () => {
const enInstance = CommonEraNames.get({ locale: 'en-US', case: 'default' });
const esInstance = CommonEraNames.get({ locale: 'es-US', case: 'default' });

expect(enInstance).not.toBe(esInstance);
});
});

describe('locale property', () => {
test.each(testLocales)('should set correct locale %s', (locale) => {
const instance = CommonEraNames.get({ locale });
expect(instance.locale).toBe(locale);
});
});

describe('case property', () => {
test.each(testCases)('should set correct case %s', (caseType) => {
const instance = CommonEraNames.get({ locale: 'en-US', case: caseType });
expect(instance.case).toBe(caseType);
});
});
});
63 changes: 63 additions & 0 deletions src/lib/names/common-era-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { getLocale } from '@agape/locale';
import { Names } from './names';
import { CommonEraNamesParams } from './types/common-era-names-params';

const commonEraNamesRegistry = new Map<string, CommonEraNames>();

export class CommonEraNames extends Names {
private _long?: readonly string[];
private _short?: readonly string[];
private _narrow?: readonly string[];

get long(): readonly string[] {
if (this._long) return this._long;

if (this.case === 'default') {
this._long = ['Before Common Era', 'Common Era'];
} else {
const defaultInstance = CommonEraNames.get({ locale: this.locale, case: 'default' });
this._long = this.applyCase(defaultInstance.long);
}

return this._long;
}

get short(): readonly string[] {
if (this._short) return this._short;

if (this.case === 'default') {
this._short = ['BCE', 'CE'];
} else {
const defaultInstance = CommonEraNames.get({ locale: this.locale, case: 'default' });
this._short = this.applyCase(defaultInstance.short);
}

return this._short;
}

get narrow(): readonly string[] {
if (this._narrow) return this._narrow;

if (this.case === 'default') {
this._narrow = ['B', 'C'];
} else {
const defaultInstance = CommonEraNames.get({ locale: this.locale, case: 'default' });
this._narrow = this.applyCase(defaultInstance.narrow);
}

return this._narrow;
}

static get(params: CommonEraNamesParams = {}): CommonEraNames {
const locale = params.locale ?? getLocale();
const caseType = params.case ?? 'default';
const key = `${locale}-${caseType}`;

const cached = commonEraNamesRegistry.get(key);
if (cached) return cached;

const created = new CommonEraNames({ locale, case: caseType });
commonEraNamesRegistry.set(key, created);
return created;
}
}
Loading