// tslint:disable:max-classes-per-file import {CellValue} from 'app/common/DocActions'; import * as gristTypes from 'app/common/gristTypes'; import * as gutil from 'app/common/gutil'; import {buildNumberFormat, NumberFormatOptions} from 'app/common/NumberFormat'; import {decodeObject, GristDateTime} from 'app/plugin/objtypes'; import isPlainObject = require('lodash/isPlainObject'); import * as moment from 'moment-timezone'; import {DocumentSettings} from 'app/common/DocumentSettings'; export {PENDING_DATA_PLACEHOLDER} from 'app/plugin/objtypes'; export interface FormatOptions { [option: string]: any; } /** * Formats a value of any type generically (with no type-specific options). */ export function formatUnknown(value: CellValue): string { return formatHelper(decodeObject(value)); } /** * Formats a decoded Grist value for displaying it. For top-level values, formats them the way we * like to see them in a cell or in, say, CSV export. For lists and objects, nested values are * formatted slighly differently, with quoted strings and ISO format for dates. */ function formatHelper(value: unknown, isTopLevel: boolean = true): string { if (typeof value === 'object' && value) { if (Array.isArray(value)) { return '[' + value.map(v => formatHelper(v, false)).join(', ') + ']'; } else if (isPlainObject(value)) { const obj: any = value; const items = Object.keys(obj).map(k => `${JSON.stringify(k)}: ${formatHelper(obj[k], false)}`); return '{' + items.join(', ') + '}'; } else if (isTopLevel && value instanceof GristDateTime) { return moment(value).tz(value.timezone).format("YYYY-MM-DD HH:mm:ssZ"); } return String(value); } if (isTopLevel) { return (value == null ? "" : String(value)); } return JSON.stringify(value); } export type IsRightTypeFunc = (value: CellValue) => boolean; export class BaseFormatter { public readonly isRightType: IsRightTypeFunc; constructor(public type: string, public widgetOpts: object, public docSettings: DocumentSettings) { this.isRightType = gristTypes.isRightType(gristTypes.extractTypeFromColType(type)) || gristTypes.isRightType('Any')!; } /** * Formats a value that matches the type of this formatter. This should be overridden by derived * classes to handle values in formatter-specific ways. */ public format(value: any): string { return String(value); } /** * Formats using this.format() if a value is of the right type for this formatter, or using * AnyFormatter otherwise. This method the recommended API. There is no need to override it. */ public formatAny(value: any): string { return this.isRightType(value) ? this.format(value) : formatUnknown(value); } } class AnyFormatter extends BaseFormatter { public format(value: any): string { return formatUnknown(value); } } export class NumericFormatter extends BaseFormatter { private _numFormat: Intl.NumberFormat; private _formatter: (val: number) => string; constructor(type: string, options: NumberFormatOptions, docSettings: DocumentSettings) { super(type, options, docSettings); this._numFormat = buildNumberFormat(options, docSettings); this._formatter = (options.numSign === 'parens') ? this._formatParens : this._formatPlain; } public format(value: any): string { return value === null ? '' : this._formatter(value); } public _formatPlain(value: number): string { return this._numFormat.format(value); } public _formatParens(value: number): string { // Surround positive numbers with spaces to align them visually to parenthesized numbers. return (value >= 0) ? ` ${this._numFormat.format(value)} ` : `(${this._numFormat.format(-value)})`; } } class IntFormatter extends NumericFormatter { constructor(type: string, opts: FormatOptions, docSettings: DocumentSettings) { super(type, {decimals: 0, ...opts}, docSettings); } } interface DateFormatOptions { dateFormat?: string; } class DateFormatter extends BaseFormatter { private _dateTimeFormat: string; private _timezone: string; constructor(type: string, widgetOpts: DateFormatOptions, docSettings: DocumentSettings, timezone: string = 'UTC') { super(type, widgetOpts, docSettings); this._dateTimeFormat = widgetOpts.dateFormat || 'YYYY-MM-DD'; this._timezone = timezone; } public format(value: any): string { if (value === null) { return ''; } const time = moment.tz(value * 1000, this._timezone); return time.format(this._dateTimeFormat); } } interface DateTimeFormatOptions extends DateFormatOptions { timeFormat?: string; } class DateTimeFormatter extends DateFormatter { constructor(type: string, widgetOpts: DateTimeFormatOptions, docSettings: DocumentSettings) { const timezone = gutil.removePrefix(type, "DateTime:") || ''; const timeFormat = widgetOpts.timeFormat === undefined ? 'h:mma' : widgetOpts.timeFormat; const dateFormat = (widgetOpts.dateFormat || 'YYYY-MM-DD') + " " + timeFormat; super(type, {dateFormat}, docSettings, timezone); } } const formatters: {[name: string]: typeof BaseFormatter} = { Numeric: NumericFormatter, Int: IntFormatter, Bool: BaseFormatter, Date: DateFormatter, DateTime: DateTimeFormatter, // We don't list anything that maps to AnyFormatter, since that's the default. }; /** * Takes column type, widget options and document settings, and returns a constructor * with a format function that can properly convert a value passed to it into the * right format for that column. */ export function createFormatter(type: string, widgetOpts: FormatOptions, docSettings: DocumentSettings): BaseFormatter { const ctor = formatters[gristTypes.extractTypeFromColType(type)] || AnyFormatter; return new ctor(type, widgetOpts, docSettings); }