mirror of
https://github.com/iritmaximus/workh.git
synced 2026-10-12 19:13:33 +03:00
[HourInput] Read 7.15 as 7h15min, not 7h9min
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
Log your daily hours, see how much you are over/under your weekly target,
|
Log your daily hours, see how much you are over/under your weekly target,
|
||||||
and keep a running balance of extra/minus hours.
|
and keep a running balance of extra/minus hours.
|
||||||
|
|
||||||
- Enter each day as a **total** (`7.5`, `7:15`, `7h15`; 0–24 h) — the default — or as
|
- Enter each day as a **total** (`7.15`, `7:15`, `7h15` = 7 h 15 min; `7.5` = 7 h 30 min; 0–24 h) — the default — or as
|
||||||
**start–end + break**, or mark it **off** (vacation, sick, holiday → no target).
|
**start–end + break**, or mark it **off** (vacation, sick, holiday → no target).
|
||||||
- Full-time target defaults to **36 h 15 min/week** (7:15 per weekday).
|
- Full-time target defaults to **36 h 15 min/week** (7:15 per weekday).
|
||||||
- **Workload periods** (Settings) scale the target from a given date, e.g. 50% or 25%.
|
- **Workload periods** (Settings) scale the target from a given date, e.g. 50% or 25%.
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ test.describe('entering total hours', () => {
|
|||||||
for (const [typed, minutes] of [
|
for (const [typed, minutes] of [
|
||||||
['7.5', 450],
|
['7.5', 450],
|
||||||
['7,5', 450],
|
['7,5', 450],
|
||||||
|
['7.30', 450],
|
||||||
|
['7,30', 450],
|
||||||
['7:30', 450],
|
['7:30', 450],
|
||||||
['7h30', 450],
|
['7h30', 450],
|
||||||
['7h 30min', 450],
|
['7h 30min', 450],
|
||||||
|
|||||||
+8
-6
@@ -37,8 +37,10 @@ export function weekDays(monday: string): string[] {
|
|||||||
// ---------- Durations ----------
|
// ---------- Durations ----------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses "7.5", "7,5", "7:30", "7h30", "7h 30min", "45m" or "-1:30" into minutes.
|
* Parses "7.5", "7,5", "7.15", "7:30", "7h30", "7h 30min", "45m" or "-1:30" into minutes.
|
||||||
* Returns null for anything it does not understand; "" also returns null.
|
* Two digits after "." or "," are minutes like after ":" ("7.15" = 7 h 15 min); one digit is
|
||||||
|
* a decimal hour ("7.5" = 7 h 30 min). Returns null for anything it does not understand;
|
||||||
|
* "" also returns null.
|
||||||
*/
|
*/
|
||||||
export function parseDuration(input: string): number | null {
|
export function parseDuration(input: string): number | null {
|
||||||
const s = input.trim().toLowerCase().replace(/\s+/g, '');
|
const s = input.trim().toLowerCase().replace(/\s+/g, '');
|
||||||
@@ -47,11 +49,11 @@ export function parseDuration(input: string): number | null {
|
|||||||
const body = s.replace(/^[+-]/, '');
|
const body = s.replace(/^[+-]/, '');
|
||||||
|
|
||||||
let m: RegExpMatchArray | null;
|
let m: RegExpMatchArray | null;
|
||||||
if ((m = body.match(/^(\d+)(?:[.,](\d+))?h?$/))) {
|
if ((m = body.match(/^(\d+)(?:[.,](\d))?h?$/))) {
|
||||||
return sign * Math.round(parseFloat(`${m[1]}.${m[2] ?? '0'}`) * 60);
|
return sign * (Number(m[1]) * 60 + Number(m[2] ?? 0) * 6);
|
||||||
}
|
}
|
||||||
if ((m = body.match(/^(\d+):(\d{1,2})$/)) && Number(m[2]) < 60) {
|
if ((m = body.match(/^(\d+)(?::(\d{1,2})|[.,](\d{2}))$/)) && Number(m[2] ?? m[3]) < 60) {
|
||||||
return sign * (Number(m[1]) * 60 + Number(m[2]));
|
return sign * (Number(m[1]) * 60 + Number(m[2] ?? m[3]));
|
||||||
}
|
}
|
||||||
if ((m = body.match(/^(\d+)h(\d{1,2})(?:m|min)?$/)) && Number(m[2]) < 60) {
|
if ((m = body.match(/^(\d+)h(\d{1,2})(?:m|min)?$/)) && Number(m[2]) < 60) {
|
||||||
return sign * (Number(m[1]) * 60 + Number(m[2]));
|
return sign * (Number(m[1]) * 60 + Number(m[2]));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Free-text duration field ("7.5", "7:15", "7h15"). Valid input is committed as you type;
|
* Free-text duration field ("7.5", "7.15", "7:15", "7h15"). Valid input is committed as you type;
|
||||||
* on blur the text is normalised to h:mm. Invalid text is kept and outlined until fixed.
|
* on blur the text is normalised to h:mm. Invalid text is kept and outlined until fixed.
|
||||||
*/
|
*/
|
||||||
export function DurationInput({ value, onChange, placeholder, ariaLabel, min = -Infinity, max = Infinity }: Props) {
|
export function DurationInput({ value, onChange, placeholder, ariaLabel, min = -Infinity, max = Infinity }: Props) {
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ const fullWeek = (minutes: number) =>
|
|||||||
describe('parseDuration', () => {
|
describe('parseDuration', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['7.5', 450],
|
['7.5', 450],
|
||||||
['7,25', 435],
|
['7,5', 450],
|
||||||
|
['7.15', 435],
|
||||||
|
['7,15', 435],
|
||||||
|
['7.45', 465],
|
||||||
|
['7.05', 425],
|
||||||
['7:15', 435],
|
['7:15', 435],
|
||||||
['7h15', 435],
|
['7h15', 435],
|
||||||
['7h 15min', 435],
|
['7h 15min', 435],
|
||||||
@@ -45,7 +49,9 @@ describe('parseDuration', () => {
|
|||||||
['8', 480],
|
['8', 480],
|
||||||
])('%s -> %i', (input, expected) => expect(parseDuration(input)).toBe(expected));
|
])('%s -> %i', (input, expected) => expect(parseDuration(input)).toBe(expected));
|
||||||
|
|
||||||
it.each(['', 'abc', '7:75', '7h99'])('rejects %j', (input) => expect(parseDuration(input)).toBeNull());
|
it.each(['', 'abc', '7:75', '7h99', '7.75', '7.123'])('rejects %j', (input) =>
|
||||||
|
expect(parseDuration(input)).toBeNull(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('formatting', () => {
|
describe('formatting', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user