diff --git a/README.md b/README.md index dde30df6b2..6a85712b72 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,13 @@ difficulties, many combining functionality from several packages) in the | Projects | Version | | |----|----|----| | [`@thi.ng/api`](./packages/api) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/api.svg)](https://www.npmjs.com/package/@thi.ng/api) | [changelog](./packages/api/CHANGELOG.md) | +| [`@thi.ng/associative`](./packages/associative) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/associative.svg)](https://www.npmjs.com/package/@thi.ng/associative) | [changelog](./packages/associative/CHANGELOG.md) | | [`@thi.ng/atom`](./packages/atom) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/atom.svg)](https://www.npmjs.com/package/@thi.ng/atom) | [changelog](./packages/atom/CHANGELOG.md) | | [`@thi.ng/bitstream`](./packages/bitstream) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/bitstream.svg)](https://www.npmjs.com/package/@thi.ng/bitstream) | [changelog](./packages/bitstream/CHANGELOG.md) | | [`@thi.ng/checks`](./packages/checks) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/checks.svg)](https://www.npmjs.com/package/@thi.ng/checks) | [changelog](./packages/checks/CHANGELOG.md) | | [`@thi.ng/csp`](./packages/csp) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/csp.svg)](https://www.npmjs.com/package/@thi.ng/csp) | [changelog](./packages/csp/CHANGELOG.md) | | [`@thi.ng/dcons`](./packages/dcons) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/dcons.svg)](https://www.npmjs.com/package/@thi.ng/dcons) | [changelog](./packages/dcons/CHANGELOG.md) | +| [`@thi.ng/dgraph`](./packages/dgraph) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/dgraph.svg)](https://www.npmjs.com/package/@thi.ng/dgraph) | [changelog](./packages/dgraph/CHANGELOG.md) | | [`@thi.ng/diff`](./packages/diff) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/diff.svg)](https://www.npmjs.com/package/@thi.ng/diff) | [changelog](./packages/diff/CHANGELOG.md) | | [`@thi.ng/hdom`](./packages/hdom) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/hdom.svg)](https://www.npmjs.com/package/@thi.ng/hdom) | [changelog](./packages/hdom/CHANGELOG.md) | | [`@thi.ng/hdom-components`](./packages/hdom-components) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/hdom-components.svg)](https://www.npmjs.com/package/@thi.ng/hdom-components) | [changelog](./packages/hdom-components/CHANGELOG.md) | diff --git a/assets/deps.png b/assets/deps.png index e29db166cf..d8c23fc384 100644 Binary files a/assets/deps.png and b/assets/deps.png differ diff --git a/examples/router-basics/src/app.ts b/examples/router-basics/src/app.ts index 009a66e56d..138121309d 100644 --- a/examples/router-basics/src/app.ts +++ b/examples/router-basics/src/app.ts @@ -8,6 +8,8 @@ import { EVENT_ROUTE_CHANGED } from "@thi.ng/router/api"; import { HTMLRouter } from "@thi.ng/router/history"; import { AppConfig, ViewSpec, AppViews, AppContext } from "./api"; +import * as ev from "./events"; +import * as fx from "./effects"; import { nav } from "./components/nav"; import { debugContainer } from "./components/debug-container"; @@ -27,9 +29,6 @@ import { debugContainer } from "./components/debug-container"; */ export class App { - static readonly EV_ROUTE_TO = "route-to"; - static readonly FX_ROUTE_TO = "route-to"; - config: AppConfig; ctx: AppContext; state: Atom; @@ -51,10 +50,10 @@ export class App { ); this.ctx.bus.addHandlers({ [EVENT_ROUTE_CHANGED]: valueSetter("route"), - [App.EV_ROUTE_TO]: (_, [__, route]) => ({ [App.FX_ROUTE_TO]: route }) + [ev.ROUTE_TO]: (_, [__, route]) => ({ [ev.ROUTE_TO]: route }) }); this.ctx.bus.addEffect( - App.FX_ROUTE_TO, + fx.ROUTE_TO, ([id, params]) => this.router.routeTo(this.router.format(id, params)) ); this.addViews({ @@ -63,7 +62,7 @@ export class App { "route.id", (id) => (this.config.components[id] || - (() => ["div", `missing component for route: ${id}`]))(this.ctx, this.config.ui) + (() => ["div", `missing component for route: ${id}`]))(this.ctx) ] }); } @@ -75,12 +74,13 @@ export class App { * @param specs */ addViews(specs: IObjectOf) { + const views = this.ctx.views; for (let id in specs) { const spec = specs[id]; if (isArray(spec)) { - this.ctx.views[id] = this.state.addView(spec[0], spec[1]); + views[id] = this.state.addView(spec[0], spec[1]); } else { - this.ctx.views[id] = this.state.addView(spec); + views[id] = this.state.addView(spec); } } } diff --git a/examples/router-basics/src/components/all-users.ts b/examples/router-basics/src/components/all-users.ts index 927cd52093..18eaa5f742 100644 --- a/examples/router-basics/src/components/all-users.ts +++ b/examples/router-basics/src/components/all-users.ts @@ -1,5 +1,6 @@ import { AppContext, StatusType, User } from "../api"; -import { EV_LOAD_USER_LIST, EV_SET_STATUS, ROUTE_USER_PROFILE } from "../config"; +import { LOAD_USER_LIST, SET_STATUS } from "../events"; +import { USER_PROFILE } from "../routes"; import { routeLink } from "./route-link"; import { status } from "./status"; @@ -13,8 +14,8 @@ import { status } from "./status"; export function allUsers(ctx: AppContext) { ctx.bus.dispatch( ctx.views.userlist.deref().length ? - [EV_SET_STATUS, [StatusType.SUCCESS, "list loaded from cache", true]] : - [EV_LOAD_USER_LIST] + [SET_STATUS, [StatusType.SUCCESS, "list loaded from cache", true]] : + [LOAD_USER_LIST] ); return ["div", status, userList]; } @@ -49,7 +50,7 @@ function user(ctx: AppContext, user: User, cached: boolean) { ["img", { ...ui.thumb, src: user.img }]], ["div", ui.body, ["h1", ui.title, - [routeLink, ROUTE_USER_PROFILE.id, { id: user.id }, null, user.name]], + [routeLink, USER_PROFILE.id, { id: user.id }, null, user.name]], ["h2", ui.subtitle, `@${user.alias}`]], cached ? ["div", ui.meta, "cached"] : diff --git a/examples/router-basics/src/components/debug-container.ts b/examples/router-basics/src/components/debug-container.ts index 00c6365f4e..321821e40a 100644 --- a/examples/router-basics/src/components/debug-container.ts +++ b/examples/router-basics/src/components/debug-container.ts @@ -1,9 +1,9 @@ import { AppContext } from "../api"; -import { EV_TOGGLE_DEBUG } from "../config"; +import { TOGGLE_DEBUG } from "../events"; import { eventLink } from "./event-link"; /** - * Collapsable component showing stringified app state. + * Collapsible component showing stringified app state. * * @param ctx injected context object * @param debug @@ -11,7 +11,7 @@ import { eventLink } from "./event-link"; */ export function debugContainer(ctx: AppContext, debug: any, json: string) { return ["div#debug", ctx.ui.column.debug[debug], - [eventLink, [EV_TOGGLE_DEBUG], ctx.ui.debugToggle, + [eventLink, [TOGGLE_DEBUG], ctx.ui.debugToggle, debug ? "close \u25bc" : "open \u25b2"], ["pre", ctx.ui.code, json] ]; diff --git a/examples/router-basics/src/components/nav.ts b/examples/router-basics/src/components/nav.ts index f6eb3fe54e..57480a3041 100644 --- a/examples/router-basics/src/components/nav.ts +++ b/examples/router-basics/src/components/nav.ts @@ -1,10 +1,10 @@ import { AppContext } from "../api"; -import { ROUTE_USER_LIST, ROUTE_HOME, ROUTE_CONTACT } from "../config"; +import { USER_LIST, HOME, CONTACT } from "../routes"; import { routeLink } from "./route-link"; /** - * Main nav component with hardcoded routes. + * Main nav component with hard coded routes. * * @param ctx injected context object */ @@ -13,9 +13,9 @@ export function nav(ctx: AppContext) { return ["nav", ["h1", ui.title, "Demo app"], ["div", ui.inner, - [routeLink, ROUTE_HOME.id, null, ui.link, "Home"], - [routeLink, ROUTE_USER_LIST.id, null, ui.link, "Users"], - [routeLink, ROUTE_CONTACT.id, null, ui.linkLast, "Contact"], + [routeLink, HOME.id, null, ui.link, "Home"], + [routeLink, USER_LIST.id, null, ui.link, "Users"], + [routeLink, CONTACT.id, null, ui.linkLast, "Contact"], ] ]; } diff --git a/examples/router-basics/src/components/route-link.ts b/examples/router-basics/src/components/route-link.ts index 82c7a2918c..7a4a4a3b99 100644 --- a/examples/router-basics/src/components/route-link.ts +++ b/examples/router-basics/src/components/route-link.ts @@ -1,5 +1,5 @@ -import { App } from "../app"; import { AppContext } from "../api"; +import { ROUTE_TO } from "../events"; /** * Customizable hyperlink component emitting EV_ROUTE_TO event when clicked. @@ -16,7 +16,7 @@ export function routeLink(ctx: AppContext, routeID: PropertyKey, routeParams: an ...attribs, onclick: (e) => { e.preventDefault(); - ctx.bus.dispatch([App.EV_ROUTE_TO, [routeID, routeParams]]); + ctx.bus.dispatch([ROUTE_TO, [routeID, routeParams]]); } }, body]; diff --git a/examples/router-basics/src/components/user-profile.ts b/examples/router-basics/src/components/user-profile.ts index a79009db21..32316f8e86 100644 --- a/examples/router-basics/src/components/user-profile.ts +++ b/examples/router-basics/src/components/user-profile.ts @@ -1,5 +1,5 @@ import { StatusType, AppContext } from "../api"; -import { EV_LOAD_USER, EV_SET_STATUS } from "../config"; +import { LOAD_USER, SET_STATUS } from "../events"; import { status } from "./status"; @@ -13,8 +13,8 @@ export function userProfile(ctx: AppContext) { const id = ctx.views.route.deref().params.id; ctx.bus.dispatch( ctx.views.users.deref()[id] ? - [EV_SET_STATUS, [StatusType.SUCCESS, "loaded from cache", true]] : - [EV_LOAD_USER, id]); + [SET_STATUS, [StatusType.SUCCESS, "loaded from cache", true]] : + [LOAD_USER, id]); return ["div", [status], [userCard, id]]; } diff --git a/examples/router-basics/src/config.ts b/examples/router-basics/src/config.ts index 7a5818890e..e2b8cb31bb 100644 --- a/examples/router-basics/src/config.ts +++ b/examples/router-basics/src/config.ts @@ -1,72 +1,15 @@ import { Event, FX_DISPATCH_ASYNC, FX_DISPATCH_NOW, EV_SET_VALUE, FX_DELAY } from "@thi.ng/interceptors/api"; -import { valueSetter, valueUpdater, trace } from "@thi.ng/interceptors/interceptors"; -import { Route, RouteMatch, EVENT_ROUTE_CHANGED } from "@thi.ng/router/api"; +import { valueUpdater, trace } from "@thi.ng/interceptors/interceptors"; import { AppConfig, StatusType } from "./api"; +import * as ev from "./events"; +import * as fx from "./effects"; +import * as routes from "./routes"; import { home } from "./components/home"; import { allUsers } from "./components/all-users"; import { userProfile } from "./components/user-profile"; import { contact } from "./components/contact"; -import { App } from "./app"; - -// route definitions: -// routes are 1st class objects and used directly throughout the app -// without ever referring to their specific string representation - -// the `match` arrays specify the individual route elements -// docs here: -// https://github.com/thi-ng/umbrella/blob/master/packages/router/ -// https://github.com/thi-ng/umbrella/blob/master/packages/router/src/api.ts#L31 - -export const ROUTE_HOME: Route = { - id: "home", - match: ["home"] -}; - -export const ROUTE_CONTACT: Route = { - id: "contact", - match: ["contact"] -}; - -export const ROUTE_USER_LIST: Route = { - id: "user-list", - match: ["users"], -}; - -// this is a parametric route w/ parameter coercion & validation -// if coercion or validation fails, the route will not be matched -// if no other route matches, the configured default route will -// be used (see full router config further below) - -export const ROUTE_USER_PROFILE: Route = { - id: "user-profile", - match: ["users", "?id"], - validate: { - id: { - coerce: (x) => parseInt(x), - check: (x) => x > 0 && x < 100 - } - } -}; - -// best practice tip: define event & effect names as consts or enums and -// avoid hardcoded strings for more safety and easier refactoring - -export const EV_DONE = "done"; -export const EV_ERROR = "error"; -export const EV_LOAD_USER = "load-user"; -export const EV_LOAD_USER_ERROR = "load-user-error" -export const EV_LOAD_USER_LIST = "load-users"; -export const EV_RECEIVE_USER = "receive-user"; -export const EV_RECEIVE_USERS = "receive-users"; -export const EV_SET_STATUS = "set-status"; -export const EV_TOGGLE_DEBUG = "toggle-debug"; - -// side effect IDs (these don't / shouldn't need to be exported. other -// parts of the app should only use events) - -const FX_JSON = "load-json"; // main App configuration export const CONFIG: AppConfig = { @@ -77,19 +20,19 @@ export const CONFIG: AppConfig = { // use URI hash for routes (KISS) useFragment: true, // route ID if no other matches (MUST be non-parametric!) - defaultRouteID: ROUTE_HOME.id, + defaultRouteID: routes.HOME.id, // IMPORTANT: rules with common prefixes MUST be specified in // order of highest precision / longest path routes: [ - ROUTE_HOME, - ROUTE_CONTACT, - ROUTE_USER_PROFILE, - ROUTE_USER_LIST, + routes.HOME, + routes.CONTACT, + routes.USER_PROFILE, + routes.USER_LIST, ] }, // event handlers events are queued and batch processed in app's RAF - // renderloop event handlers can be single functions, interceptor + // render loop event handlers can be single functions, interceptor // objects with `pre`/`post` keys or arrays of either. // the event handlers' only task is to transform the event into a @@ -101,77 +44,74 @@ export const CONFIG: AppConfig = { events: { // sets status to "done" - [EV_DONE]: () => ({ - [FX_DISPATCH_NOW]: [EV_SET_STATUS, [StatusType.DONE, "done"]] + [ev.DONE]: () => ({ + [FX_DISPATCH_NOW]: [ev.SET_STATUS, [StatusType.DONE, "done"]] }), // sets status to thrown error's message - [EV_ERROR]: (_, [__, err]) => ({ - [FX_DISPATCH_NOW]: [EV_SET_STATUS, [StatusType.ERROR, err.message]], + [ev.ERROR]: (_, [__, err]) => ({ + [FX_DISPATCH_NOW]: [ev.SET_STATUS, [StatusType.ERROR, err.message]], }), // triggers loading of JSON for single user, sets status - [EV_LOAD_USER]: (_, [__, id]) => ({ - [FX_DISPATCH_NOW]: [EV_SET_STATUS, [StatusType.INFO, `loading user data...`]], - [FX_DISPATCH_ASYNC]: [FX_JSON, `assets/user-${id}.json`, EV_RECEIVE_USER, EV_LOAD_USER_ERROR] + [ev.LOAD_USER]: (_, [__, id]) => ({ + [FX_DISPATCH_NOW]: [ev.SET_STATUS, [StatusType.INFO, `loading user data...`]], + [FX_DISPATCH_ASYNC]: [fx.JSON, `assets/user-${id}.json`, ev.RECEIVE_USER, ev.LOAD_USER_ERROR] }), // triggered after successful IO // stores received user data under `users.{id}`, sets status // note: we assign multiple value/events as array to the FX_DISPATCH_NOW side effect - [EV_RECEIVE_USER]: (_, [__, json]) => ({ + [ev.RECEIVE_USER]: (_, [__, json]) => ({ [FX_DISPATCH_NOW]: [ [EV_SET_VALUE, [["users", json.id], json]], - [EV_SET_STATUS, [StatusType.SUCCESS, "JSON succesfully loaded", true]] + [ev.SET_STATUS, [StatusType.SUCCESS, "JSON successfully loaded", true]] ], }), // error event for user profile IO requests (i.e. in this demo for user ID 3) // set status, then redirects to /users after 1sec - [EV_LOAD_USER_ERROR]: (_, [__, err]) => ({ - [FX_DISPATCH_NOW]: [EV_SET_STATUS, [StatusType.ERROR, err.message]], - [FX_DISPATCH_ASYNC]: [FX_DELAY, [1000, [ROUTE_USER_LIST.id]], App.EV_ROUTE_TO, EV_ERROR], + [ev.LOAD_USER_ERROR]: (_, [__, err]) => ({ + [FX_DISPATCH_NOW]: [ev.SET_STATUS, [StatusType.ERROR, err.message]], + [FX_DISPATCH_ASYNC]: [FX_DELAY, [1000, [routes.USER_LIST.id]], ev.ROUTE_TO, ev.ERROR], }), // triggers loading of JSON summary of all users, sets status - [EV_LOAD_USER_LIST]: () => ({ - [FX_DISPATCH_NOW]: [EV_SET_STATUS, [StatusType.INFO, `loading user data...`]], - [FX_DISPATCH_ASYNC]: [FX_JSON, `assets/users.json`, EV_RECEIVE_USERS, EV_ERROR] + [ev.LOAD_USER_LIST]: () => ({ + [FX_DISPATCH_NOW]: [ev.SET_STATUS, [StatusType.INFO, `loading user data...`]], + [FX_DISPATCH_ASYNC]: [fx.JSON, `assets/users.json`, ev.RECEIVE_USERS, ev.ERROR] }), // triggered after successful IO // note: we assign multiple value/events as array to the FX_DISPATCH_NOW side effect - [EV_RECEIVE_USERS]: (_, [__, json]) => ({ + [ev.RECEIVE_USERS]: (_, [__, json]) => ({ [FX_DISPATCH_NOW]: [ [EV_SET_VALUE, ["userlist", json]], - [EV_SET_STATUS, [StatusType.SUCCESS, "JSON succesfully loaded", true]] + [ev.SET_STATUS, [StatusType.SUCCESS, "JSON successfully loaded", true]] ], }), - // stores current route details - [EVENT_ROUTE_CHANGED]: valueSetter("route"), - // stores status (a tuple of `[type, message, done?]`) in app state // if status type != DONE & `done` == true, also triggers delayed EV_DONE // Note: we inject the `trace` interceptor to log the event to the console - [EV_SET_STATUS]: [ + [ev.SET_STATUS]: [ trace, (_, [__, status]) => ({ [FX_DISPATCH_NOW]: [EV_SET_VALUE, ["status", status]], [FX_DISPATCH_ASYNC]: (status[0] !== StatusType.DONE && status[2]) ? - [FX_DELAY, [1000], EV_DONE, EV_ERROR] : + [FX_DELAY, [1000], ev.DONE, ev.ERROR] : undefined }) ], // toggles debug state flag on/off - [EV_TOGGLE_DEBUG]: valueUpdater("debug", (x) => x ^ 1) + [ev.TOGGLE_DEBUG]: valueUpdater("debug", (x) => x ^ 1) }, // side effects effects: { // generic JSON loader via fetch() - [FX_JSON]: (req) => + [fx.JSON]: (req) => fetch(req).then((resp) => { if (!resp.ok) { throw new Error(resp.statusText); @@ -184,10 +124,10 @@ export const CONFIG: AppConfig = { // those functions are called automatically by the app's root component // base on the currently active route components: { - [ROUTE_HOME.id]: home, - [ROUTE_CONTACT.id]: contact, - [ROUTE_USER_LIST.id]: allUsers, - [ROUTE_USER_PROFILE.id]: userProfile, + [routes.HOME.id]: home, + [routes.CONTACT.id]: contact, + [routes.USER_LIST.id]: allUsers, + [routes.USER_PROFILE.id]: userProfile, }, // DOM root element (or ID) diff --git a/examples/router-basics/src/effects.ts b/examples/router-basics/src/effects.ts new file mode 100644 index 0000000000..9bd410f132 --- /dev/null +++ b/examples/router-basics/src/effects.ts @@ -0,0 +1,7 @@ +// best practice tip: define event & effect names as consts or enums +// and avoid hardcoded strings for more safety and easier refactoring +// also see pre-defined event handlers & interceptors in @thi.ng/atom: +// https://github.com/thi-ng/umbrella/blob/master/packages/interceptors/src/api.ts#L14 + +export const JSON = "load-json"; +export const ROUTE_TO = "route-to"; \ No newline at end of file diff --git a/examples/router-basics/src/events.ts b/examples/router-basics/src/events.ts new file mode 100644 index 0000000000..5403211ab7 --- /dev/null +++ b/examples/router-basics/src/events.ts @@ -0,0 +1,13 @@ +// best practice tip: define event & effect names as consts or enums and +// avoid hardcoded strings for more safety and easier refactoring + +export const DONE = "done"; +export const ERROR = "error"; +export const LOAD_USER = "load-user"; +export const LOAD_USER_ERROR = "load-user-error" +export const LOAD_USER_LIST = "load-users"; +export const RECEIVE_USER = "receive-user"; +export const RECEIVE_USERS = "receive-users"; +export const ROUTE_TO = "route-to"; +export const SET_STATUS = "set-status"; +export const TOGGLE_DEBUG = "toggle-debug"; diff --git a/examples/router-basics/src/routes.ts b/examples/router-basics/src/routes.ts new file mode 100644 index 0000000000..01a68be3fb --- /dev/null +++ b/examples/router-basics/src/routes.ts @@ -0,0 +1,41 @@ +import { Route } from "@thi.ng/router/api"; + +// route definitions: +// routes are 1st class objects and used directly throughout the app +// without ever referring to their specific string representation + +// the `match` arrays specify the individual route elements +// docs here: +// https://github.com/thi-ng/umbrella/blob/master/packages/router/ +// https://github.com/thi-ng/umbrella/blob/master/packages/router/src/api.ts#L31 + +export const HOME: Route = { + id: "home", + match: ["home"] +}; + +export const CONTACT: Route = { + id: "contact", + match: ["contact"] +}; + +export const USER_LIST: Route = { + id: "user-list", + match: ["users"], +}; + +// this is a parametric route w/ parameter coercion & validation +// if coercion or validation fails, the route will not be matched +// if no other route matches, the configured default route will +// be used (see full router config further below) + +export const USER_PROFILE: Route = { + id: "user-profile", + match: ["users", "?id"], + validate: { + id: { + coerce: (x) => parseInt(x), + check: (x) => x > 0 && x < 100 + } + } +}; diff --git a/lerna.json b/lerna.json index 10a6c65a42..e220b15a35 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "lerna": "2.8.0", + "lerna": "2.10.1", "packages": [ "packages/*" ], diff --git a/package.json b/package.json index b8b8c344b1..61f16ddc21 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ ], "devDependencies": { "benchmark": "^2.1.4", - "lerna": "^2.9.1", + "lerna": "^2.10.1", "nyc": "^11.6.0", "tslint": "^5.9.1", "typescript": "^2.8.1", diff --git a/packages/associative/.npmignore b/packages/associative/.npmignore new file mode 100644 index 0000000000..d703bda97a --- /dev/null +++ b/packages/associative/.npmignore @@ -0,0 +1,10 @@ +build +coverage +dev +doc +src* +test +.nyc_output +tsconfig.json +*.tgz +*.html diff --git a/packages/associative/CHANGELOG.md b/packages/associative/CHANGELOG.md new file mode 100644 index 0000000000..88e779be7b --- /dev/null +++ b/packages/associative/CHANGELOG.md @@ -0,0 +1,41 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + + +# [0.4.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/associative@0.3.0...@thi.ng/associative@0.4.0) (2018-04-13) + + +### Features + +* **associative:** add renameKeysMap ([bfabe80](https://github.com/thi-ng/umbrella/commit/bfabe80)) + + +### Performance Improvements + +* **associative:** update equiv() impls ([d1178ac](https://github.com/thi-ng/umbrella/commit/d1178ac)) + + + + + +# [0.3.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/associative@0.2.0...@thi.ng/associative@0.3.0) (2018-04-13) + + +### Features + +* **associative:** add SortedMap & tests, minor refactor EquivMap ([ae0eae8](https://github.com/thi-ng/umbrella/commit/ae0eae8)) +* **associative:** add SortedSet, update SortedMap ([cb4976f](https://github.com/thi-ng/umbrella/commit/cb4976f)) + + + + + +# 0.2.0 (2018-04-10) + + +### Features + +* **associative:** add EquivSet.first() ([0dc9f64](https://github.com/thi-ng/umbrella/commit/0dc9f64)) +* **associative:** initial import [@thi](https://github.com/thi).ng/associative ([cc70dbc](https://github.com/thi-ng/umbrella/commit/cc70dbc)) diff --git a/packages/associative/LICENSE b/packages/associative/LICENSE new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/packages/associative/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/associative/README.md b/packages/associative/README.md new file mode 100644 index 0000000000..ec37fc5c9d --- /dev/null +++ b/packages/associative/README.md @@ -0,0 +1,219 @@ +# @thi.ng/associative + +[![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/associative.svg)](https://www.npmjs.com/package/@thi.ng/associative) + +## About + +This package provides alternative `Set` & `Map` data type +implementations with customizable equality semantics, as well as common +operations working with these types: + +- Array based `ArraySet`, Linked List based `LLSet`, + [Skiplist](https://en.wikipedia.org/wiki/Skip_list) based `SortedMap` + & `SortedSet` and customizable `EquivMap` implement the full ES6 + Map/Set APIs and additional features: + - range query iterators (via `entries()`, `keys()`, `values()`) + (sorted types only) + - `ICopy`, `IEmpty` & `IEquiv` implementations + - `ICompare` implementation for sorted types + - multiple value additions / updates / deletions via `into()`, + `dissoc()` (maps) and `disj()` (sets) + - configurable key equality & comparison (incl. default + implementations) + - getters w/ optional "not-found" default value + - `fromObject()` converters (for maps only) +- Polymorphic set operations (union, intersection, difference) - works + with both native and custom Sets and retains their types +- Natural & selective + [joins](https://en.wikipedia.org/wiki/Relational_algebra#Joins_and_join-like_operators) + (incl. key renaming, ported from Clojure) +- Key-value pair inversion for maps and vanilla objects + - i.e. swaps `K => V` to `V => K` +- Single or multi-property index generation for maps and objects +- Key selection & renaming for maps and objects + +### Why? + +The native ES6 implementations use object reference identity to +determine key containment, but often it's more practical and useful to +use equivalent value semantics for this purpose, especially when keys +are structured data (arrays / objects). + +**Note**: It's the user's responsibility to ensure the inserted keys are +kept immutable (even if technically they're not). + +### Comparison with ES6 native types + +```ts +// first two objects w/ equal values +a = [1, 2]; +b = [1, 2]; +``` + +Using native implementations + +```ts +set = new Set(); +set.add(a); +set.has(b); +// false + +map = new Map(); +map.set(a, "foo"); +map.get(b); +// undefined +``` + +Using custom implementations: + +```ts +import * as assoc from "@thi.ng/associative"; + +set = new assoc.ArraySet(); +set.add(a); +set.add({a: 1}); +// ArraySet { [ 1, 2 ], { a: 1 } } +set.has(b); +// true +set.has({a: 1}); +// true + +set = new assoc.LLSet(); +set.add(a); +set.add({a: 1}); +// LLSet { [ 1, 2 ], { a: 1 } } +set.has(b); +// true +set.has({a: 1}); +// true + +// by default EquivMap uses ArraySet for its canonical keys +map = new assoc.EquivMap(); + +// with custom implementation +map = new assoc.EquivMap(null, { keys: assoc.ArraySet }); +map.set(a, "foo"); +// EquivMap { [ 1, 2 ] => 'foo' } +map.get(b); +// "foo" + +set = new assoc.SortedSet([a, [-1, 2], [-1, -2]]); +// SortedSet { [ -1, -2 ], [ -1, 2 ], [ 1, 2 ] } +set.has(b); +// true + +map = new assoc.SortedMap([[a, "foo"], [[-1,-2], "bar"]]); +// SortedMap { [ -1, -2 ] => 'bar', [ 1, 2 ] => 'foo' } +map.get(b); +// "foo" + +// key lookup w/ default value +map.get([3,4], "n/a"); +// "n/a" +``` + +## Installation + +``` +yarn add @thi.ng/associative +``` + +## Types + +### IEquivSet + +All `Set` implementations in this package implement the +[IEquivSet](https://github.com/thi-ng/umbrella/tree/master/packages/associative/src/api.ts#L7) +interface, an extension of the native ES6 Set API. + +### ArraySet + +Simple array based `Set` implementation which by default uses +[@thi.ng/api/equiv](https://github.com/thi-ng/umbrella/tree/master/packages/api/src/equiv.ts) +for value equivalence checking. + +### LLSet + +Similar to `ArraySet`, but uses +[@thi.ng/dcons](https://github.com/thi-ng/umbrella/tree/master/packages/dcons) linked list +as backing storage for values. + +### EquivMap + +This `Map` implementation uses a native ES6 `Map` as backing storage for +its key-value pairs and an additional `IEquivSet` implementation for +canonical keys. By default uses `ArraySet` for this purpose. + +### SortedMap + +Alternative implementation of the ES6 Map API using a Skip list as +backing store and support for configurable key equality and sorting +semantics. Like with sets, uses @thi.ng/api/equiv & @thi.ng/api/compare +by default. + +William Pugh's (creator of this data structure) description: + +> "Skip lists are probabilistic data structures that have the same +asymptotic expected time bounds as balanced trees, are simpler, faster +and use less space." + +Data structure description: + +- ftp://ftp.cs.umd.edu/pub/skipLists/skiplists.pdf +- https://en.wikipedia.org/wiki/Skip_list + + +#### Ranged queries + +```ts +map = new assoc.SortedMap([ + ["c", 3], ["a", 1], ["d", 4], ["b", 2] +]); +// SortedMap { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 } + +// forward selection w/ given start key +// also works with `keys()` and `values()` +[...map.entries("c")] +// [ [ 'c', 3 ], [ 'd', 4 ] ] + +// unknown start keys are ok +[...map.entries("cc")] +// [ [ 'd', 4 ] ] + +// reverse order +[...map.entries(undefined, true)] +// [ [ 'd', 4 ], [ 'c', 3 ], [ 'b', 2 ], [ 'a', 1 ] ] + +// reverse order from start key +[...map.entries("c", true)] +// [ [ 'c', 3 ], [ 'b', 2 ], [ 'a', 1 ] ] +``` + +### SortedSet + +Sorted set implementation with standard ES6 Set API, customizable value +equality and comparison semantics and additional functionality: + +- range queries (via `entries`, `keys`, `values`) +- multiple value addition/deletion via `into()` and `disj()` + +Furthermore, this class implements the `ICopy`, IEmpty`, `ICompare` and +`IEquiv` interfaces defined by `@thi.ng/api`. The latter two allow +instances to be used as keys themselves in other data types defined in +this (and other) package(s). + +This set uses a `SortedMap` as backing store. + +## Usage examples + +TODO... Please see +[tests](https://github.com/thi-ng/umbrella/tree/master/packages/associative/test/) +and documentation in source code for now... + +## Authors + +- Karsten Schmidt + +## License + +© 2017 - 2018 Karsten Schmidt // Apache Software License 2.0 diff --git a/packages/associative/package.json b/packages/associative/package.json new file mode 100644 index 0000000000..8cd6230b1e --- /dev/null +++ b/packages/associative/package.json @@ -0,0 +1,49 @@ +{ + "name": "@thi.ng/associative", + "version": "0.4.0", + "description": "Alternative Set & Map data type implementations with customizable equality semantics & supporting operations", + "main": "./index.js", + "typings": "./index.d.ts", + "repository": "https://github.com/thi-ng/umbrella", + "author": "Karsten Schmidt ", + "license": "Apache-2.0", + "scripts": { + "build": "yarn run clean && tsc --declaration", + "clean": "rm -rf *.js *.d.ts .nyc_output build coverage doc", + "cover": "yarn test && nyc report --reporter=lcov", + "doc": "node_modules/.bin/typedoc --mode modules --out doc src", + "pub": "yarn run build && yarn publish --access public", + "test": "rm -rf build && tsc -p test && nyc mocha build/test/*.js" + }, + "devDependencies": { + "@types/mocha": "^5.0.0", + "@types/node": "^9.6.1", + "mocha": "^5.0.5", + "nyc": "^11.6.0", + "typedoc": "^0.11.1", + "typescript": "^2.8.1" + }, + "dependencies": { + "@thi.ng/api": "^2.1.0", + "@thi.ng/dcons": "^0.2.0", + "@thi.ng/iterators": "^4.1.5" + }, + "keywords": [ + "data structures", + "difference", + "equality", + "ES6", + "intersection", + "join", + "map", + "set", + "skiplist", + "sorted map", + "sorted set", + "typescript", + "union" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/associative/src/api.ts b/packages/associative/src/api.ts new file mode 100644 index 0000000000..17fbb24cb5 --- /dev/null +++ b/packages/associative/src/api.ts @@ -0,0 +1,71 @@ +import { Comparator, ICopy, IEmpty, IEquiv, Predicate2 } from "@thi.ng/api/api"; + +export type Pair = [K, V]; + +export const SEMAPHORE = Symbol("SEMAPHORE"); + +export interface IEquivSet extends + Set, + ICopy>, + IEmpty>, + IEquiv { + + readonly [Symbol.species]: EquivSetConstructor; + into(xs: Iterable): this; + disj(xs: Iterable): this; + get(val: T, notFound?: any): any; + first(): T; + opts(): EquivSetOpts; +} + +export interface EquivSetConstructor { + new(): IEquivSet; + new (values?: Iterable, opts?: EquivSetOpts): IEquivSet; + readonly prototype: IEquivSet; +} + +export interface EquivSetOpts { + /** + * Key equivalence predicate. MUST return truthy result if given + * keys are considered equal. + * + * Default: `@thi.ng/api/equiv` + */ + equiv: Predicate2; +} + +export interface EquivMapOpts extends EquivSetOpts { + keys: EquivSetConstructor; +} + +/** + * SortedMapOpts implementation config settings. + */ +export interface SortedMapOpts extends EquivSetOpts { + /** + * Key comparison function. Must follow standard comparator contract + * and return: + * - negative if `a < b` + * - positive if `a > b` + * - `0` if `a == b` + * + * Note: The `SortedMap` implementation only uses `<` comparisons. + * + * Default: `@thi.ng/api/compare` + */ + compare: Comparator; + /** + * Initial capacity before resizing (doubling) occurs. + * This value will be rounded up to next pow2. + * + * Default: 16 + */ + capacity: number; + /** + * Probability for a value to exist in any express lane. + * Default: `1 / Math.E` + */ + probability: number; +} + +export type SortedSetOpts = SortedMapOpts; diff --git a/packages/associative/src/array-set.ts b/packages/associative/src/array-set.ts new file mode 100644 index 0000000000..0c0acdf1d6 --- /dev/null +++ b/packages/associative/src/array-set.ts @@ -0,0 +1,162 @@ +import { Predicate2 } from "@thi.ng/api/api"; +import { equiv } from "@thi.ng/api/equiv"; + +import { EquivSetOpts, IEquivSet, Pair, SEMAPHORE } from "./api"; + +interface SetProps { + vals: T[]; + equiv: Predicate2; +} + +const __private = new WeakMap, SetProps>(); + +/** + * An alternative set implementation to the native ES6 Set type. Uses + * customizable equality/equivalence predicate and so is more useful + * when dealing with structured data. Implements full API of native Set + * and by the default uses `@thi.ng/api/equiv` for equivalence checking. + * + * Additionally, the type also implements the `ICopy`, `IEmpty` and + * `IEquiv` interfaces itself. + */ +export class ArraySet extends Set implements + IEquivSet { + + constructor(vals?: Iterable, opts: Partial> = {}) { + super(); + __private.set(this, { equiv: opts.equiv || equiv, vals: [] }); + vals && this.into(vals); + } + + *[Symbol.iterator]() { + yield* __private.get(this).vals; + } + + get [Symbol.species]() { + return ArraySet; + } + + get size() { + return __private.get(this).vals.length; + } + + copy() { + const s = new ArraySet(null, this.opts()); + __private.get(s).vals = [...__private.get(this).vals]; + return s; + } + + empty() { + return new ArraySet(null, this.opts()); + } + + clear() { + __private.get(this).vals.length = 0; + } + + first() { + if (this.size) { + return __private.get(this).vals[0]; + } + } + + add(x: T) { + !this.has(x) && __private.get(this).vals.push(x); + return this; + } + + into(xs: Iterable) { + for (let x of xs) { + this.add(x); + } + return this; + } + + has(x: T) { + return this.get(x, SEMAPHORE) !== SEMAPHORE; + } + + /** + * Returns the canonical value for `x`, if present. If the set + * contains no equivalent for `x`, returns `notFound`. + * + * @param x + * @param notFound + */ + get(x: T, notFound?: any) { + const $this = __private.get(this); + const eq = $this.equiv; + const vals = $this.vals; + for (let i = vals.length - 1; i >= 0; i--) { + if (eq(vals[i], x)) { + return vals[i]; + } + } + return notFound; + } + + delete(x: T) { + const $this = __private.get(this) + const eq = $this.equiv; + const vals = $this.vals; + for (let i = vals.length - 1; i >= 0; i--) { + if (eq(vals[i], x)) { + vals.splice(i, 1); + return true; + } + } + return false; + } + + disj(xs: Iterable) { + for (let x of xs) { + this.delete(x); + } + return this; + } + + equiv(o: any) { + if (this === o) { + return true; + } + if (!(o instanceof Set)) { + return false; + } + if (this.size !== o.size) { + return false; + } + const vals = __private.get(this).vals; + for (let i = vals.length - 1; i >= 0; i--) { + if (!o.has(vals[i])) { + return false; + } + } + return true; + } + + forEach(fn: (val: T, val2: T, set: Set) => void, thisArg?: any) { + const vals = __private.get(this).vals; + for (let i = vals.length - 1; i >= 0; i--) { + const v = vals[i]; + fn.call(thisArg, v, v, this); + } + } + + *entries(): IterableIterator> { + for (let v of __private.get(this).vals) { + yield [v, v]; + } + } + + *keys() { + yield* __private.get(this).vals; + } + + *values() { + yield* this.keys(); + } + + opts(): EquivSetOpts { + return { equiv: __private.get(this).equiv }; + } +} diff --git a/packages/associative/src/difference.ts b/packages/associative/src/difference.ts new file mode 100644 index 0000000000..b3af70cb30 --- /dev/null +++ b/packages/associative/src/difference.ts @@ -0,0 +1,12 @@ +import { copy, empty } from "./utils"; + +export function difference(a: Set, b: Set): Set { + if (a === b) { + return empty(a, Set); + } + const res = copy(a, Set); + for (let i of b) { + res.delete(i); + } + return res; +} diff --git a/packages/associative/src/equiv-map.ts b/packages/associative/src/equiv-map.ts new file mode 100644 index 0000000000..3fd897bec3 --- /dev/null +++ b/packages/associative/src/equiv-map.ts @@ -0,0 +1,181 @@ +import { ICopy, IEmpty, IEquiv, IObjectOf } from "@thi.ng/api/api"; +import { equiv } from "@thi.ng/api/equiv"; + +import { EquivMapOpts, IEquivSet, Pair, SEMAPHORE } from "./api"; +import { ArraySet } from "./array-set"; + +interface MapProps { + keys: IEquivSet; + map: Map; + opts: EquivMapOpts; +} + +const __private = new WeakMap, MapProps>(); + +export class EquivMap extends Map implements + Iterable>, + ICopy>, + IEmpty>, + IEquiv { + + /** + * Converts given vanilla object into an `EquivMap` instance with + * default options and returns it. + * + * @param obj + */ + static fromObject(obj: IObjectOf): EquivMap { + const m = new EquivMap(); + for (let k in obj) { + if (obj.hasOwnProperty(k)) { + m.set(k, obj[k]); + } + } + return m; + } + + /** + * Creates a new instance with optional initial key-value pairs and + * provided options. If no `opts` are given, uses `ArraySet` for + * storing canonical keys and `@thi.ng/api/equiv` for checking key + * equivalence. + * + * @param pairs + * @param opts + */ + constructor(pairs?: Iterable>, opts?: EquivMapOpts) { + super(); + opts = Object.assign(>{ equiv, keys: ArraySet }, opts); + __private.set(this, { + keys: new (opts.keys)(null, { equiv: opts.equiv }), + map: new Map(), + opts + }); + if (pairs) { + this.into(pairs); + } + } + + [Symbol.iterator]() { + return this.entries(); + } + + get [Symbol.species]() { + return EquivMap; + } + + get size() { + return __private.get(this).keys.size; + } + + clear() { + const $this = __private.get(this); + $this.keys.clear(); + $this.map.clear(); + } + + empty() { + return new EquivMap(null, __private.get(this).opts); + } + + copy() { + const $this = __private.get(this); + const m = new EquivMap(); + __private.set(m, { + keys: $this.keys.copy(), + map: new Map($this.map), + opts: $this.opts, + }); + return m; + } + + equiv(o: any) { + if (this === o) { + return true; + } + if (!(o instanceof Map)) { + return false; + } + if (this.size !== o.size) { + return false; + } + for (let p of __private.get(this).map.entries()) { + if (!equiv(o.get(p[0]), p[1])) { + return false; + } + } + return true; + } + + delete(key: K) { + const $this = __private.get(this); + key = $this.keys.get(key, SEMAPHORE); + if (key !== SEMAPHORE) { + $this.map.delete(key); + $this.keys.delete(key); + return true; + } + return false; + } + + dissoc(...keys: K[]) { + for (let k of keys) { + this.delete(k); + } + return this; + } + + forEach(fn: (val: V, key: K, map: Map) => void, thisArg?: any) { + for (let pair of __private.get(this).map) { + fn.call(thisArg, pair[1], pair[0], this); + } + } + + get(key: K, notFound?: any) { + const $this = __private.get(this); + key = $this.keys.get(key, SEMAPHORE); + if (key !== SEMAPHORE) { + return $this.map.get(key); + } + return notFound; + } + + has(key: K) { + return __private.get(this).keys.has(key); + } + + set(key: K, value: V) { + const $this = __private.get(this); + const k = $this.keys.get(key, SEMAPHORE); + if (k !== SEMAPHORE) { + $this.map.set(k, value); + } else { + $this.keys.add(key); + $this.map.set(key, value); + } + return this; + } + + into(pairs: Iterable>) { + for (let p of pairs) { + this.set(p[0], p[1]); + } + return this; + } + + entries() { + return __private.get(this).map.entries(); + } + + keys() { + return __private.get(this).map.keys(); + } + + values() { + return __private.get(this).map.values(); + } + + opts(): EquivMapOpts { + return __private.get(this).opts; + } +} \ No newline at end of file diff --git a/packages/associative/src/index.ts b/packages/associative/src/index.ts new file mode 100644 index 0000000000..50d4f77140 --- /dev/null +++ b/packages/associative/src/index.ts @@ -0,0 +1,16 @@ +export * from "./array-set"; +export * from "./equiv-map"; +export * from "./ll-set"; +export * from "./sorted-map"; +export * from "./sorted-set"; + +export * from "./difference"; +export * from "./intersection"; +export * from "./union"; + +export * from "./indexed"; +export * from "./invert"; +export * from "./join"; +export * from "./merge"; +export * from "./rename-keys"; +export * from "./select-keys"; diff --git a/packages/associative/src/indexed.ts b/packages/associative/src/indexed.ts new file mode 100644 index 0000000000..0151ceab21 --- /dev/null +++ b/packages/associative/src/indexed.ts @@ -0,0 +1,35 @@ +import { EquivMap } from "./equiv-map"; +import { selectKeysObj } from "./select-keys"; +import { empty } from "./utils"; + +/** + * Takes a set of objects and array of indexing keys. Calls + * `selectKeysObj` on each set value and used returned objects as new + * keys to group original values. Returns a map of sets. + * + * ``` + * indexed( + * new Set([{a: 1, b: 1}, {a: 1, b: 2}, {a: 1, b: 1, c: 2}]), + * ["a","b"] + * ) + * // EquivMap { + * // { a: 1, b: 1 } => Set { { a: 1, b: 1 }, { a: 1, b: 1, c: 2 } }, + * // { a: 1, b: 2 } => Set { { a: 1, b: 2 } } } + * ``` + * + * @param records set of objects to index + * @param ks keys used for indexing + */ +export function indexed(records: Set, ks: PropertyKey[]) { + const res = new EquivMap>(); + let m, ik, rv; + for (m of records) { + ik = selectKeysObj(m, ks); + rv = res.get(ik); + if (!rv) { + res.set(ik, rv = empty(records, Set)); + } + rv.add(m); + } + return res; +} diff --git a/packages/associative/src/intersection.ts b/packages/associative/src/intersection.ts new file mode 100644 index 0000000000..31ec753705 --- /dev/null +++ b/packages/associative/src/intersection.ts @@ -0,0 +1,17 @@ +import { empty } from "./utils"; + +export function intersection(a: Set, b: Set): Set { + if (a === b) { + return a; + } + if (b.size < a.size) { + return intersection(b, a); + } + const res = empty(a, Set); + for (let i of b) { + if (a.has(i)) { + res.add(i); + } + } + return res; +} diff --git a/packages/associative/src/invert.ts b/packages/associative/src/invert.ts new file mode 100644 index 0000000000..ca4f249bfb --- /dev/null +++ b/packages/associative/src/invert.ts @@ -0,0 +1,41 @@ +import { IObjectOf } from "@thi.ng/api/api"; + +import { empty } from "./utils"; + +/** + * Returns a new map in which the original values are used as keys and + * original keys as values. + * + * ``` + * invertMap(new Map([["a", 1], ["b", 2]])); + * // Map { 1 => 'a', 2 => 'b' } + * ``` + * + * @param src + */ +export function invertMap(src: Map) { + const dest: Map = empty(src, Map); + for (let p of src) { + dest.set(p[1], p[0]); + } + return dest; +} + +/** + * Returns a new object in which the original values are used as keys + * and original keys as values. + * + * ``` + * invertObj({a: 1, b: 2}) + * // { '1': 'a', '2': 'b' } + * ``` + * + * @param src + */ +export function invertObj(src: IObjectOf) { + const dest: IObjectOf = {}; + for (let k in src) { + dest[src[k]] = k; + } + return dest; +} diff --git a/packages/associative/src/join.ts b/packages/associative/src/join.ts new file mode 100644 index 0000000000..2b619037c5 --- /dev/null +++ b/packages/associative/src/join.ts @@ -0,0 +1,116 @@ +import { IObjectOf } from "@thi.ng/api/api"; + +import { intersection } from "./intersection"; +import { indexed } from "./indexed"; +import { invertObj } from "./invert"; +import { mergeObj } from "./merge"; +import { renameKeysObj } from "./rename-keys"; +import { selectKeysObj } from "./select-keys"; +import { empty, first, objValues } from "./utils"; + +/** + * Computes the natural join between the two sets of relations. Each set + * is assumed to have plain objects as values with at least one of the + * keys present in both sides. Furthermore the objects in each set are + * assumed to have the same internal structure (i.e. sets of keys). + * + * ``` + * join( + * new Set([ + * {id: 1, name: "foo"}, + * {id: 2, name: "bar"}, + * {id: 3, name: "baz"}]), + * new Set([ + * {id: 1, color: "red"}, + * {id: 2, color: "blue"}]) + * ) + * // Set { + * // { id: 1, color: 'red', name: 'foo' }, + * // { id: 2, color: 'blue', name: 'bar' } + * // } + * ``` + * + * @param xrel + * @param yrel + */ +export function join(xrel: Set, yrel: Set) { + if (xrel.size && yrel.size) { + const ks = [...intersection( + new Set(Object.keys(first(xrel) || {})), + new Set(Object.keys(first(yrel) || {}))) + ]; + let r, s; + if (xrel.size <= yrel.size) { + r = xrel; + s = yrel; + } else { + r = yrel; + s = xrel; + } + const idx = indexed(r, ks); + const res = empty(xrel, Set); + for (let x of s) { + const found = idx.get(selectKeysObj(x, ks)); + if (found) { + for (let f of found) { + res.add(mergeObj({ ...f }, x)); + } + } + } + return res; + } + return empty(xrel, Set); +} + +/** + * Similar to `join()`, computes the join between two sets of relations, + * using the given keys in `kmap` only. `kmap` can also be used to + * rename join keys in `yrel` where needed, e.g. + * + * ``` + * joinWith( + * new Set([ + * {id: 1, name: "foo"}, + * {id: 2, name: "bar"}, + * {id: 3, name: "baz"}]), + * new Set([ + * {type: 1, color: "red"}, + * {type: 2, color: "blue"}]), + * {id: "type"} + * ) + * ``` + * If no renaming is desired, the values in `kmap` should be the same as + * their respective keys. + * + * @param xrel + * @param yrel + * @param kmap keys to compute join for + */ +export function joinWith(xrel: Set, yrel: Set, kmap: IObjectOf) { + if (xrel.size && yrel.size) { + let r: Set, s: Set; + let k: IObjectOf; + if (xrel.size <= yrel.size) { + r = xrel; + s = yrel; + k = invertObj(kmap); + } else { + r = yrel; + s = xrel; + k = kmap; + } + const idx = indexed(r, objValues(k)); + const ks = Object.keys(k); + const res = empty(xrel, Set); + for (let x of s) { + const found = idx.get(renameKeysObj(selectKeysObj(x, ks), k)); + if (found) { + for (let f of found) { + res.add(mergeObj({ ...f }, x)); + } + } + } + return res; + } + return empty(xrel, Set); +} diff --git a/packages/associative/src/ll-set.ts b/packages/associative/src/ll-set.ts new file mode 100644 index 0000000000..70dd2ee75e --- /dev/null +++ b/packages/associative/src/ll-set.ts @@ -0,0 +1,167 @@ +import { Predicate2 } from "@thi.ng/api/api"; +import { equiv } from "@thi.ng/api/equiv"; +import { DCons } from "@thi.ng/dcons"; + +import { EquivSetOpts, IEquivSet, Pair, SEMAPHORE } from "./api"; + +interface SetProps { + vals: DCons; + equiv: Predicate2; +} + +const __private = new WeakMap, SetProps>(); + +/** + * Similar to `ArraySet`, this class is an alternative implementation of + * the native ES6 Set API using a @thi.ng/dcons linked list as backing + * store and a customizable value equality / equivalence predicate. By + * the default uses `@thi.ng/api/equiv` for equivalence checking. + * + * Additionally, the type also implements the `ICopy`, `IEmpty` and + * `IEquiv` interfaces itself. + */ +export class LLSet extends Set implements + IEquivSet { + + constructor(vals?: Iterable, opts: Partial> = {}) { + super(); + __private.set(this, { equiv: opts.equiv || equiv, vals: new DCons() }); + vals && this.into(vals); + } + + *[Symbol.iterator]() { + yield* __private.get(this).vals; + } + + get [Symbol.species]() { + return LLSet; + } + + get size() { + return __private.get(this).vals.length; + } + + copy() { + const $this = __private.get(this); + const s = new LLSet(null, this.opts()); + __private.get(s).vals = $this.vals.copy(); + return s; + } + + empty() { + return new LLSet(null, this.opts()); + } + + clear() { + __private.get(this).vals.clear(); + } + + first() { + if (this.size) { + return __private.get(this).vals.head.value; + } + } + + add(x: T) { + !this.has(x) && __private.get(this).vals.push(x); + return this; + } + + into(xs: Iterable) { + for (let x of xs) { + this.add(x); + } + return this; + } + + has(x: T) { + return this.get(x, SEMAPHORE) !== SEMAPHORE; + } + + /** + * Returns the canonical value for `x`, if present. If the set + * contains no equivalent for `x`, returns `notFound`. + * + * @param x + * @param notFound + */ + get(x: T, notFound?: any) { + const $this = __private.get(this); + const eq = $this.equiv; + let i = $this.vals.head; + while (i) { + if (eq(i.value, x)) { + return i.value; + } + i = i.next; + } + return notFound; + } + + delete(x: T) { + const $this = __private.get(this) + const eq = $this.equiv; + let i = $this.vals.head; + while (i) { + if (eq(i.value, x)) { + $this.vals.splice(i, 1); + return true; + } + i = i.next; + } + return false; + } + + disj(xs: Iterable) { + for (let x of xs) { + this.delete(x); + } + return this; + } + + equiv(o: any) { + if (this === o) { + return true; + } + if (!(o instanceof Set)) { + return false; + } + if (this.size !== o.size) { + return false; + } + let i = __private.get(this).vals.head; + while (i) { + if (!o.has(i.value)) { + return false; + } + i = i.next; + } + return true; + } + + forEach(fn: (val: T, val2: T, set: Set) => void, thisArg?: any) { + let i = __private.get(this).vals.head; + while (i) { + fn.call(thisArg, i.value, i.value, this); + i = i.next; + } + } + + *entries(): IterableIterator> { + for (let v of __private.get(this).vals) { + yield [v, v]; + } + } + + *keys() { + yield* __private.get(this).vals; + } + + *values() { + yield* this.keys(); + } + + opts(): EquivSetOpts { + return { equiv: __private.get(this).equiv }; + } +} diff --git a/packages/associative/src/merge.ts b/packages/associative/src/merge.ts new file mode 100644 index 0000000000..b19dedf226 --- /dev/null +++ b/packages/associative/src/merge.ts @@ -0,0 +1,26 @@ +/** + * Merges all given maps in left-to-right order into `m`. + * Returns `m`. + * + * @param m + * @param maps + */ +export function mergeMaps(m: Map, ...maps: Map[]) { + for (let mm of maps) { + for (let p of mm) { + m.set(p[0], p[1]); + } + } + return m; +} + +/** + * Merges all given objects in left-to-right order into `m`. + * Returns `m`. + * + * @param m + * @param maps + */ +export function mergeObj(m, ...maps: any[]) { + return Object.assign(m, ...maps); +} diff --git a/packages/associative/src/rename-keys.ts b/packages/associative/src/rename-keys.ts new file mode 100644 index 0000000000..18379e99c3 --- /dev/null +++ b/packages/associative/src/rename-keys.ts @@ -0,0 +1,22 @@ +import { IObjectOf } from "@thi.ng/api/api"; + +import { empty } from "./utils"; + +export function renameKeysMap(src: Map, km: IObjectOf): Map { + const dest = empty(src, Map); + for (let p of src) { + const k = p[0]; + const kk = km[k]; + dest.set(kk !== undefined ? kk : k, p[1]); + } + return dest; +} + +export function renameKeysObj(src: any, km: IObjectOf) { + const dest = {}; + for (let k in src) { + const kk = km[k]; + dest[kk != null ? kk : k] = src[k]; + } + return dest; +} diff --git a/packages/associative/src/select-keys.ts b/packages/associative/src/select-keys.ts new file mode 100644 index 0000000000..eb6bf9111e --- /dev/null +++ b/packages/associative/src/select-keys.ts @@ -0,0 +1,37 @@ +import { IObjectOf } from "@thi.ng/api/api"; + +import { empty } from "./utils"; + +/** + * Returns a new map of same type as input only containing given keys + * (and only if they existed in the original map). + * + * @param src + * @param ks selected keys + */ +export function selectKeysMap(src: Map, ks: Iterable): any { + const dest = empty(src, Map); + for (let k of ks) { + if (src.has(k)) { + dest.set(k, src.get(k)); + } + } + return dest; +} + +/** + * Returns a new object only containing given keys (and only if they + * existed in the original). + * + * @param src + * @param ks + */ +export function selectKeysObj(src: IObjectOf, ks: Iterable): any { + const dest: IObjectOf = {}; + for (let k of ks) { + if (src.hasOwnProperty(k)) { + dest[k] = src[k]; + } + } + return dest; +} diff --git a/packages/associative/src/sorted-map.ts b/packages/associative/src/sorted-map.ts new file mode 100644 index 0000000000..89228f8df1 --- /dev/null +++ b/packages/associative/src/sorted-map.ts @@ -0,0 +1,371 @@ +import { Predicate2, Comparator, IObjectOf, ICompare, ICopy, IEmpty, IEquiv } from "@thi.ng/api/api"; +import { compare } from "@thi.ng/api/compare"; +import { equiv } from "@thi.ng/api/equiv"; +import { illegalArgs } from "@thi.ng/api/error"; +import { isArray } from "@thi.ng/checks/is-array"; +import { map } from "@thi.ng/iterators/map"; + +import { Pair, SEMAPHORE, SortedMapOpts } from "./api"; + +// stores private properties for all instances +// http://fitzgeraldnick.com/2014/01/13/hiding-implementation-details-with-e6-weakmaps.html +const __private = new WeakMap, SortedMapProps>(); + +interface SortedMapProps { + head: any[]; + tail: any[]; + update: any[]; + nil: any[]; + equiv: Predicate2; + cmp: Comparator; + level: number; + maxLevel: number; + p: number; + cap: number; + length: number; +} + +const KEY = 0; +const VAL = 1; +const PREV = 2; +const NEXT = 3; + +const makeNode = (level: number, key?, value?) => { + const node = new Array(4 + level); + node[KEY] = key; + node[VAL] = value; + return node; +} + +/** + * This class is an alternative implementation of the ES6 Map API using + * a Skip list as backing store and supports configurable key equality + * and sorting semantics. + * + * William Pugh (creator of this data structure) description: + * + * "Skip lists are probabilistic data structures that have the same + * asymptotic expected time bounds as balanced trees, are simpler, + * faster and use less space." + * + * Adapted & refactored from Python version: + * - http://pythonsweetness.tumblr.com/post/45227295342/fast-pypy-compatible-ordered-map-in-89-lines-of + * + * Data structure description: + * - ftp://ftp.cs.umd.edu/pub/skipLists/skiplists.pdf + * - https://en.wikipedia.org/wiki/Skip_list + */ +export class SortedMap extends Map implements + ICopy>, + ICompare>, + IEmpty>, + IEquiv { + + static fromObject(obj: IObjectOf): SortedMap { + const m = new SortedMap(null, { capacity: Object.keys(obj).length }); + for (let k in obj) { + m.set(k, obj[k]); + } + return m; + } + + static readonly DEFAULT_CAP = 16; + static readonly DEFAULT_P = 1 / Math.E; + + /** + * Creates new instance with optional given key-value pairs and/or + * implementation options. + * + * @param values + * @param opts + */ + constructor(values?: Iterable>, opts: Partial> = {}) { + super(); + values = this.init(values, opts); + if (values) { + this.into(values); + } + } + + get [Symbol.species]() { + return SortedMap; + } + + [Symbol.iterator]() { + return this.entries(); + } + + get size() { + return __private.get(this).length; + } + + clear() { + this.init(null, { ...this.opts(), capacity: SortedMap.DEFAULT_CAP }); + } + + empty(): SortedMap { + return new SortedMap(null, { ...this.opts(), capacity: SortedMap.DEFAULT_CAP }); + } + + copy(): SortedMap { + return new SortedMap(this, this.opts()); + } + + compare(o: Map) { + const n = this.size, m = o.size; + if (n < m) return -1; + if (n > m) return 1; + const i = this.entries(); + const j = o.entries(); + let x: IteratorResult>, y: IteratorResult>; + let c: number; + while ((x = i.next(), y = j.next(), !x.done && !y.done)) { + if ((c = compare(x.value[0], y.value[0])) !== 0) { + return c; + } + if ((c = compare(x.value[1], y.value[1])) !== 0) { + return c; + } + } + return 0; + } + + equiv(o: any) { + if (this === o) { + return true; + } + if (!(o instanceof Map)) { + return false; + } + if (this.size !== o.size) { + return false; + } + for (let p of this.entries()) { + if (!equiv(o.get(p[0]), p[1])) { + return false; + } + } + return true; + } + + first(): Pair { + const node = __private.get(this).head[NEXT]; + if (node[KEY] !== undefined) { + return [node[KEY], node[VAL]]; + } + } + + forEach(fn: (val: V, key: K, map: Map) => void, thisArg?: any) { + for (let p of this) { + fn.call(thisArg, p[1], p[0], this); + } + } + + set(key: K, value: V) { + let $this = __private.get(this); + const update = $this.update.slice(); + let node = this.findLessAndUpdate(update, key); + let prev = node; + node = node[NEXT]; + if ($this.equiv(node[KEY], key)) + node[VAL] = value; + else { + if ($this.length >= $this.cap) { + this.grow(); + $this = __private.get(this); + prev = this.findLessAndUpdate(update, key); + } + const newLevel = this.randomLevel(); + $this.level = Math.max($this.level, newLevel); + node = makeNode(newLevel, key, value); + node[PREV] = prev; + for (let i = 0; i <= newLevel; i++) { + const j = i + NEXT; + node[j] = update[i][j]; + update[i][j] = node; + } + if (node[NEXT] === $this.nil) { + $this.tail = node; + } else { + node[NEXT][PREV] = node; + } + $this.length++; + } + return this; + } + + delete(key: K) { + const $this = __private.get(this); + const update = $this.update.slice(); + let node = this.findLessAndUpdate(update, key)[NEXT]; + if ($this.equiv(node[KEY], key)) { + node[NEXT][PREV] = update[0]; + for (let i = 0; i <= $this.level; i++) { + const j = i + NEXT; + if (update[i][j] !== node) { + break; + } + update[i][j] = node[j]; + } + while ($this.level > 0 && $this.head[$this.level + NEXT][KEY] === undefined) { + $this.level--; + } + if ($this.tail === node) { + $this.tail = node[PREV]; + } + $this.length--; + return true; + } + return false; + } + + dissoc(...keys: K[]) { + for (let k of keys) { + this.delete(k); + } + return this; + } + + has(key: K) { + return this.get(key, SEMAPHORE) !== SEMAPHORE; + } + + get(key: K, notFound?: any) { + const $this = __private.get(this); + const node = this.findLess(key)[NEXT]; + return $this.equiv(node[KEY], key) ? + node[VAL] : + notFound; + } + + into(pairs: Iterable>) { + for (let p of pairs) { + this.set(p[0], p[1]); + } + return this; + } + + *entries(key?: K, reverse = false): IterableIterator> { + const $this = __private.get(this); + const step = reverse ? PREV : NEXT; + let node = reverse ? $this.tail : $this.head[NEXT]; + if (key !== undefined) { + const found = this.findLess(key); + // the original python impl doesn't handle unknown and out-of-range keys properly + node = !reverse || (found[NEXT] != $this.nil && $this.equiv(found[NEXT][KEY], key)) ? + found[NEXT] : + found; + } + while (node[KEY] !== undefined) { + yield [node[KEY], node[VAL]]; + node = node[step]; + } + } + + keys(key?: K, reverse = false): IterableIterator { + return map((p) => p[0], this.entries(key, reverse)); + } + + values(key?: K, reverse = false): IterableIterator { + return map((p) => p[1], this.entries(key, reverse)); + } + + protected init(values: Iterable>, opts: Partial>) { + let cap; + if (values && !opts.capacity) { + values = isArray(values) ? values : [...values]; + cap = Math.max((values).length, SortedMap.DEFAULT_CAP); + } + else { + cap = opts.capacity || SortedMap.DEFAULT_CAP; + } + cap < 1 && illegalArgs(`invalid capacity: ${cap}`); + const maxLevel = Math.ceil(Math.log2(cap)); + const nil = makeNode(-1); + const head = makeNode(maxLevel).fill(nil, 3); + const update = new Array(maxLevel + 1).fill(head); + __private.set(this, { + head, + update, + nil, + tail: nil, + equiv: opts.equiv || equiv, + cmp: opts.compare || compare, + level: 0, + maxLevel, + p: opts.probability || SortedMap.DEFAULT_P, + cap: Math.pow(2, maxLevel), + length: 0 + }); + return values; + } + + opts(growFactor = 1): SortedMapOpts { + const $this = __private.get(this); + return { + capacity: $this.cap * growFactor, + equiv: $this.equiv, + compare: $this.cmp, + probability: $this.p, + }; + } + + /** + * Recreates map with double capacity. + */ + protected grow() { + const tmp = new SortedMap( + this.entries(), + this.opts(2)); + __private.set(this, __private.get(tmp)); + __private.delete(tmp); + } + + protected randomLevel() { + const $this = __private.get(this); + const max = Math.min($this.maxLevel, $this.level + 1); + const p = $this.p; + let level = 0; + while (Math.random() < p && level < max) { + level++; + } + return level; + } + + protected findLess(key: K) { + const $this = __private.get(this); + const cmp = $this.cmp; + let node = $this.head; + for (let i = $this.level; i >= 0; i--) { + const j = i + NEXT; + let k = node[j][KEY]; + while (k !== undefined && cmp(k, key) < 0) { + node = node[j]; + k = node[j][KEY]; + } + } + return node; + } + + /** + * Like `findLess`, but records path in `update` array + * + * @param update + * @param key + */ + protected findLessAndUpdate(update, key: K) { + const $this = __private.get(this); + const cmp = $this.cmp; + let node = $this.head; + for (let i = $this.level; i >= 0; i--) { + const j = i + NEXT; + let k = node[j][KEY]; + while (k !== undefined && cmp(k, key) < 0) { + node = node[j]; + k = node[j][KEY]; + } + update[i] = node; + } + return node; + } +} diff --git a/packages/associative/src/sorted-set.ts b/packages/associative/src/sorted-set.ts new file mode 100644 index 0000000000..444d51b540 --- /dev/null +++ b/packages/associative/src/sorted-set.ts @@ -0,0 +1,160 @@ +import { ICompare } from "@thi.ng/api/api"; +import { map } from "@thi.ng/iterators/map"; + +import { Pair, SortedSetOpts, IEquivSet } from "./api"; +import { SortedMap } from "./sorted-map"; +import { compare } from "@thi.ng/api/compare"; + +const __private = new WeakMap, SortedMap>(); + +/** + * Sorted set implementation with standard ES6 Set API, customizable + * value equality and comparison semantics and additional functionality: + * + * - range queries (via `entries`, `keys`, `values`) + * - multiple value addition/deletion via `into()` and `disj()` + * + * Furthermore, this class implements the `ICopy`, IEmpty`, `ICompare` + * and `IEquiv` interfaces defined by `@thi.ng/api`. The latter two + * allow instances to be used as keys themselves in other data types + * defined in this (and other) package(s). + * + * This set uses a `SortedMap` as backing store and therefore has the + * same resizing characteristics. + */ +export class SortedSet extends Set implements + IEquivSet, + ICompare> { + + /** + * Creates new instance with optional given values and/or + * implementation options. The options are the same as used by + * `SortedMap`. + * + * @param values + * @param opts + */ + constructor(values?: Iterable, opts?: SortedSetOpts) { + super(); + __private.set(this, new SortedMap( + values ? map((x) => [x, x], values) : null, + opts + )); + } + + [Symbol.iterator](): IterableIterator { + return this.keys(); + } + + get [Symbol.species]() { + return SortedSet; + } + + get size() { + return __private.get(this).size; + } + + copy(): SortedSet { + return new SortedSet(this.keys(), this.opts()); + } + + empty() { + return new SortedSet(null, { ...this.opts(), capacity: SortedMap.DEFAULT_CAP }); + } + + compare(o: Set) { + const n = this.size, m = o.size; + if (n < m) return -1; + if (n > m) return 1; + const i = this.entries(); + const j = o.entries(); + let x: IteratorResult>, y: IteratorResult>; + let c: number; + while ((x = i.next(), y = j.next(), !x.done && !y.done)) { + if ((c = compare(x.value[0], y.value[0])) !== 0) { + return c; + } + } + return 0; + } + + equiv(o: any) { + if (this === o) { + return true; + } + if (!(o instanceof Set)) { + return false; + } + if (this.size !== o.size) { + return false; + } + for (let k of this.keys()) { + if (!o.has(k)) { + return false; + } + } + return true; + } + + entries(): IterableIterator> { + return __private.get(this).entries(); + } + + keys(): IterableIterator { + return __private.get(this).keys(); + } + + values(): IterableIterator { + return __private.get(this).values(); + } + + add(value: T) { + __private.get(this).set(value, value); + return this; + } + + into(xs: Iterable) { + for (let x of xs) { + this.add(x); + } + return this; + } + + clear(): void { + __private.get(this).clear(); + } + + first() { + const first = __private.get(this).first(); + return first ? first[0] : undefined; + } + + delete(value: T): boolean { + return __private.get(this).delete(value); + } + + disj(xs: Iterable) { + for (let x of xs) { + this.delete(x); + } + return this; + } + + forEach(fn: (val: T, val2: T, set: Set) => void, thisArg?: any): void { + for (let p of this) { + fn.call(thisArg, p[0], p[0], this); + } + } + + has(value: T): boolean { + return __private.get(this).has(value); + } + + get(value: T, notFound?: any) { + return __private.get(this).get(value, notFound); + } + + opts(): SortedSetOpts { + return __private.get(this).opts(); + } +} \ No newline at end of file diff --git a/packages/associative/src/union.ts b/packages/associative/src/union.ts new file mode 100644 index 0000000000..ce185e89eb --- /dev/null +++ b/packages/associative/src/union.ts @@ -0,0 +1,15 @@ +import { copy } from "./utils"; + +export function union(a: Set, b: Set): Set { + if (a === b) { + return a; + } + if (b.size > a.size) { + return union(b, a); + } + const res = copy(a, Set); + for (let i of b) { + res.add(i); + } + return res; +} \ No newline at end of file diff --git a/packages/associative/src/utils.ts b/packages/associative/src/utils.ts new file mode 100644 index 0000000000..be99465a24 --- /dev/null +++ b/packages/associative/src/utils.ts @@ -0,0 +1,15 @@ +import { implementsFunction } from "@thi.ng/checks/implements-function"; + +export const empty = (x, ctor) => implementsFunction(x, "empty") ? x.empty() : new (x[Symbol.species] || ctor)(); + +export const copy = (x, ctor) => implementsFunction(x, "copy") ? x.copy() : new (x[Symbol.species] || ctor)(x); + +export const first = (x: Iterable) => x[Symbol.iterator]().next().value; + +export const objValues = (src: any) => { + const vals = []; + for (let k in src) { + vals.push(src[k]); + } + return vals; +}; diff --git a/packages/associative/test/difference.ts b/packages/associative/test/difference.ts new file mode 100644 index 0000000000..8d92167766 --- /dev/null +++ b/packages/associative/test/difference.ts @@ -0,0 +1,39 @@ +import * as assert from "assert"; + +import { ArraySet } from "../src/array-set"; +import { difference } from "../src/difference"; + +describe("difference", () => { + + it("native (numbers)", () => { + const a = new Set([1, 2, 3, 4]); + const b = new Set([3, 4, 5]); + assert.deepEqual(difference(a, b), new Set([1, 2])); + }); + + it("array (numbers)", () => { + const a = new ArraySet([1, 2, 3, 4]); + const b = new ArraySet([3, 4, 5]); + assert.deepEqual(difference(a, b), new ArraySet([1, 2])); + }); + + it("native (obj)", () => { + const a = new Set([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]); + const b = new Set([{ a: 3 }, { a: 4 }, { a: 5 }]); + const d = difference(a, b); + assert.equal(d.size, 4); // verifies that it doesn't work w/ native sets! + assert.deepEqual(d, a); + assert.notStrictEqual(d, a); + assert.notStrictEqual(d, b); + }); + + it("array (obj)", () => { + const a = new ArraySet([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]); + const b = new ArraySet([{ a: 3 }, { a: 4 }, { a: 5 }]); + const d = difference(a, b); + assert.equal(d.size, 2); + assert.deepEqual(d, new Set([{ a: 1 }, { a: 2 }])); + assert.notStrictEqual(d, a); + assert.notStrictEqual(d, b); + }); +}); diff --git a/packages/associative/test/intersection.ts b/packages/associative/test/intersection.ts new file mode 100644 index 0000000000..45ce17114e --- /dev/null +++ b/packages/associative/test/intersection.ts @@ -0,0 +1,35 @@ +import * as assert from "assert"; + +import { ArraySet } from "../src/array-set"; +import { intersection } from "../src/intersection"; + +describe("intersection", () => { + + it("native (numbers)", () => { + const a = new Set([1, 2, 3, 4]); + const b = new Set([3, 4, 5, 6]); + assert.deepEqual(intersection(a, b), new Set([3, 4])); + }); + + it("array (numbers)", () => { + const a = new ArraySet([1, 2, 3, 4]); + const b = new ArraySet([3, 4, 5, 6]); + assert.deepEqual(intersection(a, b), new ArraySet([3, 4])); + }); + + it("native (obj)", () => { + const a = new Set([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]); + const b = new Set([{ a: 3 }, { a: 4 }, { a: 5 }]); + const i = intersection(a, b); + assert.deepEqual(i, new Set()); // verifies that it doesn't work w/ native sets! + assert.notStrictEqual(i, a); + assert.notStrictEqual(i, b); + }); + + it("array (obj)", () => { + const a = new ArraySet([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]); + const b = new ArraySet([{ a: 3 }, { a: 4 }, { a: 5 }]); + const i = intersection(a, b); + assert.deepEqual(i, new ArraySet([{ a: 3 }, { a: 4 }])); + }); +}); diff --git a/packages/associative/test/join.ts b/packages/associative/test/join.ts new file mode 100644 index 0000000000..a94e1ee602 --- /dev/null +++ b/packages/associative/test/join.ts @@ -0,0 +1,36 @@ +import * as assert from "assert"; + +import { ArraySet } from "../src/array-set"; +import { join, joinWith } from "../src/join"; + +describe("join", () => { + it("simple", () => { + const a = new ArraySet([{ a: 1 }, { a: 2 }]); + const b = new ArraySet([{ b: 1 }, { b: 2 }]); + assert.deepEqual(join(a, b), new ArraySet([{ a: 1, b: 1 }, { a: 2, b: 1 }, { a: 1, b: 2 }, { a: 2, b: 2 }])); + }); + + it("simple isec", () => { + const a = new ArraySet([{ id: "a", type: 1 }, { id: "b", type: 1 }, { id: "c", type: 2 }]); + const b = new ArraySet([{ type: 1, label: "foo" }, { type: 2, label: "bar" }, { type: 3, label: "baz" }]); + assert.deepEqual( + join(a, b), + new ArraySet([ + { id: "a", type: 1, label: "foo" }, + { id: "b", type: 1, label: "foo" }, + { id: "c", type: 2, label: "bar" } + ])); + }); + + it("joinWith", () => { + const a = new ArraySet([{ id: "a", type: 1 }, { id: "b", type: 1 }, { id: "c", type: 2 }]); + const b = new ArraySet([{ xyz: 1, label: "foo" }, { xyz: 2, label: "bar" }, { xyz: 3, label: "baz" }]); + assert.deepEqual( + joinWith(a, b, { type: "xyz" }), + new ArraySet([ + { id: "a", type: 1, xyz: 1, label: "foo" }, + { id: "b", type: 1, xyz: 1, label: "foo" }, + { id: "c", type: 2, xyz: 2, label: "bar" } + ])); + }); +}); diff --git a/packages/associative/test/sorted-map.ts b/packages/associative/test/sorted-map.ts new file mode 100644 index 0000000000..9b94d3b9dd --- /dev/null +++ b/packages/associative/test/sorted-map.ts @@ -0,0 +1,113 @@ +import * as assert from "assert"; + +import { SortedMap } from "../src/sorted-map"; + +describe("SortedMap", () => { + + let m: SortedMap; + + beforeEach(() => { + m = SortedMap.fromObject({ a: 1, b: 2, c: 3 }); + }); + + + it("size", () => { + assert.equal(m.size, 3); + m.set("a", 10); + assert.equal(m.size, 3); + m.set("d", 10); + assert.equal(m.size, 4); + m.delete("d"); + assert.equal(m.size, 3); + m.delete("d"); + assert.equal(m.size, 3); + }); + + it("has", () => { + assert(m.has("a")); + assert(m.has("b")); + assert(m.has("c")); + assert(!m.has("aa")); + assert(!m.has("d")); + assert(!m.has("@")); + }); + + it("get", () => { + assert.strictEqual(m.get("a"), 1); + assert.strictEqual(m.get("b"), 2); + assert.strictEqual(m.get("c"), 3); + assert.strictEqual(m.get("aa"), undefined); + assert.strictEqual(m.get("d"), undefined); + assert.strictEqual(m.get("@", -1), -1); + }); + + it("entries", () => { + assert.deepEqual([...m], [["a", 1], ["b", 2], ["c", 3]]); + }); + + it("entries rev", () => { + assert.deepEqual([...m.entries(undefined, true)], [["c", 3], ["b", 2], ["a", 1]]); + }); + + it("entries a", () => { + assert.deepEqual([...m.entries("a")], [["a", 1], ["b", 2], ["c", 3]]); + }); + + it("entries a rev", () => { + assert.deepEqual([...m.entries("a", true)], [["a", 1]]); + }); + + it("entries aa", () => { + assert.deepEqual([...m.entries("aa")], [["b", 2], ["c", 3]]); + }); + + it("entries aa rev", () => { + assert.deepEqual([...m.entries("aa", true)], [["a", 1]]); + }); + + it("entries bb", () => { + assert.deepEqual([...m.entries("bb")], [["c", 3]]); + }); + + it("entries bb rev", () => { + assert.deepEqual([...m.entries("bb", true)], [["b", 2], ["a", 1]]); + }); + + it("entries c", () => { + assert.deepEqual([...m.entries("c")], [["c", 3]]); + }); + + it("entries c rev", () => { + assert.deepEqual([...m.entries("c", true)], [["c", 3], ["b", 2], ["a", 1]]); + }); + + it("entries 0", () => { + assert.deepEqual([...m.entries("0")], [["a", 1], ["b", 2], ["c", 3]]); + }); + + it("entries 0 rev", () => { + assert.deepEqual([...m.entries("0", true)], []); + }); + + it("entries d", () => { + assert.deepEqual([...m.entries("d")], []); + }); + + it("entries d rev", () => { + assert.deepEqual([...m.entries("d", true)], [["c", 3], ["b", 2], ["a", 1]]); + }); + + it("keys", () => { + assert.deepEqual([...m.keys()], ["a", "b", "c"]); + m.set("aa", 0); + m.set("d", 0); + assert.deepEqual([...m.keys()], ["a", "aa", "b", "c", "d"]); + }); + + it("values", () => { + assert.deepEqual([...m.values()], [1, 2, 3]); + m.set("aa", 0); + m.set("d", 0); + assert.deepEqual([...m.values()], [1, 0, 2, 3, 0]); + }); +}); diff --git a/packages/associative/test/tsconfig.json b/packages/associative/test/tsconfig.json new file mode 100644 index 0000000000..bcf29ace54 --- /dev/null +++ b/packages/associative/test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../build" + }, + "include": [ + "./**/*.ts", + "../src/**/*.ts" + ] +} diff --git a/packages/associative/test/union.ts b/packages/associative/test/union.ts new file mode 100644 index 0000000000..948393823a --- /dev/null +++ b/packages/associative/test/union.ts @@ -0,0 +1,39 @@ +import * as assert from "assert"; + +import { ArraySet } from "../src/array-set"; +import { union } from "../src/union"; + +describe("union", () => { + + it("native (numbers)", () => { + const a = new Set([1, 2, 3, 4]); + const b = new Set([3, 4, 5, 6]); + assert.deepEqual(union(a, b), new Set([1, 2, 3, 4, 5, 6])); + }); + + it("equiv (numbers)", () => { + const a = new ArraySet([1, 2, 3, 4]); + const b = new ArraySet([3, 4, 5, 6]); + assert.deepEqual(union(a, b), new ArraySet([1, 2, 3, 4, 5, 6])); + }); + + it("native (obj)", () => { + const a = new Set([{ a: 1 }, { a: 2 }]); + const b = new Set([{ a: 2 }, { a: 3 }]); + const u = union(a, b); + assert.equal(u.size, 4); + assert.deepEqual(u, new Set([{ a: 1 }, { a: 2 }, { a: 2 }, { a: 3 }])); + assert.notStrictEqual(u, a); + assert.notStrictEqual(u, b); + }); + + it("equiv (obj)", () => { + const a = new ArraySet([{ a: 1 }, { a: 2 }]); + const b = new ArraySet([{ a: 2 }, { a: 3 }]); + const u = union(a, b); + assert.equal(u.size, 3); + assert.deepEqual(u, new ArraySet([{ a: 1 }, { a: 2 }, { a: 3 }])); + assert.notStrictEqual(u, a); + assert.notStrictEqual(u, b); + }); +}); diff --git a/packages/associative/tsconfig.json b/packages/associative/tsconfig.json new file mode 100644 index 0000000000..bd6481a5a6 --- /dev/null +++ b/packages/associative/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "." + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/packages/atom/CHANGELOG.md b/packages/atom/CHANGELOG.md index c41cc4b2f3..0b35653561 100644 --- a/packages/atom/CHANGELOG.md +++ b/packages/atom/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.2.5](https://github.com/thi-ng/umbrella/compare/@thi.ng/atom@1.2.4...@thi.ng/atom@1.2.5) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/atom + ## [1.2.4](https://github.com/thi-ng/umbrella/compare/@thi.ng/atom@1.2.3...@thi.ng/atom@1.2.4) (2018-04-08) diff --git a/packages/atom/package.json b/packages/atom/package.json index 3f62329439..7d89ff6afd 100644 --- a/packages/atom/package.json +++ b/packages/atom/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/atom", - "version": "1.2.4", + "version": "1.2.5", "description": "Mutable wrapper for immutable values", "main": "./index.js", "typings": "./index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@thi.ng/api": "^2.2.0", - "@thi.ng/paths": "^1.1.5" + "@thi.ng/paths": "^1.1.6" }, "keywords": [ "cursor", diff --git a/packages/csp/CHANGELOG.md b/packages/csp/CHANGELOG.md index 356692bf8b..bacbf1372e 100644 --- a/packages/csp/CHANGELOG.md +++ b/packages/csp/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.3.27](https://github.com/thi-ng/umbrella/compare/@thi.ng/csp@0.3.26...@thi.ng/csp@0.3.27) (2018-04-11) + + + + +**Note:** Version bump only for package @thi.ng/csp + + +## [0.3.26](https://github.com/thi-ng/umbrella/compare/@thi.ng/csp@0.3.25...@thi.ng/csp@0.3.26) (2018-04-10) + + + + +**Note:** Version bump only for package @thi.ng/csp + ## [0.3.25](https://github.com/thi-ng/umbrella/compare/@thi.ng/csp@0.3.24...@thi.ng/csp@0.3.25) (2018-04-08) diff --git a/packages/csp/package.json b/packages/csp/package.json index b976fa447e..33a0341e40 100644 --- a/packages/csp/package.json +++ b/packages/csp/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/csp", - "version": "0.3.25", + "version": "0.3.27", "description": "ES6 promise based CSP implementation", "main": "./index.js", "typings": "./index.d.ts", @@ -28,8 +28,8 @@ "typescript": "^2.8.1" }, "dependencies": { - "@thi.ng/dcons": "^0.1.19", - "@thi.ng/transducers": "^1.7.5" + "@thi.ng/dcons": "^0.2.0", + "@thi.ng/transducers": "^1.8.0" }, "keywords": [ "async", diff --git a/packages/dcons/CHANGELOG.md b/packages/dcons/CHANGELOG.md index 28257e1e8d..b0e7dfa13a 100644 --- a/packages/dcons/CHANGELOG.md +++ b/packages/dcons/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.2.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/dcons@0.1.19...@thi.ng/dcons@0.2.0) (2018-04-10) + + +### Features + +* **dcons:** add IEmpty impl, minor refactoring ([10c089a](https://github.com/thi-ng/umbrella/commit/10c089a)) + + + + ## [0.1.19](https://github.com/thi-ng/umbrella/compare/@thi.ng/dcons@0.1.18...@thi.ng/dcons@0.1.19) (2018-04-08) diff --git a/packages/dcons/package.json b/packages/dcons/package.json index dbc6235c8a..7ef18cb484 100644 --- a/packages/dcons/package.json +++ b/packages/dcons/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/dcons", - "version": "0.1.19", + "version": "0.2.0", "description": "Comprehensive doubly linked list structure w/ iterator support", "main": "./index.js", "typings": "./index.d.ts", diff --git a/packages/dcons/src/index.ts b/packages/dcons/src/index.ts index cfb98485f7..24361be445 100644 --- a/packages/dcons/src/index.ts +++ b/packages/dcons/src/index.ts @@ -13,13 +13,14 @@ export interface ConsCell { export class DCons implements api.ICompare>, api.ICopy>, + api.IEmpty>, api.IEquiv, api.ILength, api.IRelease, api.IStack> { - public head: ConsCell; - public tail: ConsCell; + head: ConsCell; + tail: ConsCell; protected _length: number = 0; constructor(src?: Iterable) { @@ -28,15 +29,23 @@ export class DCons implements } } - public get length() { + get length() { return this._length; } - public copy() { - return new DCons(this); + copy() { + return new DCons(this); } - public release() { + empty() { + return new DCons(); + } + + clear() { + this.release(); + } + + release() { let cell = this.head, next; while (cell) { next = cell.next; @@ -51,7 +60,7 @@ export class DCons implements return true; } - public compare(o: DCons) { + compare(o: DCons) { if (this._length < o._length) { return -1; } else if (this._length > o._length) { @@ -69,7 +78,7 @@ export class DCons implements } } - public equiv(o: any) { + equiv(o: any) { if ((o instanceof DCons || isArrayLike(o)) && this._length === o.length) { let cell = this.head; for (let x of o) { @@ -83,7 +92,7 @@ export class DCons implements return false; } - public *[Symbol.iterator]() { + *[Symbol.iterator]() { let cell = this.head; while (cell) { yield cell.value; @@ -91,13 +100,13 @@ export class DCons implements } } - public *cycle() { + *cycle() { while (true) { yield* this; } } - public drop() { + drop() { const cell = this.head; if (cell) { this.head = cell.next; @@ -111,7 +120,7 @@ export class DCons implements } } - public cons(value: T): DCons { + cons(value: T): DCons { const cell = >{ value, next: this.head }; if (this.head) { this.head.prev = cell; @@ -123,7 +132,7 @@ export class DCons implements return this; } - public insertBefore(cell: ConsCell, value: T): DCons { + insertBefore(cell: ConsCell, value: T): DCons { if (!cell) { illegalArgs("cell is undefined"); } @@ -138,7 +147,7 @@ export class DCons implements return this; } - public insertAfter(cell: ConsCell, value: T): DCons { + insertAfter(cell: ConsCell, value: T): DCons { if (!cell) { illegalArgs("cell is undefined"); } @@ -153,7 +162,7 @@ export class DCons implements return this; } - public insertBeforeNth(n: number, x: T) { + insertBeforeNth(n: number, x: T) { if (n < 0) { n += this._length; } @@ -164,7 +173,7 @@ export class DCons implements } } - public insertAfterNth(n: number, x: T) { + insertAfterNth(n: number, x: T) { if (n < 0) { n += this._length; } @@ -175,7 +184,7 @@ export class DCons implements } } - public insertSorted(value: T, cmp?: api.Comparator) { + insertSorted(value: T, cmp?: api.Comparator) { cmp = cmp || compare; let cell = this.head; while (cell) { @@ -187,7 +196,7 @@ export class DCons implements return this.push(value); } - public find(value: T) { + find(value: T) { let cell = this.head; while (cell) { if (cell.value === value) { @@ -197,7 +206,7 @@ export class DCons implements } } - public findWith(fn: api.Predicate) { + findWith(fn: api.Predicate) { let cell = this.head; while (cell) { if (fn(cell.value)) { @@ -207,7 +216,7 @@ export class DCons implements } } - public concat(...slices: Iterable[]) { + concat(...slices: Iterable[]) { const res = this.copy(); for (let slice of slices) { res.into(slice); @@ -215,13 +224,13 @@ export class DCons implements return res; } - public into(src: Iterable) { + into(src: Iterable) { for (let x of src) { this.push(x); } } - public slice(from = 0, to = this.length) { + slice(from = 0, to = this.length) { let a = from < 0 ? from + this._length : from; let b = to < 0 ? to + this._length : to; if (a < 0 || b < 0) { @@ -236,7 +245,7 @@ export class DCons implements return res; } - public splice(at: ConsCell | number, del = 0, insert?: Iterable): DCons { + splice(at: ConsCell | number, del = 0, insert?: Iterable): DCons { let cell: ConsCell; if (typeof at === "number") { if (at < 0) { @@ -270,7 +279,7 @@ export class DCons implements return res; } - public remove(cell: ConsCell) { + remove(cell: ConsCell) { if (cell.prev) { cell.prev.next = cell.next; } else { @@ -285,7 +294,7 @@ export class DCons implements return this; } - public swap(a: ConsCell, b: ConsCell): DCons { + swap(a: ConsCell, b: ConsCell): DCons { if (a !== b) { const t = a.value; a.value = b.value; @@ -294,7 +303,7 @@ export class DCons implements return this; } - public push(value: T): DCons { + push(value: T): DCons { if (this.tail) { const cell = >{ value, prev: this.tail }; this.tail.next = cell; @@ -306,7 +315,7 @@ export class DCons implements } } - public pop(): DCons { + pop(): DCons { const cell = this.tail; if (cell) { this.tail = cell.prev; @@ -322,15 +331,15 @@ export class DCons implements return this; } - public first() { + first() { return this.head ? this.head.value : undefined; } - public peek() { + peek() { return this.tail ? this.tail.value : undefined; } - public setHead(v: T) { + setHead(v: T) { if (this.head) { this.head.value = v; return this; @@ -338,7 +347,7 @@ export class DCons implements return this.cons(v); } - public setTail(v: T) { + setTail(v: T) { if (this.tail) { this.tail.value = v; return this; @@ -346,7 +355,7 @@ export class DCons implements return this.push(v); } - public setNth(n: number, v: T) { + setNth(n: number, v: T) { const cell = this.nthCell(n); if (!cell) { illegalArgs(`index out of bounds: ${n}`); @@ -355,12 +364,12 @@ export class DCons implements return this; } - public nth(n: number, notFound?: T) { + nth(n: number, notFound?: T) { const cell = this.nthCell(n); return cell ? cell.value : notFound; } - public nthCell(n: number) { + nthCell(n: number) { if (n < 0) { n += this._length; } @@ -382,7 +391,7 @@ export class DCons implements return cell; } - public rotateLeft() { + rotateLeft() { switch (this._length) { case 0: case 1: @@ -394,7 +403,7 @@ export class DCons implements } } - public rotateRight() { + rotateRight() { switch (this._length) { case 0: case 1: @@ -407,7 +416,7 @@ export class DCons implements } } - public map(fn: (x: T) => R) { + map(fn: (x: T) => R) { const res = new DCons(); let cell = this.head; while (cell) { @@ -417,7 +426,7 @@ export class DCons implements return res; } - public filter(pred: api.Predicate) { + filter(pred: api.Predicate) { const res = new DCons(); let cell = this.head; while (cell) { @@ -427,7 +436,7 @@ export class DCons implements return res; } - public reduce(rfn: (acc: R, x: T) => R, initial: R) { + reduce(rfn: (acc: R, x: T) => R, initial: R) { let acc: R = initial; let cell = this.head; while (cell) { @@ -438,7 +447,7 @@ export class DCons implements return acc; } - public shuffle() { + shuffle() { let n = this._length; let cell = this.tail; while (n > 0) { @@ -450,7 +459,7 @@ export class DCons implements return this; } - public reverse() { + reverse() { let head = this.head; let tail = this.tail; let n = (this._length >>> 1) + (this._length & 1); @@ -465,7 +474,7 @@ export class DCons implements return this; } - public toString() { + toString() { let res: any = []; let cell = this.head; while (cell) { @@ -477,7 +486,7 @@ export class DCons implements return res.join(", "); } - public toJSON() { + toJSON() { return [...this]; } } diff --git a/packages/dgraph/CHANGELOG.md b/packages/dgraph/CHANGELOG.md index e472c3886b..361f731709 100644 --- a/packages/dgraph/CHANGELOG.md +++ b/packages/dgraph/CHANGELOG.md @@ -3,6 +3,44 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.1.3](https://github.com/thi-ng/umbrella/compare/@thi.ng/dgraph@0.1.2...@thi.ng/dgraph@0.1.3) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/dgraph + + +## [0.1.2](https://github.com/thi-ng/umbrella/compare/@thi.ng/dgraph@0.1.1...@thi.ng/dgraph@0.1.2) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/dgraph + + +## [0.1.1](https://github.com/thi-ng/umbrella/compare/@thi.ng/dgraph@0.1.0...@thi.ng/dgraph@0.1.1) (2018-04-10) + + +### Bug Fixes + +* **dgraph:** update corrupted deps ([675847b](https://github.com/thi-ng/umbrella/commit/675847b)) + + + + + +# [0.1.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/dgraph@0.0.3...@thi.ng/dgraph@0.1.0) (2018-04-10) + + +### Features + +* **dgraph:** re-import DGraph impl & tests, update readme ([e086be6](https://github.com/thi-ng/umbrella/commit/e086be6)) + + + + ## [0.0.3](https://github.com/thi-ng/umbrella/compare/@thi.ng/dgraph@0.0.2...@thi.ng/dgraph@0.0.3) (2018-04-08) diff --git a/packages/dgraph/README.md b/packages/dgraph/README.md index a9c6693495..9cb4cda68c 100644 --- a/packages/dgraph/README.md +++ b/packages/dgraph/README.md @@ -4,9 +4,16 @@ ## About -Nothing to see here yet. Pls ignore for now. +Type-agnostic directed acyclic graph (DAG), using +[@thi.ng/associative](https://github.com/thi-ng/umbrella/tree/master/packages/associative) +maps & sets as backend. -TODO... +### Features + +- cycle detection +- accessors for direct & transitive dependencies / dependents +- topological sorting +- iterable ## Installation @@ -17,7 +24,15 @@ yarn add @thi.ng/dgraph ## Usage examples ```typescript -import * as dgraph from "@thi.ng/dgraph"; +import { DGraph } from "@thi.ng/dgraph"; + +g = new DGraph(); +g.addDependency([1, 2], [10, 20]); +g.addDependency([3, 4], [30, 40]); +g.addDependency([1, 2], [3, 4]); + +g.sort() +// [[30, 40], [3, 4], [10, 20], [1, 2]] ``` ## Authors diff --git a/packages/dgraph/package.json b/packages/dgraph/package.json index 0e0fb963f0..62f122728b 100644 --- a/packages/dgraph/package.json +++ b/packages/dgraph/package.json @@ -1,7 +1,7 @@ { "name": "@thi.ng/dgraph", - "version": "0.0.3", - "description": "TODO", + "version": "0.1.3", + "description": "Type-agnostic directed acyclic graph (DAG) & graph operations", "main": "./index.js", "typings": "./index.d.ts", "repository": "https://github.com/thi-ng/umbrella", @@ -9,7 +9,7 @@ "license": "Apache-2.0", "scripts": { "build": "yarn run clean && tsc --declaration", - "clean": "rm -rf *.js *.d.ts build doc", + "clean": "rm -rf *.js *.d.ts .nyc_output build coverage doc", "cover": "yarn test && nyc report --reporter=lcov", "doc": "node_modules/.bin/typedoc --mode modules --out doc src", "pub": "yarn run build && yarn publish --access public", @@ -24,10 +24,16 @@ "typescript": "^2.8.1" }, "dependencies": { - "@thi.ng/api": "^2.2.0" + "@thi.ng/api": "^2.2.0", + "@thi.ng/associative": "^0.4.0", + "@thi.ng/iterators": "^4.1.5" }, "keywords": [ + "data structure", + "dependencies", + "DAG", "ES6", + "graph", "typescript" ], "publishConfig": { diff --git a/packages/dgraph/src/index.ts b/packages/dgraph/src/index.ts index 7101a741ec..0d56fc3a13 100644 --- a/packages/dgraph/src/index.ts +++ b/packages/dgraph/src/index.ts @@ -1,2 +1,140 @@ -// TODO stub only -export class DGraph { } +import { ICopy } from "@thi.ng/api/api"; +import { equiv } from "@thi.ng/api/equiv"; +import { illegalArgs } from "@thi.ng/api/error"; +import { EquivMap } from "@thi.ng/associative/equiv-map"; +import { LLSet } from "@thi.ng/associative/ll-set"; +import { union } from "@thi.ng/associative/union"; +import { filter } from "@thi.ng/iterators/filter"; +import { reduce } from "@thi.ng/iterators/reduce"; + +export class DGraph implements + Iterable, + ICopy> { + + dependencies: EquivMap>; + dependents: EquivMap>; + + constructor() { + this.dependencies = new EquivMap>(); + this.dependents = new EquivMap>(); + } + + *[Symbol.iterator]() { + yield* this.sort(); + } + + get [Symbol.species]() { + return DGraph; + } + + copy() { + const g = new DGraph(); + for (let e of this.dependencies) { + g.dependencies.set(e[0], e[1].copy()); + } + for (let e of this.dependents) { + g.dependents.set(e[0], e[1].copy()); + } + return g; + } + + addDependency(node: T, dep: T) { + if (equiv(node, dep) || this.depends(dep, node)) { + illegalArgs(`Circular dependency between: ${node} & ${dep}`); + } + let d: LLSet = this.dependencies.get(node); + this.dependencies.set(node, d ? d.add(dep) : new LLSet([dep])); + d = this.dependents.get(dep); + this.dependents.set(dep, d ? d.add(node) : new LLSet([node])); + return this; + } + + removeEdge(node: T, dep: T) { + let d = this.dependencies.get(node); + if (d) { + d.delete(dep); + } + d = this.dependents.get(dep); + if (d) { + d.delete(node); + } + return this; + } + + removeNode(x: T) { + this.dependencies.delete(x); + return this; + } + + depends(x: T, y: T) { + return this.transitiveDependencies(x).has(y); + } + + dependent(x: T, y: T) { + return this.transitiveDependents(x).has(y); + } + + immediateDependencies(x: T): Set { + return this.dependencies.get(x) || new LLSet(); + } + + immediateDependents(x: T): Set { + return this.dependents.get(x) || new LLSet(); + } + + isLeaf(x: T) { + return this.immediateDependents(x).size === 0; + } + + isRoot(x: T) { + return this.immediateDependencies(x).size === 0; + } + + nodes(): Set { + return union( + new LLSet(this.dependencies.keys()), + new LLSet(this.dependents.keys()), + ); + } + + transitiveDependencies(x: T) { + return transitive(this.dependencies, x); + } + + transitiveDependents(x: T) { + return transitive(this.dependents, x); + } + + sort() { + const sorted: T[] = []; + const g = this.copy(); + let queue = new LLSet(filter((node: T) => g.isLeaf(node), g.nodes())); + while (true) { + if (!queue.size) { + return sorted.reverse(); + } + const node = queue.first(); + queue.delete(node); + for (let d of (>g.immediateDependencies(node)).copy()) { + g.removeEdge(node, d); + if (g.isLeaf(d)) { + queue.add(d); + } + } + sorted.push(node); + g.removeNode(node); + } + } +} + +function transitive(nodes: EquivMap>, x: T): LLSet { + const deps: LLSet = nodes.get(x); + if (deps) { + return reduce( + (acc, k: T) => >union(acc, transitive(nodes, k)), + deps, + deps + ); + } + return new LLSet(); +} diff --git a/packages/dgraph/test/index.ts b/packages/dgraph/test/index.ts index 5e8e91d02a..65ecff57d9 100644 --- a/packages/dgraph/test/index.ts +++ b/packages/dgraph/test/index.ts @@ -1,6 +1,53 @@ -// import * as assert from "assert"; -// import * as dgraph from "../src/index"; +import * as assert from "assert"; + +import { DGraph } from "../src/index"; describe("dgraph", () => { - it("tests pending"); + + let g: DGraph; + + beforeEach(() => { + g = new DGraph(); + g.addDependency([1, 2], [10, 20]); + g.addDependency([3, 4], [30, 40]); + g.addDependency([1, 2], [3, 4]); + }); + + it("depends", () => { + assert(g.depends([1, 2], [10, 20])); + assert(!g.depends([10, 20], [1, 2])); + }); + + it("dependent", () => { + assert(g.dependent([10, 20], [1, 2])); + assert(!g.dependent([1, 2], [10, 20])); + }); + + it("isLeaf", () => { + assert(g.isLeaf([1, 2])); + assert(!g.isLeaf([10, 20])); + assert(!g.isLeaf([3, 4])); + }); + + it("isRoot", () => { + assert(g.isRoot([10, 20])); + assert(g.isRoot([30, 40])); + assert(!g.isRoot([3, 4])); + }); + + it("cyclic", () => { + assert.throws(() => g.addDependency([10, 20], [1, 2])); + assert.throws(() => g.addDependency([1, 2], [1, 2])); + }); + + it("sort", () => { + assert.deepEqual(g.sort(), [[30, 40], [3, 4], [10, 20], [1, 2]]); + g.addDependency([30, 40], [50, 60]); + assert.deepEqual(g.sort(), [[50, 60], [30, 40], [3, 4], [10, 20], [1, 2]]); + }); + + it("iterator", () => { + assert.deepEqual([...g], [[30, 40], [3, 4], [10, 20], [1, 2]]); + assert.deepEqual([...g], [[30, 40], [3, 4], [10, 20], [1, 2]]); + }); }); diff --git a/packages/diff/CHANGELOG.md b/packages/diff/CHANGELOG.md index 05b46baa59..b3711268e1 100644 --- a/packages/diff/CHANGELOG.md +++ b/packages/diff/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.0.7](https://github.com/thi-ng/umbrella/compare/@thi.ng/diff@1.0.6...@thi.ng/diff@1.0.7) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/diff + ## [1.0.6](https://github.com/thi-ng/umbrella/compare/@thi.ng/diff@1.0.5...@thi.ng/diff@1.0.6) (2018-04-08) diff --git a/packages/diff/package.json b/packages/diff/package.json index c3015cc754..dfd9e29a91 100644 --- a/packages/diff/package.json +++ b/packages/diff/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/diff", - "version": "1.0.6", + "version": "1.0.7", "description": "Array & object Diff", "main": "./index.js", "typings": "./index.d.ts", @@ -9,7 +9,7 @@ "license": "Apache-2.0", "scripts": { "build": "yarn run clean && tsc --declaration", - "clean": "rm -rf *.js *.d.ts build doc", + "clean": "rm -rf *.js *.d.ts .nyc_output build coverage doc", "doc": "node_modules/.bin/typedoc --mode modules --out doc src", "pub": "yarn run build && yarn publish --access public", "test": "rm -rf build && tsc -p test && mocha build/test/*.js" diff --git a/packages/hdom-components/CHANGELOG.md b/packages/hdom-components/CHANGELOG.md index 97d1c1b253..79c203a04a 100644 --- a/packages/hdom-components/CHANGELOG.md +++ b/packages/hdom-components/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.0.1](https://github.com/thi-ng/umbrella/compare/@thi.ng/hdom-components@2.0.0...@thi.ng/hdom-components@2.0.1) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/hdom-components + # [2.0.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/hdom-components@1.1.2...@thi.ng/hdom-components@2.0.0) (2018-04-08) diff --git a/packages/hdom-components/package.json b/packages/hdom-components/package.json index 231937d599..bd57569d76 100644 --- a/packages/hdom-components/package.json +++ b/packages/hdom-components/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/hdom-components", - "version": "2.0.0", + "version": "2.0.1", "description": "Raw, skinnable UI & SVG components for @thi.ng/hdom", "main": "./index.js", "typings": "./index.d.ts", @@ -9,7 +9,7 @@ "license": "Apache-2.0", "scripts": { "build": "yarn run clean && tsc --declaration", - "clean": "rm -rf *.js *.d.ts .nyc_output build doc", + "clean": "rm -rf *.js *.d.ts .nyc_output build coverage doc", "cover": "yarn test && nyc report --reporter=lcov", "doc": "node_modules/.bin/typedoc --mode modules --out doc src", "pub": "yarn run build && yarn publish --access public", diff --git a/packages/hdom/AUTHORS.md b/packages/hdom/AUTHORS.md new file mode 100644 index 0000000000..2740a05f86 --- /dev/null +++ b/packages/hdom/AUTHORS.md @@ -0,0 +1,7 @@ +## Maintainer + +- Karsten Schmidt (@postspectacular) + +## Contributors + +- Kevin Nolan (@allforabit) diff --git a/packages/hdom/CHANGELOG.md b/packages/hdom/CHANGELOG.md index d6ad58b532..8bfb045280 100644 --- a/packages/hdom/CHANGELOG.md +++ b/packages/hdom/CHANGELOG.md @@ -3,6 +3,33 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [3.0.3](https://github.com/thi-ng/umbrella/compare/@thi.ng/hdom@3.0.2...@thi.ng/hdom@3.0.3) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/hdom + + +## [3.0.2](https://github.com/thi-ng/umbrella/compare/@thi.ng/hdom@3.0.1...@thi.ng/hdom@3.0.2) (2018-04-10) + + + + +**Note:** Version bump only for package @thi.ng/hdom + + +## [3.0.1](https://github.com/thi-ng/umbrella/compare/@thi.ng/hdom@3.0.0...@thi.ng/hdom@3.0.1) (2018-04-09) + + +### Performance Improvements + +* **hdom:** intern imported checks, update normalizeTree(), add docs, fix tests ([2a91e30](https://github.com/thi-ng/umbrella/commit/2a91e30)) + + + + # [3.0.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/hdom@2.3.3...@thi.ng/hdom@3.0.0) (2018-04-08) diff --git a/packages/hdom/package.json b/packages/hdom/package.json index 2d944479f1..161d4a2559 100644 --- a/packages/hdom/package.json +++ b/packages/hdom/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/hdom", - "version": "3.0.0", + "version": "3.0.3", "description": "Lightweight vanilla ES6 UI component & virtual DOM system", "main": "./index.js", "typings": "./index.d.ts", @@ -16,7 +16,7 @@ "test": "rm -rf build && tsc -p test && nyc mocha build/test/*.js" }, "devDependencies": { - "@thi.ng/atom": "^1.2.4", + "@thi.ng/atom": "^1.2.5", "@types/mocha": "^5.0.0", "@types/node": "^9.6.1", "mocha": "^5.0.5", @@ -26,9 +26,9 @@ }, "dependencies": { "@thi.ng/api": "^2.2.0", - "@thi.ng/diff": "^1.0.6", - "@thi.ng/hiccup": "^1.3.4", - "@thi.ng/iterators": "^4.1.4" + "@thi.ng/diff": "^1.0.7", + "@thi.ng/hiccup": "^1.3.5", + "@thi.ng/iterators": "^4.1.5" }, "keywords": [ "browser", diff --git a/packages/hdom/src/diff.ts b/packages/hdom/src/diff.ts index 1730892522..43aa4a3fe6 100644 --- a/packages/hdom/src/diff.ts +++ b/packages/hdom/src/diff.ts @@ -1,10 +1,12 @@ -import { isArray } from "@thi.ng/checks/is-array"; -import { isString } from "@thi.ng/checks/is-string"; +import * as isa from "@thi.ng/checks/is-array"; +import * as iss from "@thi.ng/checks/is-string"; import * as diff from "@thi.ng/diff"; // import { DEBUG } from "./api"; import { createDOM, removeAttribs, setAttrib, removeChild } from "./dom"; +const isArray = isa.isArray; +const isString = iss.isString; const diffArray = diff.diffArray; const diffObject = diff.diffObject; diff --git a/packages/hdom/src/dom.ts b/packages/hdom/src/dom.ts index 44fa892165..a9c197517a 100644 --- a/packages/hdom/src/dom.ts +++ b/packages/hdom/src/dom.ts @@ -1,15 +1,20 @@ -import { isArray } from "@thi.ng/checks/is-array"; -import { isFunction } from "@thi.ng/checks/is-function"; -import { isIterable } from "@thi.ng/checks/is-iterable"; -import { isString } from "@thi.ng/checks/is-string"; +import * as isa from "@thi.ng/checks/is-array"; +import * as isf from "@thi.ng/checks/is-function"; +import * as isi from "@thi.ng/checks/is-iterable"; +import * as iss from "@thi.ng/checks/is-string"; import { SVG_TAGS, SVG_NS } from "@thi.ng/hiccup/api"; import { css } from "@thi.ng/hiccup/css"; import { map } from "@thi.ng/iterators/map"; +const isArray = isa.isArray; +const isFunction = isf.isFunction; +const isIterable = isi.isIterable +const isString = iss.isString; + /** * Creates an actual DOM tree from given hiccup component and `parent` * element. Calls `init` with created element (user provided context and - * other args) for any components with `init` lifecycle method. Returns + * other args) for any components with `init` life cycle method. Returns * created root element(s) - usually only a single one, but can be an * array of elements, if the provided tree is an iterable. Creates DOM * text nodes for non-component values. Returns `parent` if tree is diff --git a/packages/hdom/src/normalize.ts b/packages/hdom/src/normalize.ts index 79e2988b69..f0892f6c37 100644 --- a/packages/hdom/src/normalize.ts +++ b/packages/hdom/src/normalize.ts @@ -1,12 +1,19 @@ import { illegalArgs } from "@thi.ng/api/error"; -import { isArray } from "@thi.ng/checks/is-array"; -import { implementsFunction } from "@thi.ng/checks/implements-function"; -import { isFunction } from "@thi.ng/checks/is-function"; -import { isIterable } from "@thi.ng/checks/is-iterable"; -import { isPlainObject } from "@thi.ng/checks/is-plain-object"; -import { isString } from "@thi.ng/checks/is-string"; +import * as isa from "@thi.ng/checks/is-array"; +import * as impf from "@thi.ng/checks/implements-function"; +import * as isf from "@thi.ng/checks/is-function"; +import * as isi from "@thi.ng/checks/is-iterable"; +import * as iso from "@thi.ng/checks/is-plain-object"; +import * as iss from "@thi.ng/checks/is-string"; import { TAG_REGEXP } from "@thi.ng/hiccup/api"; +const isArray = isa.isArray; +const isFunction = isf.isFunction; +const implementsFunction = impf.implementsFunction; +const isIterable = isi.isIterable +const isPlainObject = iso.isPlainObject; +const isString = iss.isString; + /** * Expands single hiccup element/component into its canonical form: * @@ -36,7 +43,7 @@ export function normalizeElement(spec: any[], keys: boolean) { if (!isString(tag) || !(match = TAG_REGEXP.exec(tag))) { illegalArgs(`${tag} is not a valid tag name`); } - // return orig if already normalized and satifies key requirement + // return orig if already normalized and satisfies key requirement if (tag === match[1] && hasAttribs && (!keys || spec[1].key)) { return spec; } @@ -86,6 +93,13 @@ const NO_SPANS = { * or will need to be replaced/removed. The `key` values are defined by * the `path` array arg. * + * In terms of life cycle methods: `render` should ALWAYS return an + * array or another function, else the component's `init` or `release` + * fns will NOT be able to be called. E.g. If the return value of + * `render` evaluates as a string or number, the return value should be + * wrapped as `["span", "foo"]`. If no `init` or `release` are used, + * this requirement is relaxed. + * * For normal usage only the first 2 args should be specified and the * rest kept at their defaults. * @@ -110,21 +124,17 @@ export function normalizeTree(tree: any, ctx?: any, path = [0], keys = true, spa // use result of function call // pass ctx as first arg and remaining array elements as rest args if (isFunction(tag)) { - return normalizeTree(tag.apply(null, [ctx, ...tree.slice(1)]), ctx, path.slice(), keys, span); + return normalizeTree(tag.apply(null, [ctx, ...tree.slice(1)]), ctx, path, keys, span); } // component object w/ life cycle methods // (render() is the only required hook) if (implementsFunction(tag, "render")) { const args = [ctx, ...tree.slice(1)]; - norm = normalizeTree(tag.render.apply(null, args), ctx, path.slice(), keys, span); - if (norm !== undefined) { - nattribs = norm[1]; - if (keys && nattribs.key === undefined) { - nattribs.key = path.join("-"); - } - norm.__init = tag.init; - norm.__release = tag.release; - norm.__args = args; + norm = normalizeTree(tag.render.apply(null, args), ctx, path, keys, span); + if (isArray(norm)) { + (norm).__init = tag.init; + (norm).__release = tag.release; + (norm).__args = args; } return norm; } @@ -166,7 +176,7 @@ export function normalizeTree(tree: any, ctx?: any, path = [0], keys = true, spa return normalizeTree(tree(ctx), ctx, path, keys, span); } if (implementsFunction(tree, "deref")) { - return normalizeTree(tree.deref(), ctx, path.slice(), keys, span); + return normalizeTree(tree.deref(), ctx, path, keys, span); } return span ? ["span", keys ? { key: path.join("-") } : {}, tree.toString()] : diff --git a/packages/hdom/test/index.ts b/packages/hdom/test/index.ts index 0b90a6481d..1a440c5d67 100644 --- a/packages/hdom/test/index.ts +++ b/packages/hdom/test/index.ts @@ -5,8 +5,8 @@ import { map } from "@thi.ng/iterators/map"; import { range } from "@thi.ng/iterators/range"; import { normalizeTree } from "../src/normalize"; -function _check(a, b) { - assert.deepEqual(normalizeTree(a, [], false, false), b); +function _check(a, b, ctx = null) { + assert.deepEqual(normalizeTree(a, ctx, [], false, false), b); } function check(id, a, b) { @@ -76,7 +76,7 @@ describe("hdom", () => { check( "tag fn w/ args", - [(id, body) => ["div#" + id, body], "foo", "bar"], + [(_, id, body) => ["div#" + id, body], "foo", "bar"], ["div", { id: "foo" }, "bar"] ); @@ -110,12 +110,19 @@ describe("hdom", () => { ["a", {}, ["b", {}]] ); - it("lifecycle", () => { - const res: any = ["div", {}]; + it("life cycle", () => { + let res: any = ["div", {}]; res.__init = res.__release = undefined; - res.__args = []; + res.__args = [null]; assert.deepEqual( - normalizeTree([{ render: () => ["div"] }], [], false, false), + normalizeTree([{ render: () => ["div"] }], null, [], false, false), + res + ); + res = ["div", { key: "0" }]; + res.__init = res.__release = undefined; + res.__args = [null]; + assert.deepEqual( + normalizeTree([{ render: () => ["div"] }], null, [0], true, false), res ); }); diff --git a/packages/hiccup-css/CHANGELOG.md b/packages/hiccup-css/CHANGELOG.md index 6ec3743779..6b9e21607a 100644 --- a/packages/hiccup-css/CHANGELOG.md +++ b/packages/hiccup-css/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.1.10](https://github.com/thi-ng/umbrella/compare/@thi.ng/hiccup-css@0.1.9...@thi.ng/hiccup-css@0.1.10) (2018-04-11) + + + + +**Note:** Version bump only for package @thi.ng/hiccup-css + ## [0.1.9](https://github.com/thi-ng/umbrella/compare/@thi.ng/hiccup-css@0.1.8...@thi.ng/hiccup-css@0.1.9) (2018-04-08) diff --git a/packages/hiccup-css/package.json b/packages/hiccup-css/package.json index e77f4bc96c..1cf044aebe 100644 --- a/packages/hiccup-css/package.json +++ b/packages/hiccup-css/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/hiccup-css", - "version": "0.1.9", + "version": "0.1.10", "description": "CSS from nested JS data structures", "main": "./index.js", "typings": "./index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@thi.ng/api": "^2.2.0", - "@thi.ng/transducers": "^1.7.5" + "@thi.ng/transducers": "^1.8.0" }, "keywords": [ "clojure", diff --git a/packages/hiccup-svg/CHANGELOG.md b/packages/hiccup-svg/CHANGELOG.md index f84df32714..94ba9361b5 100644 --- a/packages/hiccup-svg/CHANGELOG.md +++ b/packages/hiccup-svg/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.2](https://github.com/thi-ng/umbrella/compare/@thi.ng/hiccup-svg@0.2.1...@thi.ng/hiccup-svg@0.2.2) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/hiccup-svg + + +## [0.2.1](https://github.com/thi-ng/umbrella/compare/@thi.ng/hiccup-svg@0.2.0...@thi.ng/hiccup-svg@0.2.1) (2018-04-09) + + +### Bug Fixes + +* **hiccup-svg:** path(), update add null check for points() ([b9d9a49](https://github.com/thi-ng/umbrella/commit/b9d9a49)) + + + + # 0.2.0 (2018-04-08) diff --git a/packages/hiccup-svg/package.json b/packages/hiccup-svg/package.json index c2a89e81d9..0f9e467154 100644 --- a/packages/hiccup-svg/package.json +++ b/packages/hiccup-svg/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/hiccup-svg", - "version": "0.2.0", + "version": "0.2.2", "description": "SVG element functions for @thi.ng/hiccup & @thi.ng/hdom", "main": "./index.js", "typings": "./index.d.ts", @@ -9,7 +9,7 @@ "license": "Apache-2.0", "scripts": { "build": "yarn run clean && tsc --declaration", - "clean": "rm -rf *.js *.d.ts build doc", + "clean": "rm -rf *.js *.d.ts .nyc_output build coverage doc", "cover": "yarn test && nyc report --reporter=lcov", "doc": "node_modules/.bin/typedoc --mode modules --out doc src", "pub": "yarn run build && yarn publish --access public", @@ -25,7 +25,7 @@ }, "dependencies": { "@thi.ng/api": "^2.2.0", - "@thi.ng/hiccup": "^1.3.4" + "@thi.ng/hiccup": "^1.3.5" }, "keywords": [ "components", diff --git a/packages/hiccup-svg/src/format.ts b/packages/hiccup-svg/src/format.ts index 2185d33d4e..4e03630714 100644 --- a/packages/hiccup-svg/src/format.ts +++ b/packages/hiccup-svg/src/format.ts @@ -6,7 +6,7 @@ const ff = (x: number) => x.toFixed(PRECISION); const point = (p: ArrayLike) => ff(p[0]) + "," + ff(p[1]); -const points = (pts: ArrayLike[], sep = " ") => pts.map(point).join(sep); +const points = (pts: ArrayLike[], sep = " ") => pts ? pts.map(point).join(sep) : ""; export { ff, diff --git a/packages/hiccup-svg/src/path.ts b/packages/hiccup-svg/src/path.ts index 341dc07fa3..1ebd9e6bc5 100644 --- a/packages/hiccup-svg/src/path.ts +++ b/packages/hiccup-svg/src/path.ts @@ -6,6 +6,6 @@ export const path = (segments: PathSegment[], attr?) => "path", { ...attr, - d: segments.map((seg) => seg[0] + points(seg[1], ",")), + d: segments.map((seg) => seg[0] + points(seg[1], ",")).join(""), } ]; diff --git a/packages/hiccup/CHANGELOG.md b/packages/hiccup/CHANGELOG.md index f3095d8838..bf67167b70 100644 --- a/packages/hiccup/CHANGELOG.md +++ b/packages/hiccup/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.3.5](https://github.com/thi-ng/umbrella/compare/@thi.ng/hiccup@1.3.4...@thi.ng/hiccup@1.3.5) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/hiccup + ## [1.3.4](https://github.com/thi-ng/umbrella/compare/@thi.ng/hiccup@1.3.3...@thi.ng/hiccup@1.3.4) (2018-04-08) diff --git a/packages/hiccup/package.json b/packages/hiccup/package.json index 30a5e5c46a..411ed314e8 100644 --- a/packages/hiccup/package.json +++ b/packages/hiccup/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/hiccup", - "version": "1.3.4", + "version": "1.3.5", "description": "HTML/SVG/XML serialization of nested data structures, iterables & closures", "main": "./index.js", "typings": "./index.d.ts", @@ -16,7 +16,7 @@ "test": "rm -rf build && tsc -p test && nyc mocha build/test/*.js" }, "devDependencies": { - "@thi.ng/atom": "^1.2.4", + "@thi.ng/atom": "^1.2.5", "@types/mocha": "^5.0.0", "@types/node": "^9.6.1", "mocha": "^5.0.5", diff --git a/packages/interceptors/CHANGELOG.md b/packages/interceptors/CHANGELOG.md index f68d4dda54..2d421d23a1 100644 --- a/packages/interceptors/CHANGELOG.md +++ b/packages/interceptors/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.1.5](https://github.com/thi-ng/umbrella/compare/@thi.ng/interceptors@1.1.4...@thi.ng/interceptors@1.1.5) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/interceptors + ## [1.1.4](https://github.com/thi-ng/umbrella/compare/@thi.ng/interceptors@1.1.3...@thi.ng/interceptors@1.1.4) (2018-04-08) diff --git a/packages/interceptors/package.json b/packages/interceptors/package.json index 08f1010609..82b96f6f25 100644 --- a/packages/interceptors/package.json +++ b/packages/interceptors/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/interceptors", - "version": "1.1.4", + "version": "1.1.5", "description": "Interceptor based event bus, side effect & immutable state handling", "main": "./index.js", "typings": "./index.d.ts", @@ -9,7 +9,7 @@ "license": "Apache-2.0", "scripts": { "build": "yarn run clean && tsc --declaration", - "clean": "rm -rf *.js *.d.ts .nyc_output build doc", + "clean": "rm -rf *.js *.d.ts .nyc_output build coverage doc", "cover": "yarn test && nyc report --reporter=lcov", "doc": "node_modules/.bin/typedoc --mode modules --out doc src", "pub": "yarn run build && yarn publish --access public", @@ -25,8 +25,8 @@ }, "dependencies": { "@thi.ng/api": "^2.2.0", - "@thi.ng/atom": "^1.2.4", - "@thi.ng/paths": "^1.1.5" + "@thi.ng/atom": "^1.2.5", + "@thi.ng/paths": "^1.1.6" }, "keywords": [ "ES6", diff --git a/packages/iterators/CHANGELOG.md b/packages/iterators/CHANGELOG.md index 80669ff10a..8eab01ccd8 100644 --- a/packages/iterators/CHANGELOG.md +++ b/packages/iterators/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [4.1.5](https://github.com/thi-ng/umbrella/compare/@thi.ng/iterators@4.1.4...@thi.ng/iterators@4.1.5) (2018-04-10) + + + + +**Note:** Version bump only for package @thi.ng/iterators + ## [4.1.4](https://github.com/thi-ng/umbrella/compare/@thi.ng/iterators@4.1.3...@thi.ng/iterators@4.1.4) (2018-04-08) diff --git a/packages/iterators/package.json b/packages/iterators/package.json index 7b4208284b..b0ab86e365 100644 --- a/packages/iterators/package.json +++ b/packages/iterators/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/iterators", - "version": "4.1.4", + "version": "4.1.5", "description": "clojure.core inspired, composable ES6 iterators & generators", "main": "./index.js", "typings": "./index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@thi.ng/api": "^2.2.0", - "@thi.ng/dcons": "^0.1.19" + "@thi.ng/dcons": "^0.2.0" }, "keywords": [ "clojure", diff --git a/packages/paths/CHANGELOG.md b/packages/paths/CHANGELOG.md index 38743412e9..e1d6dcde28 100644 --- a/packages/paths/CHANGELOG.md +++ b/packages/paths/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.1.6](https://github.com/thi-ng/umbrella/compare/@thi.ng/paths@1.1.5...@thi.ng/paths@1.1.6) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/paths + ## [1.1.5](https://github.com/thi-ng/umbrella/compare/@thi.ng/paths@1.1.4...@thi.ng/paths@1.1.5) (2018-04-08) diff --git a/packages/paths/package.json b/packages/paths/package.json index f44a53dc1f..947e439abc 100644 --- a/packages/paths/package.json +++ b/packages/paths/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/paths", - "version": "1.1.5", + "version": "1.1.6", "description": "immutable, optimized path-based object property / array accessors", "main": "./index.js", "typings": "./index.d.ts", @@ -9,7 +9,7 @@ "license": "Apache-2.0", "scripts": { "build": "yarn run clean && tsc --declaration", - "clean": "rm -rf *.js *.d.ts .nyc_output build doc", + "clean": "rm -rf *.js *.d.ts .nyc_output build coverage doc", "cover": "yarn test && nyc report --reporter=lcov", "doc": "node_modules/.bin/typedoc --mode modules --out doc src", "pub": "yarn run build && yarn publish --access public", diff --git a/packages/pointfree-lang/CHANGELOG.md b/packages/pointfree-lang/CHANGELOG.md index dfa9cfcba7..8ea1927fac 100644 --- a/packages/pointfree-lang/CHANGELOG.md +++ b/packages/pointfree-lang/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.3](https://github.com/thi-ng/umbrella/compare/@thi.ng/pointfree-lang@0.2.2...@thi.ng/pointfree-lang@0.2.3) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/pointfree-lang + ## [0.2.2](https://github.com/thi-ng/umbrella/compare/@thi.ng/pointfree-lang@0.2.1...@thi.ng/pointfree-lang@0.2.2) (2018-04-08) diff --git a/packages/pointfree-lang/package.json b/packages/pointfree-lang/package.json index 334b2ffeef..24b9c51527 100644 --- a/packages/pointfree-lang/package.json +++ b/packages/pointfree-lang/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/pointfree-lang", - "version": "0.2.2", + "version": "0.2.3", "description": "Forth style syntax layer/compiler for the @thi.ng/pointfree DSL", "main": "./index.js", "typings": "./index.d.ts", @@ -9,7 +9,7 @@ "license": "Apache-2.0", "scripts": { "build": "yarn clean && tsc --declaration && yarn peg", - "clean": "rm -rf *.js *.d.ts .nyc_output build doc", + "clean": "rm -rf *.js *.d.ts .nyc_output build coverage doc", "cover": "yarn test && nyc report --reporter=lcov", "doc": "node_modules/.bin/typedoc --mode modules --out doc src", "peg": "pegjs -o parser.js src/grammar.pegjs", @@ -27,7 +27,7 @@ }, "dependencies": { "@thi.ng/api": "^2.2.0", - "@thi.ng/pointfree": "^0.7.2" + "@thi.ng/pointfree": "^0.7.3" }, "keywords": [ "concatenative", diff --git a/packages/pointfree/CHANGELOG.md b/packages/pointfree/CHANGELOG.md index baf609abdc..d874ca6087 100644 --- a/packages/pointfree/CHANGELOG.md +++ b/packages/pointfree/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.7.3](https://github.com/thi-ng/umbrella/compare/@thi.ng/pointfree@0.7.2...@thi.ng/pointfree@0.7.3) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/pointfree + ## [0.7.2](https://github.com/thi-ng/umbrella/compare/@thi.ng/pointfree@0.7.1...@thi.ng/pointfree@0.7.2) (2018-04-08) diff --git a/packages/pointfree/package.json b/packages/pointfree/package.json index 152584e00c..cdaffe4691 100644 --- a/packages/pointfree/package.json +++ b/packages/pointfree/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/pointfree", - "version": "0.7.2", + "version": "0.7.3", "description": "Pointfree functional composition / Forth style stack execution engine", "main": "./index.js", "typings": "./index.d.ts", @@ -9,7 +9,7 @@ "license": "Apache-2.0", "scripts": { "build": "yarn run clean && tsc --declaration", - "clean": "rm -rf *.js *.d.ts .nyc_output build doc", + "clean": "rm -rf *.js *.d.ts .nyc_output build coverage doc", "cover": "yarn test && nyc report --reporter=lcov", "doc": "node_modules/.bin/typedoc --mode modules --out doc src", "pub": "yarn run build && yarn publish --access public", diff --git a/packages/resolve-map/CHANGELOG.md b/packages/resolve-map/CHANGELOG.md index 34116f5d44..8b4f0f9ac7 100644 --- a/packages/resolve-map/CHANGELOG.md +++ b/packages/resolve-map/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.1.7](https://github.com/thi-ng/umbrella/compare/@thi.ng/resolve-map@0.1.6...@thi.ng/resolve-map@0.1.7) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/resolve-map + ## [0.1.6](https://github.com/thi-ng/umbrella/compare/@thi.ng/resolve-map@0.1.5...@thi.ng/resolve-map@0.1.6) (2018-04-08) diff --git a/packages/resolve-map/package.json b/packages/resolve-map/package.json index 84946426bd..080c65c514 100644 --- a/packages/resolve-map/package.json +++ b/packages/resolve-map/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/resolve-map", - "version": "0.1.6", + "version": "0.1.7", "description": "DAG resolution of vanilla objects & arrays with internally linked values", "main": "./index.js", "typings": "./index.d.ts", @@ -9,7 +9,7 @@ "license": "Apache-2.0", "scripts": { "build": "yarn run clean && tsc --declaration", - "clean": "rm -rf *.js *.d.ts build doc", + "clean": "rm -rf *.js *.d.ts .nyc_output build coverage doc", "doc": "node_modules/.bin/typedoc --mode modules --out doc src", "pub": "yarn run build && yarn publish --access public", "test": "rm -rf build && tsc -p test && mocha build/test/*.js" @@ -23,7 +23,7 @@ }, "dependencies": { "@thi.ng/checks": "^1.4.0", - "@thi.ng/paths": "^1.1.5" + "@thi.ng/paths": "^1.1.6" }, "keywords": [ "configuration", diff --git a/packages/router/CHANGELOG.md b/packages/router/CHANGELOG.md index df17e41301..6139088299 100644 --- a/packages/router/CHANGELOG.md +++ b/packages/router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.1.6](https://github.com/thi-ng/umbrella/compare/@thi.ng/router@0.1.5...@thi.ng/router@0.1.6) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/router + ## [0.1.5](https://github.com/thi-ng/umbrella/compare/@thi.ng/router@0.1.4...@thi.ng/router@0.1.5) (2018-04-08) diff --git a/packages/router/package.json b/packages/router/package.json index 9f4208da84..795f35c9ad 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/router", - "version": "0.1.5", + "version": "0.1.6", "description": "Generic router for browser & non-browser based applications", "main": "./index.js", "typings": "./index.d.ts", @@ -9,7 +9,7 @@ "license": "Apache-2.0", "scripts": { "build": "yarn run clean && tsc --declaration", - "clean": "rm -rf *.js *.d.ts build doc", + "clean": "rm -rf *.js *.d.ts .nyc_output build coverage doc", "doc": "node_modules/.bin/typedoc --mode modules --out doc src", "pub": "yarn run build && yarn publish --access public", "test": "rm -rf build && tsc -p test && mocha build/test/*.js" diff --git a/packages/rstream-csp/CHANGELOG.md b/packages/rstream-csp/CHANGELOG.md index 57128b4ab0..6e3087e12f 100644 --- a/packages/rstream-csp/CHANGELOG.md +++ b/packages/rstream-csp/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.1.48](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream-csp@0.1.47...@thi.ng/rstream-csp@0.1.48) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/rstream-csp + + +## [0.1.47](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream-csp@0.1.46...@thi.ng/rstream-csp@0.1.47) (2018-04-11) + + + + +**Note:** Version bump only for package @thi.ng/rstream-csp + + +## [0.1.46](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream-csp@0.1.45...@thi.ng/rstream-csp@0.1.46) (2018-04-10) + + + + +**Note:** Version bump only for package @thi.ng/rstream-csp + ## [0.1.45](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream-csp@0.1.44...@thi.ng/rstream-csp@0.1.45) (2018-04-08) diff --git a/packages/rstream-csp/package.json b/packages/rstream-csp/package.json index ba0c1493d9..4ab255f9e0 100644 --- a/packages/rstream-csp/package.json +++ b/packages/rstream-csp/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/rstream-csp", - "version": "0.1.45", + "version": "0.1.48", "description": "@thi.ng/csp bridge module for @thi.ng/rstream", "main": "./index.js", "typings": "./index.d.ts", @@ -24,8 +24,8 @@ "typescript": "^2.8.1" }, "dependencies": { - "@thi.ng/csp": "^0.3.25", - "@thi.ng/rstream": "^1.2.5" + "@thi.ng/csp": "^0.3.27", + "@thi.ng/rstream": "^1.2.7" }, "keywords": [ "bridge", diff --git a/packages/rstream-log/CHANGELOG.md b/packages/rstream-log/CHANGELOG.md index 973f20b68e..ca5b838e20 100644 --- a/packages/rstream-log/CHANGELOG.md +++ b/packages/rstream-log/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.6.7](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream-log@0.6.6...@thi.ng/rstream-log@0.6.7) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/rstream-log + + +## [0.6.6](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream-log@0.6.5...@thi.ng/rstream-log@0.6.6) (2018-04-11) + + + + +**Note:** Version bump only for package @thi.ng/rstream-log + ## [0.6.5](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream-log@0.6.4...@thi.ng/rstream-log@0.6.5) (2018-04-08) diff --git a/packages/rstream-log/package.json b/packages/rstream-log/package.json index 0b6db80ae7..d8902c561d 100644 --- a/packages/rstream-log/package.json +++ b/packages/rstream-log/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/rstream-log", - "version": "0.6.5", + "version": "0.6.7", "description": "Structured, multilevel & hierarchical loggers based on @thi.ng/rstream", "main": "./index.js", "typings": "./index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@thi.ng/api": "^2.2.0", - "@thi.ng/rstream": "^1.2.5" + "@thi.ng/rstream": "^1.2.7" }, "keywords": [ "ES6", diff --git a/packages/rstream/CHANGELOG.md b/packages/rstream/CHANGELOG.md index e18b39cf1f..7833d7ae6c 100644 --- a/packages/rstream/CHANGELOG.md +++ b/packages/rstream/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.2.7](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream@1.2.6...@thi.ng/rstream@1.2.7) (2018-04-13) + + + + +**Note:** Version bump only for package @thi.ng/rstream + + +## [1.2.6](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream@1.2.5...@thi.ng/rstream@1.2.6) (2018-04-11) + + + + +**Note:** Version bump only for package @thi.ng/rstream + ## [1.2.5](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream@1.2.4...@thi.ng/rstream@1.2.5) (2018-04-08) diff --git a/packages/rstream/package.json b/packages/rstream/package.json index fa3ede7e06..599f2fc3fa 100644 --- a/packages/rstream/package.json +++ b/packages/rstream/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/rstream", - "version": "1.2.5", + "version": "1.2.7", "description": "Reactive multi-tap streams, dataflow & transformation pipeline constructs", "main": "./index.js", "typings": "./index.d.ts", @@ -25,8 +25,8 @@ }, "dependencies": { "@thi.ng/api": "^2.2.0", - "@thi.ng/atom": "^1.2.4", - "@thi.ng/transducers": "^1.7.5" + "@thi.ng/atom": "^1.2.5", + "@thi.ng/transducers": "^1.8.0" }, "keywords": [ "datastructure", diff --git a/packages/transducers/CHANGELOG.md b/packages/transducers/CHANGELOG.md index f713cb20b8..84f5653de6 100644 --- a/packages/transducers/CHANGELOG.md +++ b/packages/transducers/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [1.8.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/transducers@1.7.5...@thi.ng/transducers@1.8.0) (2018-04-11) + + +### Features + +* **transducers:** add run() for executing side effects only, update readme ([52c7508](https://github.com/thi-ng/umbrella/commit/52c7508)) + + + + ## [1.7.5](https://github.com/thi-ng/umbrella/compare/@thi.ng/transducers@1.7.4...@thi.ng/transducers@1.7.5) (2018-04-08) diff --git a/packages/transducers/README.md b/packages/transducers/README.md index 14ab9e2059..0355b601a8 100644 --- a/packages/transducers/README.md +++ b/packages/transducers/README.md @@ -572,6 +572,14 @@ accumulator/result. Transforms iterable using given transducer and combines results with given reducer and optional initial accumulator/result. +#### `run(tx: Transducer, fx: (x: B) => void, xs: Iterable)` + +Transforms iterable with given transducer and optional side effect +without any reduction step. If `fx` is given it will be called with +every value produced by the transducer. If `fx` is *not* given, the +transducer is assumed to include at least one `sideEffect()` step +itself. Returns nothing. + ### Transducers #### `base64Decode(): Transducer` diff --git a/packages/transducers/package.json b/packages/transducers/package.json index d6d30edd03..c192bcceec 100644 --- a/packages/transducers/package.json +++ b/packages/transducers/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/transducers", - "version": "1.7.5", + "version": "1.8.0", "description": "Lightweight transducer implementations for ES6 / TypeScript", "main": "./index.js", "typings": "./index.d.ts", diff --git a/packages/transducers/src/index.ts b/packages/transducers/src/index.ts index ffd11fa4f8..f4d6e8f2bb 100644 --- a/packages/transducers/src/index.ts +++ b/packages/transducers/src/index.ts @@ -2,6 +2,7 @@ export * from "./api"; export * from "./iterator"; export * from "./reduce"; export * from "./reduced"; +export * from "./run"; export * from "./step"; export * from "./transduce"; diff --git a/packages/transducers/src/run.ts b/packages/transducers/src/run.ts new file mode 100644 index 0000000000..d1a456e20f --- /dev/null +++ b/packages/transducers/src/run.ts @@ -0,0 +1,26 @@ +import { Transducer } from "./api"; +import { transduce } from "./transduce"; + +const nop = () => { }; + +/** + * Transforms `xs` with given transducer and optional side effect + * without any reduction step. If `fx` is given it will be called with + * every value produced by the transducer. If `fx` is *not* given, the + * transducer is assumed to include at least one `sideEffect()` step + * itself. Returns nothing. + * + * @param tx + * @param fx + * @param xs + */ +export function run(tx: Transducer, xs: Iterable): void; +export function run(tx: Transducer, fx: (x: B) => void, xs: Iterable): void; +export function run(tx: Transducer, ...args: any[]) { + if (args.length === 1) { + transduce(tx, [nop, nop, nop], args[0]); + } else { + const fx = args[0]; + transduce(tx, [nop, nop, (_, x) => fx(x)], args[1]); + } +} diff --git a/yarn.lock b/yarn.lock index 79e97bb5f6..bf9a2e472a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3727,9 +3727,9 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" -lerna@^2.9.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/lerna/-/lerna-2.9.1.tgz#d7d21793ad35ae7733733ced34ce30f5b3bb1abe" +lerna@^2.10.1: + version "2.10.1" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-2.10.1.tgz#3b0ffe6b80d3312e8efdc7003a50d47d90ebdf03" dependencies: async "^1.5.0" chalk "^2.1.0"