1 | /*!
|
2 | * Copyright (c) 2017-2018 by The Funfix Project Developers.
|
3 | * Some rights reserved.
|
4 | *
|
5 | * Licensed under the Apache License, Version 2.0 (the "License");
|
6 | * you may not use this file except in compliance with the License.
|
7 | * You may obtain a copy of the License at
|
8 | *
|
9 | * http://www.apache.org/licenses/LICENSE-2.0
|
10 | *
|
11 | * Unless required by applicable law or agreed to in writing, software
|
12 | * distributed under the License is distributed on an "AS IS" BASIS,
|
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14 | * See the License for the specific language governing permissions and
|
15 | * limitations under the License.
|
16 | */
|
17 |
|
18 | import { Setoid } from "funland"
|
19 |
|
20 | /**
|
21 | * Interface for testing the equality of value objects.
|
22 | */
|
23 | export interface IEquals<A> {
|
24 | /**
|
25 | * Indicates whether some other object is "equal to" this one.
|
26 | *
|
27 | * Properties:
|
28 | *
|
29 | * - reflexive: for any value, `x.equals(x) == true`
|
30 | * - symmetric: for any values x and y, `x.equals(y) == y.equals(x)`
|
31 | * - transitive: `x.equals(y) && y.equals(z) => x.equals(z)`
|
32 | * - consistent: `x.equals(y)` always yields the same result
|
33 | *
|
34 | * Rule: equal objects MUST have equal hash codes!
|
35 | */
|
36 | equals(other: A): boolean
|
37 |
|
38 | /**
|
39 | * Returns a hash code value for this value.
|
40 | *
|
41 | * This method is supported for the benefit of hash tables.
|
42 | *
|
43 | * Properties:
|
44 | *
|
45 | * - consistent: multiple invocations always yield the same result
|
46 | * - if `x.equals(y) == true` then `x.hashCode() == y.hashCode()`
|
47 | * - if `x.equals(y) == false` it is NOT required for their hash codes
|
48 | * to be equal, i.e. this function is not injective
|
49 | */
|
50 | hashCode(): number
|
51 | }
|
52 |
|
53 | /**
|
54 | * Test if the given reference is a value object.
|
55 | *
|
56 | * Value objects are objects that implement the [[IEquals]]
|
57 | * interface.
|
58 | *
|
59 | * @param ref is the reference to test
|
60 | */
|
61 | export function isValueObject(ref: any): boolean {
|
62 | return !!(ref &&
|
63 | typeof ref.equals === "function" &&
|
64 | typeof ref.hashCode === "function")
|
65 | }
|
66 |
|
67 | /**
|
68 | * Tests for universal equality.
|
69 | *
|
70 | * First attempting a reference check with `===`,
|
71 | * after which it tries to fallback on [[IEquals]], if the
|
72 | * left-hand side is implementing it.
|
73 | *
|
74 | * ```typescript
|
75 | * equals(10, 10) // true, because 10 === 10
|
76 | *
|
77 | * class Box implements IEquals<Box> {
|
78 | * constructor(value: number) { this.value = value }
|
79 | *
|
80 | * equals(other) { return this.value === other.value }
|
81 | * hashCode() { return this.value << 2 }
|
82 | * }
|
83 | *
|
84 | * // false, because they are not the same reference
|
85 | * new Box(10) === new Box(10)
|
86 | *
|
87 | * // true, because `Box#equals` gets called
|
88 | * equals(new Box(10), new Box(10))
|
89 | * ```
|
90 | */
|
91 | export function is<A>(lh: A, rh: A): boolean {
|
92 | if (lh === rh || (lh !== lh && rh !== rh)) {
|
93 | return true
|
94 | }
|
95 | if (!lh || !rh) {
|
96 | return false
|
97 | }
|
98 | /* istanbul ignore else */
|
99 | /* tslint:disable-next-line:strict-type-predicates */
|
100 | if (typeof lh.valueOf === "function" && typeof rh.valueOf === "function") {
|
101 | const lh2 = lh.valueOf()
|
102 | const rh2 = rh.valueOf()
|
103 | if (lh2 === rh2 || (lh2 !== lh2 && rh2 !== rh2)) {
|
104 | return true
|
105 | }
|
106 | if (!lh2 || !rh2) {
|
107 | return false
|
108 | }
|
109 | }
|
110 | // noinspection PointlessBooleanExpressionJS
|
111 | return !!(
|
112 | isValueObject(lh) &&
|
113 | (lh as any).equals(rh)
|
114 | )
|
115 | }
|
116 |
|
117 | /** Alias for [[is]]. */
|
118 | export function equals<A>(lh: A, rh: A): boolean {
|
119 | return is(lh, rh)
|
120 | }
|
121 |
|
122 | /**
|
123 | * Returns a `Setoid` type-class instance that depends
|
124 | * universal equality, as defined by {@link is}.
|
125 | */
|
126 | export const universalSetoid: Setoid<any> = { equals }
|
127 |
|
128 | /**
|
129 | * Universal hash-code function.
|
130 | *
|
131 | * Depending on the given value, it calculates the hash-code like so:
|
132 | *
|
133 | * 1. if it's a `number`, then it gets truncated
|
134 | * to an integer and returned
|
135 | * 2. if it's a "value object" (see [[isValueObject]]), then
|
136 | * its `hashCode` is used
|
137 | * 3. if a `valueOf()` function is provided, then the
|
138 | * `hashCode` gets recursively invoked on its result
|
139 | * 4. if all else fails, the value gets coerced to a `String`
|
140 | * and a hash code is calculated using [[hashCodeOfString]]
|
141 | *
|
142 | * @param ref is the value to use for calculating a hash code
|
143 | * @return an integer with the aforementioned properties
|
144 | */
|
145 | export function hashCode(ref: any): number {
|
146 | if (typeof ref === "number") {
|
147 | return ref & ref
|
148 | }
|
149 | /* istanbul ignore else */
|
150 | if (typeof ref.valueOf === "function") {
|
151 | const v = ref.valueOf()
|
152 | if (v !== ref) return hashCode(v)
|
153 | }
|
154 | if (isValueObject(ref)) {
|
155 | return (ref as IEquals<any>).hashCode()
|
156 | }
|
157 | return hashCodeOfString(String(ref))
|
158 | }
|
159 |
|
160 | /**
|
161 | * Calculates a hash code out of any string.
|
162 | */
|
163 | export function hashCodeOfString(str: string): number {
|
164 | let hash = 0
|
165 | /* tslint:disable-next-line:strict-type-predicates */
|
166 | if (str == null || str.length === 0) return hash
|
167 | for (let i = 0; i < str.length; i++) {
|
168 | const character = str.charCodeAt(i)
|
169 | hash = ((hash << 5) - hash) + character
|
170 | hash = hash & hash // Convert to 32bit integer
|
171 | }
|
172 | return hash
|
173 | }
|
174 |
|
175 | /** The identity function. */
|
176 | export function id<A>(a: A): A {
|
177 | return a
|
178 | }
|
179 |
|
180 | /**
|
181 | * Utility function for implementing mixins, based on the
|
182 | * [TypeScript Mixins]{@link https://www.typescriptlang.org/docs/handbook/mixins.html}
|
183 | * documentation.
|
184 | *
|
185 | * Sample:
|
186 | *
|
187 | * ```typescript
|
188 | * class Disposable { ... }
|
189 | * class Activatable { ... }
|
190 | * class SmartObject implements Disposable, Activatable { ... }
|
191 | *
|
192 | * applyMixins(SmartObject, [Disposable, Activatable]);
|
193 | * ```
|
194 | *
|
195 | * Using `implements` instead of `extends` for base classes
|
196 | * will make the type system treat them like interfaces instead of
|
197 | * classes. And by `applyMixins` we can also supply global
|
198 | * implementations for the non-abstract members.
|
199 | */
|
200 | export function applyMixins(derivedCtor: {prototype: any}, baseCtors: {prototype: any}[]) {
|
201 | baseCtors.forEach(baseCtor => {
|
202 | Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
|
203 | if (!derivedCtor.prototype[name])
|
204 | derivedCtor.prototype[name] = baseCtor.prototype[name]
|
205 | })
|
206 | })
|
207 | }
|