Docs: Update benchmark results

This commit is contained in:
github-actions[bot]
2025-11-07 22:07:45 +00:00
parent b5f81c6e8a
commit 1687dca49c
42 changed files with 784 additions and 847 deletions

View File

@@ -1,62 +1,62 @@
{ {
"openrouter/polaris-alpha": { "openrouter/polaris-alpha": {
"1_dijkstra": 5.063492935, "1_dijkstra": 5.663508702,
"2_convex_hull": 7.998615312, "2_convex_hull": 5.853618222000001,
"3_lis": 2.2021910790000003, "3_lis": 2.7803642009999985,
"4_determinant": 2.4727166589999996, "4_determinant": 2.2549030789999995,
"5_markdown_parser": 2.1889706240000013, "5_markdown_parser": 2.428845373,
"6_csv_processor": 6.424773092, "6_csv_processor": 7.004516176000001,
"7_scheduler": 19.749020437, "7_scheduler": 14.370537171,
"8_json_validator": 3.7860971980000033 "8_json_validator": 4.488872165000001
}, },
"google/gemini-2.5-pro TEMP:0.7": { "google/gemini-2.5-pro TEMP:0.7": {
"1_dijkstra": 30.986608248000003, "1_dijkstra": 44.600899471,
"2_convex_hull": 58.090286611, "2_convex_hull": 37.66833377400001,
"3_lis": 39.50342178599999, "3_lis": 15.942395212000017,
"4_determinant": 21.314621672999987, "4_determinant": 14.880486881999998,
"5_markdown_parser": 21.140068067999994, "5_markdown_parser": 22.373131437000005,
"6_csv_processor": 46.383014058, "6_csv_processor": 44.242416465999995,
"7_scheduler": 74.787878823, "7_scheduler": 59.66216159599999,
"8_json_validator": 39.59319957399997 "8_json_validator": 56.66983818199998
}, },
"anthropic/claude-sonnet-4.5": { "anthropic/claude-sonnet-4.5": {
"1_dijkstra": 4.355259537999983, "1_dijkstra": 4.15588515399996,
"2_convex_hull": 4.980808106000011, "2_convex_hull": 4.806273176999995,
"3_lis": 2.2764143039999762, "3_lis": 2.2488794699999852,
"4_determinant": 1.8504535560000221, "4_determinant": 1.442708507000003,
"5_markdown_parser": 1.841865622000012, "5_markdown_parser": 2.1533864889999967,
"6_csv_processor": 4.9831007339999775, "6_csv_processor": 4.5494161339999994,
"7_scheduler": 12.904369474000006, "7_scheduler": 13.169441607000016,
"8_json_validator": 2.7832529310000247 "8_json_validator": 2.83180839000002
}, },
"anthropic/claude-sonnet-4.5 TEMP:0.7": { "anthropic/claude-sonnet-4.5 TEMP:0.7": {
"1_dijkstra": 3.933310278000019, "1_dijkstra": 4.402104449000035,
"2_convex_hull": 4.819035326000012, "2_convex_hull": 5.400227270999982,
"3_lis": 2.34827887899999, "3_lis": 4.495708809000033,
"4_determinant": 1.644420714000007, "4_determinant": 1.8576504829999758,
"5_markdown_parser": 2.280882503999979, "5_markdown_parser": 1.8857627109999884,
"6_csv_processor": 4.7151394149999835, "6_csv_processor": 4.558546455000004,
"7_scheduler": 13.658607328000013, "7_scheduler": 11.452500546000024,
"8_json_validator": 3.4110046910000382 "8_json_validator": 2.654919869999983
}, },
"anthropic/claude-sonnet-4.5 TEMP:0.4": { "anthropic/claude-sonnet-4.5 TEMP:0.4": {
"1_dijkstra": 3.954044443999999, "1_dijkstra": 4.231384166999953,
"2_convex_hull": 4.5914942489999815, "2_convex_hull": 4.297011561000021,
"3_lis": 2.4153946509999806, "3_lis": 2.270648061999993,
"4_determinant": 1.7196861260000151, "4_determinant": 1.5894071249999688,
"5_markdown_parser": 1.801734215000004, "5_markdown_parser": 1.8096978460000246,
"6_csv_processor": 4.646366793999972, "6_csv_processor": 4.622239387999987,
"7_scheduler": 12.781063763999962, "7_scheduler": 12.332949335000013,
"8_json_validator": 3.3014615179999964 "8_json_validator": 2.6777597769999995
}, },
"openai/gpt-5-codex": { "openai/gpt-5-codex": {
"1_dijkstra": 36.88321140999993, "1_dijkstra": 86.38025543199991,
"2_convex_hull": 26.59820080500003, "2_convex_hull": 37.86684929500008,
"3_lis": 12.8049466599999, "3_lis": 9.727075501000042,
"4_determinant": 10.523928330000023, "4_determinant": 8.948688077000087,
"5_markdown_parser": 57.74550891099998, "5_markdown_parser": 8.000620244999999,
"6_csv_processor": 105.18542238100001, "6_csv_processor": 31.738115685999976,
"7_scheduler": 127.650813921, "7_scheduler": 155.32837211300003,
"8_json_validator": 24.58168081499997 "8_json_validator": 27.551844137000035
} }
} }

View File

@@ -1,28 +1,31 @@
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: PriorityQueue } = await import('https://cdn.skypack.dev/js-priority-queue');
const distances = {}; const dist = Object.keys(graph).reduce((acc, node) => ({ ...acc, [node]: Infinity }), {});
const pq = new PriorityQueue({ comparator: (a, b) => a.dist - b.dist }); dist[start] = 0;
for (const node in graph) distances[node] = Infinity; const pq = new PriorityQueue({ comparator: (a, b) => a[1] - b[1] });
distances[start] = 0; pq.queue([start, 0]);
pq.queue({ node: start, dist: 0 });
const visited = new Set();
while (pq.length) { while (pq.length) {
const { node, dist } = pq.dequeue(); const [node, d] = 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 d;
const newDist = dist + graph[node][neighbor];
if (newDist < distances[neighbor]) { for (const [neighbor, weight] of Object.entries(graph[node] || {})) {
distances[neighbor] = newDist; const newDist = d + weight;
pq.queue({ node: neighbor, dist: newDist }); if (newDist < dist[neighbor]) {
dist[neighbor] = newDist;
pq.queue([neighbor, newDist]);
} }
} }
} }
return distances[end]; return dist[end];
} }
export default findShortestPath; export default findShortestPath;

View File

@@ -1,19 +1,15 @@
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 dist = { [start]: 0 }; const dist = { [start]: 0 };
const pq = new PriorityQueue({ comparator: (a, b) => a[1] - b[1] }); const pq = new PQ({ comparator: (a, b) => a[1] - b[1] });
const visited = new Set();
pq.queue([start, 0]); pq.queue([start, 0]);
while (pq.length) { while (pq.length) {
const [node, d] = pq.dequeue(); const [node, d] = pq.dequeue();
if (visited.has(node)) continue;
visited.add(node);
if (node === end) return d; if (node === end) return d;
if (d > (dist[node] ?? Infinity)) continue;
for (const [neighbor, weight] of Object.entries(graph[node] || {})) { for (const [neighbor, weight] of Object.entries(graph[node] || {})) {
const newDist = d + weight; const newDist = d + weight;

View File

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

View File

@@ -1,24 +1,29 @@
let u let pq;
const o=()=>u||(u=import('https://cdn.skypack.dev/js-priority-queue').then(r=>r.default||r)) const loadPQ=()=>pq||(pq=import('https://cdn.skypack.dev/js-priority-queue').then(m=>m.default));
async function findShortestPath(g,s,t){
const PQ=await o() async function findShortestPath(graph,start,end){
if(s===t)return 0 const PQ=await loadPQ();
const d={},q=new PQ({comparator:(a,b)=>a[1]-b[1]}) if(start===end) return 0;
for(const k in g)d[k]=Infinity if(!graph||typeof graph!=='object') return Infinity;
d[s]=0 const d=new Map([[start,0]]);
q.queue([s,0]) const q=new PQ({comparator:(a,b)=>a[0]-b[0]});
while(q.length){ q.queue([0,start]);
const[n,w]=q.dequeue() while(q.length){
if(w>d[n])continue const [w,n]=q.dequeue();
if(n===t)return w if(w>(d.get(n)??Infinity)) continue;
for(const[nb,c]of Object.entries(g[n]||{})){ if(n===end) return w;
const nw=w+c const edges=graph[n];
if(nw<d[nb]){ if(!edges||typeof edges!=='object') continue;
d[nb]=nw for(const k of Object.keys(edges)){
q.queue([nb,nw]) const c=edges[k];
} if(typeof c!=='number'||c<0||!Number.isFinite(c)) continue;
} const nw=w+c;
} if(nw<(d.get(k)??Infinity)){
return Infinity d.set(k,nw);
q.queue([nw,k]);
}
}
}
return Infinity;
} }
export default findShortestPath; export default findShortestPath;

View File

@@ -1,24 +1,30 @@
async function findShortestPath(g,s,e){ async function findShortestPath(g,s,e){
if(!g||!g[s]||!g[e])return Infinity const {default:PriorityQueue} = await import('https://cdn.jsdelivr.net/npm/js-priority-queue@0.1.5/jsprioryqueue.min.js').catch(()=>({default:class{
const{default:PriorityQueue}=await import('https://cdn.jsdelivr.net/npm/js-priority-queue@0.1.5/js/priority-queue.min.js') constructor(o){this.c=o.comparator||((a,b)=>a-b);this.q=[]}
const d={},v={} queue(v){this.q.push(v);this.q.sort(this.c)}
Object.keys(g).forEach(k=>d[k]=k===s?0:Infinity) dequeue(){return this.q.shift()}
const q=new PriorityQueue({comparator:(a,b)=>a.w-b.w}) peek(){return this.q[0]}
q.queue({n:s,w:0}) get length(){return this.q.length}
while(q.length){ }}));
const{n,w}=q.dequeue() if(!g||!g[s]||!g[e])return Infinity;
if(v[n])continue const d={},v={},pq=new PriorityQueue({comparator:(a,b)=>a.w-b.w});
v[n]=1 for(const n in g)d[n]=Infinity;
if(n===e)return w d[s]=0;
const nbrs=g[n] pq.queue({n:s,w:0});
for(const x in nbrs){ while(pq.length){
const nw=w+nbrs[x] const {n,w}=pq.dequeue();
if(nw<d[x]){ if(v[n])continue;
d[x]=nw v[n]=1;
q.queue({n:x,w:nw}) if(n===e)return w;
const nb=g[n];
for(const k in nb){
const nw=w+nb[k];
if(nw<d[k]){
d[k]=nw;
pq.queue({n:k,w:nw});
} }
} }
} }
return Infinity return Infinity;
} }
export default findShortestPath; export default findShortestPath;

View File

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

View File

@@ -1,17 +1,17 @@
async function findConvexHull(points) { async function findConvexHull(points) {
const _ = await import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.min.js').then(m => m.default); const { sortBy, uniqWith } = await import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js');
if (points.length < 3) return points; if (points.length < 3) return points;
const sorted = _.sortBy(_.uniqWith(points, _.isEqual), ['x', 'y']); const sorted = sortBy(uniqWith(points, (a, b) => a.x === b.x && a.y === b.y), ['x', 'y']);
if (sorted.length < 3) return sorted; if (sorted.length < 3) return sorted;
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x); const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
const buildHull = arr => { const buildHull = pts => {
const hull = []; const hull = [];
for (const p of arr) { for (const p of pts) {
while (hull.length >= 2 && cross(hull[hull.length - 2], hull[hull.length - 1], p) <= 0) { while (hull.length >= 2 && cross(hull[hull.length - 2], hull[hull.length - 1], p) <= 0) {
hull.pop(); hull.pop();
} }
@@ -21,9 +21,6 @@ async function findConvexHull(points) {
return hull; return hull;
}; };
const lower = buildHull(sorted); return [...buildHull(sorted), ...buildHull(sorted.slice().reverse())];
const upper = buildHull([...sorted].reverse());
return [...lower, ...upper].reverse();
} }
export default findConvexHull; export default findConvexHull;

View File

@@ -1,15 +1,15 @@
const findConvexHull = async (points) => { async function findConvexHull(points) {
const { sortBy, uniqWith } = 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').then(m => m.default);
if (points.length < 3) return points; if (points.length < 3) return points;
const sorted = sortBy(uniqWith(points, (a, b) => a.x === b.x && a.y === b.y), ['x', 'y']); const sorted = _.sortBy(_.uniqWith(points, _.isEqual), ['x', 'y']);
if (sorted.length < 3) return sorted; if (sorted.length < 3) return sorted;
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x); const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
const buildHalf = (pts) => { const buildHull = pts => {
const hull = []; const hull = [];
for (const p of pts) { for (const p of pts) {
while (hull.length >= 2 && cross(hull[hull.length - 2], hull[hull.length - 1], p) <= 0) { while (hull.length >= 2 && cross(hull[hull.length - 2], hull[hull.length - 1], p) <= 0) {
@@ -21,9 +21,6 @@ const findConvexHull = async (points) => {
return hull; return hull;
}; };
const lower = buildHalf(sorted); return [...buildHull(sorted), ...buildHull(sorted.slice().reverse())];
const upper = buildHalf([...sorted].reverse()); }
return [...lower, ...upper].reverse();
};
export default findConvexHull; export default findConvexHull;

View File

@@ -1,29 +1,29 @@
async function findConvexHull(points) { async function findConvexHull(points) {
const _ = await import('https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js'); const { sortBy, uniqWith, isEqual } = await import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/+esm');
const sortedUniquePoints = _.uniqWith(_.sortBy(points, ['x', 'y']), _.isEqual); if (points.length < 3) {
return [...points];
if (sortedUniquePoints.length < 3) {
return sortedUniquePoints;
} }
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 buildHalfHull = (pts) => { const sortedPoints = sortBy(points, ['x', 'y']);
const hull = [];
for (const p of pts) { const buildHull = (pts) =>
while (hull.length >= 2 && crossProduct(hull.at(-2), hull.at(-1), p) <= 0) { pts.reduce((hull, p) => {
while (hull.length >= 2 && crossProduct(hull[hull.length - 2], hull.at(-1), p) <= 0) {
hull.pop(); hull.pop();
} }
hull.push(p); hull.push(p);
}
return hull; return hull;
}; }, []);
const lowerHull = buildHalfHull(sortedUniquePoints); const lowerHull = buildHull(sortedPoints);
const upperHull = buildHalfHull([...sortedUniquePoints].reverse()); const upperHull = buildHull([...sortedPoints].reverse());
return [...upperHull.slice(0, -1), ...lowerHull.slice(0, -1)]; const combinedHull = [...lowerHull, ...upperHull];
return uniqWith(combinedHull, isEqual);
} }
export default findConvexHull; export default findConvexHull;

View File

@@ -1,19 +1,15 @@
async function findConvexHull(points){ let h
const {default:_}=await import('https://cdn.skypack.dev/lodash-es'); const findConvexHull=async v=>{
const pts=_.uniqWith(_.sortBy(points,['x','y']),(a,b)=>a.x===b.x&&a.y===b.y); const {sortBy,uniqWith,isEqual}=await(h??=import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/+esm'))
if(pts.length<3) return pts.slice(); const s=uniqWith(sortBy(v,['x','y']),isEqual)
const c=(o,a,b)=>(a.x-o.x)*(b.y-o.y)-(a.y-o.y)*(b.x-o.x); if(s.length<3)return s.slice()
const l=[],u=[]; const r=(o,a,b)=>(a.x-o.x)*(b.y-o.y)-(a.y-o.y)*(b.x-o.x)
for(const p of pts){ const l=[]
while(l.length>1&&c(l.at(-2),l.at(-1),p)<=0)l.pop(); for(const p of s){while(l.length>1&&r(l.at(-2),l.at(-1),p)<=0)l.pop();l.push(p)}
l.push(p); const u=[]
} for(let i=s.length;i--;){const p=s[i];while(u.length>1&&r(u.at(-2),u.at(-1),p)<=0)u.pop();u.push(p)}
for(let i=pts.length;i--;){ l.pop()
const p=pts[i]; u.pop()
while(u.length>1&&c(u.at(-2),u.at(-1),p)<=0)u.pop(); return l.concat(u)
u.push(p);
}
l.pop();u.pop();
return [...l,...u].reverse();
} }
export default findConvexHull; export default findConvexHull;

View File

@@ -1,29 +1,23 @@
async function findConvexHull(points) { async function findConvexHull(points) {
if (!Array.isArray(points) || points.length < 3) return points || []; if (!Array.isArray(points) || points.length < 1) return [];
const { default: _ } = await import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.default.min.js'); const { default: _ } = await import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.default.min.js');
const ps = _.uniqWith(points, (a, b) => a.x === b.x && a.y === b.y); const s = _.uniqBy(points, p => `${p.x},${p.y}`);
if (ps.length < 3) return ps; if (s.length < 2) return s.slice();
const pts = _.sortBy(ps, ['x', 'y']); const pts = _.sortBy(s, ['x', 'y']);
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x); const c = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
const lower = []; const l = [];
for (let p of pts) { for (let p of pts) {
for (; lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0;) lower.pop(); for (; l.length > 1 && c(l[l.length - 2], l[l.length - 1], p) <= 0;) l.pop();
lower.push(p); l.push(p);
} }
const upper = []; const u = [];
for (let i = pts.length - 1; i >= 0; i--) { for (let i = pts.length - 1; i >= 0; i--) {
const p = pts[i]; const p = pts[i];
for (; upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0;) upper.pop(); for (; u.length > 1 && c(u[u.length - 2], u[u.length - 1], p) <= 0;) u.pop();
upper.push(p); u.push(p);
} }
const hull = lower.slice(0, -1).concat(upper.slice(0, -1)); l.pop();
if (hull.length <= 2) return hull; u.pop();
let area = 0; return l.concat(u);
for (let i = 0; i < hull.length; i++) {
const j = (i + 1) % hull.length;
area += hull[i].x * hull[j].y - hull[j].x * hull[i].y;
}
if (area > 0) hull.reverse();
return hull;
} }
export default findConvexHull; export default findConvexHull;

View File

@@ -1,7 +1,8 @@
const findLISLength = async (arr) => { const findLISLength = async (arr) => {
if (!arr?.length) return 0; if (!arr?.length) return 0;
const { bisectLeft } = await import('https://cdn.skypack.dev/d3-array'); 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) {

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
const findLISLength=async a=>{ let l;
if(!Array.isArray(a)) throw new TypeError('Expected an array') const m=()=>l??=import('https://cdn.jsdelivr.net/npm/d3-array@3/+esm').then(({bisectLeft})=>bisectLeft);
const {bisectLeft:b}=await (findLISLength.m??=import('https://cdn.jsdelivr.net/npm/d3-array@3/+esm')) export async function findLISLength(a){
const t=[] if(!Array.isArray(a)) throw new TypeError('Expected array');
for(const v of a){ const b=await m();
if(!Number.isFinite(v)) throw new TypeError('Array values must be finite numbers') const t=[];
const i=b(t,v) for(const n of a){
if(i===t.length) t.push(v) if(typeof n!=='number'||!Number.isFinite(n)) throw new TypeError('Expected finite numbers');
else t[i]=v const i=b(t,n);
t[i]=n;
} }
return t.length return t.length;
} }
window.findLISLength=findLISLength
export default findLISLength; export default findLISLength;

View File

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

View File

@@ -1,5 +1,7 @@
async function calculateDeterminant(m){ let c
const {det}=await import('https://cdn.jsdelivr.net/npm/mathjs@11.11.0/dist/esm/math.js') const m=()=>c||(c=import('https://cdn.jsdelivr.net/npm/mathjs@11.11.1/+esm'))
return det(m) async function calculateDeterminant(a){
const{det}=await m()
return det(a)
} }
export default calculateDeterminant; export default calculateDeterminant;

View File

@@ -1,7 +1,7 @@
async function calculateDeterminant(m){ async function calculateDeterminant(m){
if(!Array.isArray(m)||!m.length||!m.every(r=>Array.isArray(r)&&r.length===m.length))throw new Error('Input must be an n x n matrix'); if(!Array.isArray(m)||!m.length||!m.every(r=>Array.isArray(r)&&r.length===m.length))throw new Error('Matrix must be square')
const {create,all,det}=await import('https://cdn.jsdelivr.net/npm/mathjs@11.11.0/+esm'); const {create,all} = await import('https://cdn.jsdelivr.net/npm/mathjs@11.11.0/+esm')
const math=create(all); const math = create(all)
return math.det(m); return math.det(m)
} }
export default calculateDeterminant; export default calculateDeterminant;

View File

@@ -1,5 +1,5 @@
async function parseMarkdown(md) { const parseMarkdown = async (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.1.1/+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) => { async function parseMarkdown(markdown) {
const { marked } = await import('https://cdn.jsdelivr.net/npm/marked@5/+esm'); const cdn = 'https://cdn.jsdelivr.net/npm/marked@12/lib/marked.esm.js';
return marked.parse(md); const { marked } = await import(cdn);
}; return marked.parse(markdown);
}
export default parseMarkdown; export default parseMarkdown;

View File

@@ -1,15 +1,6 @@
let md,pd const parseMarkdown=async t=>{
const getMd=()=>md||(md=import('https://cdn.jsdelivr.net/npm/marked@12.0.2/+esm').then(x=>{ parseMarkdown.l??=import('https://cdn.jsdelivr.net/npm/marked@12.0.2/lib/marked.esm.js');
let m=x.marked||x.default const{marked}=await parseMarkdown.l;
m.setOptions?.({gfm:true,breaks:true,smartypants:true}) return marked.parse(t||'');
return s=>m.parse?m.parse(s):m(s) };
}))
const getPurify=()=>pd||(pd=import('https://esm.sh/dompurify@3?target=es2020').then(x=>{
let d=x.default||x.DOMPurify||x
return typeof d.sanitize=='function'?d:d(window)
}))
async function parseMarkdown(s){
let [render,pur]=await Promise.all([getMd(),getPurify()])
return pur.sanitize(render(s))
}
export default parseMarkdown; export default parseMarkdown;

View File

@@ -1,11 +1,14 @@
async function parseMarkdown(markdown) { async function parseMarkdown(md) {
const {marked} = await import('https://cdn.jsdelivr.net/npm/marked@12.0.2/lib/marked.esm.js') if (typeof md !== "string") md = String(md ?? "");
const [{ default: marked }] = await Promise.all([
import("https://cdn.jsdelivr.net/npm/marked@12.0.2/lib/marked.esm.js")
]);
marked.setOptions({ marked.setOptions({
gfm:true, gfm: true,
breaks:false, breaks: false,
headerIds:true, headerIds: true,
mangle:false mangle: false
}) });
return marked.parse(String(markdown ?? '')) return marked.parse(md);
} }
export default parseMarkdown; export default parseMarkdown;

View File

@@ -1,26 +1,29 @@
async function processCSV(csv, config) { const processCSV = async (csv, config) => {
const { parse } = await import('https://cdn.skypack.dev/papaparse@5.4.1'); const { parse } = await import('https://cdn.jsdelivr.net/npm/papaparse@5.4.1/+esm');
const { filterColumn: fc, filterValue: fv, groupBy: gb, aggregateColumn: ac, operation: op } = config;
const { data } = parse(csv, { header: true, skipEmptyLines: true }); const { data } = parse(csv, { header: true, skipEmptyLines: true });
const filtered = data.filter(r => r[fc] == fv); const filtered = data.filter(row =>
String(row[config.filterColumn]).trim() === String(config.filterValue).trim()
);
const grouped = filtered.reduce((acc, row) => { const grouped = filtered.reduce((acc, row) => {
const key = row[gb]; const key = row[config.groupBy];
if (!acc[key]) acc[key] = []; if (!acc[key]) acc[key] = [];
acc[key].push(row); acc[key].push(row);
return acc; return acc;
}, {}); }, {});
return Object.entries(grouped).map(([key, rows]) => { const aggregate = (rows) => {
const vals = rows.map(r => parseFloat(r[ac]) || 0); const values = rows.map(r => parseFloat(r[config.aggregateColumn]) || 0);
const result = op === 'sum' if (config.operation === 'sum') return values.reduce((a, b) => a + b, 0);
? vals.reduce((a, b) => a + b, 0) if (config.operation === 'avg') return values.reduce((a, b) => a + b, 0) / values.length;
: op === 'avg' return values.length;
? vals.reduce((a, b) => a + b, 0) / vals.length };
: vals.length;
return { [gb]: key, result }; return Object.entries(grouped).map(([key, rows]) => ({
}); [config.groupBy]: key,
} result: aggregate(rows)
}));
};
export default processCSV; export default processCSV;

View File

@@ -1,12 +1,12 @@
const processCSV = async (csv, { filterColumn, filterValue, groupBy, aggregateColumn, operation }) => { const processCSV = async (csvString, config) => {
const { default: Papa } = await import('https://cdn.jsdelivr.net/npm/papaparse@5.4.1/+esm'); const { parse } = await import('https://cdn.skypack.dev/papaparse');
const { data } = Papa.parse(csv, { header: true, skipEmptyLines: true }); const { data } = parse(csvString, { header: true, skipEmptyLines: true });
const filtered = data.filter(row => row[filterColumn] == filterValue); const filtered = data.filter(row => row[config.filterColumn] == config.filterValue);
const grouped = filtered.reduce((acc, row) => { const grouped = filtered.reduce((acc, row) => {
const key = row[groupBy]; const key = row[config.groupBy];
if (!acc[key]) acc[key] = []; if (!acc[key]) acc[key] = [];
acc[key].push(row); acc[key].push(row);
return acc; return acc;
@@ -14,17 +14,15 @@ const processCSV = async (csv, { filterColumn, filterValue, groupBy, aggregateCo
return Object.entries(grouped).map(([key, rows]) => { return Object.entries(grouped).map(([key, rows]) => {
let result; let result;
if (config.operation === 'count') {
if (operation === 'count') {
result = rows.length; result = rows.length;
} else { } else {
const values = rows.map(r => parseFloat(r[aggregateColumn]) || 0); const values = rows.map(r => parseFloat(r[config.aggregateColumn]) || 0);
result = operation === 'sum' result = config.operation === 'sum'
? values.reduce((a, b) => a + b, 0) ? values.reduce((a, b) => a + b, 0)
: values.reduce((a, b) => a + b, 0) / values.length; : values.reduce((a, b) => a + b, 0) / values.length;
} }
return { [config.groupBy]: key, result };
return { [groupBy]: key, [operation]: result };
}); });
}; };
export default processCSV; export default processCSV;

View File

@@ -1,30 +1,26 @@
async function processCSV(csvString, config) { async function processCSV(csv, config) {
const { parse } = await import('https://cdn.skypack.dev/papaparse@5.4.1'); const { parse } = await import('https://esm.sh/papaparse@5.4.1');
const { groupBy, sumBy, meanBy, countBy } = await import('https://esm.sh/lodash-es@4.17.21');
const { filterColumn: fc, filterValue: fv, groupBy: gb, aggregateColumn: ac, operation: op } = config; const { data } = parse(csv, { header: true, skipEmptyLines: true });
const { data } = parse(csvString, { header: true, skipEmptyLines: true }); const filtered = data.filter(row =>
String(row[config.filterColumn]) === String(config.filterValue)
);
const filtered = data.filter(row => row[fc] == fv); const grouped = Object.entries(
groupBy(filtered, config.groupBy)
);
const grouped = filtered.reduce((acc, row) => { const ops = {
const key = row[gb]; sum: arr => sumBy(arr, r => parseFloat(r[config.aggregateColumn]) || 0),
if (!acc[key]) acc[key] = []; avg: arr => meanBy(arr, r => parseFloat(r[config.aggregateColumn]) || 0),
acc[key].push(row); count: arr => arr.length
return acc; };
}, {});
return Object.entries(grouped).map(([key, rows]) => { return grouped.map(([key, rows]) => ({
let result; [config.groupBy]: key,
if (op === 'count') { result: ops[config.operation](rows)
result = rows.length; }));
} else {
const values = rows.map(r => parseFloat(r[ac]) || 0);
result = op === 'sum'
? values.reduce((a, b) => a + b, 0)
: values.reduce((a, b) => a + b, 0) / values.length;
}
return { [gb]: key, result };
});
} }
export default processCSV; export default processCSV;

View File

@@ -1,40 +1,42 @@
const processCSV = async (csvString, { const processCSV = async (
csvString, {
filterColumn, filterColumn,
filterValue, filterValue,
groupBy, groupBy,
aggregateColumn, aggregateColumn,
operation operation
}) => { }
const { default: Papa } = await import('https://cdn.jsdelivr.net/npm/papaparse@5.4.1/+esm'); ) => {
const [Papa, {
default: _
}] = await Promise.all([
import('https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js'),
import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/+esm'),
]);
const aggregations = { const {
sum: g => g.s, data
count: g => g.c, } = Papa.parse(csvString, {
avg: g => g.c ? g.s / g.c : 0, header: true,
dynamicTyping: true,
skipEmptyLines: true,
});
const aggregators = {
sum: g => _.sumBy(g, aggregateColumn),
avg: g => _.meanBy(g, aggregateColumn),
count: g => g.length,
}; };
const groupedData = Papa.parse(csvString, { return _.chain(data)
header: true, .filter({
skipEmptyLines: true, [filterColumn]: filterValue
dynamicTyping: true, })
}).data .groupBy(groupBy)
.filter(row => row[filterColumn] === filterValue) .map((rows, key) => ({
.reduce((acc, row) => { [groupBy]: Number.isNaN(+key) ? key : +key,
const key = row[groupBy]; result: aggregators[operation](rows),
const val = row[aggregateColumn]; }))
.value();
acc[key] = acc[key] || { s: 0, c: 0 };
if (typeof val === 'number' && !isNaN(val)) {
acc[key].s += val;
}
acc[key].c++;
return acc;
}, {});
return Object.entries(groupedData).map(([key, group]) => ({
[groupBy]: /^-?\d+(\.\d+)?$/.test(key) ? Number(key) : key,
[operation]: aggregations[operation](group),
}));
}; };
export default processCSV; export default processCSV;

View File

@@ -1,20 +1,18 @@
let cache; let cache;
async function processCSV(csv,cfg){ const load=()=>cache||(cache=Promise.all([
const {csvParse,rollups}=await (cache??=import('https://cdn.jsdelivr.net/npm/d3@7/+esm')); import('https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.mjs'),
const {filterColumn,filterValue,groupBy,aggregateColumn,operation}=cfg; import('https://cdn.jsdelivr.net/npm/d3-array@3/+esm')
if(!['sum','avg','count'].includes(operation))throw new Error('Unsupported operation'); ]));
if(!csv||!filterColumn||!groupBy)throw new Error('Missing essentials'); const processCSV=async(csv,cfg)=>{
if(operation!=='count'&&!aggregateColumn)throw new Error('Missing aggregateColumn'); const[{default:Papa},{rollup}]=await load();
const rows=csvParse(csv).filter(r=>r[filterColumn]==filterValue); const{filterColumn:f,filterValue:v,groupBy:g,aggregateColumn:a,operation:o}=cfg;
const rows=Papa.parse(csv,{header:1,dynamicTyping:1,skipEmptyLines:1}).data.filter(r=>r[f]===v);
if(!rows.length)return[]; if(!rows.length)return[];
const fn=operation==='count'?v=>v.length:v=>{ const map=rollup(rows,s=>{
let s=0,n=0; if(o==='count')return s.length;
for(const row of v){ const t=s.reduce((p,c)=>p+(Number(c[a])||0),0);
const val=+row[aggregateColumn]; return o==='avg'?t/s.length:t;
if(!Number.isNaN(val)){s+=val;n++;} },r=>r[g]);
} return Array.from(map,([k,val])=>({[g]:k,result:val}));
return operation==='sum'?s:n?s/n:0; };
};
return rollups(rows,fn,r=>r[groupBy]).map(([k,v])=>({[groupBy]:k,[operation]:v}));
}
export default processCSV; export default processCSV;

View File

@@ -1,30 +1,35 @@
async function processCSV(csv, cfg) { async function processCSV(csv, c) {
const {filterColumn,filterValue,groupBy,aggregateColumn,operation} = cfg if (typeof csv !== 'string') throw new TypeError('csv must be string')
if(!csv||!filterColumn||!groupBy||!operation) throw new Error('Invalid configuration') if (!c || typeof c !== 'object') throw new TypeError('config required')
const [{parse},{default:lodash}] = await Promise.all([ let { filterColumn, filterValue, groupBy, aggregateColumn, operation } = c
import('https://cdn.jsdelivr.net/npm/papaparse@5.4.1/+esm'), if (!filterColumn || !groupBy || !operation) throw new Error('missing config')
import('https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/+esm') if (!['sum', 'avg', 'count'].includes(operation)) throw new Error('bad op')
])
const {data,errors} = parse(csv,{header:true,skipEmptyLines:true,dynamicTyping:true}) const { parse } = await import('https://cdn.skypack.dev/papaparse@5.4.1')
if(errors?.length) throw new Error('CSV parse error') const _ = (await import('https://cdn.skypack.dev/lodash-es@4.17.21')).default
const rows = data.filter(r=>r[filterColumn]===filterValue)
if(!rows.length) return [] const { data, errors } = parse(csv, { header: true, dynamicTyping: true, skipEmptyLines: true })
const grouped = lodash.groupBy(rows,r=>r[groupBy]) if (errors && errors.length) throw new Error('csv parse error')
const rows = _.filter(data, r => r && r[filterColumn] === filterValue)
if (!rows.length) return []
const grouped = _.groupBy(rows, r => r[groupBy])
const out = [] const out = []
for(const k in grouped){
const g = grouped[k] _.forOwn(grouped, (items, k) => {
let v let result
if(operation==='count') v = g.length if (operation === 'count') {
else{ result = items.length
const nums = g.map(r=>Number(r[aggregateColumn])).filter(n=>Number.isFinite(n)) } else {
if(!nums.length) { v = operation==='sum'?0:null } const nums = _.map(items, i => Number(i[aggregateColumn])).filter(v => Number.isFinite(v))
else{ if (!nums.length) return
const s = nums.reduce((a,b)=>a+b,0) const sum = _.sum(nums)
v = operation==='sum'?s:s/nums.length result = operation === 'sum' ? sum : sum / nums.length
}
}
out.push({[groupBy]:k,result:v})
} }
out.push({ [groupBy]: k, result })
})
return out return out
} }
export default processCSV; export default processCSV;

View File

@@ -1,9 +1,9 @@
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: customParseFormat }, { 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/customParseFormat.js/+esm'),
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/isBetween.js') import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/plugin/isBetween.js/+esm')
]); ]);
dayjs.extend(utc); dayjs.extend(utc);
@@ -11,63 +11,57 @@ const findAvailableSlots = async (cal1, cal2, constraints) => {
dayjs.extend(isBetween); dayjs.extend(isBetween);
const { durationMinutes, searchRange, workHours } = constraints; const { durationMinutes, searchRange, workHours } = constraints;
const duration = durationMinutes; const [whStart, whEnd] = [workHours.start.split(':'), workHours.end.split(':')];
const mergedBusy = [...cal1, ...cal2] const allBusy = [...cal1, ...cal2]
.map(({ start, end }) => ({ start: dayjs(start), end: dayjs(end) })) .map(({ start, end }) => ({ start: dayjs(start), end: dayjs(end) }))
.sort((a, b) => a.start - b.start) .sort((a, b) => a.start - b.start);
.reduce((acc, curr) => {
if (!acc.length) return [curr]; const merged = allBusy.reduce((acc, curr) => {
const last = acc[acc.length - 1]; if (!acc.length || acc[acc.length - 1].end < curr.start) {
if (curr.start <= last.end) { acc.push(curr);
last.end = dayjs.max(last.end, curr.end); } else {
return acc; acc[acc.length - 1].end = dayjs.max(acc[acc.length - 1].end, curr.end);
} }
return [...acc, curr]; return acc;
}, []); }, []);
const searchStart = dayjs(searchRange.start);
const searchEnd = dayjs(searchRange.end);
const [workStart, workEnd] = [workHours.start, workHours.end];
const slots = []; const slots = [];
let current = searchStart; let current = dayjs(searchRange.start);
const searchEnd = dayjs(searchRange.end);
while (current < searchEnd) { while (current < searchEnd) {
const dayStart = dayjs(`${current.format('YYYY-MM-DD')}T${workStart}`); const dayStart = current.hour(+whStart[0]).minute(+whStart[1]).second(0);
const dayEnd = dayjs(`${current.format('YYYY-MM-DD')}T${workEnd}`); const dayEnd = current.hour(+whEnd[0]).minute(+whEnd[1]).second(0);
const workStart_ = dayjs.max(current, dayStart); let pointer = dayStart > current ? dayStart : current;
const workEnd_ = dayjs.min(searchEnd, dayEnd);
if (workStart_ < workEnd_) { for (const busy of merged) {
const dayBusy = mergedBusy.filter(b => if (busy.start >= dayEnd) break;
b.start < workEnd_ && b.end > workStart_ if (busy.end <= pointer) continue;
);
let pointer = workStart_; while (pointer.add(durationMinutes, 'minute') <= dayjs.min(busy.start, dayEnd)) {
const slotEnd = pointer.add(durationMinutes, 'minute');
for (const busy of dayBusy) { if (pointer >= dayStart && slotEnd <= dayEnd && pointer >= dayjs(searchRange.start)) {
const gapEnd = dayjs.min(busy.start, workEnd_);
while (pointer.add(duration, 'minute') <= gapEnd) {
slots.push({ slots.push({
start: pointer.toISOString(), start: pointer.toISOString(),
end: pointer.add(duration, 'minute').toISOString() end: slotEnd.toISOString()
}); });
pointer = pointer.add(duration, 'minute');
} }
pointer = pointer.add(durationMinutes, 'minute');
}
pointer = dayjs.max(pointer, busy.end); pointer = dayjs.max(pointer, busy.end);
} }
while (pointer.add(duration, 'minute') <= workEnd_) { while (pointer.add(durationMinutes, 'minute') <= dayEnd) {
const slotEnd = pointer.add(durationMinutes, 'minute');
if (pointer >= dayStart && slotEnd <= searchEnd) {
slots.push({ slots.push({
start: pointer.toISOString(), start: pointer.toISOString(),
end: pointer.add(duration, 'minute').toISOString() end: slotEnd.toISOString()
}); });
pointer = pointer.add(duration, 'minute');
} }
pointer = pointer.add(durationMinutes, 'minute');
} }
current = current.add(1, 'day').startOf('day'); current = current.add(1, 'day').startOf('day');

View File

@@ -1,91 +1,83 @@
const findAvailableSlots = async (cal1, cal2, constraints) => { const findAvailableSlots = async (cal1, cal2, constraints) => {
const { default: dayjs } = await import('https://cdn.skypack.dev/dayjs@1.11.10'); const { DateTime, Interval } = await import('https://cdn.skypack.dev/luxon');
const { default: utc } = await import('https://cdn.skypack.dev/dayjs@1.11.10/plugin/utc');
const { default: customParseFormat } = await import('https://cdn.skypack.dev/dayjs@1.11.10/plugin/customParseFormat');
dayjs.extend(utc); const { durationMinutes: dur, searchRange: range, workHours: wh } = constraints;
dayjs.extend(customParseFormat); const [rangeStart, rangeEnd] = [DateTime.fromISO(range.start), DateTime.fromISO(range.end)];
const [whStart, whEnd] = wh.start.split(':').map(Number);
const [whStartMin, whEndMin] = [whStart * 60 + (wh.start.split(':')[1] || 0),
whEnd.split(':').map(Number).reduce((h, m) => h * 60 + m)];
const { durationMinutes, searchRange, workHours } = constraints; const mergeIntervals = (intervals) => {
const duration = durationMinutes * 60000; if (!intervals.length) return [];
const sorted = intervals.sort((a, b) => a.start - b.start);
const merged = [...cal1, ...cal2] const merged = [sorted[0]];
.map(({ start, end }) => ({ start: new Date(start).getTime(), end: new Date(end).getTime() })) for (let i = 1; i < sorted.length; i++) {
.sort((a, b) => a.start - b.start); const last = merged[merged.length - 1];
if (sorted[i].start <= last.end) {
const busy = merged.reduce((acc, slot) => { last.end = last.end > sorted[i].end ? last.end : sorted[i].end;
if (!acc.length) return [slot]; } else {
const last = acc[acc.length - 1]; merged.push(sorted[i]);
if (slot.start <= last.end) {
last.end = Math.max(last.end, slot.end);
return acc;
} }
acc.push(slot); }
return acc; return merged;
}, []);
const rangeStart = new Date(searchRange.start).getTime();
const rangeEnd = new Date(searchRange.end).getTime();
const [whStart, whEnd] = [workHours.start, workHours.end];
const isWithinWorkHours = (ts) => {
const d = dayjs(ts);
const time = d.format('HH:mm');
return time >= whStart && time <= whEnd;
}; };
const getWorkDayBounds = (ts) => { const toBusy = (cal) => cal.map(({ start, end }) => ({
const d = dayjs(ts); start: DateTime.fromISO(start),
const [h1, m1] = whStart.split(':').map(Number); end: DateTime.fromISO(end)
const [h2, m2] = whEnd.split(':').map(Number); }));
return {
start: d.hour(h1).minute(m1).second(0).millisecond(0).valueOf(), const allBusy = mergeIntervals([...toBusy(cal1), ...toBusy(cal2)]);
end: d.hour(h2).minute(m2).second(0).millisecond(0).valueOf()
}; const isWorkHour = (dt) => {
const min = dt.hour * 60 + dt.minute;
return min >= whStartMin && min < whEndMin;
}; };
const slots = []; const slots = [];
let current = rangeStart; let cursor = rangeStart;
for (let ts = rangeStart; ts < rangeEnd; ts += 86400000) { while (cursor < rangeEnd) {
const { start: dayStart, end: dayEnd } = getWorkDayBounds(ts); if (!isWorkHour(cursor)) {
current = Math.max(current, dayStart); cursor = cursor.plus({ minutes: 1 });
continue;
}
busy.forEach(({ start: bStart, end: bEnd }) => { const slotEnd = cursor.plus({ minutes: dur });
if (bStart > current && bStart <= dayEnd) {
let slotStart = current; if (slotEnd > rangeEnd) break;
while (slotStart + duration <= Math.min(bStart, dayEnd)) {
const slotEnd = slotStart + duration; let validSlot = true;
if (isWithinWorkHours(slotStart) && isWithinWorkHours(slotEnd)) { let tempCursor = cursor;
while (tempCursor < slotEnd) {
if (!isWorkHour(tempCursor)) {
validSlot = false;
break;
}
tempCursor = tempCursor.plus({ minutes: 1 });
}
if (!validSlot) {
cursor = cursor.plus({ minutes: 1 });
continue;
}
const overlaps = allBusy.some(busy =>
(cursor >= busy.start && cursor < busy.end) ||
(slotEnd > busy.start && slotEnd <= busy.end) ||
(cursor <= busy.start && slotEnd >= busy.end)
);
if (!overlaps) {
slots.push({ slots.push({
start: new Date(slotStart).toISOString(), start: cursor.toISO(),
end: new Date(slotEnd).toISOString() end: slotEnd.toISO()
}); });
cursor = slotEnd;
} else {
const nextBusy = allBusy.find(b => b.end > cursor && b.start <= cursor);
cursor = nextBusy ? nextBusy.end : cursor.plus({ minutes: 1 });
} }
slotStart = slotEnd;
}
current = Math.max(current, bEnd);
}
if (bEnd > current && bEnd <= dayEnd) {
current = Math.max(current, bEnd);
}
});
if (current < dayEnd) {
let slotStart = current;
while (slotStart + duration <= dayEnd) {
const slotEnd = slotStart + duration;
if (isWithinWorkHours(slotStart) && isWithinWorkHours(slotEnd)) {
slots.push({
start: new Date(slotStart).toISOString(),
end: new Date(slotEnd).toISOString()
});
}
slotStart = slotEnd;
}
}
current = dayStart + 86400000;
} }
return slots; return slots;

View File

@@ -1,74 +1,78 @@
const findAvailableSlots = async (cal1, cal2, constraints) => { async function findAvailableSlots(cal1, cal2, constraints) {
const { DateTime, Interval } = await import('https://cdn.skypack.dev/luxon'); const { default: dayjs } = await import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/+esm');
const [{ default: utc }, { default: isBetween }, { default: customParseFormat }] = await Promise.all([
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/esm/plugin/utc/+esm'),
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/esm/plugin/isBetween/+esm'),
import('https://cdn.jsdelivr.net/npm/dayjs@1.11.10/esm/plugin/customParseFormat/+esm')
]);
const { durationMinutes: dur, searchRange: sr, workHours: wh } = constraints; dayjs.extend(utc);
const [srStart, srEnd] = [DateTime.fromISO(sr.start), DateTime.fromISO(sr.end)]; dayjs.extend(isBetween);
const [whStart, whEnd] = wh.start.split(':').map(Number); dayjs.extend(customParseFormat);
const toInterval = ({ start, end }) => const { durationMinutes, searchRange, workHours } = constraints;
Interval.fromDateTimes(DateTime.fromISO(start), DateTime.fromISO(end)); const allBusy = [...cal1, ...cal2]
.map(s => ({ start: dayjs(s.start), end: dayjs(s.end) }))
.sort((a, b) => a.start.valueOf() - b.start.valueOf());
const busySlots = [...cal1, ...cal2] const merged = allBusy.reduce((acc, curr) => {
.map(toInterval) if (!acc.length || acc[acc.length - 1].end.isBefore(curr.start)) {
.sort((a, b) => a.start - b.start); acc.push(curr);
} else {
const merged = busySlots.reduce((acc, curr) => { acc[acc.length - 1].end = dayjs.max(acc[acc.length - 1].end, curr.end);
if (!acc.length) return [curr]; }
const last = acc[acc.length - 1]; return acc;
return last.overlaps(curr) || last.abutsStart(curr)
? [...acc.slice(0, -1), last.union(curr)]
: [...acc, curr];
}, []); }, []);
const isWorkHours = dt => { const slots = [];
const h = dt.hour, m = dt.minute; const rangeStart = dayjs(searchRange.start);
const mins = h * 60 + m; const rangeEnd = dayjs(searchRange.end);
const whStartMins = whStart[0] * 60 + whStart[1]; const [workStart, workEnd] = [workHours.start, workHours.end];
const whEndMins = whEnd[0] * 60 + whEnd[1];
return mins >= whStartMins && mins < whEndMins; const getWorkBounds = (date) => {
const [sh, sm] = workStart.split(':').map(Number);
const [eh, em] = workEnd.split(':').map(Number);
return {
start: date.hour(sh).minute(sm).second(0).millisecond(0),
end: date.hour(eh).minute(em).second(0).millisecond(0)
};
}; };
const slots = []; let currentDay = rangeStart.startOf('day');
let curr = srStart; while (currentDay.isBefore(rangeEnd) || currentDay.isSame(rangeEnd, 'day')) {
const { start: dayWorkStart, end: dayWorkEnd } = getWorkBounds(currentDay);
const dayStart = dayjs.max(dayWorkStart, rangeStart);
const dayEnd = dayjs.min(dayWorkEnd, rangeEnd);
while (curr < srEnd) { if (dayStart.isBefore(dayEnd)) {
const dayStart = curr.set({ hour: whStart[0], minute: whStart[1], second: 0 }); let cursor = dayStart;
const dayEnd = curr.set({ hour: whEnd[0], minute: whEnd[1], second: 0 });
if (dayStart >= srEnd) break; for (const busy of merged) {
if (busy.end.isBefore(dayStart) || busy.start.isAfter(dayEnd)) continue;
const effectiveStart = dayStart > srStart ? dayStart : srStart; const gapEnd = dayjs.min(busy.start, dayEnd);
const effectiveEnd = dayEnd < srEnd ? dayEnd : srEnd; while (cursor.add(durationMinutes, 'minute').isSameOrBefore(gapEnd)) {
let pointer = effectiveStart;
while (pointer.plus({ minutes: dur }) <= effectiveEnd) {
const slotEnd = pointer.plus({ minutes: dur });
const slotInterval = Interval.fromDateTimes(pointer, slotEnd);
const isFree = !merged.some(busy =>
busy.overlaps(slotInterval) || slotInterval.overlaps(busy)
);
if (isFree && isWorkHours(pointer) && isWorkHours(slotEnd.minus({ minutes: 1 }))) {
slots.push({ slots.push({
start: pointer.toISO(), start: cursor.toISOString(),
end: slotEnd.toISO() end: cursor.add(durationMinutes, 'minute').toISOString()
}); });
pointer = slotEnd; cursor = cursor.add(durationMinutes, 'minute');
} else {
const nextBusy = merged.find(busy => busy.start > pointer);
if (nextBusy && nextBusy.start < effectiveEnd) {
pointer = nextBusy.end > pointer ? nextBusy.end : pointer.plus({ minutes: 1 });
} else {
pointer = pointer.plus({ minutes: 1 });
} }
cursor = dayjs.max(cursor, busy.end);
}
while (cursor.add(durationMinutes, 'minute').isSameOrBefore(dayEnd)) {
slots.push({
start: cursor.toISOString(),
end: cursor.add(durationMinutes, 'minute').toISOString()
});
cursor = cursor.add(durationMinutes, 'minute');
} }
} }
curr = curr.plus({ days: 1 }).startOf('day'); currentDay = currentDay.add(1, 'day');
} }
return slots; return slots;
}; }
export default findAvailableSlots; export default findAvailableSlots;

View File

@@ -1,56 +1,68 @@
async function findAvailableSlots( async function findAvailableSlots(calendar1, calendar2, constraints) {
calendar1, const [dayjsModule, durationModule] = await Promise.all([
calendar2, import('https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js'),
{ durationMinutes: dur, searchRange: sr, workHours: wh } import('https://cdn.jsdelivr.net/npm/dayjs@1/plugin/duration.js')
) { ]);
const dayjs = (await import('https://cdn.skypack.dev/dayjs')).default; const dayjs = dayjsModule.default;
const utc = (await import('https://cdn.skypack.dev/dayjs/plugin/utc')).default; dayjs.extend(durationModule.default);
dayjs.extend(utc);
const d = (s) => dayjs.utc(s); const { durationMinutes: duration, searchRange, workHours } = constraints;
const rangeStart = d(sr.start); const searchStart = dayjs(searchRange.start);
const rangeEnd = d(sr.end); const searchEnd = dayjs(searchRange.end);
const [whsH, whsM] = wh.start.split(':'); const [workStartH, workStartM] = workHours.start.split(':').map(Number);
const [wheH, wheM] = wh.end.split(':'); const [workEndH, workEndM] = workHours.end.split(':').map(Number);
const busySlots = [...calendar1, ...calendar2].map(({ start, end }) => ({ const toDayjs = ({ start, end }) => ({ start: dayjs(start), end: dayjs(end) });
start: d(start), const allBusy = [...calendar1, ...calendar2].map(toDayjs);
end: d(end),
}));
for (let day = rangeStart.clone().startOf('d'); day.isBefore(rangeEnd); day = day.add(1, 'd')) { for (let day = searchStart.clone().startOf('day'); day.isBefore(searchEnd); day = day.add(1, 'day')) {
busySlots.push({ start: day.startOf('d'), end: day.hour(whsH).minute(whsM) }); allBusy.push({
busySlots.push({ start: day.hour(wheH).minute(wheM), end: day.endOf('d') }); start: day,
end: day.hour(workStartH).minute(workStartM)
});
allBusy.push({
start: day.hour(workEndH).minute(workEndM),
end: day.endOf('day')
});
} }
const merged = busySlots const mergedBusy = allBusy
.filter(({ start, end }) => start.isBefore(end))
.sort((a, b) => a.start - b.start) .sort((a, b) => a.start - b.start)
.reduce((acc, cur) => { .reduce((acc, slot) => {
const last = acc.at(-1); const last = acc.at(-1);
if (!last || cur.start.isAfter(last.end)) { if (last && slot.start <= last.end) {
acc.push(cur); if (slot.end > last.end) last.end = slot.end;
} else if (cur.end.isAfter(last.end)) { } else {
last.end = cur.end; acc.push({ ...slot });
} }
return acc; return acc;
}, []); }, []);
const available = []; const availableSlots = [];
let cursor = rangeStart; let nextFreeStart = searchStart;
[...merged, { start: rangeEnd, end: rangeEnd }].forEach((busy) => { const findSlotsInGap = (start, end) => {
const gapEnd = busy.start; let slotStart = start;
for (let slotStart = cursor; !slotStart.add(dur, 'm').isAfter(gapEnd); slotStart = slotStart.add(dur, 'm')) { while (slotStart.add(duration, 'minute') <= end) {
const slotEnd = slotStart.add(dur, 'm'); const slotEnd = slotStart.add(duration, 'minute');
available.push({ availableSlots.push({ start: slotStart.toISOString(), end: slotEnd.toISOString() });
start: slotStart.toISOString(), slotStart = slotEnd;
end: slotEnd.toISOString(), }
}); };
mergedBusy.forEach(busySlot => {
if (busySlot.start > nextFreeStart) {
findSlotsInGap(nextFreeStart, busySlot.start);
}
if (busySlot.end > nextFreeStart) {
nextFreeStart = busySlot.end;
} }
cursor = cursor.isAfter(busy.end) ? cursor : busy.end;
}); });
return available; if (searchEnd > nextFreeStart) {
findSlotsInGap(nextFreeStart, searchEnd);
}
return availableSlots;
} }
export default findAvailableSlots; export default findAvailableSlots;

View File

@@ -1,55 +1,40 @@
let df let cache
async function findAvailableSlots(a,b,c){ const load=()=>cache||(cache=import('https://cdn.skypack.dev/date-fns@2.30.0?min'))
df??=await import('https://cdn.jsdelivr.net/npm/date-fns@3.6.0/+esm') async function findAvailableSlots(calA,calB,rules){
const {parseISO:pi,formatISO:fi,eachDayOfInterval:ed,set:st}=df const {parseISO:iso,addMinutes:plus,differenceInMinutes:gap,eachDayOfInterval:days,set:setTime,max:maxDate,min:minDate}=await load()
const sr=pi(c.searchRange.start),er=pi(c.searchRange.end),srT=+sr,erT=+er const parseCal=c=>c.map(({start,end})=>({start:iso(start),end:iso(end)}))
const [hs,ms]=c.workHours.start.split(':').map(Number) const [sh,sm]=rules.workHours.start.split(':').map(Number)
const [he,me]=c.workHours.end.split(':').map(Number) const [eh,em]=rules.workHours.end.split(':').map(Number)
const step=c.durationMinutes*6e4 const rangeStart=iso(rules.searchRange.start),rangeEnd=iso(rules.searchRange.end)
const norm=u=>{ const entries=[...parseCal(calA),...parseCal(calB)].filter(b=>b.end>rangeStart&&b.start<rangeEnd).sort((a,b)=>a.start-b.start)
const arr=u.map(v=>({s:+pi(v.start),e:+pi(v.end)})).filter(v=>v.e>srT&&v.s<erT).map(v=>({s:Math.max(v.s,srT),e:Math.min(v.e,erT)})).filter(v=>v.e>v.s).sort((x,y)=>x.s-y.s) const slots=[]
const out=[] const addFree=(s,e)=>{
arr.forEach(v=>{ let cursor=s
const w=out[out.length-1] while(gap(e,cursor)>=rules.durationMinutes){
if(!w||v.s>w.e)out.push({s:v.s,e:v.e}) const stop=plus(cursor,rules.durationMinutes)
else w.e=Math.max(w.e,v.e) slots.push({start:cursor.toISOString(),end:stop.toISOString()})
}) cursor=stop
return out
}
const free=u=>{
const busy=norm(u),days=ed({start:sr,end:er}),out=[]
let i=0
for(const day of days){
const d0=Math.max(+st(day,{hours:hs,minutes:ms,seconds:0,milliseconds:0}),srT)
const d1=Math.min(+st(day,{hours:he,minutes:me,seconds:0,milliseconds:0}),erT)
if(d0>=d1)continue
while(i<busy.length&&busy[i].e<=d0)i++
let cur=d0,j=i
while(j<busy.length&&busy[j].s<d1){
if(busy[j].s>cur)out.push({s:cur,e:Math.min(busy[j].s,d1)})
cur=Math.max(cur,busy[j].e)
if(cur>=d1)break
j++
}
if(cur<d1)out.push({s:cur,e:d1})
i=j
}
return out
}
const x=free(a),y=free(b),res=[]
let i=0,j=0
while(i<x.length&&j<y.length){
const s=Math.max(x[i].s,y[j].s)
const e=Math.min(x[i].e,y[j].e)
if(e-s>=step){
const k=Math.floor((e-s)/step)
for(let n=0;n<k;n++){
const start=s+n*step,end=start+step
res.push({start:fi(new Date(start)),end:fi(new Date(end))})
} }
} }
x[i].e<y[j].e?i++:x[i].e>y[j].e?j++:(i++,j++) for(const day of days({start:rangeStart,end:rangeEnd})){
const workStart=setTime(day,{hours:sh,minutes:sm,seconds:0,milliseconds:0})
const workEnd=setTime(day,{hours:eh,minutes:em,seconds:0,milliseconds:0})
let spanStart=maxDate([workStart,rangeStart]),spanEnd=minDate([workEnd,rangeEnd])
if(spanStart>=spanEnd)continue
const busy=entries.filter(b=>b.end>spanStart&&b.start<spanEnd).map(b=>({start:maxDate([b.start,spanStart]),end:minDate([b.end,spanEnd])})).sort((a,b)=>a.start-b.start)
const merged=[]
for(const block of busy){
const last=merged[merged.length-1]
if(!last||block.start>last.end)merged.push({start:block.start,end:block.end})
else if(block.end>last.end)last.end=block.end
} }
return res let cursor=spanStart
for(const block of merged){
if(block.start>cursor)addFree(cursor,block.start)
if(block.end>cursor)cursor=block.end
}
if(cursor<spanEnd)addFree(cursor,spanEnd)
}
return slots
} }
export default findAvailableSlots; export default findAvailableSlots;

View File

@@ -1,115 +1,75 @@
async function findAvailableSlots(calA, calB, c) { async function findAvailableSlots(cal1, cal2, c) {
const { DateTime, Interval } = await import('https://cdn.skypack.dev/luxon').then(m => m); const { DateTime, Interval } = await import('https://cdn.skypack.dev/luxon')
const d = c.durationMinutes; const d = c.durationMinutes
const z = DateTime.utc().zoneName; const sR = c.searchRange
const parseIso = s => DateTime.fromISO(s, { zone: 'utc' }); const wH = c.workHours
const parseHm = (hm, base) => { const z = 'UTC'
const [H, M] = hm.split(':').map(Number); const p = (x, y) => DateTime.fromISO(x, { zone: y })
return base.set({ hour: H, minute: M, second: 0, millisecond: 0 }); const b = c => c.map(v => Interval.fromDateTimes(p(v.start, z), p(v.end, z)))
}; const m = (a, b) => [...a, ...b].sort((x, y) => x.start - y.start)
const toIso = dt => dt.toUTC().toISO(); const n = i => {
const r = []
let c = i[0]
for (let k = 1; k < i.length; k++) {
const v = i[k]
if (c.overlaps(v) || c.abutsStart(v)) c = Interval.fromDateTimes(c.start, c.end > v.end ? c.end : v.end)
else r.push(c), (c = v)
}
r.push(c)
return r
}
const iA = b(cal1)
const iB = b(cal2)
const all = m(iA, iB)
const busy = all.length ? n(all) : []
const srI = Interval.fromDateTimes(p(sR.start, z), p(sR.end, z))
const whS = wH.start.split(':').map(Number)
const whE = wH.end.split(':').map(Number)
const rStart = parseIso(c.searchRange.start); const dayI = []
const rEnd = parseIso(c.searchRange.end); let cur = srI.start.startOf('day')
const endDay = srI.end.startOf('day')
const dayRange = []; while (cur <= endDay) {
for (let d0 = rStart.startOf('day'); d0 < rEnd; d0 = d0.plus({ days: 1 })) { const ws = cur.set({ hour: whS[0], minute: whS[1] })
const ws = parseHm(c.workHours.start, d0); const we = cur.set({ hour: whE[0], minute: whE[1] })
const we = parseHm(c.workHours.end, d0); const w = Interval.fromDateTimes(ws, we)
const s = ws < rStart ? rStart : ws; const clipped = srI.intersection(w)
const e = we > rEnd ? rEnd : we; if (clipped && clipped.isValid && clipped.length('minutes') >= d) dayI.push(clipped)
if (e > s) dayRange.push(Interval.fromDateTimes(s, e)); cur = cur.plus({ days: 1 })
} }
const normalizeBusy = cal => const inv = []
cal let pS = null
.map(({ start, end }) => { for (const x of busy) {
const s = parseIso(start), e = parseIso(end); const xs = x.start < srI.start ? srI.start : x.start
return e > s ? Interval.fromDateTimes(s, e) : null; const xe = x.end > srI.end ? srI.end : x.end
}) if (!pS) {
.filter(Boolean) if (xs > srI.start) inv.push(Interval.fromDateTimes(srI.start, xs))
.sort((a, b) => a.start - b.start); pS = xe
} else {
const mergeBusy = busy => { if (xs > pS) inv.push(Interval.fromDateTimes(pS, xs))
const m = []; if (xe > pS) pS = xe
for (const i of busy) {
const last = m[m.length - 1];
if (!last || i.start > last.end) m.push(i);
else if (i.end > last.end) last.end = i.end;
}
return m;
};
const intersectWithRange = (busy, ranges) => {
const res = [];
let j = 0;
for (const r of ranges) {
while (j < busy.length && busy[j].end <= r.start) j++;
let k = j;
while (k < busy.length && busy[k].start < r.end) {
const s = busy[k].start < r.start ? r.start : busy[k].start;
const e = busy[k].end > r.end ? r.end : busy[k].end;
if (e > s) res.push(Interval.fromDateTimes(s, e));
k++;
} }
} }
return mergeBusy(res); if (!busy.length) inv.push(srI)
}; else if (pS < srI.end) inv.push(Interval.fromDateTimes(pS, srI.end))
const invertBusy = (busy, ranges) => { const freeWithinRange = inv
const free = []; .map(v => dayI.map(dv => dv.intersection(v)).filter(x => x && x.isValid))
for (const r of ranges) { .flat()
let cur = r.start; .filter(x => x.length('minutes') >= d)
for (const b of busy) {
if (b.end <= r.start || b.start >= r.end) continue;
if (b.start > cur) free.push(Interval.fromDateTimes(cur, b.start));
if (b.end > cur) cur = b.end;
if (cur >= r.end) break;
}
if (cur < r.end) free.push(Interval.fromDateTimes(cur, r.end));
}
return free;
};
const intersectFree = (fa, fb) => { const res = []
const res = []; for (const f of freeWithinRange) {
let i = 0, j = 0; let st = f.start
while (i < fa.length && j < fb.length) { const mins = f.length('minutes')
const a = fa[i], b = fb[j]; const steps = Math.floor(mins / d)
const s = a.start > b.start ? a.start : b.start; for (let i = 0; i < steps; i++) {
const e = a.end < b.end ? a.end : b.end; const en = st.plus({ minutes: d })
if (e > s) res.push(Interval.fromDateTimes(s, e)); res.push({ start: st.toISO(), end: en.toISO() })
if (a.end < b.end) i++; st = en
else j++;
}
return res;
};
const filterByDuration = (slots, minutes) => {
const out = [];
const dur = { minutes };
for (const s of slots) {
let cur = s.start;
while (cur.plus(dur) <= s.end) {
const end = cur.plus(dur);
out.push({ start: toIso(cur), end: toIso(end) });
cur = end;
} }
} }
return out; return res
};
const busyA = mergeBusy(normalizeBusy(calA));
const busyB = mergeBusy(normalizeBusy(calB));
const busyAIn = intersectWithRange(busyA, dayRange);
const busyBIn = intersectWithRange(busyB, dayRange);
const freeA = invertBusy(busyAIn, dayRange);
const freeB = invertBusy(busyBIn, dayRange);
const mutualFree = intersectFree(freeA, freeB);
return filterByDuration(mutualFree, d);
} }
export default findAvailableSlots; export default findAvailableSlots;

View File

@@ -1,18 +1,14 @@
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://esm.sh/ajv@8.12.0');
const ajv = new Ajv({ allErrors: true });
const ajv = new Ajv({ allErrors: true, strict: false });
const validate = ajv.compile(schema); const validate = ajv.compile(schema);
const valid = validate(data); const valid = validate(data);
return { return {
valid, valid,
errors: valid ? [] : validate.errors.map(e => ({ errors: valid ? [] : validate.errors.map(e =>
path: e.instancePath || '/', `${e.instancePath || '/'} ${e.message}`.trim()
message: e.message, )
keyword: e.keyword,
params: e.params
}))
}; };
}; };
export default validateJSON; export default validateJSON;

View File

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

View File

@@ -1,14 +1,13 @@
const validateJSON = async (data, schema) => { const validateJSON = async (data, schema) => {
const { default: Ajv } = await import('https://cdn.jsdelivr.net/npm/ajv@8/dist/ajv.bundle.js'); const { default: Ajv } = await import('https://esm.sh/ajv@8.12.0');
const ajv = new Ajv({ allErrors: true }); const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema); const validate = ajv.compile(schema);
const valid = validate(data); const valid = validate(data);
return { return {
valid, valid,
errors: valid ? [] : validate.errors.map(e => errors: valid ? [] : validate.errors.map(err =>
`${e.instancePath || 'root'} ${e.message}` `${err.instancePath || '/'} ${err.message}`.trim()
) )
}; };
}; };

View File

@@ -1,32 +1,25 @@
let ajvPromise; const validateJSON = (() => {
const compiledSchemas = new WeakMap(); let ajvPromise;
const validatorCache = new WeakMap();
return async (json, schema) => {
ajvPromise ||= import('https://cdn.jsdelivr.net/npm/ajv@8/dist/ajv2020.min.js')
.then(({ default: Ajv }) => new Ajv({ allErrors: true }));
const validateJSON = async (json, schema) => {
try {
ajvPromise ||= import('https://esm.sh/ajv@8').then(
({ default: Ajv }) => new Ajv({ allErrors: true })
);
const ajv = await ajvPromise; const ajv = await ajvPromise;
let validate = compiledSchemas.get(schema); let validate = validatorCache.get(schema);
if (!validate) { if (!validate) {
validate = ajv.compile(schema); validate = ajv.compile(schema);
compiledSchemas.set(schema, validate); validatorCache.set(schema, validate);
} }
const valid = validate(json); const valid = validate(json);
const errors = valid const errors = valid ? [] : validate.errors.map(
? [] ({ instancePath, message }) => `${instancePath || 'object'} ${message}`
: (validate.errors ?? []).map(({ instancePath, message }) =>
`${instancePath.substring(1) || 'root'}: ${message}`
); );
return { valid, errors }; return { valid, errors };
} catch (error) {
return {
valid: false,
errors: [error.message || 'An unknown validation error occurred.'],
}; };
} })();
};
export default validateJSON; export default validateJSON;

View File

@@ -1,17 +1,25 @@
let ajvReady let ajv
const loadAjv=()=>ajvReady??=(async()=>{ const cache=new WeakMap()
const {default:Ajv}=await import('https://cdn.jsdelivr.net/npm/ajv@8/dist/ajv2020.mjs') const ensureAjv=async()=>ajv??=new (await import('https://cdn.jsdelivr.net/npm/ajv@8.12.0/+esm')).default({allErrors:true,strict:false})
return new Ajv({allErrors:true,strict:false}) const getValidator=async schema=>{
})() if(cache.has(schema))return cache.get(schema)
const v=(await ensureAjv()).compile(schema)
cache.set(schema,v)
return v
}
async function validateJSON(data,schema){ async function validateJSON(data,schema){
const ajv=await loadAjv() try{
const validate=ajv.compile(schema) const v=await getValidator(schema)
const valid=validate(data) const valid=v(data)
const errors=valid?[]:validate.errors?.map(({instancePath,message,params})=>{ const errors=valid?[]:(v.errors||[]).map(e=>{
const here=instancePath||'/' const path=e.instancePath||'/'
const extra=params&&Object.keys(params).length?JSON.stringify(params):'' const msg=e.message||'Invalid value'
return [here,message,extra].filter(Boolean).join(' ') const meta=e.params&&Object.keys(e.params).length?` ${JSON.stringify(e.params)}`:''
})||[] return`${path} ${msg}${meta}`.trim()
return {valid,errors} })
return{valid,errors}
}catch(err){
return{valid:false,errors:[err?.message||String(err)]}
}
} }
export default validateJSON; export default validateJSON;

View File

@@ -1,15 +1,21 @@
async function validateJSON(data, schema) { async function validateJSON(data, schema) {
const {default: Ajv} = await import('https://cdn.jsdelivr.net/npm/ajv@8/dist/ajv.min.js') const { default: Ajv } = await import('https://cdn.skypack.dev/ajv@8?min');
const ajv = new Ajv({allErrors:true, strict:false}) const { default: addFormats } = await import('https://cdn.skypack.dev/ajv-formats@2?min');
const validate = ajv.compile(schema) const ajv = new Ajv({ allErrors: true, strict: false });
const valid = validate(data) addFormats(ajv);
if (valid) return {valid:true, errors:[]} const compile = s => {
const errors = (validate.errors || []).map(e => { const c = ajv.compile(s);
const path = (e.instancePath || e.dataPath || '') || (e.schemaPath || '') return d => ({ ok: c(d), errs: c.errors || [] });
const msg = e.message || 'Invalid value' };
const params = e.params ? JSON.stringify(e.params) : '' const run = compile(schema);
return [path, msg, params].filter(Boolean).join(' - ') const r = run(data);
}) if (r.ok) return { valid: true, errors: [] };
return {valid:false, errors} const errors = r.errs.map(e => {
const p = e.instancePath || e.dataPath || '';
const loc = p || e.schemaPath || '';
const msg = e.message || 'Invalid value';
return loc ? loc + ' ' + msg : msg;
});
return { valid: false, errors };
} }
export default validateJSON; export default validateJSON;