mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-21 23:19:30 +02:00
Merge branch 'main' into chesterkxng
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
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';
|
||||
@@ -9,5 +10,6 @@ export const timeTools = [
|
||||
hoursToDays,
|
||||
convertSecondsToTime,
|
||||
convertTimetoSeconds,
|
||||
truncateClockTime
|
||||
truncateClockTime,
|
||||
timeBetweenDates
|
||||
];
|
||||
|
250
src/pages/tools/time/time-between-dates/index.tsx
Normal file
250
src/pages/tools/time/time-between-dates/index.tsx
Normal file
@@ -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<InitialValuesType>[] = [
|
||||
{
|
||||
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<string>('');
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title="Time Between Dates"
|
||||
inputComponent={null}
|
||||
resultComponent={
|
||||
result ? (
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 3,
|
||||
borderLeft: '5px solid',
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: 'background.paper',
|
||||
maxWidth: '100%',
|
||||
mx: 'auto'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h4"
|
||||
align="center"
|
||||
sx={{ fontWeight: 'bold', color: 'primary.main' }}
|
||||
>
|
||||
{result}
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : 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: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description="Start Date"
|
||||
value={values.startDate}
|
||||
onOwnChange={(val) => updateField('startDate', val)}
|
||||
type="date"
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description="Start Time"
|
||||
value={values.startTime}
|
||||
onOwnChange={(val) => updateField('startTime', val)}
|
||||
type="time"
|
||||
/>
|
||||
<SelectWithDesc
|
||||
description="Start Timezone"
|
||||
selected={values.startTimezone}
|
||||
onChange={(val: string) => updateField('startTimezone', val)}
|
||||
options={timezoneOptions}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'End Date & Time',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description="End Date"
|
||||
value={values.endDate}
|
||||
onOwnChange={(val) => updateField('endDate', val)}
|
||||
type="date"
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description="End Time"
|
||||
value={values.endTime}
|
||||
onOwnChange={(val) => updateField('endTime', val)}
|
||||
type="time"
|
||||
/>
|
||||
<SelectWithDesc
|
||||
description="End Timezone"
|
||||
selected={values.endTimezone}
|
||||
onChange={(val: string) => updateField('endTimezone', val)}
|
||||
options={timezoneOptions}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
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'
|
||||
}`
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
22
src/pages/tools/time/time-between-dates/meta.ts
Normal file
22
src/pages/tools/time/time-between-dates/meta.ts
Normal file
@@ -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: 'tabler:clock-minus',
|
||||
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'))
|
||||
});
|
129
src/pages/tools/time/time-between-dates/service.ts
Normal file
129
src/pages/tools/time/time-between-dates/service.ts
Normal file
@@ -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<TimeUnit, number>;
|
||||
|
||||
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);
|
||||
};
|
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user