-
Notifications
You must be signed in to change notification settings - Fork 4k
/
util.ts
152 lines (145 loc) · 5.36 KB
/
util.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**
* Compares two objects for equality, deeply. The function handles arguments that are
* +null+, +undefined+, arrays and objects. For objects, the function will not take the
* object prototype into account for the purpose of the comparison, only the values of
* properties reported by +Object.keys+.
*
* If both operands can be parsed to equivalent numbers, will return true.
* This makes diff consistent with CloudFormation, where a numeric 10 and a literal "10"
* are considered equivalent.
*
* @param lvalue the left operand of the equality comparison.
* @param rvalue the right operand of the equality comparison.
*
* @returns +true+ if both +lvalue+ and +rvalue+ are equivalent to each other.
*/
export function deepEqual(lvalue: any, rvalue: any): boolean {
if (lvalue === rvalue) { return true; }
// CloudFormation allows passing strings into boolean-typed fields
if (((typeof lvalue === 'string' && typeof rvalue === 'boolean') ||
(typeof lvalue === 'boolean' && typeof rvalue === 'string')) &&
lvalue.toString() === rvalue.toString()) {
return true;
}
// allows a numeric 10 and a literal "10" to be equivalent;
// this is consistent with CloudFormation.
if ((typeof lvalue === 'string' || typeof rvalue === 'string') &&
safeParseFloat(lvalue) === safeParseFloat(rvalue)) {
return true;
}
if (typeof lvalue !== typeof rvalue) { return false; }
if (Array.isArray(lvalue) !== Array.isArray(rvalue)) { return false; }
if (Array.isArray(lvalue) /* && Array.isArray(rvalue) */) {
if (lvalue.length !== rvalue.length) { return false; }
for (let i = 0 ; i < lvalue.length ; i++) {
if (!deepEqual(lvalue[i], rvalue[i])) { return false; }
}
return true;
}
if (typeof lvalue === 'object' /* && typeof rvalue === 'object' */) {
if (lvalue === null || rvalue === null) {
// If both were null, they'd have been ===
return false;
}
const keys = Object.keys(lvalue);
if (keys.length !== Object.keys(rvalue).length) { return false; }
for (const key of keys) {
if (!rvalue.hasOwnProperty(key)) { return false; }
if (key === 'DependsOn') {
if (!dependsOnEqual(lvalue[key], rvalue[key])) { return false; };
// check differences other than `DependsOn`
continue;
}
if (!deepEqual(lvalue[key], rvalue[key])) { return false; }
}
return true;
}
// Neither object, nor array: I deduce this is primitive type
// Primitive type and not ===, so I deduce not deepEqual
return false;
}
/**
* Compares two arguments to DependsOn for equality.
*
* @param lvalue the left operand of the equality comparison.
* @param rvalue the right operand of the equality comparison.
*
* @returns +true+ if both +lvalue+ and +rvalue+ are equivalent to each other.
*/
function dependsOnEqual(lvalue: any, rvalue: any): boolean {
// allows ['Value'] and 'Value' to be equal
if (Array.isArray(lvalue) !== Array.isArray(rvalue)) {
const array = Array.isArray(lvalue) ? lvalue : rvalue;
const nonArray = Array.isArray(lvalue) ? rvalue : lvalue;
if (array.length === 1 && deepEqual(array[0], nonArray)) {
return true;
}
return false;
}
// allows arrays passed to DependsOn to be equivalent irrespective of element order
if (Array.isArray(lvalue) && Array.isArray(rvalue)) {
if (lvalue.length !== rvalue.length) { return false; }
for (let i = 0 ; i < lvalue.length ; i++) {
for (let j = 0 ; j < lvalue.length ; j++) {
if ((!deepEqual(lvalue[i], rvalue[j])) && (j === lvalue.length - 1)) {
return false;
}
break;
}
}
return true;
}
return false;
}
/**
* Produce the differences between two maps, as a map, using a specified diff function.
*
* @param oldValue the old map.
* @param newValue the new map.
* @param elementDiff the diff function.
*
* @returns a map representing the differences between +oldValue+ and +newValue+.
*/
export function diffKeyedEntities<T>(
oldValue: { [key: string]: any } | undefined,
newValue: { [key: string]: any } | undefined,
elementDiff: (oldElement: any, newElement: any, key: string) => T): { [name: string]: T } {
const result: { [name: string]: T } = {};
for (const logicalId of unionOf(Object.keys(oldValue || {}), Object.keys(newValue || {}))) {
const oldElement = oldValue && oldValue[logicalId];
const newElement = newValue && newValue[logicalId];
result[logicalId] = elementDiff(oldElement, newElement, logicalId);
}
return result;
}
/**
* Computes the union of two sets of strings.
*
* @param lv the left set of strings.
* @param rv the right set of strings.
*
* @returns a new array containing all elemebts from +lv+ and +rv+, with no duplicates.
*/
export function unionOf(lv: string[] | Set<string>, rv: string[] | Set<string>): string[] {
const result = new Set(lv);
for (const v of rv) {
result.add(v);
}
return new Array(...result);
}
/**
* A parseFloat implementation that does the right thing for
* strings like '0.0.0'
* (for which JavaScript's parseFloat() returns 0).
*/
function safeParseFloat(str: string): number {
const ret = parseFloat(str);
if (ret === 0) {
// if the str is exactly '0', that's OK;
// but parseFloat() also returns 0 for things like '0.0';
// in this case, return NaN, so we'll fall back to string comparison
return str === '0' ? ret : NaN;
} else {
return ret;
}
}