[READ-ONLY] Mirror of https://github.com/probablykasper/date-picker-svelte. Date and time picker for Svelte date-picker-svelte.kasper.space
calendar date date-picker date-time-picker datepicker package svelte time
0

Configure Feed

Select the types of activity you want to include in your feed.

Add MMM format to allow short month names (e.g. Mar) (#91)

* Adds 'M' to allow short month names (e.g. 31-Mar-24')

* Changed to using 'MMM' instead of 'M'

* Run the code through format

* WIP on getting locales to work

* Correct processing of Locale

Previously, createFormat() created a FormatToken[]. Have introduced a new class FormatTokens which behaves exactly like FormatToken[] but also includes a reference to the InnerLocale. That way parse() knows which locale strings to use for months.

Changed the tests to reflect the new structure.

* Updated demo app to include dropdown for locale

* Update locale prop demo

* Format

* Fix lint errors

* Fix `createFormat` parsing locale

* Change parsing logic, make generic parseEnum

* Make parsing case insensitive

* Fix Prop

* Fix build error

* Add locale dropdown to DatePicker demo

* Update CHANGELOG.md

---------

Co-authored-by: Kasper <kasperkh.kh@gmail.com>

authored by

peterbell215
Kasper
and committed by
GitHub
(May 23, 2024, 1:03 AM +0200) b1b321e3 1048ebeb

+197 -55
+3
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + ## Next 4 + - Add MMM format to allow short month names (@peterbell215) 5 + 3 6 ## 2.12.0 - 2024 Apr 9 4 7 - Disable autocomplete for date input (@gianarb) 5 8
+5 -5
src/lib/DateInput.svelte
··· 56 56 let classes = '' 57 57 export { classes as class } 58 58 59 - /** Format string */ 60 - export let format = 'yyyy-MM-dd HH:mm:ss' 61 - let formatTokens = createFormat(format) 62 - $: formatTokens = createFormat(format) 63 - 64 59 /** Locale object for internationalization */ 65 60 export let locale: Locale = {} 61 + 62 + /** Format string */ 63 + export let format = 'yyyy-MM-dd HH:mm:ss' 64 + let formatTokens = createFormat(format, locale) 65 + $: formatTokens = createFormat(format, locale) 66 66 67 67 function valueUpdate(value: Date | null, formatTokens: FormatToken[]) { 68 68 text = toText(value, formatTokens)
+20 -2
src/lib/locale.ts
··· 1 1 export type Locale = { 2 2 weekdays?: string[] 3 3 months?: string[] 4 + shortMonths?: string[] 4 5 weekStartsOn?: number 5 6 } 6 - type InnerLocale = { 7 + export type InnerLocale = { 7 8 weekdays: string[] 8 9 months: string[] 10 + shortMonths: string[] 9 11 weekStartsOn: number 10 12 } 11 13 export function getLocaleDefaults(): InnerLocale { ··· 25 27 'November', 26 28 'December', 27 29 ], 30 + shortMonths: [ 31 + 'Jan', 32 + 'Feb', 33 + 'Mar', 34 + 'Apr', 35 + 'May', 36 + 'Jun', 37 + 'Jul', 38 + 'Aug', 39 + 'Sep', 40 + 'Oct', 41 + 'Nov', 42 + 'Dec', 43 + ], 28 44 weekStartsOn: 1, 29 45 } 30 46 } 31 - export function getInnerLocale(locale: Locale = {}): InnerLocale { 47 + export function getInnerLocale(locale: Locale): InnerLocale { 32 48 const innerLocale = getLocaleDefaults() 33 49 if (typeof locale.weekStartsOn === 'number') { 34 50 innerLocale.weekStartsOn = locale.weekStartsOn 35 51 } 36 52 if (locale.months) innerLocale.months = locale.months 53 + if (locale.shortMonths) innerLocale.shortMonths = locale.shortMonths 37 54 if (locale.weekdays) innerLocale.weekdays = locale.weekdays 38 55 return innerLocale 39 56 } ··· 61 78 62 79 for (let i = 0; i < 12; i++) { 63 80 locale.months[i] = dateFnsLocale.localize.month(i, { width: 'wide' }) 81 + locale.shortMonths[i] = dateFnsLocale.localize.month(i, { width: 'abbreviated' }) 64 82 } 65 83 } 66 84 return locale
+66 -37
src/lib/parse.ts
··· 1 - import { getMonthLength } from './date-utils.js' 1 + import { getMonthLength } from '$lib/date-utils.js' 2 + import { type Locale, type InnerLocale, getInnerLocale } from '$lib/locale' 2 3 3 4 type RuleToken = { 4 5 id: string 6 + allowedValues?: string[] 5 7 toString: (d: Date) => string 6 8 } 7 9 ··· 11 13 date: Date | null 12 14 missingPunctuation: string 13 15 } 16 + 14 17 /** Parse a string according to the supplied format tokens. Returns a date if successful, and the missing punctuation if there is any that should be after the string */ 15 18 export function parse(str: string, tokens: FormatToken[], baseDate: Date | null): ParseResult { 16 19 let missingPunctuation = '' ··· 54 57 } 55 58 } 56 59 60 + function parseEnum(allowedValues: string[]) { 61 + const n = allowedValues.findIndex((allowedValue) => { 62 + return allowedValue.toLowerCase() === str.slice(0, allowedValue.length).toLowerCase() 63 + }) 64 + 65 + if (n >= 0) { 66 + str = str.slice(allowedValues[n].length) 67 + return n 68 + } else { 69 + valid = false 70 + return null 71 + } 72 + } 73 + 57 74 function parseToken(token: FormatToken) { 58 75 if (typeof token === 'string') { 59 76 parseString(token) ··· 66 83 } else if (token.id === 'MM') { 67 84 const value = parseUint(/^[0-9]{2}/, 1, 12) 68 85 if (value !== null) month = value - 1 86 + } else if (token.id === 'MMM') { 87 + const value = parseEnum(token.allowedValues || []) 88 + if (value !== null) month = value 69 89 } else if (token.id === 'dd') { 70 90 const value = parseUint(/^[0-9]{2}/, 1, 31) 71 91 if (value !== null) day = value ··· 101 121 return ('0' + value.toString()).slice(-2) 102 122 } 103 123 104 - const ruleTokens: RuleToken[] = [ 105 - { 106 - id: 'yyyy', 107 - toString: (d: Date) => d.getFullYear().toString(), 108 - }, 109 - { 110 - id: 'yy', 111 - toString: (d: Date) => d.getFullYear().toString().slice(-2), 112 - }, 113 - { 114 - id: 'MM', 115 - toString: (d: Date) => twoDigit(d.getMonth() + 1), 116 - }, 117 - { 118 - id: 'dd', 119 - toString: (d: Date) => twoDigit(d.getDate()), 120 - }, 121 - { 122 - id: 'HH', 123 - toString: (d: Date) => twoDigit(d.getHours()), 124 - }, 125 - { 126 - id: 'mm', 127 - toString: (d: Date) => twoDigit(d.getMinutes()), 128 - }, 129 - { 130 - id: 'ss', 131 - toString: (d: Date) => twoDigit(d.getSeconds()), 132 - }, 133 - ] 134 - function parseRule(s: string) { 135 - for (const token of ruleTokens) { 136 - if (s.startsWith(token.id)) { 137 - return token 124 + function parseRule(s: string, innerLocale: InnerLocale) { 125 + if (s.startsWith('yyyy')) { 126 + return { 127 + id: 'yyyy', 128 + toString: (d: Date) => d.getFullYear().toString(), 129 + } 130 + } else if (s.startsWith('yy')) { 131 + return { 132 + id: 'yy', 133 + toString: (d: Date) => d.getFullYear().toString().slice(-2), 134 + } 135 + } else if (s.startsWith('MMM')) { 136 + return { 137 + id: 'MMM', 138 + allowedValues: innerLocale.shortMonths, 139 + toString: (d: Date) => innerLocale.shortMonths[d.getMonth()], 140 + } 141 + } else if (s.startsWith('MM')) { 142 + return { 143 + id: 'MM', 144 + toString: (d: Date) => twoDigit(d.getMonth() + 1), 145 + } 146 + } else if (s.startsWith('dd')) { 147 + return { 148 + id: 'dd', 149 + toString: (d: Date) => twoDigit(d.getDate()), 150 + } 151 + } else if (s.startsWith('HH')) { 152 + return { 153 + id: 'HH', 154 + toString: (d: Date) => twoDigit(d.getHours()), 155 + } 156 + } else if (s.startsWith('mm')) { 157 + return { 158 + id: 'mm', 159 + toString: (d: Date) => twoDigit(d.getMinutes()), 160 + } 161 + } else if (s.startsWith('ss')) { 162 + return { 163 + id: 'ss', 164 + toString: (d: Date) => twoDigit(d.getSeconds()), 138 165 } 139 166 } 140 167 } 141 168 142 - export function createFormat(s: string): FormatToken[] { 169 + export function createFormat(s: string, locale: Locale = {}): FormatToken[] { 170 + const innerLocale = getInnerLocale(locale) 143 171 const tokens = [] 144 172 while (s.length > 0) { 145 - const token = parseRule(s) 173 + const token = parseRule(s, innerLocale) 146 174 if (token) { 147 175 // parsed a token like "yyyy" 148 176 tokens.push(token) ··· 157 185 s = s.slice(1) 158 186 } 159 187 } 188 + 160 189 return tokens 161 190 }
+62 -5
src/lib/utils.test.ts
··· 80 80 }) 81 81 }) 82 82 83 - test('toText', () => { 84 - const format = createFormat('yyyy-MM-dd HH:mm:ss') 85 - const text = toText(new Date(2020, 0, 1, 0, 0, 0, 0), format) 86 - expect(text).toEqual('2020-01-01 00:00:00') 83 + describe('toText', () => { 84 + test('basic conversion', () => { 85 + const format = createFormat('yyyy-MM-dd HH:mm:ss') 86 + const text = toText(new Date(2020, 0, 1, 0, 0, 0, 0), format) 87 + expect(text).toEqual('2020-01-01 00:00:00') 88 + }) 89 + 90 + test('conversion to month string', () => { 91 + const format = createFormat('dd MMM yyyy HH:mm:ss') 92 + const text = toText(new Date(2020, 0, 1, 0, 0, 0, 0), format) 93 + expect(text).toEqual('01 Jan 2020 00:00:00') 94 + }) 95 + 96 + test('conversion to month string using non-en locale', () => { 97 + const format = createFormat('dd MMM yyyy HH:mm:ss', localeFromDateFnsLocale(nb)) 98 + const text = toText(new Date(2020, 0, 1, 0, 0, 0, 0), format) 99 + expect(text).toEqual('01 jan. 2020 00:00:00') 100 + }) 87 101 }) 88 102 89 - describe('Formatting', () => { 103 + describe('parse()', () => { 90 104 const baseDate = new Date(1234, 0, 1, 0, 0, 0, 999) 91 105 const format = createFormat('yyyy--MM-dd HH:mm:ss') 92 106 ··· 98 112 }) 99 113 }) 100 114 115 + it('works with a short month date', () => { 116 + const format = createFormat('dd MMM yyyy HH:mm:ss') 117 + const result = parse('31 Dec 2022 23:59:59', format, baseDate) 118 + expect(result).toEqual({ 119 + date: new Date(2022, 11, 31, 23, 59, 59, 999), 120 + missingPunctuation: '', 121 + }) 122 + }) 123 + 124 + it('works with a short month date in non-En locale', () => { 125 + const format = createFormat('dd MMM yyyy HH:mm:ss', localeFromDateFnsLocale(nb)) 126 + const result = parse('31 des. 2022 23:59:59', format, baseDate) 127 + expect(result).toEqual({ 128 + date: new Date(2022, 11, 31, 23, 59, 59, 999), 129 + missingPunctuation: '', 130 + }) 131 + }) 132 + 133 + it('handles badly formed month name', () => { 134 + const format = createFormat('dd MMM yyyy HH:mm:ss') 135 + const result = parse('31 Dex 2022 23:59:59', format, baseDate) 136 + expect(result).toEqual({ 137 + date: null, 138 + missingPunctuation: '', 139 + }) 140 + }) 141 + 101 142 it('handles missing punctuation', () => { 102 143 const result = parse('2345', format, baseDate) 103 144 expect(result).toEqual({ ··· 149 190 'november', 150 191 'desember', 151 192 ], 193 + shortMonths: [ 194 + 'jan.', 195 + 'feb.', 196 + 'mars', 197 + 'apr.', 198 + 'mai', 199 + 'juni', 200 + 'juli', 201 + 'aug.', 202 + 'sep.', 203 + 'okt.', 204 + 'nov.', 205 + 'des.', 206 + ], 152 207 weekStartsOn: 1, 153 208 } 154 209 155 210 test('getInnerLocale', () => { 156 211 const locale = getInnerLocale({ 157 212 months: nbLocale.months, 213 + shortMonths: nbLocale.shortMonths, 158 214 weekStartsOn: 4, 159 215 }) 160 216 161 217 expect(locale).toEqual({ 162 218 weekdays: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], 163 219 months: nbLocale.months, 220 + shortMonths: nbLocale.shortMonths, 164 221 weekStartsOn: 4, 165 222 }) 166 223 })
+16 -1
src/routes/DateInput.svelte
··· 2 2 import DateInput from '$lib/DateInput.svelte' 3 3 import Prop from './prop.svelte' 4 4 import Split from './split.svelte' 5 + import { localeFromDateFnsLocale } from '$lib' 6 + 7 + // had to import it this way to avoid errors 8 + // in `npm run build:site` or `npm run check`: 9 + import hy from 'date-fns/locale/hy/index' 10 + import de from 'date-fns/locale/de/index' 11 + import nb from 'date-fns/locale/nb/index' 5 12 6 13 let id: string 7 14 let placeholder: string ··· 17 24 let format: string 18 25 let dynamicPositioning: boolean = true 19 26 let timePrecision: 'minute' | 'second' | 'millisecond' | null = null 27 + let locales = [ 28 + { key: 'default', value: localeFromDateFnsLocale({}) }, 29 + { key: 'nb (date-fns)', value: localeFromDateFnsLocale(nb) }, 30 + { key: 'de (date-fns)', value: localeFromDateFnsLocale(de) }, 31 + { key: 'hy (date-fns)', value: localeFromDateFnsLocale(hy) }, 32 + ] 33 + let locale = locales[0] 20 34 </script> 21 35 22 36 <Split> ··· 36 50 bind:browseWithoutSelecting 37 51 bind:dynamicPositioning 38 52 bind:timePrecision 53 + bind:locale={locale.value} 39 54 /> 40 55 41 56 <svelte:fragment slot="right"> ··· 53 68 <Prop label="closeOnSelection" bind:value={closeOnSelection} /> 54 69 <Prop label="browseWithoutSelecting" bind:value={browseWithoutSelecting} /> 55 70 <Prop label="dynamicPositioning" bind:value={dynamicPositioning} /> 56 - <Prop label="locale">Default</Prop> 71 + <Prop label="locale" bind:value={locale} values={locales} /> 57 72 <Prop 58 73 label="timePrecision" 59 74 bind:value={timePrecision}
+20 -4
src/routes/DatePicker.svelte
··· 6 6 7 7 // had to import it this way to avoid errors 8 8 // in `npm run build:site` or `npm run check`: 9 - import hy from 'date-fns/locale/hy/index.js' 9 + import hy from 'date-fns/locale/hy/index' 10 + import de from 'date-fns/locale/de/index' 11 + import nb from 'date-fns/locale/nb/index' 12 + 10 13 let value: Date 11 14 let min: Date 12 15 let max: Date 13 - let locale = localeFromDateFnsLocale(hy) 16 + let locales = [ 17 + { key: 'default', value: localeFromDateFnsLocale({}) }, 18 + { key: 'nb (date-fns)', value: localeFromDateFnsLocale(nb) }, 19 + { key: 'de (date-fns)', value: localeFromDateFnsLocale(de) }, 20 + { key: 'hy (date-fns)', value: localeFromDateFnsLocale(hy) }, 21 + ] 22 + let locale = locales[0] 14 23 let browseWithoutSelecting: boolean 15 24 let timePrecision: 'minute' | 'second' | 'millisecond' | null = 'millisecond' 16 25 </script> 17 26 18 27 <Split> 19 28 <div class="left" slot="left"> 20 - <DatePicker bind:value bind:min bind:max {locale} bind:browseWithoutSelecting {timePrecision} /> 29 + <DatePicker 30 + bind:value 31 + bind:min 32 + bind:max 33 + locale={locale.value} 34 + bind:browseWithoutSelecting 35 + {timePrecision} 36 + /> 21 37 </div> 22 38 <div slot="right"> 23 39 <h3 class="no-top">Props</h3> 24 40 <Prop label="value">{value}</Prop> 25 41 <Prop label="min" bind:value={min} /> 26 42 <Prop label="max" bind:value={max} /> 27 - <Prop label="locale">date-fns <code>hy</code></Prop> 43 + <Prop label="locale" bind:value={locale} values={locales} /> 28 44 <Prop label="browseWithoutSelecting" bind:value={browseWithoutSelecting} /> 29 45 <Prop 30 46 label="timePrecision"
+5 -1
src/routes/prop.svelte
··· 22 22 {#if values} 23 23 <select bind:value> 24 24 {#each values as value} 25 - <option {value}>{String(value)}</option> 25 + {#if value && typeof value === 'object' && 'key' in value && 'value' in value} 26 + <option {value}>{String(value.key)}</option> 27 + {:else} 28 + <option {value}>{String(value)}</option> 29 + {/if} 26 30 {/each} 27 31 </select> 28 32 {:else if typeof value === 'string'}