Additional datetime funcs

b96996dc12c52fdefb6da21a52aa9ad23b81066a
This commit is contained in:
4eb0da
2024-05-21 12:55:36 +03:00
parent 8dcad541b2
commit ee39df37be
23 changed files with 2166 additions and 64 deletions
+2
View File
@@ -12839,6 +12839,7 @@
"client/web/divkit/src/utils/flattenAnimation.ts":"divkit/public/client/web/divkit/src/utils/flattenAnimation.ts",
"client/web/divkit/src/utils/flattenTransition.ts":"divkit/public/client/web/divkit/src/utils/flattenTransition.ts",
"client/web/divkit/src/utils/fontWeightToCss.ts":"divkit/public/client/web/divkit/src/utils/fontWeightToCss.ts",
"client/web/divkit/src/utils/formatDate.ts":"divkit/public/client/web/divkit/src/utils/formatDate.ts",
"client/web/divkit/src/utils/genClassName.ts":"divkit/public/client/web/divkit/src/utils/genClassName.ts",
"client/web/divkit/src/utils/getMarginBox.ts":"divkit/public/client/web/divkit/src/utils/getMarginBox.ts",
"client/web/divkit/src/utils/getTouchCoords.ts":"divkit/public/client/web/divkit/src/utils/getTouchCoords.ts",
@@ -15094,6 +15095,7 @@
"client/web/divkit/tests/utils/flattenAnimation.test.ts":"divkit/public/client/web/divkit/tests/utils/flattenAnimation.test.ts",
"client/web/divkit/tests/utils/flattenTransition.test.ts":"divkit/public/client/web/divkit/tests/utils/flattenTransition.test.ts",
"client/web/divkit/tests/utils/fontWeightToCss.test.ts":"divkit/public/client/web/divkit/tests/utils/fontWeightToCss.test.ts",
"client/web/divkit/tests/utils/formatDate.test.ts":"divkit/public/client/web/divkit/tests/utils/formatDate.test.ts",
"client/web/divkit/tests/utils/genClassName.test.ts":"divkit/public/client/web/divkit/tests/utils/genClassName.test.ts",
"client/web/divkit/tests/utils/gridCalcTemplates.test.ts":"divkit/public/client/web/divkit/tests/utils/gridCalcTemplates.test.ts",
"client/web/divkit/tests/utils/htmlFilter.test.ts":"divkit/public/client/web/divkit/tests/utils/htmlFilter.test.ts",
+6
View File
@@ -371,6 +371,12 @@ Additional external storage. The `getValue` and `setValue` methods should be imp
Please note that these methods must be synchronous! DivKit does not have asynchronous render methods, so any `getValue` function cannot be asynchronous. It is possible to process `setValue` asynchronously, but if the value from `getValue` is critical for you and cannot be read synchronously, you must read it before calling `render`.
#### weekStartDay
Number, optional. 0 by default.
Affects the formatting of dates.
#### theme (DEPRECATED)
`system` | `light` | `dark`
+10 -10
View File
@@ -1,12 +1,12 @@
{
"name": "@divkitframework/divkit",
"version": "29.11.0",
"version": "30.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@divkitframework/divkit",
"version": "29.11.0",
"version": "30.1.0",
"license": "Apache-2.0",
"devDependencies": {
"@babel/core": "7.21.4",
@@ -39,7 +39,7 @@
"svelte-check": "3.5.0",
"svelte-loader": "3.1.9",
"svelte-preprocess": "5.0.4",
"typescript": "5.0.4",
"typescript": "5.4.5",
"webpack": "5.91.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.4"
@@ -13639,16 +13639,16 @@
}
},
"node_modules/typescript": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=12.20"
"node": ">=14.17"
}
},
"node_modules/unbox-primitive": {
@@ -24505,9 +24505,9 @@
}
},
"typescript": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true
},
"unbox-primitive": {
+1 -1
View File
@@ -121,7 +121,7 @@
"svelte-check": "3.5.0",
"svelte-loader": "3.1.9",
"svelte-preprocess": "5.0.4",
"typescript": "5.0.4",
"typescript": "5.4.5",
"webpack": "5.91.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.4"
+1
View File
@@ -31,6 +31,7 @@ export function render(opts: {
tooltipRoot?: HTMLElement;
customComponents?: Map<string, CustomComponentDescription> | undefined;
store?: Store;
weekStartDay?: number;
}): DivkitDebugInstance {
const { target, hydrate, ...rest } = opts;
+1
View File
@@ -40,6 +40,7 @@ export function render(opts: {
customComponents?: Map<string, CustomComponentDescription> | undefined;
direction?: Direction;
store?: Store;
weekStartDay?: number;
}): DivkitInstance {
const { target, hydrate, ...rest } = opts;
+6 -3
View File
@@ -103,6 +103,7 @@
export let customComponents: Map<string, CustomComponentDescription> | undefined = undefined;
export let direction: Direction = 'ltr';
export let store: Store | undefined = undefined;
export let weekStartDay = 0;
let isDesktop = writable(platform === 'desktop');
if (platform === 'auto' && typeof matchMedia !== 'undefined') {
@@ -260,7 +261,7 @@
const vars = mergeVars(variables, additionalVars);
const prepared = prepareVars(jsonProp, logError, store);
const prepared = prepareVars(jsonProp, logError, store, weekStartDay);
if (!prepared.vars.length) {
if (prepared.hasExpression) {
return constStore(prepared.applyVars(vars));
@@ -280,7 +281,7 @@
additionalVars?: Map<string, Variable>,
keepComplex = false
): MaybeMissing<T> {
const prepared = prepareVars(jsonProp, logError, store);
const prepared = prepareVars(jsonProp, logError, store, weekStartDay);
if (!prepared.hasExpression) {
return jsonProp;
@@ -1589,7 +1590,9 @@
const stores = exprVars.map(name => variables.get(name) || awaitVariableChanges(name));
derived(stores, () => {
const res = evalExpression(variables, store, ast);
const res = evalExpression(variables, store, ast, {
weekStartDay
});
res.warnings.forEach(logError);
+132 -4
View File
@@ -6,15 +6,143 @@ import Lottie from 'lottie-web/build/player/lottie';
import { initComponents } from './devCustomComponents';
const json = {
"templates": {},
"templates": {
"tutorialCard": {
"type": "container",
"border": {
"corner_radius": 16,
"stroke": {
"color": "#aaaaaa",
"width": 1
}
},
"items": [
{
"type": "text",
"font_size": 21,
"font_weight": "bold",
"margins": {
"bottom": 8
},
"$text": "title"
},
{
"type": "text",
"font_size": 16,
"margins": {
"bottom": 4
},
"$text": "body"
},
{
"type": "container",
"$items": "links"
}
],
"margins": {
"bottom": 6
},
"orientation": "vertical",
"paddings": {
"top": 12,
"bottom": 12,
"left": 12,
"right": 12
}
},
"link": {
"type": "text",
"action": {
"$url": "link",
"$log_id": "log"
},
"font_size": 14,
"margins": {
"bottom": 2
},
"text_color": "#0000ff",
"underline": "single",
"$text": "link_text"
}
},
"card": {
"log_id": "snapshot_test_card",
"log_id": "div2_sample_card",
"states": [
{
"state_id": 0,
"div": {
"type": "text",
"text": "Hello world"
"type": "container",
"items": [
{
"type": "tutorialCard",
"title": "@{getHours(parseUnixTimeAsLocal(1715855877))}",
"body": "Server side driven ui Яндекса\nПолезные ссылки:",
"links": [
{
"type": "link",
"link_text": "Официальная документация",
"link": "https://doc.yandex-team.ru/divkit/overview/",
"log": "docs"
},
{
"type": "link",
"link_text": "Примеры карточек",
"link": "https://a.yandex-team.ru/arcadia/divkit/public/test_data/samples",
"log": "samples"
},
{
"type": "link",
"link_text": "Клуб в Этушке",
"link": "https://clubs.at.yandex-team.ru/divkit/",
"log": "atushka"
},
{
"type": "link",
"link_text": "Канал в Мессенджере",
"link": "https://q.yandex-team.ru/#/join/b7263a0a-c2ef-4c02-9669-e8a1f4cf3762",
"log": "slack"
}
]
},
{
"type": "tutorialCard",
"title": "Горячие клавиши редактора",
"body": "ctrl+space - вызвать автодополнение\nctrl+f2 - заменить всё\nctrl+shift+i - отформатировать json\nctrl+shift+a закомментировать блок\nСхолпнуть json-блоки можно с помощью стрелочек, что появляются при наведении на блок с номерами строк слева\nf1 - вызвать меню со всеми доступными командами и горячими клавишами",
"links": [
{
"type": "separator",
"delimiter_style": {
"color": "#00000000"
}
}
]
},
{
"type": "tutorialCard",
"title": "Интерактивная верстка на ваших устройствах",
"body": "Для того, чтобы посмотреть вашу верстку на android:\n1) Скачайте demo apk по ссылке ниже\n2) Установите apk на устройство и перейдите во вкладку \"Div2\"\n3) Нажмите на share на этой страничке и отсканируйте QR-код, нажав соответствующую кнопку в приложении, либо вставьте урл из поля \"Link to json\"\n4) Для каждого подключенного устройства в редакторе появится вкладка с тем, что отображается сейчас на экране",
"links": [
{
"type": "link",
"link_text": "Android демо-приложение",
"link": "https://beta.m.soft.yandex.ru/description?app=divkit&platform_shortcut=android&branch=unknown",
"log": "android"
},
{
"type": "link",
"link_text": "iOS демо-приложение",
"link": "https://teamcity.yandex-team.ru/buildConfiguration/MobileNew_Monorepo_DivKit_iOS_trunk?branch=&mode=builds",
"log": "ios"
}
]
}
],
"margins": {
"top": 8,
"bottom": 8,
"left": 8,
"right": 8
}
}
}
]
+6 -2
View File
@@ -100,6 +100,7 @@ export interface EvalContext {
warnings: WrappedError[];
safeIntegerOverflow: boolean;
store?: Store;
weekStartDay: number;
}
register();
@@ -560,7 +561,9 @@ export function evalAny(ctx: EvalContext, expr: Node): EvalValue {
throw new Error('Unsupported expression');
}
export function evalExpression(vars: VariablesMap, store: Store | undefined, expr: Node): {
export function evalExpression(vars: VariablesMap, store: Store | undefined, expr: Node, opts?: {
weekStartDay?: number;
}): {
result: EvalResult;
warnings: WrappedError[];
} {
@@ -569,7 +572,8 @@ export function evalExpression(vars: VariablesMap, store: Store | undefined, exp
variables: vars,
warnings: [],
safeIntegerOverflow: false,
store
store,
weekStartDay: opts?.weekStartDay || 0
};
const result = evalAny(ctx, expr);
@@ -1,8 +1,10 @@
import type { DatetimeValue, EvalContext, EvalValue, IntegerValue } from '../eval';
/* eslint-disable no-nested-ternary */
import type { DatetimeValue, EvalContext, EvalValue, IntegerValue, StringValue } from '../eval';
import { registerFunc } from './funcs';
import { DATETIME, INTEGER } from '../const';
import { DATETIME, INTEGER, STRING } from '../const';
import { valToString } from '../utils';
import { toBigInt } from '../bigint';
import { formatDate } from '../../utils/formatDate';
function getMaxDate(date: Date): number {
const copy = new Date(date);
@@ -20,6 +22,17 @@ function parseUnixTime(_ctx: EvalContext, arg: IntegerValue): EvalValue {
};
}
function parseUnixTimeAsLocal(_ctx: EvalContext, arg: IntegerValue): EvalValue {
const date = new Date(Number(arg.value) * 1000);
const offset = date.getTimezoneOffset();
date.setMinutes(date.getMinutes() - offset);
return {
type: DATETIME,
value: date
};
}
function nowLocal(): EvalValue {
return {
type: DATETIME,
@@ -164,6 +177,19 @@ const getter = (
};
};
function makeFormat(isUTC: boolean) {
return (ctx: EvalContext, datetime: DatetimeValue, format: StringValue, locale?: StringValue): EvalValue => {
return {
type: STRING,
value: formatDate(datetime.value, format.value, {
locale: locale?.value,
isUTC,
weekStartDay: ctx.weekStartDay
})
};
};
}
const getYear = getter('getUTCFullYear');
const getMonth = getter('getUTCMonth');
const getDay = getter('getUTCDate');
@@ -173,8 +199,12 @@ const getMinutes = getter('getUTCMinutes');
const getSeconds = getter('getUTCSeconds');
const getMillis = getter('getUTCMilliseconds');
const formatAsLocal = makeFormat(false);
const formatAsUTC = makeFormat(true);
export function registerDatetime(): void {
registerFunc('parseUnixTime', [INTEGER], parseUnixTime);
registerFunc('parseUnixTimeAsLocal', [INTEGER], parseUnixTimeAsLocal);
registerFunc('nowLocal', [], nowLocal);
registerFunc('addMillis', [DATETIME, INTEGER], addMillis);
@@ -194,4 +224,9 @@ export function registerDatetime(): void {
registerFunc('getMinutes', [DATETIME], getMinutes);
registerFunc('getSeconds', [DATETIME], getSeconds);
registerFunc('getMillis', [DATETIME], getMillis);
registerFunc('formatDateAsLocal', [DATETIME, STRING], formatAsLocal);
registerFunc('formatDateAsUTC', [DATETIME, STRING], formatAsUTC);
registerFunc('formatDateAsLocalWithLocale', [DATETIME, STRING, STRING], formatAsLocal);
registerFunc('formatDateAsUTCWithLocale', [DATETIME, STRING, STRING], formatAsUTC);
}
+35 -15
View File
@@ -23,13 +23,24 @@ class ExpressionBinding {
* @param logError
*/
apply(
variables: VariablesMap,
logError: LogError,
store: Store | undefined,
keepComplex = false
{
variables,
logError,
store,
weekStartDay,
keepComplex
}: {
variables: VariablesMap;
logError: LogError;
store: Store | undefined;
weekStartDay: number;
keepComplex?: boolean;
}
): VariableValue | string | undefined {
try {
const res = evalExpression(variables, store, this.ast);
const res = evalExpression(variables, store, this.ast, {
weekStartDay
});
res.warnings.forEach(logError);
const result = res.result;
@@ -180,28 +191,31 @@ function prepareVarsObj<T>(
function applyVars(
jsonProp: unknown,
variables: VariablesMap,
logError: LogError,
store: Store | undefined,
keepComplex = false
opts: {
variables: VariablesMap;
logError: LogError;
store: Store | undefined;
weekStartDay: number;
keepComplex?: boolean;
}
): unknown {
if (jsonProp) {
if (
(process.env.ENABLE_EXPRESSIONS || process.env.ENABLE_EXPRESSIONS === undefined) &&
jsonProp instanceof ExpressionBinding
) {
return jsonProp.apply(variables, logError, store, keepComplex);
return jsonProp.apply(opts);
} else if (
(!process.env.ENABLE_EXPRESSIONS && process.env.ENABLE_EXPRESSIONS !== undefined) &&
jsonProp instanceof VariableBinding
) {
return jsonProp.apply(variables, logError);
return jsonProp.apply(opts.variables, opts.logError);
} else if (Array.isArray(jsonProp)) {
return jsonProp.map(it => applyVars(it, variables, logError, store));
return jsonProp.map(it => applyVars(it, opts));
} else if (typeof jsonProp === 'object') {
const res: Record<string, unknown> = {};
for (const key in jsonProp) {
res[key] = applyVars(jsonProp[key as keyof typeof jsonProp], variables, logError, store);
res[key] = applyVars(jsonProp[key as keyof typeof jsonProp], opts);
}
return res;
}
@@ -209,7 +223,7 @@ function applyVars(
return jsonProp;
}
export function prepareVars<T>(jsonProp: T, logError: LogError, store: Store | undefined): {
export function prepareVars<T>(jsonProp: T, logError: LogError, store: Store | undefined, weekStartDay: number): {
vars: string[];
hasExpression: boolean;
applyVars: (variables: VariablesMap, keepComplex?: boolean) => MaybeMissing<T>;
@@ -229,7 +243,13 @@ export function prepareVars<T>(jsonProp: T, logError: LogError, store: Store | u
vars,
hasExpression: result.hasExpression,
applyVars(variables: VariablesMap, keepComplex?: boolean) {
return applyVars(root, variables, logError, store, keepComplex) as MaybeMissing<T>;
return applyVars(root, {
variables,
logError,
store,
weekStartDay,
keepComplex
}) as MaybeMissing<T>;
}
};
}
+1
View File
@@ -17,6 +17,7 @@ export function render(opts: {
customComponents?: Map<string, CustomComponentDescription> | undefined;
direction?: Direction;
store?: Store;
weekStartDay?: number;
}) {
// Root has client-side typings, not a server one
return (Root as any).render(opts).html;
+368
View File
@@ -0,0 +1,368 @@
// Some code based on the https://github.com/date-fns (MIT License)
type FormatGetter = (opts: Intl.DateTimeFormatOptions & {
date?: Date;
}, field: Intl.DateTimeFormatPartTypes | 'week' | 'weekyear' | 'extendedyear' | 'weekofmonth' | 'dayofyear' | 'dayofweekinmonth' | 'weekdaynumeric') => string | undefined;
function formatNumber(num: string | undefined, len: number | undefined): string | undefined {
if (!num || !len) {
return num;
}
return num.padStart(len, '0');
}
const formatters: Record<string, (token: number, getter: FormatGetter) => string | undefined> = {
G(token, getter) {
let era: Intl.DateTimeFormatOptions['era'];
if (token < 4) {
era = 'short';
} else if (token === 5) {
era = 'narrow';
} else {
era = 'long';
}
return getter({
era
}, 'era');
},
d(token, getter) {
return formatNumber(getter({
day: 'numeric'
}, 'day'), token > 1 ? token : 0);
},
D(token, getter) {
return formatNumber(getter({}, 'dayofyear'), token > 1 ? token : 0);
},
F(token, getter) {
return formatNumber(getter({}, 'dayofweekinmonth'), token > 1 ? token : 0);
},
M(token, getter) {
let month: Intl.DateTimeFormatOptions['month'];
if (token === 1) {
month = 'numeric';
} else if (token === 2) {
month = '2-digit';
} else if (token === 3) {
month = 'short';
} else if (token === 5) {
month = 'narrow';
} else {
month = 'long';
}
return getter({
month,
// to get a genitive case of month
day: 'numeric'
}, 'month');
},
y(token, getter) {
return formatNumber(getter({
year: token === 2 ? '2-digit' : 'numeric'
}, 'year'), token > 2 ? token : undefined);
},
Y(token, getter) {
return formatNumber(getter({
year: token === 2 ? '2-digit' : 'numeric'
}, 'weekyear'), token > 2 ? token : undefined);
},
u(token, getter) {
return formatNumber(getter({
year: 'numeric'
}, 'extendedyear'), token > 1 ? token : undefined);
},
E(token, getter) {
let weekday: Intl.DateTimeFormatOptions['weekday'];
if (token <= 3) {
weekday = 'short';
} else if (token === 5) {
weekday = 'narrow';
} else {
weekday = 'long';
}
return getter({
weekday
}, 'weekday');
},
e(token, getter) {
if (token > 2) {
return formatters.E(token, getter);
}
return formatNumber(getter({}, 'weekdaynumeric'), token > 1 ? token : undefined);
},
w(token, getter) {
return formatNumber(getter({}, 'week'), token > 1 ? token : undefined);
},
W(token, getter) {
return formatNumber(getter({}, 'weekofmonth'), token > 1 ? token : undefined);
},
H(token, getter) {
const hours = getter({
hour: 'numeric',
hour12: false,
hourCycle: 'h23',
}, 'hour');
if (!hours) {
return;
}
// v8 may return '24' with 'h23' style
const fixed = String(Number(hours) % 24);
return formatNumber(fixed, token > 1 ? token : undefined);
},
h(token, getter) {
return formatNumber(getter({
hour: 'numeric',
hour12: true,
hourCycle: 'h12',
}, 'hour'), token > 1 ? token : undefined);
},
K(token, getter) {
const hours = getter({
hour: 'numeric',
hour12: true,
hourCycle: 'h11',
}, 'hour');
if (!hours) {
return;
}
const fixed = String(Number(hours) % 12);
return formatNumber(fixed, token > 1 ? token : undefined);
},
k(token, getter) {
return formatNumber(getter({
hour: 'numeric',
hour12: false,
hourCycle: 'h24',
}, 'hour'), token > 2 ? token : undefined);
},
a(_token, getter) {
return getter({
hour: 'numeric',
hour12: true,
hourCycle: 'h11',
dayPeriod: undefined
}, 'dayPeriod');
},
m(token, getter) {
return formatNumber(getter({
minute: 'numeric'
}, 'minute'), token > 1 ? token : undefined);
},
s(token, getter) {
return formatNumber(getter({
second: 'numeric'
}, 'second'), token > 1 ? token : undefined);
},
S(token, getter) {
const res = getter({
fractionalSecondDigits: Math.min(3, token) as 1 | 2 | 3
}, 'fractionalSecond');
if (res && token > 3) {
return res.padEnd(token, '0');
}
return res;
},
};
const formattingTokensRegExp =
/(\w)\1*|''|'(''|[^'])+('|$)|./g;
const escapedStringRegExp = /^'([^]*?)'?$/;
const doubleQuoteRegExp = /''/g;
const unescapedLatinCharacterRegExp = /[a-zA-Z]/;
const msInDay = 1000 * 60 * 60 * 24;
function cleanEscapedString(input: string): string {
const matched = input.match(escapedStringRegExp);
if (!matched) {
return input;
}
return matched[1].replace(doubleQuoteRegExp, "'");
}
function getWeekFirstDay(date: Date, isUTC: boolean, weekStartDay: number): Date {
const weekDay = date[isUTC ? 'getUTCDay' : 'getDay']();
// Monday logic
const offset = (weekDay < weekStartDay) ?
weekStartDay - weekDay - 7 :
weekStartDay - weekDay;
return new Date(date.getTime() + msInDay * offset);
}
function getFirstWeekFirstDay(date: Date, isUTC: boolean, weekStartDay: number): Date {
const firstDay = new Date(date);
firstDay[isUTC ? 'setUTCDate' : 'setDate'](1);
firstDay[isUTC ? 'setUTCMonth' : 'setMonth'](0);
return getWeekFirstDay(firstDay, isUTC, weekStartDay);
}
function dayDiff(date0: Date, date1: Date): number {
return Math.round((date0.getTime() - date1.getTime()) / msInDay);
}
function getIOSWeekYear(date: Date, isUTC: boolean, weekStartDay: number): {
week: number;
year: number;
} {
let week = 0;
const firstWeekFirstDay = getFirstWeekFirstDay(date, isUTC || false, weekStartDay);
const nextYearDate = new Date(date);
nextYearDate[isUTC ? 'setUTCFullYear' : 'setFullYear'](date[isUTC ? 'getUTCFullYear' : 'getFullYear']() + 1);
const nextYearFirstWeekFirstDay = getFirstWeekFirstDay(nextYearDate, isUTC || false, weekStartDay);
const isBeforeFirstWeek = date.getTime() < firstWeekFirstDay.getTime();
const isAfterLastWeek = date.getTime() >= nextYearFirstWeekFirstDay.getTime();
let year = date[isUTC ? 'getUTCFullYear' : 'getFullYear']();
if (isBeforeFirstWeek) {
--year;
firstWeekFirstDay[isUTC ? 'setUTCFullYear' : 'setFullYear'](firstWeekFirstDay[isUTC ? 'getUTCFullYear' : 'getFullYear']() - 1);
const dayOfYear = dayDiff(getWeekFirstDay(date, isUTC, weekStartDay), firstWeekFirstDay);
week = Math.round(dayOfYear / 7) + 1;
} else if (isAfterLastWeek) {
++year;
week = 1;
} else {
const dayOfYear = dayDiff(getWeekFirstDay(date, isUTC, weekStartDay), firstWeekFirstDay);
week = Math.round(dayOfYear / 7) + 1;
}
return {
week,
year
};
}
export function formatDate(date: Date, format: string, {
locale,
isUTC,
weekStartDay = 0
}: {
locale?: string;
isUTC?: boolean;
weekStartDay?: number;
} = {}): string {
const getter: FormatGetter = (opts, field): string | undefined => {
if (field === 'week') {
const { week } = getIOSWeekYear(date, isUTC || false, weekStartDay);
return String(week);
}
if (field === 'weekofmonth') {
const weekday = date[isUTC ? 'getUTCDay' : 'getDay']();
const firstDay = new Date(date);
firstDay[isUTC ? 'setUTCDate' : 'setDate'](1);
const firstWeekday = firstDay[isUTC ? 'getUTCDay' : 'getDay']();
const dayOfMonth = date[isUTC ? 'getUTCDate' : 'getDate']();
return String(Math.ceil(dayOfMonth / 7) + (weekday < firstWeekday ? 1 : 0));
}
if (field === 'dayofweekinmonth') {
const dayOfMonth = date[isUTC ? 'getUTCDate' : 'getDate']();
return String(Math.ceil(dayOfMonth / 7));
}
if (field === 'weekdaynumeric') {
let weekday = date[isUTC ? 'getUTCDay' : 'getDay']();
if (weekday < weekStartDay) {
weekday += 7;
}
return String(weekday - weekStartDay + 1);
}
if (field === 'dayofyear') {
const firstDay = new Date(date);
firstDay[isUTC ? 'setUTCMonth' : 'setMonth'](0);
firstDay[isUTC ? 'setUTCDate' : 'setDate'](1);
firstDay[isUTC ? 'setUTCHours' : 'setHours'](1);
firstDay[isUTC ? 'setUTCMinutes' : 'setMinutes'](1);
firstDay[isUTC ? 'setUTCSeconds' : 'setSeconds'](1);
const dayOfYear = Math.ceil((date.getTime() - firstDay.getTime()) / msInDay);
return String(dayOfYear);
}
if (field === 'weekyear') {
let { year } = getIOSWeekYear(date, isUTC || false, weekStartDay);
if (year < 1) {
year = 1 - year;
}
if (opts.year === '2-digit') {
return String(year % 100);
}
return String(year);
}
if (field === 'extendedyear') {
const year = date[isUTC ? 'getUTCFullYear' : 'getFullYear']();
if (opts.year === '2-digit') {
return String(year % 100);
}
return String(year);
}
if (isUTC) {
opts.timeZone = 'UTC';
}
const formatter = new Intl.DateTimeFormat(locale, opts);
const parts = formatter.formatToParts(date);
for (let i = 0; i < parts.length; ++i) {
if (parts[i].type === field) {
return parts[i].value;
}
}
};
return (format
.match(formattingTokensRegExp) || [])
.map(substring => {
if (substring === "''") {
return "'";
}
const firstCharacter = substring[0];
if (firstCharacter === "'") {
return cleanEscapedString(substring);
}
if (firstCharacter === 'z' || firstCharacter === 'Z') {
throw new Error(`z/Z not supported in [${format}].`);
}
if (formatters[firstCharacter]) {
return formatters[firstCharacter](substring.length, getter);
}
if (firstCharacter.match(unescapedLatinCharacterRegExp)) {
throw new Error(
`Format string contains an unescaped latin alphabet character "${firstCharacter}"`
);
}
return substring;
})
.join('');
}
@@ -55,7 +55,9 @@ function runCase(item: any) {
}
return;
}
const res = evalExpression(vars, undefined, ast);
const res = evalExpression(vars, undefined, ast, {
weekStartDay: item.platform_specific?.web?.weekStartDay || 0
});
if (item.expected.value !== '' || res.result.type !== 'error') {
if (res.result.type === 'number' && item.expected.type === 'number') {
expect(res.result.value).toBeCloseTo(item.expected.value);
File diff suppressed because it is too large Load Diff
@@ -14,7 +14,7 @@ describe('correctPositiveNumber', () => {
test('no vars', () => {
const prepared = prepareVars({
prop: 123
}, logError, undefined);
}, logError, undefined, 0);
expect(prepared.vars).toStrictEqual([]);
expect(prepared.applyVars(new Map())).toStrictEqual({
prop: 123
@@ -24,7 +24,7 @@ describe('correctPositiveNumber', () => {
test('simple', () => {
const prepared = prepareVars({
prop: '@{var}'
}, logError, undefined);
}, logError, undefined, 0);
expect(prepared.vars).toStrictEqual(['var']);
const map = new Map();
const variable = new IntegerVariable('var', 123);
@@ -38,7 +38,7 @@ describe('correctPositiveNumber', () => {
test('simple no val', () => {
const prepared = prepareVars({
prop: '@{var}'
}, noError, undefined);
}, noError, undefined, 0);
expect(prepared.vars).toStrictEqual(['var']);
const map = new Map();
expect(prepared.applyVars(map)).toStrictEqual({
@@ -53,7 +53,7 @@ describe('correctPositiveNumber', () => {
'smth',
456
]
}, logError, undefined);
}, logError, undefined, 0);
expect(prepared.vars).toStrictEqual(['var']);
const map = new Map();
const variable = new IntegerVariable('var', 123);
@@ -70,7 +70,7 @@ describe('correctPositiveNumber', () => {
test('interpolation', () => {
const prepared = prepareVars({
prop: 'Hello, @{var}'
}, logError, undefined);
}, logError, undefined, 0);
expect(prepared.vars).toStrictEqual(['var']);
const map = new Map();
const variable = new IntegerVariable('var', 123);
@@ -83,7 +83,7 @@ describe('correctPositiveNumber', () => {
test('multiple vars', () => {
const prepared = prepareVars({
prop: 'Hello, @{var}. Good bye, @{var2}'
}, logError, undefined);
}, logError, undefined, 0);
expect(prepared.vars).toStrictEqual(['var', 'var2']);
const map = new Map();
const variable = new IntegerVariable('var', 123);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2017",
"target": "ES2021",
"strict": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
+3
View File
@@ -15,6 +15,7 @@ import type {
import type { GlobalVariablesController, Variable } from './variables';
import type { WrappedError } from './common';
import type { CustomComponentDescription } from './custom';
import type { Store } from './store';
export interface DivkitDebugInstance extends DivkitInstance {
getDebugVariables(): Map<string, Variable>;
@@ -42,6 +43,8 @@ export function render(opts: {
fetchInit?: FetchInit;
tooltipRoot?: HTMLElement;
customComponents?: Map<string, CustomComponentDescription> | undefined;
store?: Store;
weekStartDay?: number;
}): DivkitDebugInstance;
export { createVariable, createGlobalVariablesController } from './variables';
+1
View File
@@ -36,6 +36,7 @@ export function render(opts: {
tooltipRoot?: HTMLElement;
customComponents?: Map<string, CustomComponentDescription> | undefined;
store?: Store;
weekStartDay?: number;
}): DivkitInstance;
export { createVariable, createGlobalVariablesController } from './variables';
+1
View File
@@ -35,6 +35,7 @@ export function render(opts: {
tooltipRoot?: HTMLElement;
customComponents?: Map<string, CustomComponentDescription> | undefined;
store?: Store;
weekStartDay?: number;
}): DivkitInstance;
export { createVariable, createGlobalVariablesController } from './variables';
+1
View File
@@ -19,4 +19,5 @@ export function render(opts: {
typefaceProvider?: TypefaceProvider;
customComponents?: Map<string, CustomComponentDescription> | undefined;
store?: Store;
weekStartDay?: number;
}): string;
@@ -30,7 +30,8 @@
"result_type": "datetime",
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
@@ -366,7 +367,8 @@
"result_type": "string",
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
@@ -386,7 +388,8 @@
"result_type": "string",
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
@@ -410,7 +413,8 @@
"result_type": "string",
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
@@ -434,7 +438,8 @@
"result_type": "string",
"platforms": [
"android",
"ios"
"ios",
"web"
]
}
]
@@ -9,7 +9,8 @@
},
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
@@ -21,7 +22,8 @@
},
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
@@ -33,7 +35,8 @@
},
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
@@ -45,10 +48,11 @@
},
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
{
"name": "formatDateAsUTCWithLocale(parseUnixTime(0), en-US) => 0:00 AM",
"expression": "@{formatDateAsUTCWithLocale(parseUnixTime(0), 'K:mm a', 'en-US')}",
"expected": {
@@ -57,10 +61,11 @@
},
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
{
"name": "formatDateAsUTCWithLocale(setDay(parseUnixTime(0), 13), en-US) => 1970-01-01T00:00:00.000",
"expression": "@{formatDateAsUTCWithLocale(setDay(parseUnixTime(0), 13), 'yyyy-MM-dd\\'T\\'HH:mm:ss.SSS', 'en-US')}",
"expected": {
@@ -69,7 +74,8 @@
},
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
@@ -81,19 +87,26 @@
},
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
"name": "formatDateAsUTCWithLocale(parseUnixTime(3927600), 'ru-RU') => 1970-02-03, week of year 7",
"expression": "@{formatDateAsUTCWithLocale(parseUnixTime(3927600), 'yyyy-MM-dd\\', week of year \\'w', 'ru-RU')}",
"platform_specific": {
"web": {
"weekStartDay": 1
}
},
"expected": {
"type": "string",
"value": "1970-02-15, week of year 7"
},
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
@@ -105,7 +118,8 @@
},
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
@@ -116,7 +130,8 @@
"value": "Year 1970, extended year 1970"
},
"platforms": [
"ios"
"ios",
"web"
]
},
{
@@ -128,7 +143,8 @@
},
"platforms": [
"android",
"ios"
"ios",
"web"
]
},
{
@@ -140,7 +156,8 @@
},
"platforms": [
"android",
"ios"
"ios",
"web"
]
}
]