Skip to content

Commit 3beb67f

Browse files
committed
feat: add no specific value #25
1 parent 7b09c1e commit 3beb67f

File tree

8 files changed

+160
-46
lines changed

8 files changed

+160
-46
lines changed

core/src/components/__tests__/cron-core.spec.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { describe, expect, it } from 'vitest'
22

3-
import { useCron, type CronFormat } from '../cron-core';
3+
import { useCron, type CronFormat } from '../cron-core'
44

55
type UseCronReturn = ReturnType<typeof useCron>
66

7-
function cronToString({selected: { value: selected }, period, }:UseCronReturn): string {
8-
const fields = selected.map((seg) => {
9-
const prefix = seg.prefix.value ? seg.prefix.value+' ' : '';
10-
const suffix = seg.suffix.value ? ' '+seg.suffix.value : '';
11-
return prefix+seg.text.value+suffix
12-
}).join(' ')
7+
function cronToString({ selected: { value: selected }, period }: UseCronReturn): string {
8+
const fields = selected
9+
.map((seg) => {
10+
const prefix = seg.prefix.value ? seg.prefix.value + ' ' : ''
11+
const suffix = seg.suffix.value ? ' ' + seg.suffix.value : ''
12+
return prefix + seg.text.value + suffix
13+
})
14+
.join(' ')
1315

14-
const prefix = period.prefix.value ? period.prefix.value+' ' : '';
15-
const suffix = period.suffix.value ? ' '+period.suffix.value : '';
16+
const prefix = period.prefix.value ? period.prefix.value + ' ' : ''
17+
const suffix = period.suffix.value ? ' ' + period.suffix.value : ''
1618
return `${prefix}${period.selected.value.text}${suffix} ${fields}`
1719
}
1820

1921
describe('useCron', () => {
2022
it('renders properly', () => {
2123
const tests: {
22-
format: CronFormat,
23-
value: string,
24-
expected: string,
24+
format: CronFormat
25+
value: string
26+
expected: string
2527
}[] = [
2628
{
2729
format: 'crontab',
@@ -35,8 +37,8 @@ describe('useCron', () => {
3537
}
3638
]
3739

38-
for(const t of tests) {
39-
const cron = useCron({format: t.format, initialValue: t.value})
40+
for (const t of tests) {
41+
const cron = useCron({ format: t.format, initialValue: t.value })
4042
expect(cronToString(cron)).toEqual(t.expected)
4143
}
4244
})

core/src/components/cron-core.ts

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
import { AnySegment, EverySegment, NoSpecificSegment, RangeSegment, ValueSegment } from '@/cron'
12
import type { Localization } from '@/locale/types'
23
import { computed, defineComponent, ref, watch, type PropType } from 'vue'
34
import { getLocale } from '../locale'
45
import { FieldWrapper, TextPosition, type Field, type Period } from '../types'
56
import { defaultItems } from '../util'
6-
import { useCronSegment } from './cron-segment'
7+
import { useCronSegment, type UseCronSegmentReturn } from './cron-segment'
78

89
export type CronFormat = 'crontab' | 'quartz'
910

10-
interface CronOptions {
11+
export interface CronOptions {
1112
initialValue?: string
1213
locale?: string
1314
fields?: Field[]
@@ -16,6 +17,10 @@ interface CronOptions {
1617
format?: CronFormat
1718
}
1819

20+
export interface CronContext {
21+
segmentMap: Map<string, UseCronSegmentReturn>
22+
}
23+
1924
function createCron(len: number, seg: string = '*') {
2025
return new Array(len).fill(seg).join(' ')
2126
}
@@ -33,24 +38,65 @@ class DefaultCronOptions {
3338
return createCron(len, seg)
3439
}
3540

36-
fields(format: CronFormat, locale: string) {
41+
fields(format: CronFormat, locale: string): Field[] {
42+
const isQuartz = format == 'quartz'
3743
const items = defaultItems(locale)
3844

45+
const setNoSpecific = (fieldId: string) => {
46+
return (value: UseCronSegmentReturn, { segmentMap }: CronContext) => {
47+
if (value.cron.value == '?') {
48+
return
49+
}
50+
51+
const segment = segmentMap.get(fieldId)
52+
if (!segment) {
53+
return
54+
}
55+
segment.cron.value = '?'
56+
}
57+
}
58+
3959
return [
40-
...(format == 'quartz' ? [{ id: 'second', items: items.secondItems }] : []),
60+
...(isQuartz ? [{ id: 'second', items: items.secondItems }] : []),
4161
{ id: 'minute', items: items.minuteItems },
4262
{ id: 'hour', items: items.hourItems },
43-
{ id: 'day', items: items.dayItems },
63+
{
64+
id: 'day',
65+
items: items.dayItems,
66+
onChange: isQuartz ? setNoSpecific('dayOfWeek') : undefined,
67+
segmentFactories: isQuartz
68+
? [
69+
AnySegment.fromString,
70+
NoSpecificSegment.fromString,
71+
EverySegment.fromString,
72+
RangeSegment.fromString,
73+
ValueSegment.fromString
74+
]
75+
: undefined
76+
},
4477
{ id: 'month', items: items.monthItems },
45-
{ id: 'dayOfWeek', items: items.dayOfWeekItems }
78+
{
79+
id: 'dayOfWeek',
80+
items: items.dayOfWeekItems,
81+
onChange: isQuartz ? setNoSpecific('day') : undefined,
82+
segmentFactories: isQuartz
83+
? [
84+
AnySegment.fromString,
85+
NoSpecificSegment.fromString,
86+
EverySegment.fromString,
87+
RangeSegment.fromString,
88+
ValueSegment.fromString
89+
]
90+
: undefined
91+
}
4692
]
4793
}
4894

4995
periods(format: CronFormat) {
50-
const isQuartz = format == 'quartz';
96+
const isQuartz = format == 'quartz'
5197
const second = isQuartz ? [{ id: 'q-second', value: [] }] : []
5298
const secondField = isQuartz ? ['second'] : []
53-
const prefix = isQuartz ? 'q-' : '';
99+
const prefix = isQuartz ? 'q-' : ''
54100

55101
return [
56102
...second,
@@ -143,7 +189,10 @@ export function useCron(options: CronOptions) {
143189
})
144190

145191
segments.forEach((s) => {
146-
watch(s.cron, toCron)
192+
watch(s.cron, () => {
193+
s.onChange?.(s, { segmentMap })
194+
toCron()
195+
})
147196
watch(s.error, (value) => {
148197
error.value = value
149198
})

core/src/components/cron-segment.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { CombinedSegment, arrayToSegment, cronToSegment, type CronSegment } from '@/cron'
1+
import { CombinedSegment, arrayToSegment, cronToSegment } from '@/cron'
22
import type { Locale } from '@/locale'
3-
import { TextPosition, type FieldWrapper, type Period } from '@/types'
3+
import { TextPosition, type CronSegment, type FieldWrapper, type Period } from '@/types'
44
import { ref, watch, type Ref } from 'vue'
55

66
export interface FieldOptions {
@@ -48,6 +48,10 @@ export function useCronSegment(options: FieldOptions) {
4848
}
4949

5050
const toCron = (value: number[]) => {
51+
if (cron.value == '?' && value.length == 0) {
52+
return
53+
}
54+
5155
const seg = arrayToSegment(value, field)
5256
if (seg != null) {
5357
cron.value = seg.toCron()
@@ -84,6 +88,7 @@ export function useCronSegment(options: FieldOptions) {
8488
return {
8589
id: field.id,
8690
items: field.items,
91+
onChange: field.onChange,
8792
cron,
8893
selected,
8994
error,
@@ -93,3 +98,5 @@ export function useCronSegment(options: FieldOptions) {
9398
suffix
9499
}
95100
}
101+
102+
export type UseCronSegmentReturn = ReturnType<typeof useCronSegment>

core/src/cron.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
1-
import { CronType, FieldWrapper, type FieldItem } from './types'
1+
import { CronType, FieldWrapper, type CronSegment, type SegmentFromString } from './types'
22
import { isSquence, range, unimplemented } from './util'
33

4-
type SegmentFromArray = (arr: number[], field: FieldWrapper) => CronSegment | null
5-
type SegmentFromString = (str: string, field: FieldWrapper) => CronSegment | null
6-
7-
export interface CronSegment {
4+
class NoSpecificSegment implements CronSegment {
85
field: FieldWrapper
9-
type: CronType
10-
toCron: () => string
11-
toArray: () => number[]
12-
items: Record<string, FieldItem>
6+
type: CronType = CronType.NoSpecific
7+
8+
constructor(field: FieldWrapper) {
9+
this.field = field
10+
}
11+
12+
toCron() {
13+
return '?'
14+
}
15+
16+
toArray() {
17+
return []
18+
}
19+
20+
get items() {
21+
return {}
22+
}
23+
24+
static fromString(str: string, field: FieldWrapper) {
25+
if (str !== '?') {
26+
return null
27+
}
28+
return new NoSpecificSegment(field)
29+
}
1330
}
1431

1532
class AnySegment implements CronSegment {
@@ -284,6 +301,7 @@ class CombinedSegment implements CronSegment {
284301
}
285302

286303
static fromString(str: string, field: FieldWrapper) {
304+
const factories = field.segmentFactories ?? CombinedSegment.segmentFactories
287305
let segments: CronSegment[] = []
288306
for (const strSeg of str.split(',')) {
289307
if (strSeg === '*') {
@@ -292,7 +310,7 @@ class CombinedSegment implements CronSegment {
292310
}
293311

294312
let segment = null
295-
for (const fromString of CombinedSegment.segmentFactories) {
313+
for (const fromString of factories) {
296314
segment = fromString(strSeg, field)
297315
if (segment !== null) {
298316
break
@@ -358,6 +376,7 @@ export {
358376
AnySegment,
359377
CombinedSegment,
360378
EverySegment,
379+
NoSpecificSegment,
361380
RangeSegment,
362381
ValueSegment,
363382
arrayToSegment,

core/src/locale/de.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ const locale: Localization = {
2828
everyX: {
2929
prefix: '',
3030
text: 'alle {{every.value}} Tage'
31+
},
32+
noSpecific: {
33+
prefix: 'an',
34+
text: 'keinem bestimmten Tag'
3135
}
3236
},
3337
dayOfWeek: {
@@ -37,7 +41,11 @@ const locale: Localization = {
3741
text: 'jedem Wochentag'
3842
},
3943
value: { text: '{{value.alt}}' },
40-
range: { text: '{{start.alt}}-{{end.alt}}' }
44+
range: { text: '{{start.alt}}-{{end.alt}}' },
45+
noSpecific: {
46+
prefix: 'und',
47+
text: 'keinem bestimmten Wochentag'
48+
}
4149
},
4250
hour: {
4351
'*': { prefix: 'um' },
@@ -104,8 +112,8 @@ const locale: Localization = {
104112
text: 'Minute',
105113
second: {
106114
'*': {
107-
prefix: 'und',
108-
},
115+
prefix: 'und'
116+
}
109117
}
110118
},
111119
'q-hour': {
@@ -120,7 +128,7 @@ const locale: Localization = {
120128
prefix: 'und'
121129
}
122130
}
123-
},
131+
}
124132
}
125133

126134
export default locale

core/src/locale/en.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@ const locale: Localization = {
1717
range: { text: '{{start.alt}}-{{end.alt}}' }
1818
},
1919
day: {
20-
'*': { prefix: 'on' }
20+
'*': { prefix: 'on' },
21+
noSpecific: {
22+
text: 'no specific day'
23+
}
2124
},
2225
dayOfWeek: {
2326
'*': { prefix: 'on' },
2427
empty: { text: 'every day of the week' },
2528
value: { text: '{{value.alt}}' },
26-
range: { text: '{{start.alt}}-{{end.alt}}' }
29+
range: { text: '{{start.alt}}-{{end.alt}}' },
30+
noSpecific: {
31+
text: 'no specific day of the week'
32+
}
2733
},
2834
hour: {
2935
'*': { prefix: 'at' }
@@ -88,7 +94,7 @@ const locale: Localization = {
8894
prefix: 'at'
8995
}
9096
}
91-
},
97+
}
9298
}
9399

94100
export default locale

core/src/types.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1+
import type { CronContext } from './components/cron-core'
2+
import type { UseCronSegmentReturn } from './components/cron-segment'
3+
4+
export interface CronSegment {
5+
field: FieldWrapper
6+
type: CronType
7+
toCron: () => string
8+
toArray: () => number[]
9+
items: Record<string, FieldItem>
10+
}
11+
12+
export type SegmentFromArray = (arr: number[], field: FieldWrapper) => CronSegment | null
13+
export type SegmentFromString = (str: string, field: FieldWrapper) => CronSegment | null
14+
115
export enum CronType {
216
Empty = 'empty',
317
Value = 'value',
418
Range = 'range',
519
EveryX = 'everyX',
6-
Combined = 'combined'
20+
Combined = 'combined',
21+
NoSpecific = 'noSpecific'
722
}
823

924
export enum TextPosition {
@@ -21,6 +36,8 @@ export interface FieldItem {
2136
export interface Field {
2237
id: string
2338
items: FieldItem[]
39+
onChange?: (segment: UseCronSegmentReturn, ctx: CronContext) => void
40+
segmentFactories?: SegmentFromString[]
2441
}
2542

2643
export interface Period {
@@ -50,6 +67,12 @@ export class FieldWrapper {
5067
get items() {
5168
return this.field.items
5269
}
70+
get onChange() {
71+
return this.field.onChange
72+
}
73+
get segmentFactories() {
74+
return this.field.segmentFactories
75+
}
5376

5477
get min() {
5578
return this.items[0].value

0 commit comments

Comments
 (0)