Files

75 lines
3.5 KiB
JavaScript

'use strict';
const PUBLIC_MODES = new Set(['missing', 'latest', 'last', 'first', 'chapter', 'after', 'volume']);
function chapterNumber(value, field = 'chapter') {
const raw = String(value ?? '').trim();
if (!/^\d+(?:\.\d+)?$/.test(raw)) throw new Error(`${field} must be a numeric chapter`);
const [whole, decimal = ''] = raw.split('.');
const normalizedWhole = whole.replace(/^0+(?=\d)/, '') || '0';
const normalizedDecimal = decimal.replace(/0+$/, '');
return normalizedDecimal ? `${normalizedWhole}.${normalizedDecimal}` : normalizedWhole;
}
function positiveCount(value) {
const count = Number(value);
if (!Number.isInteger(count) || count < 1 || count > 10000) throw new Error('count must be an integer from 1 to 10000');
return count;
}
function normalizeScope(input = {}) {
const raw = input && typeof input === 'object' ? input : {};
const mode = String(raw.mode || 'missing').toLowerCase();
if (mode === 'ids') {
if (!Array.isArray(raw.ids) || !raw.ids.length || raw.ids.length > 10000) throw new Error('ids scope requires 1 to 10000 source chapter ids');
return { mode, ids: [...new Set(raw.ids.map(value => String(value).trim()).filter(Boolean))] };
}
if (!PUBLIC_MODES.has(mode)) throw new Error(`Unsupported queue scope: ${mode}`);
if (mode === 'last' || mode === 'first') return { mode, count: positiveCount(raw.count) };
if (mode === 'chapter' || mode === 'after') return { mode, chapter: chapterNumber(raw.chapter) };
if (mode === 'volume') {
const volume = String(raw.volume ?? '').trim();
if (!/^\d+(?:\.\d+)?$/.test(volume)) throw new Error('volume must be numeric');
return { mode, volume: chapterNumber(volume, 'volume') };
}
return { mode };
}
function volumeOf(ref) {
const value = ref.volume ?? ref.book?.number;
if (value === undefined || value === null || value === '') return null;
try { return chapterNumber(value, 'volume'); } catch { return null; }
}
function selectByScope(chapters, input) {
const scope = normalizeScope(input);
const list = [...chapters];
if (scope.mode === 'missing') return list;
if (scope.mode === 'latest') return list.length ? [list[list.length - 1]] : [];
if (scope.mode === 'last') return list.slice(-scope.count);
if (scope.mode === 'first') return list.slice(0, scope.count);
const numbered = list.filter(ref => /^\d+(?:\.\d+)?$/.test(String(ref.number)));
if (scope.mode === 'chapter') return numbered.filter(ref => chapterNumber(ref.number) === scope.chapter);
if (scope.mode === 'after') return numbered.filter(ref => Number(chapterNumber(ref.number)) > Number(scope.chapter));
if (scope.mode === 'volume') return list.filter(ref => volumeOf(ref) === scope.volume);
if (scope.mode === 'ids') {
const ids = new Set(scope.ids);
return list.filter(ref => ids.has(String(ref.id)));
}
return [];
}
function scopeLabel(input) {
const scope = normalizeScope(input);
if (scope.mode === 'missing') return 'all missing chapters';
if (scope.mode === 'latest') return 'newest chapter';
if (scope.mode === 'last') return `last ${scope.count} chapters`;
if (scope.mode === 'first') return `first ${scope.count} chapters`;
if (scope.mode === 'chapter') return `chapter ${scope.chapter}`;
if (scope.mode === 'after') return `chapters after ${scope.chapter}`;
if (scope.mode === 'volume') return `volume ${scope.volume}`;
return `${scope.ids.length} discovered chapters`;
}
module.exports = { normalizeScope, selectByScope, scopeLabel, chapterNumber, volumeOf };