Docs: Update benchmark results

This commit is contained in:
github-actions[bot]
2025-11-13 21:50:29 +00:00
parent 59752cb111
commit f2ef5831a7
31 changed files with 416 additions and 433 deletions

View File

@@ -1,32 +1,42 @@
{ {
"openai/gpt-5.1-codex": { "openai/gpt-5.1-codex": {
"1_dijkstra": 12.73643722, "1_dijkstra": null,
"2_convex_hull": 20.551442495, "2_convex_hull": null,
"3_lis": 6.9065933989999975, "3_lis": 4.134811772999999,
"4_determinant": 3.340895964000003, "4_determinant": 3.128456531999999,
"5_markdown_parser": 3.5223763349999935, "5_markdown_parser": 2.4388916820000004,
"6_csv_processor": 16.396112775000002, "6_csv_processor": 43.478935838999995,
"7_scheduler": 85.407877246, "7_scheduler": 84.317196593,
"8_json_validator": 9.581871734999993 "8_json_validator": 6.765057070999989
},
"openai/gpt-5.1-chat": {
"1_dijkstra": null,
"2_convex_hull": 4.03333652099999,
"3_lis": 3.2548832980000006,
"4_determinant": 3.480435989999998,
"5_markdown_parser": 12.157845766999992,
"6_csv_processor": null,
"7_scheduler": 3.6146848039999897,
"8_json_validator": 3.9886151490000192
}, },
"google/gemini-2.5-pro": { "google/gemini-2.5-pro": {
"1_dijkstra": 52.880149714, "1_dijkstra": 44.864160204000015,
"2_convex_hull": 53.18372889399997, "2_convex_hull": 33.892027145,
"3_lis": 28.706411030000016, "3_lis": 36.906535906,
"4_determinant": 12.91266780300002, "4_determinant": 11.497210719000025,
"5_markdown_parser": 19.207484959000023, "5_markdown_parser": 13.62311761299998,
"6_csv_processor": 37.37156615500001, "6_csv_processor": 42.61058959099999,
"7_scheduler": 108.64825142700003, "7_scheduler": 12.12732526100002,
"8_json_validator": 25.360757163000002 "8_json_validator": 50.54006011399999
}, },
"anthropic/claude-sonnet-4.5 TEMP:0.7": { "anthropic/claude-sonnet-4.5 TEMP:0.7": {
"1_dijkstra": 3.8821057339999823, "1_dijkstra": 4.8069697199999935,
"2_convex_hull": 4.63132335000002, "2_convex_hull": 4.475060508999974,
"3_lis": 2.5366414799999912, "3_lis": 2.835424330000009,
"4_determinant": 1.642024412000028, "4_determinant": 2.3172859399999726,
"5_markdown_parser": 2.235130207999959, "5_markdown_parser": 2.080127800999966,
"6_csv_processor": 4.568896986000007, "6_csv_processor": 5.218456623000034,
"7_scheduler": 15.728725427000084, "7_scheduler": 11.33699588100001,
"8_json_validator": 3.0146192130000564 "8_json_validator": 3.298284380999976
} }
} }

View File

@@ -1,21 +1,24 @@
async function findShortestPath(graph, start, end) { async function findShortestPath(graph, start, end) {
const { default: PriorityQueue } = await import('https://cdn.skypack.dev/js-priority-queue'); const { default: PQ } = await import('https://cdn.jsdelivr.net/npm/js-priority-queue@0.1.5/+esm');
const distances = {}; const distances = Object.keys(graph).reduce((acc, node) => ({ ...acc, [node]: Infinity }), {});
const pq = new PriorityQueue({ comparator: (a, b) => a.dist - b.dist });
for (const node in graph) distances[node] = Infinity;
distances[start] = 0; distances[start] = 0;
const pq = new PQ({ comparator: (a, b) => a.dist - b.dist });
pq.queue({ node: start, dist: 0 }); pq.queue({ node: start, dist: 0 });
const visited = new Set();
while (pq.length) { while (pq.length) {
const { node, dist } = pq.dequeue(); const { node, dist } = pq.dequeue();
if (node === end) return dist; if (visited.has(node)) continue;
if (dist > distances[node]) continue; visited.add(node);
for (const neighbor in graph[node]) { if (node === end) return dist;
const newDist = dist + graph[node][neighbor];
for (const [neighbor, weight] of Object.entries(graph[node] || {})) {
const newDist = dist + weight;
if (newDist < distances[neighbor]) { if (newDist < distances[neighbor]) {
distances[neighbor] = newDist; distances[neighbor] = newDist;
pq.queue({ node: neighbor, dist: newDist }); pq.queue({ node: neighbor, dist: newDist });

View File

@@ -1,33 +1,33 @@
async function findShortestPath(graph, startNode, endNode) { async function findShortestPath(graph, start, end) {
const { default: PriorityQueue } = await import('https://esm.sh/js-priority-queue'); const { default: PriorityQueue } = await import('https://cdn.jsdelivr.net/npm/js-priority-queue@0.1.5/priority-queue.min.js');
const distances = { [startNode]: 0 }; const distances = Object.fromEntries(
const queue = new PriorityQueue({ comparator: (a, b) => a.priority - b.priority }); Object.keys(graph).map(node => [node, Infinity])
);
distances[start] = 0;
queue.queue({ node: startNode, priority: 0 }); const pq = new PriorityQueue({
comparator: (a, b) => distances[b] - distances[a]
});
pq.queue(start);
while (queue.length) { while (pq.length) {
const { node: currentNode, priority: currentDist } = queue.dequeue(); const currentNode = pq.dequeue();
if (currentDist > (distances[currentNode] ?? Infinity)) { if (currentNode === end) break;
continue;
}
if (currentNode === endNode) { if (!graph[currentNode]) continue;
return currentDist;
}
for (const neighbor in graph[currentNode] || {}) { for (const [neighbor, weight] of Object.entries(graph[currentNode])) {
const weight = graph[currentNode][neighbor]; const newDist = distances[currentNode] + weight;
const newDist = currentDist + weight;
if (newDist < (distances[neighbor] ?? Infinity)) { if (newDist < distances[neighbor]) {
distances[neighbor] = newDist; distances[neighbor] = newDist;
queue.queue({ node: neighbor, priority: newDist }); pq.queue(neighbor);
} }
} }
} }
return Infinity; return distances[end];
} }
export default findShortestPath; export default findShortestPath;

View File

@@ -1,19 +0,0 @@
async function findShortestPath(g,s,e){
const {default:Q}=await import('https://cdn.skypack.dev/js-priority-queue')
const d={[s]:0},q=new Q({comparator:(a,b)=>a.w-b.w})
q.queue({n:s,w:0})
while(q.length){
const {n,w}=q.dequeue()
if(w>(d[n]??Infinity))continue
if(n===e)return w
for(const k in g[n]||{}){
const nw=w+g[n][k]
if(nw<(d[k]??Infinity)){
d[k]=nw
q.queue({n:k,w:nw})
}
}
}
return Infinity
}
export default findShortestPath;

View File

@@ -21,6 +21,6 @@ async function findConvexHull(points) {
return hull; return hull;
}; };
return [...buildHull(sorted), ...buildHull(sorted.slice().reverse())]; return [...buildHull(sorted), ...buildHull([...sorted].reverse())];
} }
export default findConvexHull; export default findConvexHull;

View File

@@ -1,27 +1,29 @@
async function findConvexHull(points) { const findConvexHull = async (points) => {
const { sortBy, uniqWith, isEqual } = await import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.min.js'); const _ = await import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.min.js');
const uniquePoints = uniqWith(points, isEqual); const sortedPoints = _.sortBy(_.uniqWith(points, _.isEqual), ['x', 'y']);
if (uniquePoints.length < 3) {
return uniquePoints; if (sortedPoints.length < 3) {
return sortedPoints;
} }
const sortedPoints = sortBy(uniquePoints, ['x', 'y']);
const crossProduct = (p1, p2, p3) => const crossProduct = (p1, p2, p3) =>
(p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x); (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x);
const buildHull = pts => pts.reduce((hull, p) => { const buildHalfHull = (pointSet) => {
while (hull.length >= 2 && crossProduct(hull.at(-2), hull.at(-1), p) <= 0) { const hull = [];
hull.pop(); for (const p of pointSet) {
while (hull.length >= 2 && crossProduct(hull.at(-2), hull.at(-1), p) <= 0) {
hull.pop();
}
hull.push(p);
} }
hull.push(p);
return hull; return hull;
}, []); };
const lower = buildHull(sortedPoints); const lower = buildHalfHull(sortedPoints);
const upper = buildHull([...sortedPoints].reverse()); const upper = buildHalfHull([...sortedPoints].reverse());
return [...lower.slice(0, -1), ...upper.slice(0, -1)]; return [...lower.slice(0, -1), ...upper.slice(0, -1)];
} };
export default findConvexHull; export default findConvexHull;

View File

@@ -0,0 +1,20 @@
export async function findConvexHull(a){
const _ = (await import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.min.js')).default
a = _.uniqWith(a,_.isEqual)
a = _.sortBy(a,['x','y'])
const c=(o,p,q)=>((p.x-o.x)*(q.y-o.y)-(p.y-o.y)*(q.x-o.x))
const b=[]
for(const p of a){
while(b.length>1 && c(b[b.length-2],b[b.length-1],p)<=0) b.pop()
b.push(p)
}
const u=[]
for(const p of [...a].reverse()){
while(u.length>1 && c(u[u.length-2],u[u.length-1],p)<=0) u.pop()
u.push(p)
}
u.pop()
b.pop()
return [...b,...u]
}
export default findConvexHull;

View File

@@ -1,23 +0,0 @@
let cache
const loader=()=>cache??=import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js')
export const findConvexHull=async pts=>{
const {sortBy:s,uniqWith:u,isEqual:e}=await loader()
const data=u(s(pts,[v=>v.x,v=>v.y]),e)
if(data.length<2)return data.slice()
const cross=(o,a,b)=>(a.x-o.x)*(b.y-o.y)-(a.y-o.y)*(b.x-o.x)
const lower=[],upper=[]
for(const p of data){
while(lower.length>1&&cross(lower.at(-2),lower.at(-1),p)<=0)lower.pop()
lower.push(p)
}
for(let i=data.length;i--;){
const p=data[i]
while(upper.length>1&&cross(upper.at(-2),upper.at(-1),p)<=0)upper.pop()
upper.push(p)
}
lower.pop()
upper.pop()
return lower.concat(upper)
}
export default findConvexHull;

View File

@@ -1,13 +1,13 @@
const findLISLength = async (arr) => { const findLISLength = async (arr) => {
const { bisectLeft } = await import('https://cdn.jsdelivr.net/npm/d3-array@3/+esm');
if (!arr?.length) return 0; if (!arr?.length) return 0;
const { bisectLeft } = await import('https://cdn.jsdelivr.net/npm/d3-array@3/+esm');
const tails = []; const tails = [];
for (const num of arr) { for (const num of arr) {
const idx = bisectLeft(tails, num); const pos = bisectLeft(tails, num);
tails[idx] = num; tails[pos] = num;
} }
return tails.length; return tails.length;

View File

@@ -1,16 +1,12 @@
const findLISLength = async (nums) => { async function findLISLength(nums) {
if (!nums?.length) { const { bisectLeft } = await import('https://cdn.jsdelivr.net/npm/d3-array@3/+esm');
return 0;
}
const { bisectLeft } = await import('https://esm.sh/d3-array');
const tails = []; const tails = [];
for (const num of nums) { for (const num of nums || []) {
tails[bisectLeft(tails, num)] = num; tails[bisectLeft(tails, num)] = num;
} }
return tails.length; return tails.length;
}; }
export default findLISLength; export default findLISLength;

View File

@@ -0,0 +1,10 @@
async function findLISLength(a) {
const {bisectLeft:b} = await import('https://cdn.jsdelivr.net/npm/d3-array@3/+esm')
const t = []
for (const x of a) {
const i = b(t, x)
t[i] = x
}
return t.length
}
export default findLISLength;

View File

@@ -1,9 +1,10 @@
const m = import('https://cdn.jsdelivr.net/npm/d3-array@3/+esm'); async function findLISLength(nums){
const {bisectLeft}=await import('https://cdn.jsdelivr.net/npm/d3-array@3/+esm')
async function findLISLength(nums) { const tails=[]
const { bisectLeft: b } = await m; for(const n of nums){
const tails = []; const i=bisectLeft(tails,n)
for (const n of nums) tails[b(tails, n)] = n; tails[i]=n
return tails.length; }
return tails.length
} }
export default findLISLength; export default findLISLength;

View File

@@ -1,5 +1,5 @@
const calculateDeterminant = async (matrix) => { async function calculateDeterminant(matrix) {
const { det } = await import('https://cdn.jsdelivr.net/npm/mathjs@11.11.0/+esm'); const { det } = await import('https://cdn.jsdelivr.net/npm/mathjs@11.11.0/+esm');
return det(matrix); return det(matrix);
}; }
export default calculateDeterminant; export default calculateDeterminant;

View File

@@ -1,5 +1,5 @@
const calculateDeterminant = async m => { const calculateDeterminant = async matrix => {
const { det } = await import('https://cdn.jsdelivr.net/npm/mathjs/+esm'); const { det } = await import('https://cdn.jsdelivr.net/npm/mathjs@12.4.2/es/index.js');
return det(m); return det(matrix);
}; };
export default calculateDeterminant; export default calculateDeterminant;

View File

@@ -0,0 +1,5 @@
async function calculateDeterminant(m){
const {det}=await import('https://cdn.jsdelivr.net/npm/mathjs@11.11.0/+esm')
return det(m)
}
export default calculateDeterminant;

View File

@@ -1,5 +1,5 @@
export const calculateDeterminant = async matrix => { async function calculateDeterminant(m){
const { det } = await import('https://cdn.jsdelivr.net/npm/mathjs@11.11.0/+esm'); const {det}=await import('https://cdn.jsdelivr.net/npm/mathjs@11.11.0/+esm')
return det(matrix); return det(m)
}; }
export default calculateDeterminant; export default calculateDeterminant;

View File

@@ -1,5 +1,5 @@
const parseMarkdown = async (md) => { async function parseMarkdown(md) {
const { marked } = await import('https://cdn.jsdelivr.net/npm/marked@11.1.1/+esm'); const { marked } = await import('https://cdn.jsdelivr.net/npm/marked@11/+esm');
return marked.parse(md); return marked.parse(md);
}; }
export default parseMarkdown; export default parseMarkdown;

View File

@@ -1,5 +1,6 @@
const parseMarkdown = async (md) => { const parseMarkdown = async (md) => {
const { marked } = await import('https://cdn.jsdelivr.net/npm/marked@12.0.2/+esm'); const cdn = 'https://cdn.jsdelivr.net/npm/marked@13.0.0/lib/marked.esm.js';
const { marked } = await import(cdn);
return marked.parse(md); return marked.parse(md);
}; };
export default parseMarkdown; export default parseMarkdown;

View File

@@ -0,0 +1,2 @@
export default parseMarkdown;

View File

@@ -1,7 +1,5 @@
let m const parseMarkdown = async m => {
export const parseMarkdown=async t=>{ const { marked } = await import('https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js')
m||(m=import('https://cdn.jsdelivr.net/npm/marked@12.0.2/lib/marked.esm.js')) return marked.parse(m)
const {marked}=await m
return marked.parse(t)
} }
export default parseMarkdown; export default parseMarkdown;

View File

@@ -1,14 +1,10 @@
const processCSV = async (csvString, config) => { async function processCSV(csvString, config) {
const { parse } = await import('https://cdn.jsdelivr.net/npm/papaparse@5.4.1/+esm'); const { parse } = await import('https://cdn.skypack.dev/papaparse@5.4.1');
const { data } = parse(csvString, { const { data } = parse(csvString, { header: true, skipEmptyLines: true });
header: true,
skipEmptyLines: true,
dynamicTyping: true
});
const filtered = data.filter(row => const filtered = data.filter(row =>
row[config.filterColumn] == config.filterValue String(row[config.filterColumn]) === String(config.filterValue)
); );
const grouped = filtered.reduce((acc, row) => { const grouped = filtered.reduce((acc, row) => {
@@ -18,15 +14,19 @@ const processCSV = async (csvString, config) => {
return acc; return acc;
}, {}); }, {});
const operations = { return Object.entries(grouped).map(([key, rows]) => {
sum: arr => arr.reduce((s, r) => s + (Number(r[config.aggregateColumn]) || 0), 0), let result;
avg: arr => operations.sum(arr) / arr.length || 0, const vals = rows.map(r => parseFloat(r[config.aggregateColumn]) || 0);
count: arr => arr.length
};
return Object.entries(grouped).map(([key, rows]) => ({ if (config.operation === 'sum') {
[config.groupBy]: key, result = vals.reduce((a, b) => a + b, 0);
result: operations[config.operation](rows) } else if (config.operation === 'avg') {
})); result = vals.reduce((a, b) => a + b, 0) / vals.length;
}; } else {
result = rows.length;
}
return { [config.groupBy]: key, result };
});
}
export default processCSV; export default processCSV;

View File

@@ -1,4 +1,4 @@
const processCSV = async (csvString, config) => { async function processCSV(csvString, config) {
const { const {
filterColumn, filterColumn,
filterValue, filterValue,
@@ -7,32 +7,38 @@ const processCSV = async (csvString, config) => {
operation, operation,
} = config; } = config;
const [Papa, _] = await Promise.all([ const { default: Papa } = await import('https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js');
import('https://cdn.jsdelivr.net/npm/papaparse@5.4.1/+esm'),
import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/+esm'), const { data } = Papa.parse(csvString, {
]); header: true,
dynamicTyping: true,
skipEmptyLines: true,
});
const aggregators = { const aggregators = {
sum: items => _.sumBy(items, aggregateColumn), sum: (acc, val) => (acc || 0) + val,
count: items => items.length, count: (acc, _val) => (acc || 0) + 1,
avg: items => _.sumBy(items, aggregateColumn) / items.length || 0, avg: (acc, val) => {
const state = acc || { sum: 0, count: 0 };
state.sum += val;
state.count += 1;
return state;
},
}; };
const { data } = await new Promise(resolve => const groups = data
Papa.parse(csvString, { .filter(row => row[filterColumn] == filterValue)
header: true, .reduce((acc, row) => {
dynamicTyping: true, const key = row[groupBy];
skipEmptyLines: true, acc[key] = aggregators[operation](acc[key], row[aggregateColumn]);
complete: resolve, return acc;
}) }, {});
);
const filteredData = data.filter(row => row[filterColumn] === filterValue); return Object.entries(groups).map(([key, value]) => ({
const groupedData = _.groupBy(filteredData, groupBy); [groupBy]: key,
result: operation === 'avg'
return Object.entries(groupedData).map(([key, group]) => ({ ? (value.count ? value.sum / value.count : 0)
[groupBy]: isNaN(Number(key)) ? key : Number(key), : value,
result: aggregators[operation](group),
})); }));
}; }
export default processCSV; export default processCSV;

View File

@@ -1,16 +1,21 @@
export const processCSV=async(csv,cfg)=>{ const libs=(()=>{let c;return()=>c||(c=Promise.all([
const [{parse},{groupBy,sumBy}]=await Promise.all([ import('https://cdn.jsdelivr.net/npm/d3-dsv@3/+esm'),
import('https://cdn.jsdelivr.net/npm/papaparse@5.4.1/+esm'), import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/groupBy.js')
import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/+esm') ]).then(([dsv,gb])=>({parse:dsv.csvParse,groupBy:gb.default||gb})));})();
]);
const {filterColumn:fc,filterValue:fv,groupBy:gb,aggregateColumn:ac,operation:op}=cfg; const toNum=v=>{const n=+v;return Number.isFinite(n)?n:0};
const rows=parse(csv,{header:!0,skipEmptyLines:!0}).data.filter(r=>r[fc]==fv);
const grouped=groupBy(rows,r=>r[gb]); async function processCSV(csv,opts){
const agg=v=>{ const {parse,groupBy}=await libs();
if(op=='count')return v.length; const rows=parse(csv);
const total=sumBy(v,x=>+x[ac]||0); const {filterColumn:fc,filterValue:fv,groupBy:gc,aggregateColumn:ac,operation:op}=opts;
return op=='avg'&&v.length?total/v.length:total; const pool=groupBy(rows.filter(r=>r[fc]==fv),r=>r[gc]??'');
}; const calc={
return Object.entries(grouped).map(([k,v])=>({[gb]:k,result:agg(v)})); sum:x=>x.reduce((t,r)=>t+toNum(r[ac]),0),
}; avg:x=>x.length?x.reduce((t,r)=>t+toNum(r[ac]),0)/x.length:0,
count:x=>x.length
}[op];
if(!calc) throw new Error('Unsupported operation');
return Object.entries(pool).map(([k,x])=>({[gc]:k,result:calc(x)}));
}
export default processCSV; export default processCSV;

View File

@@ -1,23 +1,22 @@
const findAvailableSlots = async (cal1, cal2, constraints) => { const findAvailableSlots = async (cal1, cal2, constraints) => {
const { default: dayjs } = await import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/+esm'); const { default: dayjs } = await import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/+esm');
const [{ default: utc }, { default: customParseFormat }, { default: isBetween }] = await Promise.all([ const [{ default: utc }, { default: isBetween }] = await Promise.all([
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/utc.js'), import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/utc.js/+esm'),
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/customParseFormat.js'), import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/isBetween.js/+esm')
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/isBetween.js')
]); ]);
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(customParseFormat);
dayjs.extend(isBetween); dayjs.extend(isBetween);
const { durationMinutes, searchRange, workHours } = constraints; const { durationMinutes, searchRange, workHours } = constraints;
const allBusy = [...cal1, ...cal2].map(s => ({ const [whStart, whEnd] = [workHours.start.split(':'), workHours.end.split(':')];
start: dayjs(s.start),
end: dayjs(s.end)
})).sort((a, b) => a.start - b.start);
const merged = allBusy.reduce((acc, curr) => { const merged = [...cal1, ...cal2]
if (!acc.length || acc[acc.length - 1].end < curr.start) { .map(s => ({ start: dayjs(s.start), end: dayjs(s.end) }))
.sort((a, b) => a.start - b.start);
const busy = merged.reduce((acc, curr) => {
if (!acc.length || curr.start > acc[acc.length - 1].end) {
acc.push(curr); acc.push(curr);
} else { } else {
acc[acc.length - 1].end = dayjs.max(acc[acc.length - 1].end, curr.end); acc[acc.length - 1].end = dayjs.max(acc[acc.length - 1].end, curr.end);
@@ -26,81 +25,45 @@ const findAvailableSlots = async (cal1, cal2, constraints) => {
}, []); }, []);
const slots = []; const slots = [];
const searchStart = dayjs(searchRange.start); let current = dayjs(searchRange.start);
const searchEnd = dayjs(searchRange.end); const rangeEnd = dayjs(searchRange.end);
const [whStart, whEnd] = [
dayjs(workHours.start, 'HH:mm'),
dayjs(workHours.end, 'HH:mm')
];
const isInWorkHours = (dt) => { while (current < rangeEnd) {
const time = dayjs().hour(dt.hour()).minute(dt.minute()); const dayStart = current.hour(+whStart[0]).minute(+whStart[1]).second(0);
return time.isBetween(whStart, whEnd, null, '[)'); const dayEnd = current.hour(+whEnd[0]).minute(+whEnd[1]).second(0);
};
const addSlot = (start, end) => { let slotStart = dayStart < current ? current : dayStart;
let curr = start;
while (curr.add(durationMinutes, 'minute') <= end) { for (const b of busy) {
if (isInWorkHours(curr) && isInWorkHours(curr.add(durationMinutes, 'minute').subtract(1, 'second'))) { if (b.end <= slotStart || b.start >= dayEnd) continue;
const slotEnd = curr.add(durationMinutes, 'minute');
const dayEnd = curr.hour(whEnd.hour()).minute(whEnd.minute()); if (b.start > slotStart) {
if (slotEnd <= dayEnd) { const slotEnd = dayjs.min(b.start, dayEnd);
let probe = slotStart;
while (probe.add(durationMinutes, 'minute') <= slotEnd) {
slots.push({ slots.push({
start: curr.toISOString(), start: probe.toISOString(),
end: slotEnd.toISOString() end: probe.add(durationMinutes, 'minute').toISOString()
}); });
probe = probe.add(durationMinutes, 'minute');
} }
} }
curr = curr.add(15, 'minute'); slotStart = dayjs.max(slotStart, b.end);
} }
};
let prevEnd = searchStart.hour(whStart.hour()).minute(whStart.minute()); if (slotStart < dayEnd) {
if (prevEnd < searchStart) prevEnd = searchStart; let probe = slotStart;
while (probe.add(durationMinutes, 'minute') <= dayEnd) {
merged.forEach(busy => { slots.push({
if (busy.start > prevEnd) { start: probe.toISOString(),
let gapStart = prevEnd; end: probe.add(durationMinutes, 'minute').toISOString()
let gapEnd = busy.start; });
probe = probe.add(durationMinutes, 'minute');
let day = gapStart.startOf('day');
while (day <= gapEnd) {
const dayStart = dayjs.max(
gapStart,
day.hour(whStart.hour()).minute(whStart.minute())
);
const dayEnd = dayjs.min(
gapEnd,
day.hour(whEnd.hour()).minute(whEnd.minute())
);
if (dayStart < dayEnd && dayStart >= searchStart && dayEnd <= searchEnd) {
addSlot(dayStart, dayEnd);
}
day = day.add(1, 'day');
} }
} }
prevEnd = dayjs.max(prevEnd, busy.end);
});
const finalEnd = searchEnd.hour(whEnd.hour()).minute(whEnd.minute()); current = current.add(1, 'day').startOf('day');
if (prevEnd < finalEnd) {
let day = prevEnd.startOf('day');
while (day <= finalEnd) {
const dayStart = dayjs.max(
prevEnd,
day.hour(whStart.hour()).minute(whStart.minute())
);
const dayEnd = dayjs.min(
finalEnd,
day.hour(whEnd.hour()).minute(whEnd.minute())
);
if (dayStart < dayEnd && dayStart >= searchStart && dayEnd <= searchEnd) {
addSlot(dayStart, dayEnd);
}
day = day.add(1, 'day');
}
} }
return slots; return slots;

View File

@@ -1,69 +1,2 @@
async function findAvailableSlots(cal1, cal2, c) {
const {
parseISO, formatISO, addMinutes, isAfter, max, startOfDay,
setHours, setMinutes, setSeconds, setMilliseconds, eachDayOfInterval
} = await import('https://cdn.jsdelivr.net/npm/date-fns@3.6.0/index.mjs');
const { durationMinutes: dur, searchRange: sr, workHours: wh } = c;
const sStart = parseISO(sr.start);
const sEnd = parseISO(sr.end);
const toDate = s => parseISO(s);
const [whsH, whsM] = wh.start.split(':').map(Number);
const [wheH, wheM] = wh.end.split(':').map(Number);
const applyTime = (d, h, m) =>
setMilliseconds(setSeconds(setMinutes(setHours(startOfDay(d), h), m), 0), 0);
const busy = [...cal1, ...cal2].map(({ start, end }) => ({
start: toDate(start),
end: toDate(end),
}));
const nonWork = [];
if (sStart < sEnd) {
for (const day of eachDayOfInterval({ start: sStart, end: sEnd })) {
nonWork.push({
start: startOfDay(day),
end: applyTime(day, whsH, whsM)
});
nonWork.push({
start: applyTime(day, wheH, wheM),
end: addMinutes(startOfDay(day), 1440)
});
}
}
const unavailable = [...busy, ...nonWork].sort((a, b) => a.start - b.start);
const merged = unavailable.reduce((acc, slot) => {
const last = acc[acc.length - 1];
if (last && slot.start <= last.end) {
last.end = max(last.end, slot.end);
} else {
acc.push({ ...slot });
}
return acc;
}, []);
const results = [];
let ptr = sStart;
const allBlocks = [...merged, { start: sEnd, end: sEnd }];
for (const block of allBlocks) {
let slotStart = ptr;
while (true) {
const slotEnd = addMinutes(slotStart, dur);
if (isAfter(slotEnd, block.start)) break;
results.push({ start: slotStart, end: slotEnd });
slotStart = slotEnd;
}
ptr = max(ptr, block.end);
}
return results.map(({ start, end }) => ({
start: formatISO(start),
end: formatISO(end),
}));
}
export default findAvailableSlots; export default findAvailableSlots;

View File

@@ -0,0 +1,50 @@
async function findAvailableSlots(a,b,c){
const d=await import('https://esm.sh/date-fns@3.6.0')
const {parseISO,formatISO,max,min,addMinutes,isBefore}=d
const p=t=>parseISO(t)
const wh=(dt,s)=>{
const [H,M]=s.split(':')
const x=new Date(dt)
x.setHours(H,M,0,0)
return x
}
const rng=[p(c.searchRange.start),p(c.searchRange.end)]
const dur=c.durationMinutes
const norm=x=>x.map(v=>[p(v.start),p(v.end)])
const A=norm(a),B=norm(b)
const merge=x=>{
let r=x.sort((x,y)=>x[0]-y[0]),o=[r[0]]
for(let i=1;i<r.length;i++){
let [s,e]=r[i],l=o[o.length-1]
if(s<=l[1]) l[1]=new Date(Math.max(e,l[1]))
else o.push([s,e])
}
return o
}
const busy=merge(A.concat(B))
const free=[]
let cur=rng[0]
for(let i=0;i<=busy.length;i++){
let s=i<busy.length?max(cur,busy[i][1]):cur
let e=i<busy.length?max(cur,busy[i][0]):rng[1]
if(i<busy.length){
if(busy[i][0]>cur) free.push([cur,busy[i][0]])
cur=max(cur,busy[i][1])
}else free.push([s,e])
}
const out=[]
for(let [s,e]of free){
s=max(s,rng[0])
e=min(e,rng[1])
let ws=wh(s,c.workHours.start)
let we=wh(s,c.workHours.end)
if(ws>we){let t=ws;ws=we;we=t}
let ss=max(s,ws),ee=min(e,we)
while(isBefore(addMinutes(ss,dur),ee)){
out.push({start:formatISO(ss),end:formatISO(addMinutes(ss,dur))})
ss=addMinutes(ss,dur)
}
}
return out
}
export default findAvailableSlots;

View File

@@ -1,59 +1,55 @@
const findAvailableSlots = async (a, b, o) => { const luxon=import('https://cdn.skypack.dev/luxon');
const { parseISO, formatISO, addDays, startOfDay } = await import('https://cdn.skypack.dev/date-fns@2.30.0');
const dur = o.durationMinutes * 6e4; const findAvailableSlots=async(c1,c2,k)=>{
const rng = [parseISO(o.searchRange.start), parseISO(o.searchRange.end)]; const {DateTime}=await luxon;
const split = t => t.split(':').map(Number); const {durationMinutes:d,searchRange:r,workHours:w}=k;
const hrs = { s: split(o.workHours.start), e: split(o.workHours.end) }; const zone=DateTime.fromISO(r.start).zoneName;
const busy = [...a, ...b] const iso=v=>DateTime.fromISO(v,{zone});
.map(v => ({ start: parseISO(v.start), end: parseISO(v.end) })) const rangeStart=iso(r.start);
.map(v => { const rangeEnd=iso(r.end);
const s = new Date(Math.max(v.start, rng[0])); if(rangeEnd<=rangeStart)return[];
const e = new Date(Math.min(v.end, rng[1])); const [hs,ms]=w.start.split(':').map(Number);
return s < e ? { start: s, end: e } : null; const [he,me]=w.end.split(':').map(Number);
}) const daysEnd=rangeEnd.startOf('day');
.filter(Boolean) const windows=[];
.sort((x, y) => x.start - y.start) for(let day=rangeStart.startOf('day');day<=daysEnd;day=day.plus({days:1})){
.reduce((m, v) => { let s=day.set({hour:hs,minute:ms,second:0,millisecond:0});
const last = m[m.length - 1]; let e=day.set({hour:he,minute:me,second:0,millisecond:0});
if (!last || v.start > last.end) m.push({ start: new Date(v.start), end: new Date(v.end) }); if(e<=s||e<=rangeStart||s>=rangeEnd)continue;
else if (v.end > last.end) last.end = new Date(v.end); if(s<rangeStart)s=rangeStart;
return m; if(e>rangeEnd)e=rangeEnd;
}, []); windows.push({start:s,end:e});
const slots = [];
const collect = (s, e) => {
let a = s.getTime(), b = e.getTime();
while (a + dur <= b) {
const r = a + dur;
slots.push({ start: formatISO(new Date(a)), end: formatISO(new Date(r)) });
a = r;
}
};
let idx = 0;
for (let d = startOfDay(rng[0]); d <= rng[1]; d = addDays(d, 1)) {
const ws = new Date(d);
ws.setHours(hrs.s[0], hrs.s[1], 0, 0);
const we = new Date(d);
we.setHours(hrs.e[0], hrs.e[1], 0, 0);
const winStart = new Date(Math.max(ws, rng[0]));
const winEnd = new Date(Math.min(we, rng[1]));
if (winStart >= winEnd) continue;
let cursor = winStart;
while (idx < busy.length && busy[idx].end <= winStart) idx++;
let view = idx;
let done = false;
while (view < busy.length && busy[view].start < winEnd) {
const gapEnd = new Date(Math.min(busy[view].start, winEnd));
if (gapEnd > cursor) collect(cursor, gapEnd);
cursor = new Date(Math.max(cursor, busy[view].end));
if (cursor >= winEnd) {
done = true;
break;
}
view++;
}
if (!done && cursor < winEnd) collect(cursor, winEnd);
idx = view;
} }
return slots; if(!windows.length)return[];
const busy=[...c1,...c2].map(v=>{
let s=iso(v.start),e=iso(v.end);
if(e<=s||e<=rangeStart||s>=rangeEnd)return null;
if(s<rangeStart)s=rangeStart;
if(e>rangeEnd)e=rangeEnd;
return{start:s,end:e};
}).filter(Boolean).sort((a,b)=>a.start.valueOf()-b.start.valueOf());
const merged=[];
for(const slot of busy){
const last=merged[merged.length-1];
if(last&&slot.start<=last.end){
if(slot.end>last.end)last.end=slot.end;
}else merged.push({start:slot.start,end:slot.end});
}
const out=[];
const push=(s,e)=>e.diff(s,'minutes').minutes>=d&&out.push({start:s.toISO(),end:e.toISO()});
for(const wSlot of windows){
let cur=wSlot.start;
for(const b of merged){
if(b.start>=wSlot.end)break;
if(b.end<=wSlot.start)continue;
const bs=b.start>wSlot.start?b.start:wSlot.start;
const be=b.end<wSlot.end?b.end:wSlot.end;
if(bs>cur)push(cur,bs);
if(be>cur)cur=be;
if(cur>=wSlot.end)break;
}
if(cur<wSlot.end)push(cur,wSlot.end);
}
return out;
}; };
export default findAvailableSlots; export default findAvailableSlots;

View File

@@ -1,5 +1,5 @@
const validateJSON = async (data, schema) => { const validateJSON = async (data, schema) => {
const { default: Ajv } = await import('https://cdn.jsdelivr.net/npm/ajv@8.12.0/dist/2020.min.js'); const { default: Ajv } = await import('https://cdn.jsdelivr.net/npm/ajv@8/dist/ajv.bundle.js');
const ajv = new Ajv({ allErrors: true, verbose: true }); const ajv = new Ajv({ allErrors: true, verbose: true });
const validate = ajv.compile(schema); const validate = ajv.compile(schema);
@@ -7,12 +7,9 @@ const validateJSON = async (data, schema) => {
return { return {
valid, valid,
errors: valid ? [] : validate.errors.map(e => ({ errors: valid ? [] : validate.errors.map(e =>
path: e.instancePath || '/', `${e.instancePath || '/'} ${e.message}${e.params ? ': ' + JSON.stringify(e.params) : ''}`
message: e.message, )
keyword: e.keyword,
params: e.params
}))
}; };
}; };
export default validateJSON; export default validateJSON;

View File

@@ -1,16 +1,35 @@
let ajvPromise; const validateJSON = async (jsonObject, schemaObject) => {
try {
const [{
default: Ajv
}, {
default: addFormats
}] = await Promise.all([
import('https://esm.sh/ajv'),
import('https://esm.sh/ajv-formats')
]);
const validateJSON = async (data, schema) => { const ajv = new Ajv({
const { default: Ajv } = await (ajvPromise ||= import('https://cdn.jsdelivr.net/npm/ajv@8.12.0/+esm')); allErrors: true
});
addFormats(ajv);
const ajv = new Ajv({ allErrors: true }); const validate = ajv.compile(schemaObject);
const validate = ajv.compile(schema); const valid = validate(jsonObject);
const valid = validate(data);
const errors = valid ? [] : validate.errors.map( const errors = valid ? [] : validate.errors.map(
e => (`${e.instancePath || 'root'} ${e.message}`).trim() e => `${e.instancePath || '<root>'}: ${e.message}`
); );
return { valid, errors }; return {
valid,
errors
};
} catch (e) {
return {
valid: false,
errors: [`Validation setup failed: ${e.message}`]
};
}
}; };
export default validateJSON; export default validateJSON;

View File

@@ -0,0 +1,8 @@
async function validateJSON(o, s) {
const { default: Ajv } = await import('https://esm.sh/ajv@8');
const a = new Ajv({ allErrors: true });
const v = a.compile(s);
const ok = v(o);
return { valid: ok, errors: ok ? [] : v.errors.map(e => e.message) };
}
export default validateJSON;

View File

@@ -1,10 +1,10 @@
const ajvP=import('https://cdn.skypack.dev/ajv@8?min').then(m=>m.default||m) const validateJSON=async(j,s)=>{
const ajvI=ajvP.then(Ajv=>new Ajv({allErrors:true})) const{default:Ajv}=await import('https://esm.sh/ajv@8')
export async function validateJSON(data,schema){ const{default:f}=await import('https://esm.sh/ajv-formats@2')
const ajv=await ajvI const a=new Ajv({allErrors:1,strict:false})
let validate f(a)
try{validate=ajv.compile(schema)}catch(e){return{valid:false,errors:[e.message]}} const v=a.compile(s)
const valid=validate(data) const ok=v(j)
return valid?{valid:true,errors:[]}:{valid:false,errors:validate.errors.map(e=>(e.instancePath||'/')+' '+e.message)} return{valid:ok,errors:ok?[]:(v.errors||[]).map(e=>`${e.instancePath||'/'} ${e.message}`.trim())}
} }
export default validateJSON; export default validateJSON;