diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 0d3b2eb..d32e2e3 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,9 +4,10 @@
-
+
+
-
+
@@ -23,7 +24,7 @@
@@ -42,59 +43,66 @@
"state": "OPEN"
}
}
- {
- "prStates": [
+
+}]]>
{
"selectedUrlAndAccountId": {
"url": "https://github.com/iib0011/omni-tools.git",
@@ -123,55 +131,56 @@
- {
- "keyToString": {
- "ASKED_ADD_EXTERNAL_FILES": "true",
- "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
- "Docker.Dockerfile build.executor": "Run",
- "Docker.Dockerfile.executor": "Run",
- "Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
- "Playwright.JoinText Component.executor": "Run",
- "Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
- "RunOnceActivity.OpenProjectViewOnStart": "true",
- "RunOnceActivity.ShowReadmeOnStart": "true",
- "RunOnceActivity.git.unshallow": "true",
- "Vitest.compute function (1).executor": "Run",
- "Vitest.compute function.executor": "Run",
- "Vitest.mergeText.executor": "Run",
- "Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
- "Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
- "Vitest.parsePageRanges.executor": "Run",
- "Vitest.removeDuplicateLines function.executor": "Run",
- "Vitest.removeDuplicateLines function.newlines option.executor": "Run",
- "Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
- "Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
- "Vitest.replaceText function.executor": "Run",
- "git-widget-placeholder": "main",
- "ignore.virus.scanning.warn.message": "true",
- "kotlin-language-version-configured": "true",
- "last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src/components/input",
- "node.js.detected.package.eslint": "true",
- "node.js.detected.package.tslint": "true",
- "node.js.selected.package.eslint": "(autodetect)",
- "node.js.selected.package.tslint": "(autodetect)",
- "nodejs_package_manager_path": "npm",
- "npm.build.executor": "Run",
- "npm.dev.executor": "Run",
- "npm.lint.executor": "Run",
- "npm.prebuild.executor": "Run",
- "npm.script:create:tool.executor": "Run",
- "npm.test.executor": "Run",
- "npm.test:e2e.executor": "Run",
- "npm.test:e2e:run.executor": "Run",
- "prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
- "project.structure.last.edited": "Problems",
- "project.structure.proportion": "0.0",
- "project.structure.side.proportion": "0.2",
- "settings.editor.selected.configurable": "refactai_advanced_settings",
- "ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
- "vue.rearranger.settings.migration": "true"
+
+}]]>
-
-
+
+
-
+
+
-
-
+
+
-
-
+
@@ -238,6 +247,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -254,30 +276,20 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
@@ -363,22 +375,7 @@
-
-
-
- 1740504100676
-
-
-
- 1740504100676
-
-
-
- 1740505390205
-
-
-
- 1740505390205
+
@@ -756,7 +753,23 @@
1743051792459
-
+
+
+ 1743052111988
+
+
+
+ 1743052111988
+
+
+
+ 1743106796406
+
+
+
+ 1743106796406
+
+
@@ -803,8 +816,6 @@
-
-
@@ -828,7 +839,9 @@
-
+
+
+
diff --git a/src/pages/tools/time/index.ts b/src/pages/tools/time/index.ts
index f229937..785dfe7 100644
--- a/src/pages/tools/time/index.ts
+++ b/src/pages/tools/time/index.ts
@@ -1,13 +1,16 @@
+import { tool as timeBetweenDates } from './time-between-dates/meta';
import { tool as daysDoHours } from './convert-days-to-hours/meta';
import { tool as hoursToDays } from './convert-hours-to-days/meta';
import { tool as convertSecondsToTime } from './convert-seconds-to-time/meta';
import { tool as convertTimetoSeconds } from './convert-time-to-seconds/meta';
import { tool as truncateClockTime } from './truncate-clock-time/meta';
+
export const timeTools = [
daysDoHours,
hoursToDays,
convertSecondsToTime,
convertTimetoSeconds,
- truncateClockTime
+ truncateClockTime,
+ timeBetweenDates
];
diff --git a/src/pages/tools/time/time-between-dates/index.tsx b/src/pages/tools/time/time-between-dates/index.tsx
new file mode 100644
index 0000000..a02ee70
--- /dev/null
+++ b/src/pages/tools/time/time-between-dates/index.tsx
@@ -0,0 +1,250 @@
+import { Box, Paper, Typography } from '@mui/material';
+import React, { useState } from 'react';
+import ToolContent from '@components/ToolContent';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import SelectWithDesc from '@components/options/SelectWithDesc';
+import {
+ calculateTimeBetweenDates,
+ formatTimeWithLargestUnit,
+ getTimeWithTimezone,
+ unitHierarchy
+} from './service';
+import * as Yup from 'yup';
+import { CardExampleType } from '@components/examples/ToolExamples';
+
+type TimeUnit =
+ | 'milliseconds'
+ | 'seconds'
+ | 'minutes'
+ | 'hours'
+ | 'days'
+ | 'months'
+ | 'years';
+
+type InitialValuesType = {
+ startDate: string;
+ startTime: string;
+ endDate: string;
+ endTime: string;
+ startTimezone: string;
+ endTimezone: string;
+};
+
+const initialValues: InitialValuesType = {
+ startDate: new Date().toISOString().split('T')[0],
+ startTime: '00:00',
+ endDate: new Date().toISOString().split('T')[0],
+ endTime: '12:00',
+ startTimezone: 'local',
+ endTimezone: 'local'
+};
+
+const validationSchema = Yup.object({
+ startDate: Yup.string().required('Start date is required'),
+ startTime: Yup.string().required('Start time is required'),
+ endDate: Yup.string().required('End date is required'),
+ endTime: Yup.string().required('End time is required'),
+ startTimezone: Yup.string(),
+ endTimezone: Yup.string()
+});
+
+const timezoneOptions = [
+ { value: 'local', label: 'Local Time' },
+ ...Intl.supportedValuesOf('timeZone')
+ .map((tz) => {
+ const formatter = new Intl.DateTimeFormat('en', {
+ timeZone: tz,
+ timeZoneName: 'shortOffset'
+ });
+
+ const offset =
+ formatter
+ .formatToParts(new Date())
+ .find((part) => part.type === 'timeZoneName')?.value || '';
+
+ return {
+ value: offset.replace('UTC', 'GMT'),
+ label: `${offset.replace('UTC', 'GMT')} (${tz})`
+ };
+ })
+ .sort((a, b) =>
+ a.value.localeCompare(b.value, undefined, { numeric: true })
+ )
+];
+
+const exampleCards: CardExampleType[] = [
+ {
+ title: 'One Year Difference',
+ description: 'Calculate the time between dates that are one year apart',
+ sampleOptions: {
+ startDate: '2023-01-01',
+ startTime: '12:00',
+ endDate: '2024-01-01',
+ endTime: '12:00',
+ startTimezone: 'local',
+ endTimezone: 'local'
+ },
+ sampleResult: '1 year'
+ },
+ {
+ title: 'Different Timezones',
+ description: 'Calculate the time difference between New York and London',
+ sampleOptions: {
+ startDate: '2023-01-01',
+ startTime: '12:00',
+ endDate: '2023-01-01',
+ endTime: '12:00',
+ startTimezone: 'GMT-5',
+ endTimezone: 'GMT'
+ },
+ sampleResult: '5 hours'
+ },
+ {
+ title: 'Detailed Time Breakdown',
+ description: 'Show a detailed breakdown of a time difference',
+ sampleOptions: {
+ startDate: '2023-01-01',
+ startTime: '09:30',
+ endDate: '2023-01-03',
+ endTime: '14:45',
+ startTimezone: 'local',
+ endTimezone: 'local'
+ },
+ sampleResult: '2 days, 5 hours, 15 minutes'
+ }
+];
+
+export default function TimeBetweenDates() {
+ const [result, setResult] = useState('');
+
+ return (
+
+
+ {result}
+
+
+ ) : null
+ }
+ initialValues={initialValues}
+ validationSchema={validationSchema}
+ exampleCards={exampleCards}
+ toolInfo={{
+ title: 'Time Between Dates Calculator',
+ description:
+ 'Calculate the exact time difference between two dates and times, with support for different timezones. This tool provides a detailed breakdown of the time difference in various units (years, months, days, hours, minutes, and seconds).'
+ }}
+ getGroups={({ values, updateField }) => [
+ {
+ title: 'Start Date & Time',
+ component: (
+
+ updateField('startDate', val)}
+ type="date"
+ />
+ updateField('startTime', val)}
+ type="time"
+ />
+ updateField('startTimezone', val)}
+ options={timezoneOptions}
+ />
+
+ )
+ },
+ {
+ title: 'End Date & Time',
+ component: (
+
+ updateField('endDate', val)}
+ type="date"
+ />
+ updateField('endTime', val)}
+ type="time"
+ />
+ updateField('endTimezone', val)}
+ options={timezoneOptions}
+ />
+
+ )
+ }
+ ]}
+ compute={(values) => {
+ try {
+ const startDateTime = getTimeWithTimezone(
+ values.startDate,
+ values.startTime,
+ values.startTimezone
+ );
+
+ const endDateTime = getTimeWithTimezone(
+ values.endDate,
+ values.endTime,
+ values.endTimezone
+ );
+
+ // Calculate time difference
+ const difference = calculateTimeBetweenDates(
+ startDateTime,
+ endDateTime
+ );
+
+ // Auto-determine the best unit to display based on the time difference
+ const bestUnit: TimeUnit =
+ unitHierarchy.find((unit) => difference[unit] > 0) ||
+ 'milliseconds';
+
+ const formattedDifference = formatTimeWithLargestUnit(
+ difference,
+ bestUnit
+ );
+
+ setResult(formattedDifference);
+ } catch (error) {
+ setResult(
+ `Error: ${
+ error instanceof Error
+ ? error.message
+ : 'Failed to calculate time difference'
+ }`
+ );
+ }
+ }}
+ />
+ );
+}
diff --git a/src/pages/tools/time/time-between-dates/meta.ts b/src/pages/tools/time/time-between-dates/meta.ts
new file mode 100644
index 0000000..3676dda
--- /dev/null
+++ b/src/pages/tools/time/time-between-dates/meta.ts
@@ -0,0 +1,22 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+
+export const tool = defineTool('time', {
+ name: 'Time Between Dates',
+ path: 'time-between-dates',
+ icon: 'mdi:calendar-clock',
+ description:
+ 'Calculate the exact time difference between two dates and times, with support for different timezones. This tool provides a detailed breakdown of the time difference in various units (years, months, days, hours, minutes, and seconds).',
+ shortDescription:
+ 'Calculate the precise time duration between two dates with timezone support.',
+ keywords: [
+ 'time',
+ 'dates',
+ 'difference',
+ 'duration',
+ 'calculator',
+ 'timezones',
+ 'interval'
+ ],
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/tools/time/time-between-dates/service.ts b/src/pages/tools/time/time-between-dates/service.ts
new file mode 100644
index 0000000..785f392
--- /dev/null
+++ b/src/pages/tools/time/time-between-dates/service.ts
@@ -0,0 +1,129 @@
+export const unitHierarchy = [
+ 'years',
+ 'months',
+ 'days',
+ 'hours',
+ 'minutes',
+ 'seconds',
+ 'milliseconds'
+] as const;
+
+export type TimeUnit = (typeof unitHierarchy)[number];
+export type TimeDifference = Record;
+
+export const calculateTimeBetweenDates = (
+ startDate: Date,
+ endDate: Date
+): TimeDifference => {
+ if (endDate < startDate) {
+ const temp = startDate;
+ startDate = endDate;
+ endDate = temp;
+ }
+
+ const milliseconds = endDate.getTime() - startDate.getTime();
+ const seconds = Math.floor(milliseconds / 1000);
+ const minutes = Math.floor(seconds / 60);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+
+ // Approximate months and years
+ const startYear = startDate.getFullYear();
+ const startMonth = startDate.getMonth();
+ const endYear = endDate.getFullYear();
+ const endMonth = endDate.getMonth();
+
+ const months = (endYear - startYear) * 12 + (endMonth - startMonth);
+ const years = Math.floor(months / 12);
+
+ return {
+ milliseconds,
+ seconds,
+ minutes,
+ hours,
+ days,
+ months,
+ years
+ };
+};
+
+export const formatTimeDifference = (
+ difference: TimeDifference,
+ includeUnits: TimeUnit[] = unitHierarchy.slice(0, -1)
+): string => {
+ const timeUnits: { key: TimeUnit; value: number; divisor?: number }[] = [
+ { key: 'years', value: difference.years },
+ { key: 'months', value: difference.months, divisor: 12 },
+ { key: 'days', value: difference.days, divisor: 30 },
+ { key: 'hours', value: difference.hours, divisor: 24 },
+ { key: 'minutes', value: difference.minutes, divisor: 60 },
+ { key: 'seconds', value: difference.seconds, divisor: 60 }
+ ];
+
+ const parts = timeUnits
+ .filter(({ key }) => includeUnits.includes(key))
+ .map(({ key, value, divisor }) => {
+ const remaining = divisor ? value % divisor : value;
+ return remaining > 0 ? `${remaining} ${key}` : '';
+ })
+ .filter(Boolean);
+
+ if (parts.length === 0) {
+ if (includeUnits.includes('milliseconds')) {
+ return `${difference.milliseconds} millisecond${
+ difference.milliseconds === 1 ? '' : 's'
+ }`;
+ }
+ return '0 seconds';
+ }
+
+ return parts.join(', ');
+};
+
+export const getTimeWithTimezone = (
+ dateString: string,
+ timeString: string,
+ timezone: string
+): Date => {
+ // Combine date and time
+ const dateTimeString = `${dateString}T${timeString}Z`; // Append 'Z' to enforce UTC parsing
+ const utcDate = new Date(dateTimeString);
+
+ if (isNaN(utcDate.getTime())) {
+ throw new Error('Invalid date or time format');
+ }
+
+ // If timezone is "local", return the local date
+ if (timezone === 'local') {
+ return utcDate;
+ }
+
+ // Extract offset from timezone (e.g., "GMT+5:30" or "GMT-4")
+ const match = timezone.match(/^GMT(?:([+-]\d{1,2})(?::(\d{2}))?)?$/);
+ if (!match) {
+ throw new Error('Invalid timezone format');
+ }
+
+ const offsetHours = match[1] ? parseInt(match[1], 10) : 0;
+ const offsetMinutes = match[2] ? parseInt(match[2], 10) : 0;
+
+ const totalOffsetMinutes =
+ offsetHours * 60 + (offsetHours < 0 ? -offsetMinutes : offsetMinutes);
+
+ // Adjust the UTC date by the timezone offset
+ return new Date(utcDate.getTime() - totalOffsetMinutes * 60 * 1000);
+};
+
+// Helper function to format time based on largest unit
+export const formatTimeWithLargestUnit = (
+ difference: TimeDifference,
+ largestUnit: TimeUnit
+): string => {
+ const largestUnitIndex = unitHierarchy.indexOf(largestUnit);
+ const unitsToInclude = unitHierarchy.slice(largestUnitIndex);
+
+ // Preserve only whole values, do not apply fractional conversions
+ const adjustedDifference: TimeDifference = { ...difference };
+
+ return formatTimeDifference(adjustedDifference, unitsToInclude);
+};
diff --git a/src/pages/tools/time/time-between-dates/time-between-dates.service.test.ts b/src/pages/tools/time/time-between-dates/time-between-dates.service.test.ts
new file mode 100644
index 0000000..5a8db7f
--- /dev/null
+++ b/src/pages/tools/time/time-between-dates/time-between-dates.service.test.ts
@@ -0,0 +1,96 @@
+import { describe, expect, it } from 'vitest';
+import {
+ calculateTimeBetweenDates,
+ formatTimeDifference,
+ formatTimeWithLargestUnit,
+ getTimeWithTimezone
+} from './service';
+
+// Utility function to create a date
+const createDate = (
+ year: number,
+ month: number,
+ day: number,
+ hours = 0,
+ minutes = 0,
+ seconds = 0
+) => new Date(Date.UTC(year, month - 1, day, hours, minutes, seconds));
+describe('calculateTimeBetweenDates', () => {
+ it('should calculate the correct time difference', () => {
+ const startDate = createDate(2023, 1, 1);
+ const endDate = createDate(2024, 1, 1);
+ const result = calculateTimeBetweenDates(startDate, endDate);
+
+ expect(result.years).toBe(1);
+ expect(result.months).toBe(12);
+ expect(result.days).toBeGreaterThanOrEqual(365);
+ });
+
+ it('should swap dates if startDate is after endDate', () => {
+ const startDate = createDate(2024, 1, 1);
+ const endDate = createDate(2023, 1, 1);
+ const result = calculateTimeBetweenDates(startDate, endDate);
+ expect(result.years).toBe(1);
+ });
+});
+
+describe('formatTimeDifference', () => {
+ it('should format time difference correctly', () => {
+ const difference = {
+ years: 1,
+ months: 2,
+ days: 10,
+ hours: 5,
+ minutes: 30,
+ seconds: 0,
+ milliseconds: 0
+ };
+ expect(formatTimeDifference(difference)).toBe(
+ '1 years, 2 months, 10 days, 5 hours, 30 minutes'
+ );
+ });
+
+ it('should return 0 seconds if all values are zero', () => {
+ expect(
+ formatTimeDifference({
+ years: 0,
+ months: 0,
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ seconds: 0,
+ milliseconds: 0
+ })
+ ).toBe('0 seconds');
+ });
+});
+
+describe('getTimeWithTimezone', () => {
+ it('should convert UTC date to specified timezone', () => {
+ const date = getTimeWithTimezone('2025-03-27', '12:00:00', 'GMT+2');
+ expect(date.getUTCHours()).toBe(10); // 12:00 GMT+2 is 10:00 UTC
+ });
+
+ it('should throw error for invalid timezone', () => {
+ expect(() =>
+ getTimeWithTimezone('2025-03-27', '12:00:00', 'INVALID')
+ ).toThrow('Invalid timezone format');
+ });
+});
+
+describe('formatTimeWithLargestUnit', () => {
+ it('should format time with the largest unit', () => {
+ const difference = {
+ years: 0,
+ months: 1,
+ days: 15,
+ hours: 12,
+ minutes: 0,
+ seconds: 0,
+ milliseconds: 0
+ };
+ expect(formatTimeWithLargestUnit(difference, 'days')).toContain(
+ '15 days, 12 hours'
+ );
+ });
+});
diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx
index e32b940..5263c6f 100644
--- a/src/tools/defineTool.tsx
+++ b/src/tools/defineTool.tsx
@@ -21,6 +21,7 @@ export type ToolCategory =
| 'video'
| 'list'
| 'json'
+ | 'time'
| 'csv'
| 'time'
| 'pdf';
diff --git a/src/tools/index.ts b/src/tools/index.ts
index e57bdcc..401cbb3 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -66,6 +66,12 @@ const categoriesConfig: {
value:
'Tools for working with JSON data structures – prettify and minify JSON objects, flatten JSON arrays, stringify JSON values, analyze data, and much more'
},
+ {
+ type: 'time',
+ icon: 'mdi:clock-time-five',
+ value:
+ 'Tools for working with time and date – calculate time differences, convert between time zones, format dates, generate date sequences, and much more.'
+ },
{
type: 'csv',
icon: 'material-symbols-light:csv-outline',