§ (Number, Text) → Text
- Left slice.
+ Left slice: only retain characters after the position specified by the left argument (positive: count from left; negative: count from right).
§ (Text, Number) → Text
- Right slice.
+ Right slice: only retain characters before the position specified by the right argument (positive: count from left; negative: count from right).
@@ -56,8 +56,10 @@
Binary Operators
Examples
-
{t § 3} (t = foobar)
{3 § t} (t = foobar)
+
{-2 § t} (t = foobar)
+
{t § 3} (t = foobar)
+
{t § -2} (t = foobar)
{(2 § t) § 3} (t = foobar)
{2 § (t § 3)} (t = foobar)
@@ -202,13 +204,14 @@
Formatting
%%: A literal percent character.
- If no specification is given, Date values default to %Y-%m-%d %H:%M:%S.
+ If no specification is given, Date values default to the original input text or %Y-%m-%d %H:%M:%S if none exists.
Examples
-
{d:%d.%m.%Y} (d = 2023-05-27 18:00)
+
{d:%d.%m.%Y} (d = 2023-05-27T14:02:05)
+
{d:%d.%m.%Y %H:%M} (d = 2023-05-27T14:02:05)
@@ -290,7 +293,7 @@
Examples
Formatting
DateDelta does not provide formatting options at the moment.
- By default values are formatted as +Dd HH:MM:SS.
+ By default values are returned as they were input or are formatted as +Dd HH:MM:SS if no original input exists.
Use attributes to assemble a custom format.
diff --git a/package.json b/package.json
index 114c708..885504c 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
{
"devDependencies": {
- "less": "^4.1.3",
- "mocha": "^10.2.0",
- "ts-loader": "^9.4.2",
- "typescript": "^5.0.4",
- "webpack": "^5.82.0",
- "webpack-cli": "^5.1.0"
+ "less": "^4.2.2",
+ "mocha": "^11.1.0",
+ "ts-loader": "^9.5.2",
+ "typescript": "^5.7.3",
+ "webpack": "^5.97.1",
+ "webpack-cli": "^6.0.1"
}
}
diff --git a/src/bundles/flottplot.ts b/src/bundles/flottplot.ts
index 4614214..54b0b3e 100644
--- a/src/bundles/flottplot.ts
+++ b/src/bundles/flottplot.ts
@@ -14,6 +14,8 @@ import { FPCursors } from "../elements/cursors";
import { FPOverlay } from "../elements/overlay";
import { FPPlot } from "../elements/plot";
import { FPStack } from "../elements/stack";
+import { FPState } from "../elements/state";
+import { FPText } from "../elements/text";
import { FPVideo } from "../elements/video";
import { rangeFrom, selectFrom } from "../elements/items"
@@ -28,26 +30,15 @@ Flottplot.registerTag("fp-plot", FPPlot.from, false);
Flottplot.registerTag("fp-range", rangeFrom, false);
Flottplot.registerTag("fp-select", selectFrom, false);
Flottplot.registerTag("fp-stack", FPStack.from, true);
+Flottplot.registerTag("fp-state", FPState.from, false);
+Flottplot.registerTag("fp-text", FPText.from, true);
Flottplot.registerTag("fp-video", FPVideo.from, false);
// ...
export { VERSION, Flottplot, ElementMixin, Value, FlottplotError, dom };
// TODO return {
-// TODO VERSION = "2.2.0",
-// TODO Flottplot: Flottplot,
-// TODO FPElement: FPElement,
-// TODO Value: Value,
// TODO OptionsItems: OptionsItems,
// TODO RangeItems: RangeItems,
-// TODO FlottplotError: FlottplotError,
-// TODO ElementError: ElementError,
-// TODO dom: dom,
// TODO };
-
-// Create element registry and initialize with core elements
-
-// TODO Flottplot.registerTag("fp-state", FPState.from, false);
-// TODO Flottplot.registerTag("fp-text", FPText.from, true);
-
diff --git a/src/element.ts b/src/element.ts
index 168f77a..b22e178 100644
--- a/src/element.ts
+++ b/src/element.ts
@@ -1,4 +1,4 @@
-import { Identifier, Action, Expression, Manager, Pattern, Substitution } from "./interface";
+import { Identifier, Action, Expression, Manager, Pattern, FormatSpec, Substitution } from "./interface";
import { ElementError } from "./errors";
import { Expr } from "./expression";
import { Value } from "./values";
@@ -19,7 +19,7 @@ export class ElementMixin {
readonly id: Identifier;
node: HTMLElement | null;
- patterns: Map;
+ patterns: Map;
dependencies: Set;
actions: Set;
private _manager: Manager | null;
@@ -34,8 +34,7 @@ export class ElementMixin {
} else if (/^[A-Za-z][A-Za-z0-9_]*$/.test(id)) {
this.id = id;
} else throw new ElementError(
- "invalid id '" + id + "' for " + this.constructor.name
- + " (names must begin with A-z and only contain A-z, 0-9 and _)"
+ `invalid element id '${id}' (names must begin with A-z and only contain A-z, 0-9 and _)`
);
this.node = null;
// Element is initially not connected to a flottplot supervisor. This
@@ -64,14 +63,11 @@ export class ElementMixin {
this._manager = manager;
}
- // Throw an error, provides additional context generated for this element
- fail(message: string): void {
- this.failWith(new ElementError(
- "in " + this.constructor.name + " '" + this.id + "': " + message
- ));
- }
-
- failWith(error: Error) {
+ warn(message: string | Error): ElementError {
+ if (message instanceof Error) {
+ message = message.message;
+ }
+ const error = new ElementError(`in element '${this.id}': ${message}`);
if (this.node != null) {
if (this._errorBox == null) {
this._errorBox = newNode("div", {
@@ -95,9 +91,12 @@ export class ElementMixin {
// Condition ensures there is at least one child, skip null check
this._errorBox.firstChild!.remove();
}
- } else {
- throw error;
}
+ return error;
+ }
+
+ fail(message: string | Error): never {
+ throw this.warn(message);
}
// Invoke an action of the element, update the element and notify all
@@ -141,7 +140,7 @@ export class ElementMixin {
// them into the patterns and dependencies attributes of this element so
// the supervisor can provide appropriate substitutions in updates
setDependenciesFrom(...templates: Array): void {
- this.patterns = new Map();
+ this.patterns = new Map();
this.dependencies = new Set();
for (const template of templates) {
const reg = /{.+?}/g;
@@ -151,7 +150,8 @@ export class ElementMixin {
break
}
const pattern = match[0];
- const [patExpr, format] = pattern.slice(1, -1).split(":");
+ const [patExpr, ...formatParts] = pattern.slice(1, -1).split(":");
+ const format = (formatParts.length > 0) ? formatParts.join(":") : undefined;
const expression = Expr.parse(patExpr);
// format will have undefined assigned if not given
this.patterns.set(pattern, [expression, format]);
diff --git a/src/elements/animation.ts b/src/elements/animation.ts
index fae197e..c6bbd20 100644
--- a/src/elements/animation.ts
+++ b/src/elements/animation.ts
@@ -1,8 +1,10 @@
-import { Identifier, Action, FPElement } from "../interface";
+import { Identifier, Action, FPElement, ElementState } from "../interface";
import { ElementMixin } from "../element";
import { newNode, newButton, Attributes } from "../dom";
-
+
+type FPAnimationState = [boolean, number]; // playing, speed
+
export class FPAnimation extends ElementMixin implements FPElement {
readonly targets: Array;
@@ -43,23 +45,34 @@ export class FPAnimation extends ElementMixin implements FPElement {
);
}
+ get isPlaying(): boolean {
+ return (this.timeout != null);
+ }
+
get value(): undefined {
return undefined;
}
- get state(): any { // TODO
- return {
- playing: (this.timeout != null),
- speed: this.speed
- };
+ get state(): FPAnimationState {
+ return [this.isPlaying, this.speed];
}
- set state(state: any) { // TODO
- this.speed = state.speed;
- if (state.playing) {
- this.start();
+ set state(state: ElementState) { // TODO
+ const ok = (
+ Array.isArray(state)
+ && state.length === 2
+ && typeof state[0] === "boolean"
+ && typeof state[1] === "number"
+ );
+ if (ok) {
+ this.speed = state[1];
+ if (state[0]) {
+ this.start();
+ } else {
+ this.stop();
+ }
} else {
- this.stop();
+ this.warn(`cannot recover from state ${state}`); // TODO StateError
}
}
diff --git a/src/elements/calendar.ts b/src/elements/calendar.ts
index 82ec80d..4b944d7 100644
--- a/src/elements/calendar.ts
+++ b/src/elements/calendar.ts
@@ -1,10 +1,12 @@
-import { Identifier, FPElement } from "../interface";
+import { Identifier, FPElement, ElementState } from "../interface";
import { ElementMixin } from "../element";
-import { ValueError, ParseError } from "../errors";
+import { ValueError } from "../errors";
import { newNode, Attributes } from "../dom";
import { Value, DateValue } from "../values";
+type FPCalendarState = string;
+
export class FPCalendar extends ElementMixin implements FPElement {
override node: HTMLInputElement;
@@ -19,7 +21,7 @@ export class FPCalendar extends ElementMixin implements FPElement {
} else if (init instanceof DateValue) {
this.resetValue = init.toString("%Y-%m-%d");
} else throw new ValueError(
- "cannot initialize calendar with " + init.constructor.name
+ "cannot initialize calendar with " + init._typeName
);
// HTML offers an input type with a nice date selector
if (attrs == null) {
@@ -49,16 +51,25 @@ export class FPCalendar extends ElementMixin implements FPElement {
);
}
- get value(): Value {
+ get value(): DateValue {
const value = Value.from(this.node.value);
- if (value == null) throw new ParseError(
- `unexpected issue parsing ${this.node.value} as value`
- );
- return value;
+ if (value instanceof DateValue) {
+ return value;
+ } else {
+ this.fail(`unexpected issue parsing ${this.node.value} as a DateValue`); // TODO ParseError?
+ }
}
- get state(): undefined {
- return undefined; // TODO
+ get state(): FPCalendarState {
+ return this.node.value;
+ }
+
+ set state(state: ElementState) {
+ if (typeof state === "string") {
+ this.node.value = state;
+ } else {
+ this.warn(`cannot recover from state ${state}`); // TODO StateError
+ }
}
private get date() {
diff --git a/src/elements/items.ts b/src/elements/items.ts
index 15fcd43..9ef37ac 100644
--- a/src/elements/items.ts
+++ b/src/elements/items.ts
@@ -1,4 +1,4 @@
-import { Identifier, Calls, FormatSpec, FPElement, Collection, CollectionEvent } from "../interface";
+import { Identifier, Calls, FormatSpec, FPElement, ElementState, Collection, CollectionEvent } from "../interface";
import { ElementError } from "../errors";
import { ElementMixin } from "../element";
import { newNode, newButton, Attributes } from "../dom";
@@ -57,6 +57,7 @@ export function selectFrom(node: HTMLElement): FPItems {
+type FPItemsState = number;
class FPItems extends ElementMixin implements FPElement {
// Base class for control elements wrapping a Items instance. Most
@@ -103,18 +104,21 @@ class FPItems extends ElementMixin implements FPElement {
assertFinite(): void {
if (!this.items.isFinite) {
this.fail("list of items is not finite");
- return;
}
}
// (De-)Serialization
- get state(): number {
+ get state(): FPItemsState {
return this.items.index;
}
- set state(state: number) {
- this.items.index = state;
+ set state(state: ElementState) {
+ if (typeof state === "number") {
+ this.items.index = state;
+ } else {
+ this.warn(`cannot recover from state ${state}`); // TODO StateError
+ };
}
// Actions
diff --git a/src/elements/state.ts b/src/elements/state.ts
index bcf87ad..cdecb54 100644
--- a/src/elements/state.ts
+++ b/src/elements/state.ts
@@ -1,11 +1,14 @@
-import { Identifier, FPElement } from "../interface";
+import { Identifier, FPElement, ManagerState } from "../interface";
import { ElementMixin } from "../element";
import { Attributes } from "../dom";
-class FPState extends FPElement {
+export class FPState extends ElementMixin implements FPElement {
- constructor(id, useURL) {
+ private readonly useURL: boolean;
+ private savedState: null | ManagerState;
+
+ constructor(id: Identifier | undefined, useURL: boolean) {
super(id);
this.useURL = useURL;
this.savedState = null;
@@ -13,27 +16,35 @@ class FPState extends FPElement {
this.actions.add("restore");
}
- initialize() {
+ override initialize() {
if (this.useURL === true) {
this.flottplot.urlstate = true;
}
}
- save() {
+ get value(): undefined {
+ return undefined;
+ }
+
+ get state(): undefined {
+ return undefined;
+ }
+
+ save(): void {
this.savedState = this.flottplot.state;
}
- restore() {
+ restore(): void {
if (this.savedState != null) {
this.flottplot.state = this.savedState;
}
}
- static from(node) {
- const attrs = dom.Attributes.from(node);
+ static from(node: HTMLElement): FPState {
+ const attrs = Attributes.from(node);
return new FPState(
attrs.id,
- attrs.pop("url", false, "BOOL")
+ attrs.getAsBool("url", "false", true)
);
}
diff --git a/src/elements/text.ts b/src/elements/text.ts
index d234958..ce42e1d 100644
--- a/src/elements/text.ts
+++ b/src/elements/text.ts
@@ -1,24 +1,35 @@
-import { Identifier, FPElement } from "../interface";
+import { Identifier, Substitution, FPElement } from "../interface";
import { ElementMixin } from "../element";
-import { Attributes } from "../dom";
+import { newNode } from "../dom";
-class FPText extends FPElement {
+export class FPText extends ElementMixin implements FPElement {
- constructor(id, text) {
+ override node: HTMLSpanElement;
+ private readonly text: string;
+
+ constructor(id: Identifier | undefined, text: string) {
super(id);
- this.node = dom.newNode("span", { "id": id });
+ this.node = newNode("span", { "id": id });
this.text = text;
this.setDependenciesFrom(text);
}
- update(subst) {
- let text = this.substitute(this.text, subst);
- this.node.textContent = text;
+ override update(subst: Substitution): void {
+ this.node.textContent = this.substitute(this.text, subst);
+ }
+
+ get value(): undefined {
+ return undefined;
+ }
+
+ get state(): undefined {
+ return undefined;
}
- static from(node) {
- return new FPText(node.id, node.textContent);
+ static from(node: HTMLElement): FPText {
+ const text = node.textContent;
+ return new FPText(node.id, (text == null) ? "" : text);
}
}
diff --git a/src/errors.ts b/src/errors.ts
index 260a2ef..6fab60d 100644
--- a/src/errors.ts
+++ b/src/errors.ts
@@ -1,7 +1,14 @@
-export class FlottplotError extends Error {}
+export class FlottplotError extends Error {
+
+ constructor(message?: string) {
+ super(`Flottplot Error: ${message}`);
+ }
+
+}
export class ParseError extends FlottplotError {}
export class FormatError extends FlottplotError {}
+export class StateError extends FlottplotError {}
export class ValueError extends FlottplotError {}
export class ItemsError extends FlottplotError {}
diff --git a/src/expression.ts b/src/expression.ts
index d9e0b4b..74a9bda 100644
--- a/src/expression.ts
+++ b/src/expression.ts
@@ -272,7 +272,7 @@ export class Expr implements Expression {
// Both forward and backward operators either don't exist or return
// undefined. The operation is not possible.
throw new ValueError(
- "operator '" + this.op.method + "' not defined for " + args.map(_ => _.constructor.name).join(" and ")
+ "operator '" + this.op.method + "' not defined for " + args.map(_ => _._typeName).join(" and ")
);
}
diff --git a/src/interface.ts b/src/interface.ts
index 2fe5ba3..03c6210 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -1,5 +1,6 @@
import { Value } from "./values";
import { Fullscreen } from "./dom";
+import { FlottplotError } from "./errors";
// Identifiers must be usable as Map keys
export type Identifier = string;
@@ -15,7 +16,7 @@ export type Substitution = Map;
// Formatting specification for values
-export type FormatSpec = string;
+export type FormatSpec = string | undefined;
export interface Expression {
toString(): string;
@@ -42,8 +43,8 @@ export enum CollectionEvent {
}
// ...
-export type ElementState = any; // TODO
-export type ManagerState = any; // TODO
+export type ElementState = unknown;
+export type ManagerState = Record;
export interface Manager {
// ...
@@ -60,6 +61,7 @@ export interface Manager {
state: ManagerState;
overlay: any; // TODO
fullscreen: Fullscreen; // TODO
+ urlstate: boolean;
}
export interface FPElement {
@@ -79,7 +81,7 @@ export interface FPElement {
update(substitution?: Substitution): void;
invoke(action: Action): void; // TODO: args
notify(): void;
- fail(message: string): void;
- failWith(error: Error): void;
+ warn(message: string | Error): FlottplotError;
+ fail(message: string | Error): never;
}
diff --git a/src/items.ts b/src/items.ts
index 25684c8..2e36022 100644
--- a/src/items.ts
+++ b/src/items.ts
@@ -133,6 +133,9 @@ export class RangeItems extends Items implements Collection {
_factor: Value;
_selected: number;
+ readonly valueType: any;
+ readonly valueTypeName: string;
+
indexMin: number;
indexMax: number;
@@ -156,16 +159,19 @@ export class RangeItems extends Items implements Collection {
"range requires specification of at least one of init, min or max"
);
// The type of this._offset determines the type of all values this
- // range produces. This type is accessible via the valueType property
- // for instanceof comparisons. First, verify that min and max have the
- // appropriate type. init does not need to be checked (if it is not
- // null, it is this._offset). step is allowed to have a different type
- // (e.g., the step between dates is a datedelta).
+ // range produces. Make this type accessible via the valueType property
+ // for instanceof comparisons.
+ this.valueType = this._offset.constructor;
+ this.valueTypeName = this._offset._typeName;
+ // Verify that min and max have the appropriate type. init does not
+ // need to be checked (if it is not null, it is this._offset). step is
+ // allowed to have a different type (e.g., the step between dates is
+ // a datedelta).
if (!(min == null || min instanceof this.valueType)) throw new ItemsError(
- "min is a " + min.constructor.name + " but range expects " + this.valueType.name
+ "min is a " + min._typeName + " value but range expects " + this.valueTypeName
);
if (!(max == null || max instanceof this.valueType)) throw new ItemsError(
- "max is a " + max.constructor.name + " but range expects " + this.valueType.name
+ "max is a " + max._typeName + " value but range expects " + this.valueTypeName
);
// TODO
if (step == null) throw new ItemsError(
@@ -188,10 +194,6 @@ export class RangeItems extends Items implements Collection {
this.value = this._offset;
}
- get valueType() {
- return this._offset.constructor;
- }
-
_genValue(index: number): Value {
// TODO: attach index?
// Because this is not going through proper Expression evaluation, the
@@ -202,12 +204,12 @@ export class RangeItems extends Items implements Collection {
}
_genIndex(value: unknown): number {
- value = Value.from(value);
- if (value instanceof this.valueType) {
+ const v = Value.from(value);
+ if (v instanceof this.valueType) {
// Clip value into range
- return Math.round((value as any)._sub(this._offset)._div(this._factor)._value); // TODO any
+ return Math.round((v as any)._sub(this._offset)._div(this._factor)._value); // TODO any
} else throw new ItemsError(
- `range expects ${this.valueType.name} but received ${value!.constructor.name}`
+ `range expects ${this.valueTypeName} value but received ${v!._typeName}`
);
}
diff --git a/src/manager.ts b/src/manager.ts
index 9d7e69b..763c594 100644
--- a/src/manager.ts
+++ b/src/manager.ts
@@ -1,4 +1,4 @@
-import { Identifier, Action, Calls, Substitution, Pattern, FPElement, Manager, ManagerState } from "./interface";
+import { Identifier, Action, Calls, Substitution, FPElement, Manager, ManagerState } from "./interface";
import { ElementError, FlottplotError } from "./errors";
import { UpdateGraph } from "./graph";
import * as dom from "./dom";
@@ -14,6 +14,12 @@ type TagRegistration = {
isRecursive: boolean;
};
+
+function containsState(obj: unknown): obj is ManagerState {
+ return (typeof obj === "object" && !Array.isArray(obj) && obj !== null);
+}
+
+
export class Flottplot implements Manager {
_elements: Map;
@@ -49,8 +55,8 @@ export class Flottplot implements Manager {
const duplicate = this._elements.get(element.id);
// Make sure id doesn't already exist in the collection
if (duplicate != null) {
- element.fail("duplicate id");
- duplicate.fail("duplicate id");
+ element.warn("duplicate id");
+ duplicate.warn("duplicate id");
}
// Add element to collection
this._elements.set(element.id, element);
@@ -96,7 +102,6 @@ export class Flottplot implements Manager {
node.replaceWith(dom.newNode("div", {
"style": "border:3px solid #F00;background-color:#FCC;padding:3px;",
}, [
- dom.newNode("b", {}, [error.constructor.name, ": "]),
error.message
]));
// Additionally log the error on the console
@@ -129,7 +134,7 @@ export class Flottplot implements Manager {
try {
element.initialize(this._substitutionFor(element));
} catch (error) {
- element.failWith(error);
+ element.warn(error);
console.error(error);
}
}
@@ -170,7 +175,7 @@ export class Flottplot implements Manager {
values.set(dep, dep_element.value);
}
// Evaluate expressions in all patterns with values and format
- const out: Map = new Map();
+ const out: Substitution = new Map();
for (const [pattern, [expression, format]] of element.patterns) {
out.set(pattern, expression._eval(values).toString(format));
}
@@ -225,17 +230,18 @@ export class Flottplot implements Manager {
return out;
}
- set state(state: ManagerState) {
- for (const id of this._graph.orderedNodes) {
- if (!state.hasOwnProperty(id)) {
- continue;
+ set state(state: unknown) {
+ if (!containsState(state)) throw new FlottplotError(
+ "TODO 2389" // TODO
+ );
+ for (const id in state) {
+ const element = this._elements.get(id);
+ if (element != null) {
+ element.state = state[id];
+ element.update(this._substitutionFor(element));
+ element.notify();
+ // TODO pause changing of hash during update and notify?
}
- // Skip null check since id comes from graph
- const element: FPElement = this._elements.get(id)!;
- element.state = state[id];
- element.update(this._substitutionFor(element));
- element.notify();
- // TODO pause changing of hash during update and notify?
}
}
diff --git a/src/values.ts b/src/values.ts
index 31487cb..e452cb6 100644
--- a/src/values.ts
+++ b/src/values.ts
@@ -8,6 +8,7 @@ export abstract class Value {
_TEXT?: string;
abstract toString(spec?: FormatSpec): string;
+ abstract readonly _typeName: string;
// Every value gets a user-accessible TEXT attribute, which should be set
// to the raw text input used to generate the value. If the value wasn't
@@ -58,7 +59,7 @@ export abstract class Value {
export class TextValue extends Value implements Expression {
- _value: string;
+ readonly _value: string;
constructor(value: unknown) {
super();
@@ -68,7 +69,11 @@ export class TextValue extends Value implements Expression {
this._value = value.toString();
}
- toString(spec?: string): string {
+ get _typeName(): string {
+ return "Text";
+ }
+
+ toString(spec?: FormatSpec): string {
// No format specification given, return text as-is
if (spec == null) {
return this._value;
@@ -113,7 +118,7 @@ export class TextValue extends Value implements Expression {
export class NumberValue extends Value implements Expression {
- _value: number;
+ readonly _value: number;
constructor(value: unknown) {
super();
@@ -131,10 +136,17 @@ export class NumberValue extends Value implements Expression {
}
}
- toString(spec?: string): string {
+ get _typeName(): string {
+ return "Number";
+ }
+
+ toString(spec?: FormatSpec): string {
+ // No format specification given: return value as it was entered by the
+ // user or fall back to default representation
if (spec == null) {
- return this._value.toString();
+ return (this._TEXT != null) ? this._TEXT : this._value.toString();
}
+ // Try to apply format specification
const aspy = pyformat(this._value, spec);
if (aspy != null) {
return aspy;
@@ -256,7 +268,7 @@ function asDate(value: string): Date {
export class DateValue extends Value implements Expression {
- _value: Date;
+ readonly _value: Date;
YEAR: NumberValue;
MONTH: NumberValue;
@@ -284,8 +296,21 @@ export class DateValue extends Value implements Expression {
this.SECOND = new NumberValue(this._value.getUTCSeconds());
}
- toString(spec?: string): string {
- const aspy = pystrftime(this._value, (spec != null) ? spec : "%Y-%m-%d %H:%M:%S");
+ get _typeName(): string {
+ return "Date";
+ }
+
+ toString(spec?: FormatSpec): string {
+ // If no format specification is given try to return original user
+ // input or supply the default specification: YYYY-mm-dd HH:MM:SS
+ if (spec == null) {
+ if (this._TEXT != null) {
+ return this._TEXT;
+ } else {
+ spec = "%Y-%m-%d %H:%M:%S";
+ }
+ }
+ const aspy = pystrftime(this._value, spec);
if (aspy != null) {
return aspy;
}
@@ -326,7 +351,7 @@ export class DateValue extends Value implements Expression {
export class DateDeltaValue extends Value implements Expression {
- _value: number;
+ readonly _value: number;
SIGN: NumberValue;
DAYS: NumberValue;
@@ -377,19 +402,30 @@ export class DateDeltaValue extends Value implements Expression {
this.TOTAL_SECONDS = new NumberValue(this._value);
}
- toString(spec?: string): string {
- // User should use number formatting options of attributes instead
- if (spec != null) throw new FormatError(
+ get _typeName(): string {
+ return "DateDelta";
+ }
+
+ toString(spec?: FormatSpec): string {
+ // If no format specification is given try to return original user
+ // input or fall back to a default format: ±DDd HH:MM:SS
+ if (spec == null) {
+ if (this._TEXT != null) {
+ return this._TEXT;
+ } else {
+ return (
+ (this.SIGN._value < 0 ? "-" : "+")
+ + this.DAYS.toString() + "d "
+ + this.HOURS.toString("0>2") + ":"
+ + this.MINUTES.toString("0>2") + ":"
+ + this.SECONDS.toString("0>2")
+ );
+ }
+ }
+ // For now, users should use attributes and number formatting
+ throw new FormatError(
"invalid specification '" + spec + "' for date delta value '" + this.toString() + "'"
);
- // Return in format '±DDd HH:MM:SS'
- return (
- (this.SIGN._value < 0 ? "-" : "+")
- + this.DAYS.toString() + "d "
- + this.HOURS.toString("0>2") + ":"
- + this.MINUTES.toString("0>2") + ":"
- + this.SECONDS.toString("0>2")
- );
}
_pos(): DateDeltaValue {
@@ -444,7 +480,7 @@ export class DateDeltaValue extends Value implements Expression {
// Attribute names are special objects
export class AttributeValue extends Value implements Expression {
- _name: string;
+ readonly _name: string;
constructor(name: string) {
super();
@@ -454,6 +490,10 @@ export class AttributeValue extends Value implements Expression {
);
}
+ get _typeName(): string {
+ return "Attribute";
+ }
+
toString(spec?: string): string {
if (spec != null) throw new FormatError("sdjfjasdf"); // TODO
return this._name;
diff --git a/test/test_values.js b/test/test_values.js
index 6ee0a87..108c37b 100644
--- a/test/test_values.js
+++ b/test/test_values.js
@@ -120,11 +120,17 @@ describe("Value formatting", function () {
});
it("DateDeltaValue default format", function() {
- let pos = new DateDeltaValue("+5m");
+ const pos = new DateDeltaValue("+5m");
+ assert.strictEqual(pos.toString(), "+5m");
+ pos._TEXT = null; // TODO create a proper mechanism to obtain default formatting
assert.strictEqual(pos.toString(), "+0d 00:05:00");
- let neg = new DateDeltaValue("-251h");
+ const neg = new DateDeltaValue("-251h");
+ assert.strictEqual(neg.toString(), "-251h");
+ neg._TEXT = null; // TODO create a proper mechanism to obtain default formatting
assert.strictEqual(neg.toString(), "-10d 11:00:00");
- let zero = new DateDeltaValue("0d");
+ const zero = new DateDeltaValue("0d");
+ assert.strictEqual(zero.toString(), "0d");
+ zero._TEXT = null; // TODO create a proper mechanism to obtain default formatting
assert.strictEqual(zero.toString(), "+0d 00:00:00");
});
@@ -161,6 +167,11 @@ describe("Value operators", function () {
["-3 § foo ", "ijk"],
[" foo § 3", "abc"],
[" foo § -3", "abcdefgh"],
+ [" 3 § foo § 3", "def"],
+ ["-3 § foo § 1", "i"],
+ [" 3 § foo § -2", "defghi"],
+ ["-3 § foo § -2", "i"],
+ ["-3 § foo § -4", ""]
], [
["foo", new TextValue("abcdefghijk")]
]));
diff --git a/util/generate_dependencies.py b/util/generate_dependencies.py
new file mode 100644
index 0000000..e448b37
--- /dev/null
+++ b/util/generate_dependencies.py
@@ -0,0 +1,46 @@
+import argparse
+from pathlib import Path
+import os
+import re
+
+EXT = ".ts"
+IMPORT = re.compile(r"^import .* from \"(.+)\";?$");
+
+
+def get_deps(path):
+ out = set()
+ with open(path, "r") as f:
+ for line in f:
+ match = IMPORT.match(line);
+ if match is not None:
+ out.add(Path(match.group(1) + EXT))
+ return out
+
+def get_deps_recursive(path, checked=None):
+ path = Path(path).resolve()
+ if checked is None:
+ checked = set()
+ checked.add(path)
+ for dep in get_deps(path):
+ dep = (path.parent / dep).resolve()
+ if dep not in checked:
+ get_deps_recursive(dep, checked)
+ return checked
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("entry")
+
+if __name__ == "__main__":
+ args = parser.parse_args()
+
+ entry = Path(args.entry).resolve()
+
+ deps = get_deps_recursive(entry)
+ deps.remove(entry)
+ deps = [os.path.relpath(dep) for dep in sorted(deps)]
+ deps = " ".join(deps)
+
+ print(f"{os.path.relpath(entry)}: {deps}")
+ print(f"\ttouch $@")
+