Skip to content

Commit f28128a

Browse files
committed
feat(Date Converter): LDAP and Win32FileTime formats
Added LDAP format Added Win32FileTime / Windows timestamp format Fix CorentinTh#1483
1 parent ab75cd5 commit f28128a

6 files changed

Lines changed: 156 additions & 5 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
"lodash.defaultsdeep": "^4.6.1",
179179
"lodash.flattendeep": "^4.4.0",
180180
"lodash.last": "^3.0.0",
181+
"long": "^5.3.1",
181182
"lorem-ipsum-japanese": "^1.0.1",
182183
"luhn-js": "^1.1.2",
183184
"luxon": "^3.5.0",

pnpm-lock.yaml

Lines changed: 7 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tools/date-time-converter/date-time-converter.models.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
import { describe, expect, test } from 'vitest';
22
import {
33
dateToExcelFormat,
4+
dateToLDAPTimestamp,
5+
dateToWin32FileTime,
46
excelFormatToDate,
57
fromJSDate,
68
fromTimestamp,
79
isExcelFormat,
810
isISO8601DateTimeString,
911
isISO9075DateString,
1012
isJSDate,
13+
isLDAPTimestamp,
1114
isMongoObjectId,
1215
isRFC3339DateString,
1316
isRFC7231DateString,
1417
isTimestamp,
1518
isTimestampMicroSeconds,
1619
isUTCDateString,
1720
isUnixTimestamp,
21+
isWin32FileTime,
22+
lDAPTimestampToDate,
1823
toJSDate,
24+
win32FileTimeToUnix,
1925
} from './date-time-converter.models';
2026

2127
describe('date-time-converter models', () => {
@@ -107,6 +113,29 @@ describe('date-time-converter models', () => {
107113
});
108114
});
109115

116+
describe('isWin32FileTime', () => {
117+
test('should return true for valid Win32 file time', () => {
118+
expect(isWin32FileTime('131461446367662144')).toBe(true);
119+
});
120+
121+
test('should return false for invalid Win32 file time', () => {
122+
expect(isWin32FileTime('92233720368547758071')).toBe(false); // too big
123+
expect(isWin32FileTime('foo')).toBe(false);
124+
expect(isWin32FileTime('')).toBe(false);
125+
});
126+
});
127+
128+
describe('isLDAPTimestamp', () => {
129+
test('should return true for valid LDAP timestamps', () => {
130+
expect(isLDAPTimestamp('20250309122345Z')).toBe(true);
131+
});
132+
133+
test('should return false for invalid LDAP timestamps', () => {
134+
expect(isLDAPTimestamp('foo')).toBe(false);
135+
expect(isLDAPTimestamp('')).toBe(false);
136+
});
137+
});
138+
110139
describe('isTimestamp', () => {
111140
test('should return true for valid Unix timestamps in milliseconds', () => {
112141
expect(isTimestamp('1649792026123')).toBe(true);
@@ -254,3 +283,52 @@ describe('date-time-converter models', () => {
254283
});
255284
});
256285
});
286+
287+
describe('win32FileTimeToUnix', () => {
288+
test('should correctly convert Win32 FILETIME to UNIX timestamp', () => {
289+
const fileTime = '132271200000000000';
290+
const expectedDate = new Date(Date.UTC(2020, 1, 25, 16, 0, 0));
291+
expect(win32FileTimeToUnix(fileTime)).toEqual(expectedDate);
292+
});
293+
294+
test('should handle zero FILETIME', () => {
295+
expect(win32FileTimeToUnix('0')).toEqual(new Date(1601, 0, 1, 0, 0, 0));
296+
});
297+
});
298+
299+
describe('dateToWin32FileTime', () => {
300+
test('should correctly convert Date to Win32 FILETIME', () => {
301+
const date = new Date(Date.UTC(2020, 1, 25, 16, 0, 0));
302+
const expectedFileTime = '132271200000000000';
303+
expect(dateToWin32FileTime(date)).toBe(expectedFileTime);
304+
});
305+
306+
test('should handle epoch (1970-01-01)', () => {
307+
const date = new Date(0);
308+
const expectedFileTime = '116444736000000000';
309+
expect(dateToWin32FileTime(date)).toBe(expectedFileTime);
310+
});
311+
});
312+
313+
describe('lDAPTimestampToDate', () => {
314+
test('should correctly parse LDAP timestamp', () => {
315+
const ldapTimestamp = '20240101120013Z'; // January 1, 2024, 12:00:13 UTC
316+
expect(lDAPTimestampToDate(ldapTimestamp)).toEqual(new Date(Date.UTC(2024, 0, 1, 12, 0, 13)));
317+
});
318+
319+
test('should return current date on invalid input', () => {
320+
expect(lDAPTimestampToDate('invalid')).toBeInstanceOf(Date);
321+
});
322+
});
323+
324+
describe('dateToLDAPTimestamp', () => {
325+
test('should correctly convert Date to LDAP timestamp', () => {
326+
const date = new Date(Date.UTC(2024, 0, 1, 12, 0, 13));
327+
expect(dateToLDAPTimestamp(date)).toBe('20240101120013Z');
328+
});
329+
330+
test('should correctly pad single-digit values', () => {
331+
const date = new Date(Date.UTC(2024, 4, 9, 4, 5, 6)); // May 9, 2024, 04:05:06 UTC
332+
expect(dateToLDAPTimestamp(date)).toBe('20240509040506Z');
333+
});
334+
});

src/tools/date-time-converter/date-time-converter.models.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import _ from 'lodash';
22
import { addMilliseconds } from 'date-fns';
3+
import Long from 'long';
34

45
export {
56
isISO8601DateTimeString,
@@ -18,6 +19,12 @@ export {
1819
isJSDate,
1920
fromJSDate,
2021
toJSDate,
22+
isLDAPTimestamp,
23+
isWin32FileTime,
24+
win32FileTimeToUnix,
25+
dateToWin32FileTime,
26+
lDAPTimestampToDate,
27+
dateToLDAPTimestamp,
2128
};
2229

2330
const ISO8601_REGEX
@@ -34,6 +41,8 @@ const EXCEL_FORMAT_REGEX = /^-?\d+(\.\d+)?$/;
3441

3542
const JS_DATE_REGEX = /^new\s+Date\(\s*(?:(\d+)\s*,\s*)(?:(\d|11)\s*,\s*(?:(\d+)\s*,\s*(?:(\d+)\s*,\s*(?:(\d+)\s*,\s*(?:(\d+)\s*,\s*)?)?)?)?)?(\d+)\)\s*;?$/;
3643

44+
const LDAP_TIMESTAMP_REGEX = /^([0-9]{4})(0[0-9]|1[012])([012][0-9]|3[01])([01][0-9]|2[0123])([0-5][0-9])([0-5][0-9])Z$/;
45+
3746
function createRegexMatcher(regex: RegExp) {
3847
return (date?: string) => !_.isNil(date) && regex.test(date);
3948
}
@@ -47,6 +56,8 @@ const isTimestamp = createRegexMatcher(/^([0-9]{1,13}|[0-9]{16})$/);
4756
const isTimestampMilliSeconds = createRegexMatcher(/^[0-9]{1,13}$/);
4857
const isTimestampMicroSeconds = createRegexMatcher(/^[0-9]{16}$/);
4958
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
59+
const isLDAPTimestamp = createRegexMatcher(LDAP_TIMESTAMP_REGEX);
60+
const isWin32FileTime = createRegexMatcher(/^[0-9]{18}$/);
5061

5162
const isJSDate = createRegexMatcher(JS_DATE_REGEX);
5263
function fromJSDate(date: string): Date {
@@ -89,3 +100,43 @@ function fromTimestamp(timestamp: string, type: 'auto' | 'milliseconds' | 'micro
89100
}
90101
return addMilliseconds(new Date(0), milliSeconds);
91102
}
103+
104+
function win32FileTimeToUnix(ft: string) {
105+
const ulong = Long.fromString(ft, true, 10).div(10000);
106+
let epochBase = ulong.sub(11644473600000);
107+
108+
if (epochBase.greaterThan(ulong)) {
109+
epochBase = epochBase.toSigned();
110+
}
111+
112+
return new Date(epochBase.toNumber());
113+
}
114+
115+
function dateToWin32FileTime(date: Date) {
116+
const timestamp = +date;
117+
const long = Long
118+
.fromNumber(timestamp, timestamp >= 0)
119+
.add(11644473600000)
120+
.mul(10000);
121+
122+
return long.toString(10);
123+
}
124+
125+
function lDAPTimestampToDate(ldapTimestamp: string) {
126+
const [, yy, mm, dd, hh, nn, ss] = LDAP_TIMESTAMP_REGEX.exec(ldapTimestamp) || [];
127+
if (!yy || !mm) {
128+
return new Date();
129+
}
130+
return new Date(
131+
Number.parseInt(yy, 10),
132+
Number.parseInt(mm, 10) - 1,
133+
Number.parseInt(dd, 10),
134+
Number.parseInt(hh, 10),
135+
Number.parseInt(nn, 10),
136+
Number.parseInt(ss, 10));
137+
}
138+
139+
function dateToLDAPTimestamp(date: Date) {
140+
const pad2 = (n: number) => n.toString().padStart(2, '0');
141+
return `${date.getUTCFullYear()}${pad2(date.getUTCMonth() + 1)}${pad2(date.getUTCDate())}${pad2(date.getUTCHours())}${pad2(date.getUTCMinutes())}${pad2(date.getUTCSeconds())}Z`;
142+
}

src/tools/date-time-converter/date-time-converter.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,26 @@ import { UTCDate } from '@date-fns/utc';
1515
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
1616
import {
1717
dateToExcelFormat,
18+
dateToLDAPTimestamp,
19+
dateToWin32FileTime,
1820
excelFormatToDate,
1921
fromJSDate,
2022
fromTimestamp,
2123
isExcelFormat,
2224
isISO8601DateTimeString,
2325
isISO9075DateString,
2426
isJSDate,
27+
isLDAPTimestamp,
2528
isMongoObjectId,
2629
isRFC3339DateString,
2730
isRFC7231DateString,
2831
isTimestamp,
2932
isUTCDateString,
3033
isUnixTimestamp,
34+
isWin32FileTime,
35+
lDAPTimestampToDate,
3136
toJSDate,
37+
win32FileTimeToUnix,
3238
} from './date-time-converter.models';
3339
import { withDefaultOnError } from '@/utils/defaults';
3440
import { useValidation } from '@/composable/validation';
@@ -110,6 +116,18 @@ const formats: DateFormat[] = [
110116
toDate: date => fromJSDate(date),
111117
formatMatcher: isJSDate,
112118
},
119+
{
120+
name: 'LDAP YMD Timestamp',
121+
fromDate: date => dateToLDAPTimestamp(date),
122+
toDate: date => lDAPTimestampToDate(date),
123+
formatMatcher: isLDAPTimestamp,
124+
},
125+
{
126+
name: 'Win32 FileTime/LDAP 18 digits Timestamp',
127+
fromDate: date => dateToWin32FileTime(date),
128+
toDate: date => win32FileTimeToUnix(date),
129+
formatMatcher: isWin32FileTime,
130+
},
113131
];
114132
115133
const formatIndex = ref(6);

src/tools/date-time-converter/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const tool = defineTool({
66
name: translate('tools.date-converter.title'),
77
path: '/date-converter',
88
description: translate('tools.date-converter.description'),
9-
keywords: ['date', 'time', 'converter', 'iso', 'utc', 'unix', 'epoch', 'timezone', 'year', 'month', 'day', 'minute', 'seconde'],
9+
keywords: ['date', 'time', 'converter', 'iso', 'utc', 'unix', 'epoch', 'timezone', 'year', 'month', 'day', 'minute', 'seconde', 'filetime', 'ldap', 'win32'],
1010
component: () => import('./date-time-converter.vue'),
1111
icon: Calendar,
1212
npmPackages: ['date-fns'],

0 commit comments

Comments
 (0)