-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCode.js
269 lines (243 loc) · 9.29 KB
/
Code.js
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
function isDate(input) {
return input instanceof Date;
}
function isInteger(input) {
return Number.isInteger(input);
}
function isArray(input) {
return Array.isArray(input);
}
/**
* Grant: An amount of equity that vests over 16 quarters starting on
* a certain date.
*
* @constructor
* @param {number} sharesGranted - the total number of RSUs over 4 years
* @param {Date} dateVestFrom - the Date that these RSUs start vesting
*/
class Grant {
constructor(sharesGranted, dateVestFrom) {
this.sharesGranted = sharesGranted;
this.dateVestFrom = dateVestFrom;
}
}
/**
* Distribution: An event where some quantity (1/16th) of the total RSUs in a Grant vests on
* a certain date.
*
* @constructor
* @param {number} quantity - the number of RSUs vesting on this day
* @param {Date} date - the Date that these RSUs vest
*/
class Distribution {
constructor(quantity, date) {
this.quantity = quantity;
this.date = date;
}
}
/**
* Create a new Grant based on an array that looks like either of these:
*
* [ {integer}, {Date} ]
* [ {Date}, {integer} ]
*/
function createGrantFromArray(array) {
let sharesGranted;
let dateOrStringVestFrom;
let dateVestFrom;
if (isInteger(array[0])) {
sharesGranted = array[0];
dateOrStringVestFrom = array[1];
} else if (isInteger(array[1])) {
sharesGranted = array[1];
dateOrStringVestFrom = array[0];
} else {
throw "Invalid argument(s): must supply a whole number of sharesGranted";
}
if (isDate(dateOrStringVestFrom)) {
dateVestFrom = dateOrStringVestFrom;
} else {
dateVestFrom = new Date(dateOrStringVestFrom);
}
return new Grant(sharesGranted, dateVestFrom);
}
/**
* Create an array of Grant(s) based on inputs provided in a Google Sheet.
*
* It can accept invocations that look like any of these:
*
* =RSUDIST(A1:A2)
* =RSUDIST("1/1/2020", 16)
* =RSUDIST(16, "1/1/2020")
*/
function parseArguments_(rsuDistArgs) {
const grants = [];
if (rsuDistArgs.length == 1 && isArray(rsuDistArgs[0])) {
//
// Handling invocation by reference to a range of
// integer/Date pairs:
//
// -> RSUDIST(A1:A2) or RSUDIST(A1:B2)
//
const rangeValues = rsuDistArgs[0];
for(let i = 0; i < rangeValues.length; i++) {
grants.push(createGrantFromArray(rangeValues[i]));
}
}
if (rsuDistArgs.length == 2) {
//
// Handling invocation by value with two arguments that are
// integer/String pairs:
//
// -> =RSUDIST(16, "1/1/2020") or =RSUDIST("1/1/2020", 16)
//
grants.push(createGrantFromArray(rsuDistArgs));
}
return grants;
}
function combineAllDistributions_(arrayOfArrayOfDistributions) {
let arrayOfAllDistributions = [];
arrayOfArrayOfDistributions.forEach(function(arrayOfDistributions) {
arrayOfAllDistributions = arrayOfAllDistributions.concat(arrayOfDistributions);
});
let arrayOfAllDistributionsSorted = arrayOfAllDistributions.sort(function(a, b) {
return a.date.getTime() - b.date.getTime();
});
// Go through the sorted array of Distributions and if any Distributions have the same
// date, then combine the quantity of the RSUs to be received on that day
const firstDistribution = arrayOfAllDistributionsSorted[0];
const mergedDistributions = [ new Distribution(firstDistribution.quantity, firstDistribution.date) ];
let lastMergedDistribution = mergedDistributions[0];
for(let i = 1; i < arrayOfAllDistributionsSorted.length; i++) {
let distributionToConsider = arrayOfAllDistributionsSorted[i];
if (distributionToConsider.date.getTime() === lastMergedDistribution.date.getTime()) {
lastMergedDistribution.quantity += distributionToConsider.quantity;
} else {
lastMergedDistribution = new Distribution(distributionToConsider.quantity, distributionToConsider.date);
mergedDistributions.push(lastMergedDistribution);
}
}
return mergedDistributions;
}
/**
* Determine the date (at midnight) upon which an equity event will occur given
* information about when it starts.
*
* Midnight is relevant here because if you set a date (and time) to midnight
* it looks like a simple date in Google Sheets, eg. "1/1/2019." If you're off by an hour
* you'll get something like: "1/1/2019 1:00"
*
* @param {String} vestFromMonth The month the grant began in, such as "01"
* @param {String} vestFromYear The year the grant began in, such as "2019"
* *param {number} distributionNumber The number of the equity event (between 1 -> 16)
* *param {string} sheetTimeZone The timezone to use for determining when midnight is, such as "America/New_York"
*/
function findDateOfDistribution_(vestFromMonth, vestFromYear, distributionNumber, sheetTimeZone) {
// Determine **year** of distribution (distYear)
const numberOfMonthsInFuture = vestFromMonth + distributionNumber * 3;
let yearsInFuture;
if (numberOfMonthsInFuture % 12 == 0) {
yearsInFuture = (numberOfMonthsInFuture / 12) - 1;
} else {
yearsInFuture = Math.floor(numberOfMonthsInFuture / 12);
}
const distYear = vestFromYear + yearsInFuture;
// Determine **month** of distribution (distYear)
let distMonth = (vestFromMonth + ((distributionNumber % 4) * 3)) % 12;
if (distMonth == 0) {
distMonth = 12;
}
const distMonthString = distMonth < 10 ? "0" + distMonth : distMonth;
// Determine exact time that midnight is on this date in the future
const firstAttemptString = distYear + "-" + distMonthString + "-01T" + "00:00:00Z";
const firstAttempt = new Date(firstAttemptString);
const hourOfFirstAttemptInTimeZone = Utilities.formatDate(firstAttempt, sheetTimeZone, "HH");
const hoursToAdd = 24 - parseInt(hourOfFirstAttemptInTimeZone, 10);
const secondAttempt = new Date(distYear + "-" + distMonthString + "-01T" + "0" + hoursToAdd + ":00:00Z");
const result = new Date(secondAttempt);
return result;
}
const GRANT_VEST_COUNT = 16;
function getYearFor(date, timeZone) {
return Utilities.formatDate(date, timeZone, "YYYY");
}
function getMonthFor(date, timeZone) {
return Utilities.formatDate(date, timeZone, "M");
}
/**
* Create an array where each entry represents a distribution of equity to someone.
*
* Each distribution of equity is a pairing of a number (how many RSUs will someone
* get?) and a Date (when it will happen?)
*
* @param {Grant} grant Information about the total number of RSUs and when it begins to vest
* @param {string} timeZone The timezone of the Google Sheet that will determine when midnight is
*/
function createAllDistributionsForGrant_(grant, timeZone) {
let sharesGranted = grant.sharesGranted;
let dateVestFrom = grant.dateVestFrom;
// If vests could include fractional RSUs-- this is what they'd each be:
const evenlyDividedRSUsPerVest = sharesGranted / GRANT_VEST_COUNT;
// But they can't be fractional. So here's the smallest amount you'd get:
const smallestRsusPerGrant = Math.floor(evenlyDividedRSUsPerVest);
// And here's a fractional amount that's due to *you* each time you get
// the smallest amount
const fractionalRSUsPerVest = evenlyDividedRSUsPerVest - smallestRsusPerGrant;
// If your amount of granted RSUs is evenly divisible by 16-- then this will always
// be zero. But! In most cases there will be some left over and we'll use this to
// keep track of that left over.
let rsusLeftover = 0;
const vestFromMonth = parseInt(getMonthFor(dateVestFrom, timeZone), 10);
const vestFromYear = parseInt(getYearFor(dateVestFrom, timeZone), 10);
const result = [];
for(let i = 0; i < 16; i++) {
let dateOfDistribution = findDateOfDistribution_(vestFromMonth, vestFromYear, i + 1, timeZone)
let rsusOfDistribution = smallestRsusPerGrant;
rsusLeftover = rsusLeftover + fractionalRSUsPerVest;
if (rsusLeftover >= 1) {
rsusOfDistribution = rsusOfDistribution + 1;
rsusLeftover = rsusLeftover - 1;
}
result.push(new Distribution(rsusOfDistribution, dateOfDistribution));
}
return result;
}
/**
* Translate an array of Distributions into an array of arrays to meet the
* expectation that a Custom Function in Google Sheets provides a grid of data
* via returning an array of arrays.
*/
function translateDistributionsToArray_(distributions) {
return distributions.map(function(distribution) {
return [ distribution.quantity, distribution.date ];
});
}
/**
* RSUDIST takes one or more grants and generates a distribution schedule.
*
* RSUDIST(16, "1/1/2020")
* RSUDIST("1/1/2020", 16)
* RSUDIST(A1:B2)
* RSUDIST(A1:B3)
*
* @param {number} sharesGranted the number of RSUs in a grant
* @param {Date} dateVestFrom the date the RSUs start vesting
*/
function RSUDIST() {
// This is the one line that requires oAuth consent: we need to read the timezone of the
// individual spreadsheet in which this code is running.
const timeZone = SpreadsheetApp.getActive().getSpreadsheetTimeZone();
const grants = parseArguments_(arguments);
let distributions;
if (grants.length === 1) {
distributions = createAllDistributionsForGrant_(grants[0], timeZone);
} else {
const allDistributions = [];
for(let i = 0; i < grants.length; i++) {
let distributionsForThisOneGrant = createAllDistributionsForGrant_(grants[i], timeZone);
allDistributions.push(distributionsForThisOneGrant);
}
distributions = combineAllDistributions_(allDistributions);
}
return translateDistributionsToArray_(distributions);
}