-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathgenerate-expiration-data.js
179 lines (159 loc) · 6.64 KB
/
generate-expiration-data.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
// sanity check - how many entries to expect at least?
const minEntries = 360;
import { strict as assert } from 'assert';
import * as jf from 'jsonfile';
const writeJsonFile = jf.default.writeFileSync;
import fetch from 'node-fetch';
import * as cheerio from 'cheerio';
import cheerioTableparser from 'cheerio-tableparser';
import { decodeHTML as decode } from 'entities';
const output = "src/generated/expiration-data.json";
const url = 'https://support.google.com/chrome/a/answer/6220366?hl=en';
const extraExpirationInfo = {
// Lenovo IdeaPad 5 Chromebook (82M8001X) series missing
// came out in 2021, so probably at least till 2028
"Lenovo IdeaPad 5 Chromebook": {
"brand": "Lenovo",
"model": "IdeaPad 5 Chromebook",
"expiration": "2028-05-31T22:00:00.000Z" // just guessing here
},
// Lenovo IdeaPad 3 Chromebook 15 (82N40010) series missing
// came out in 2021, so probably at least till 2028
"Lenovo IdeaPad 3 Chromebook 15": {
"brand": "Lenovo",
"model": "IdeaPad 3 Chromebook 15",
"expiration": "2028-05-31T22:00:00.000Z" // just guessing here
},
};
function debug(...args) {
if ("DEBUG" in process.env) {
console.debug(...args);
}
}
// return string formatted as DB key
function getDbKey(t) {
return t.replace(/[.#$/[\]]/g, '_');
}
function getDateFromMonthYear(input) {
var parts = input.split("(");
return new Date("1 " + parts[0]).toISOString();
}
export function extractModels(input) {
input = decode(input)
.trim()
.replace(/[\s\u{0000A0}]+/ug, " ") // match also unicode spaces
.replace(".", "_"); // Use _ for . so that model can be used as dict key
if (
input.match(/^[^/,()]+$/g) // no special separators
|| input.match(/^.*\([\w- ]+\)$/g) // single word in () at the end
) {
input = input.replace(/\s*\(/, " (") // ensure that there is exactly 1 blank before (
debug(" Single: " + input);
return [input];
}
if (
input.match(/^.*[^)]$/g) // no parenthesis at the end, but must have / or , since otherwise it would match simple above
) {
debug(" Multiple without ) at end: " + input);
const parts = input.split(/ ?[\/,] ?/g);
// result can be [ 'Chromebook NL7T-360', 'NL7TW-360' ]
// or [ 'Chromebox CXI3', 'Chromebox Enterprise CXI3' ]
// or [ 'J2', 'J4 Chromebook' ]
// count spaces in first and last element to handle this
assert(parts.length > 1, "Expect at last two parts from: " + input);
var firstPart = parts[0];
if (firstPart.indexOf(" ") > 0) {
// first part has space, check other parts
var results = [];
const prefix = firstPart.replace(/[\w-]+$/, ""); // prefix is everything till the last word
var addedParenthesis = false;
results.push(...parts.slice(1).map((part) => {
// prepend prefix to every part
if (part.indexOf(" ") === -1) {
addedParenthesis = true;
return prefix + "(" + part + ")";
}
return part;
}));
if (addedParenthesis) {
firstPart = firstPart.replace(/([\w-]+)$/, "($1)");
}
results.unshift(firstPart);
return results;
} else {
// get suffix from last element
var lastPart = parts.slice(-1)[0];
const postfix = lastPart.replace(/^[\w-]+/, ""); // postfix is everything after the first word
var results = parts.slice(0, -1).map((part) => {
if (part.indexOf(" ") === -1) {
return part + postfix;
}
return part;
});
results.push(lastPart)
return results;
}
}
const matches = input.match(/^(.+)\((.+)\)$/); // something (part1/part2) with or without space before (
if (matches) {
let [_, prefix, parts] = matches;
prefix = prefix.trim(); // don't care if there was a space between model and (
debug(" Prefix and parts:", prefix, parts);
return parts.split(/[ ,\/]+/g).map(part => prefix + " (" + part + ")");
}
throw Error("Cannot parse: " + input);
}
function getExpirationDataFromSection(html) {
var $ = cheerio.load(html);
cheerioTableparser($);
var rawData = $("table").parsetable(false, false, true);
var results = [];
rawData[0].forEach((models, index) => {
if (models == "Product") return; // skip column headings
var expirationRawData = rawData[1][index];
var expiration = getDateFromMonthYear(expirationRawData);
extractModels(models).map((model) => {
debug(`- ${model} → ${expirationRawData}`);
results.push([model, expiration]);
});
});
return results;
}
if (import.meta.url === `file://${process.argv[1]}`) {
fetch(url)
.then(res => res.text())
.then((rawData) => {
var sections = rawData.split(/<a class="zippy".*>(.*)<\/a>/);
sections.shift(); // get rid of HTML boilerplate
assert.ok(sections.length % 2 == 0, "After dropping HTML boilerplate, there should be an even amount of elements of Brand and Tabledata");
var expirationData = extraExpirationInfo;
while (sections.length > 0) {
var brand = decode(sections.shift());
assert.ok(brand !== undefined, "Brand cannot be undefined");
var tableData = sections.shift();
debug(`==== Brand ${brand} ====`);
var table = getExpirationDataFromSection(tableData);
table.forEach((row) => {
var model = row[0];
var expiration = row[1];
var key = getDbKey(`${brand} ${model}`);
expirationData[key] = {
brand: brand,
model: model,
expiration: expiration,
};
});
}
const entryCount = Object.keys(expirationData).length;
assert.ok(entryCount >= minEntries, `Expect at least ${minEntries} entries for expiration data but got only ${entryCount}`);
var result = {
expirationData: expirationData,
expirationTimestamp: new Date()
};
writeJsonFile(output, result, { spaces: 2});
console.log(`Wrote ${entryCount} expiration records to >${output}<`);
}).catch((err) => {
console.error(err);
process.exit(1);
});
}