UNPKG

@randsum/notation

Version:

Dice notation parser and types for the @randsum ecosystem

4 lines (2 loc) 17.6 kB
function c(e){return e}var f=/[+-]?\d+[Dd][1-9]\d*/;function oe(e,t,r,n){if(n===0)return 0;if(t.index===void 0)return 0;let u=r?Number(r.index)+r[0].length:0,p=e.slice(u,t.index),h=/([+-])/.exec(p);if(h?.[1])return u+p.indexOf(h[1]);return t.index}function _(e,t){return t.map((r,n)=>{if(r.index===void 0)return null;let u=t[n-1],p=t[n+1],h=oe(e,r,u,n),a=p?.index??e.length;return e.slice(h,a).trim()}).filter((r)=>r!==null&&r.length>0)}function N(e){let r=e.replace(/[{}]/g,"").split(",").map((p)=>p.trim()).filter(Boolean),n={},u=[];for(let p of r)if(p.startsWith(">="))n.greaterThanOrEqual=Number(p.slice(2));else if(p.startsWith("<="))n.lessThanOrEqual=Number(p.slice(2));else if(p.startsWith(">"))n.greaterThan=Number(p.slice(1));else if(p.startsWith("<"))n.lessThan=Number(p.slice(1));else if(p.startsWith("="))u.push(Number(p.slice(1)));else{let h=Number(p);if(!isNaN(h))u.push(h)}if(u.length>0)n.exact=u;return n}function K(e){return e.greaterThan!==void 0||e.greaterThanOrEqual!==void 0||e.lessThan!==void 0||e.lessThanOrEqual!==void 0||e.exact!==void 0&&e.exact.length>0}function m(e){if(!e.length)return"";if(e.length===1)return`${e[0]}`;let t=e.map((n)=>`${n}`),r=t.pop();return`${t.join(" ")} and ${r}`}function L({greaterThan:e,greaterThanOrEqual:t,lessThan:r,lessThanOrEqual:n,exact:u}){let p=[];if(u?.length)p.push(m(u));if(t!==void 0)p.push(`greater than or equal to ${t}`);if(e!==void 0)p.push(`greater than ${e}`);if(n!==void 0)p.push(`less than or equal to ${n}`);if(r!==void 0)p.push(`less than ${r}`);return p}function $({greaterThan:e,greaterThanOrEqual:t,lessThan:r,lessThanOrEqual:n,exact:u}){let p=[];if(u?.length)p.push(...u.map(String));if(t!==void 0)p.push(`>=${t}`);if(e!==void 0)p.push(`>${e}`);if(n!==void 0)p.push(`<=${n}`);if(r!==void 0)p.push(`<${r}`);return p}var Q=/[Cc]\{((?:>=|<=|>|<|=)?\d+(?:,(?:>=|<=|>|<|=)?\d+)*)\}/,y=c({name:"cap",priority:10,pattern:Q,parse:(e)=>{let t=Q.exec(e);if(!t?.[1])return{};let r=N(t[1]),n={};if(r.greaterThan!==void 0)n.greaterThan=r.greaterThan;if(r.greaterThanOrEqual!==void 0)n.greaterThanOrEqual=r.greaterThanOrEqual;if(r.lessThan!==void 0)n.lessThan=r.lessThan;if(r.lessThanOrEqual!==void 0)n.lessThanOrEqual=r.lessThanOrEqual;if(r.exact?.length)n.exact=r.exact;return Object.keys(n).length>0?{cap:n}:{}},toNotation:(e)=>{let t=$(e);return t.length?`C{${t.join(",")}}`:void 0},toDescription:(e)=>{let{exact:t,...r}=e,n=[];if(t?.length)n.push(...t.map((p)=>`No Rolls Greater Than ${p}`));let u=L(r);return n.push(...u.map((p)=>`No Rolls ${p}`)),n}});var ce=/[Hh](\d+)?/g,le=/(?<![Kk])[Ll](\d+)?/g,fe=/[Dd]\{((?:>=|<=|>|<|=)?\d+(?:,(?:>=|<=|>|<|=)?\d+)*)\}/,O=c({name:"drop",priority:20,pattern:/([Hh](\d+)?|(?<![Kk])[Ll](\d+)?|[Dd]\{((?:>=|<=|>|<|=)?\d+(?:,(?:>=|<=|>|<|=)?\d+)*)\})/,parse:(e)=>{let t={},r=Array.from(e.matchAll(ce));if(r.length>0)t.highest=r.reduce((p,h)=>{return p+(h[1]?Number(h[1]):1)},0);let n=Array.from(e.matchAll(le));if(n.length>0)t.lowest=n.reduce((p,h)=>{return p+(h[1]?Number(h[1]):1)},0);let u=fe.exec(e);if(u?.[1]){let p=N(u[1]);if(p.greaterThan!==void 0)t.greaterThan=p.greaterThan;if(p.greaterThanOrEqual!==void 0)t.greaterThanOrEqual=p.greaterThanOrEqual;if(p.lessThan!==void 0)t.lessThan=p.lessThan;if(p.lessThanOrEqual!==void 0)t.lessThanOrEqual=p.lessThanOrEqual;if(p.exact)t.exact=p.exact}return K(t)||t.highest!==void 0||t.lowest!==void 0?{drop:t}:{}},toNotation:(e)=>{let{highest:t,lowest:r,greaterThan:n,greaterThanOrEqual:u,lessThan:p,lessThanOrEqual:h,exact:a}=e,o=[];if(t)o.push(t===1?"H":`H${t}`);if(r)o.push(r===1?"L":`L${r}`);let l=[];if(u!==void 0)l.push(`>=${u}`);if(n!==void 0)l.push(`>${n}`);if(h!==void 0)l.push(`<=${h}`);if(p!==void 0)l.push(`<${p}`);if(a)a.forEach((s)=>l.push(`${s}`));if(l.length>0)o.push(`D{${l.join(",")}}`);return o.length?o.join(""):void 0},toDescription:(e)=>{let{highest:t,lowest:r,greaterThan:n,greaterThanOrEqual:u,lessThan:p,lessThanOrEqual:h,exact:a}=e,o=[];if(t)o.push(t>1?`Drop highest ${t}`:"Drop highest");if(r)o.push(r>1?`Drop lowest ${r}`:"Drop lowest");if(a)o.push(`Drop ${m(a)}`);if(u!==void 0)o.push(`Drop greater than or equal to ${u}`);if(n!==void 0)o.push(`Drop greater than ${n}`);if(h!==void 0)o.push(`Drop less than or equal to ${h}`);if(p!==void 0)o.push(`Drop less than ${p}`);return o}});var me=/[Kk](?![Ll])(\d+)?/,Ne=/[Kk][Ll](\d+)?/i,b=c({name:"keep",priority:21,pattern:/[Kk]([Ll])?(\d+)?/,parse:(e)=>{let t={},r=Ne.exec(e);if(r)return t.lowest=r[1]?Number(r[1]):1,{keep:t};let n=me.exec(e);if(n)return t.highest=n[1]?Number(n[1]):1,{keep:t};return{}},toNotation:(e)=>{let{highest:t,lowest:r}=e,n=[];if(t)n.push(t===1?"K":`K${t}`);if(r)n.push(r===1?"kl":`kl${r}`);return n.length?n.join(""):void 0},toDescription:(e)=>{let{highest:t,lowest:r}=e,n=[];if(t)n.push(t>1?`Keep highest ${t}`:"Keep highest");if(r)n.push(r>1?`Keep lowest ${r}`:"Keep lowest");return n}});var F=/[Vv]\{((?:>=|<=|>|<)?\d+=\d+(?:,(?:>=|<=|>|<)?\d+=\d+)*)\}/,x=c({name:"replace",priority:30,pattern:F,parse:(e)=>{let t=F.exec(e);if(!t)return{};let r=t[1];if(!r)return{};return{replace:r.split(",").map((p)=>p.trim()).map((p)=>{let h=/^((?:>=|<=|>|<)?\d+)=(\d+)$/.exec(p);if(!h?.[1]||!h[2])return{from:0,to:0};let[,a,o]=h;return{from:a.startsWith(">=")?{greaterThanOrEqual:Number(a.slice(2))}:a.startsWith("<=")?{lessThanOrEqual:Number(a.slice(2))}:a.startsWith(">")?{greaterThan:Number(a.slice(1))}:a.startsWith("<")?{lessThan:Number(a.slice(1))}:Number(a),to:Number(o)}})}},toNotation:(e)=>{let r=(Array.isArray(e)?e:[e]).map(({from:n,to:u})=>{if(typeof n==="object")return $(n).map((h)=>`${h}=${u}`).join(",");return`${n}=${u}`});return r.length?`V{${r.join(",")}}`:void 0},toDescription:(e)=>{return(Array.isArray(e)?e:[e]).map(({from:r,to:n})=>{if(typeof r==="object")return`Replace ${L(r).join(" and ")} with ${n}`;return`Replace ${r} with ${n}`})}});var $e=/[Rr]\{((?:>=|<=|>|<|=)?\d+(?:,(?:>=|<=|>|<|=)?\d+)*)\}(\d+)?/g,g=c({name:"reroll",priority:40,pattern:/[Rr]\{((?:>=|<=|>|<|=)?\d+(?:,(?:>=|<=|>|<|=)?\d+)*)\}(\d+)?/,parse:(e)=>{let t=Array.from(e.matchAll($e));if(t.length===0)return{};let r={};for(let n of t){let u=n[1],p=n[2]?Number(n[2]):void 0;if(p)r.max=p;if(u){let h=N(u);if(h.greaterThan!==void 0)r.greaterThan=h.greaterThan;if(h.greaterThanOrEqual!==void 0)r.greaterThanOrEqual=h.greaterThanOrEqual;if(h.lessThan!==void 0)r.lessThan=h.lessThan;if(h.lessThanOrEqual!==void 0)r.lessThanOrEqual=h.lessThanOrEqual;if(h.exact)r.exact=[...r.exact??[],...h.exact]}}return K(r)||r.max!==void 0?{reroll:r}:{}},toNotation:(e)=>{let t=$(e);if(!t.length)return;let r=e.max?`${e.max}`:"";return`R{${t.join(",")}}${r}`},toDescription:(e)=>{let{exact:t,greaterThan:r,greaterThanOrEqual:n,lessThan:u,lessThanOrEqual:p,max:h}=e,a=[];if(t)t.forEach((Z)=>a.push(`${Z}`));let o=[];if(n!==void 0)o.push(`greater than or equal to ${n}`);if(r!==void 0)o.push(`greater than ${r}`);if(p!==void 0)o.push(`less than or equal to ${p}`);if(u!==void 0)o.push(`less than ${u}`);let l=m(a.map(Number)),s=o.join(" and "),X=[l,s].filter(Boolean).join(", ");if(!X)return[];let Y=h!==void 0?` (up to ${h} times)`:"";return[`Reroll ${X}${Y}`]}});var se=/(?<!!)!(?!!)/,T=c({name:"explode",priority:50,pattern:/(?<!!)!(?!!|p)/,parse:(e)=>{if(se.test(e))return{explode:!0};return{}},toNotation:(e)=>{return e?"!":void 0},toDescription:(e)=>{if(!e)return[];return["Exploding Dice"]}});var v=/!!(\d+)?/,q=c({name:"compound",priority:51,pattern:v,parse:(e)=>{let t=v.exec(e);if(!t)return{};let r=t[1];if(r===void 0)return{compound:!0};return{compound:Number(r)}},toNotation:(e)=>{if(e===!0)return"!!";if(typeof e==="number")return`!!${e}`;return},toDescription:(e)=>{if(e===!0)return["Compounding Dice"];if(e===0)return["Compounding Dice (unlimited)"];if(typeof e==="number")return[`Compounding Dice (max ${e} times)`];return[]}});var k=/!p(\d+)?/i,C=c({name:"penetrate",priority:52,pattern:k,parse:(e)=>{let t=k.exec(e);if(!t)return{};let r=t[1];if(r===void 0)return{penetrate:!0};return{penetrate:Number(r)}},toNotation:(e)=>{if(e===!0)return"!p";if(typeof e==="number")return`!p${e}`;return},toDescription:(e)=>{if(e===!0)return["Penetrating Dice"];if(e===0)return["Penetrating Dice (unlimited)"];if(typeof e==="number")return[`Penetrating Dice (max ${e} times)`];return[]}});var ee=/[Uu](?:\{([^}]{1,50})\})?/,D=c({name:"unique",priority:60,pattern:ee,parse:(e)=>{let t=ee.exec(e);if(!t)return{};if(!t[1])return{unique:!0};return{unique:{notUnique:t[1].split(",").map((n)=>Number(n.trim())).filter((n)=>!isNaN(n))}}},toNotation:(e)=>{if(e===!0)return"U";if(typeof e==="object")return`U{${e.notUnique.join(",")}}`;return},toDescription:(e)=>{if(e===!0)return["No Duplicate Rolls"];if(typeof e==="object")return[`No Duplicates (except ${m(e.notUnique)})`];return[]}});var te=/[Ss]\{(\d+)(?:,(\d+))?\}/,E=c({name:"countSuccesses",priority:95,pattern:te,parse:(e)=>{let t=te.exec(e);if(!t)return{};let r=Number(t[1]),n=t[2]?Number(t[2]):void 0;if(n!==void 0)return{countSuccesses:{threshold:r,botchThreshold:n}};return{countSuccesses:{threshold:r}}},toNotation:(e)=>{if(e.botchThreshold!==void 0)return`S{${e.threshold},${e.botchThreshold}}`;return`S{${e.threshold}}`},toDescription:(e)=>{if(e.botchThreshold!==void 0)return[`Count successes >= ${e.threshold}, botches <= ${e.botchThreshold}`];return[`Count successes >= ${e.threshold}`]}});var re=/(?<!\*)\*(?!\*)(\d+)/,i=c({name:"multiply",priority:85,pattern:re,parse:(e)=>{let t=re.exec(e);if(!t)return{};return{multiply:Number(t[1])}},toNotation:(e)=>{return`*${e}`},toDescription:(e)=>{return[`Multiply dice by ${e}`]}});function H(e){let{name:t,priority:r,operator:n,verb:u}=e,p=n==="+"?"\\+":"-",h=new RegExp(`${p}(\\d+)`),a=new RegExp(`${p}(\\d+)`,"g");return c({name:t,priority:r,pattern:h,parse:(o)=>{let l=Array.from(o.matchAll(a));if(l.length===0)return{};let s=l.reduce((Y,Z)=>Y+Number(Z[1]),0);return{[t]:s}},toNotation:(o)=>{if(t==="plus"&&o<0)return`-${Math.abs(o)}`;return`${n}${o}`},toDescription:(o)=>{return[`${u} ${o}`]}})}var A=H({name:"plus",priority:90,operator:"+",verb:"Add"});var R=H({name:"minus",priority:91,operator:"-",verb:"Subtract"});var ne=/\*\*(\d+)/,S=c({name:"multiplyTotal",priority:100,pattern:ne,parse:(e)=>{let t=ne.exec(e);if(!t)return{};return{multiplyTotal:Number(t[1])}},toNotation:(e)=>{return`**${e}`},toDescription:(e)=>{return[`Multiply total by ${e}`]}});var pe=[y,O,b,x,g,T,q,C,D,E,i,A,R,S];function ue(e){let t={};for(let r of pe)if(r.pattern.test(e))r.pattern.lastIndex=0,Object.assign(t,r.parse(e));return t}function he(){let e=[...pe].sort((t,r)=>t.priority-r.priority).map((t)=>t.pattern.source);return new RegExp(e.join("|"),"g")}function z(e){let t=e.trim(),r=t.match(f)?.at(0)??"",n=t.replace(r,""),[u,p=""]=r.split(/[Dd]/),h={quantity:Math.abs(Number(u)),arithmetic:Number(u)<0?"subtract":"add",sides:Number(p)};if(n.length===0)return h;return{...h,modifiers:ue(n)}}var ye=new RegExp(f.source,"g");function M(e){let t=Array.from(e.matchAll(ye));if(t.length<=1)return[z(e)];return _(e,t).map(z)}function Oe(e){let t=/^(\d+)(\d+)/.exec(e);if(t?.[1]&&t[2]){let r=t[1],n=t[2];if(Number.parseInt(r,10)<=100&&Number.parseInt(n,10)>=2&&Number.parseInt(n,10)<=1000)return`${r}d${n}`}return}function be(e){if(/^[dD]\d+/.test(e))return e.replace(/^[dD]/,"1d");return}function xe(e){let t=e.replace(/\s+/g,"");if(t!==e&&/^\d+[dD]\d+/.test(t))return t;return}function V(e){let t=e.trim(),r=be(t);if(r)return r;let n=xe(t);if(n)return n;let u=Oe(t);if(u)return u;let h=/^(\d+)d(\d+)/i.exec(t);if(h?.[0])return h[0];if(/^\d+/.test(t)&&!t.includes("d")&&!t.includes("D")){let a=/^(\d+)(\d+)/.exec(t);if(a?.[1]&&a[2])return`${a[1]}d${a[2]}`}return}var I=null;function ge(){return I??=new RegExp([f.source,he().source].join("|"),"g"),I.lastIndex=0,I}function d(e){if(typeof e!=="string")return!1;let t=e.trim();if(!f.test(t))return!1;return t.replace(/\s/g,"").replaceAll(ge(),"").length===0}class P extends Error{code="INVALID_NOTATION";suggestion;constructor(e,t,r){let n=r?`Invalid notation "${e}": ${t}. Did you mean "${r}"?`:`Invalid notation "${e}": ${t}`;super(n);this.name="NotationParseError",this.suggestion=r}}function Te(e){if(!d(e)){let t=V(e);throw new P(e,"String does not match dice notation pattern",t)}return e}function U(e){if(!e)return"";let t=[];if(e.cap!==void 0){let r=y.toNotation(e.cap);if(r!==void 0)t.push(r)}if(e.drop!==void 0){let r=O.toNotation(e.drop);if(r!==void 0)t.push(r)}if(e.keep!==void 0){let r=b.toNotation(e.keep);if(r!==void 0)t.push(r)}if(e.replace!==void 0){let r=x.toNotation(e.replace);if(r!==void 0)t.push(r)}if(e.reroll!==void 0){let r=g.toNotation(e.reroll);if(r!==void 0)t.push(r)}if(e.explode!==void 0){let r=T.toNotation(e.explode);if(r!==void 0)t.push(r)}if(e.compound!==void 0){let r=q.toNotation(e.compound);if(r!==void 0)t.push(r)}if(e.penetrate!==void 0){let r=C.toNotation(e.penetrate);if(r!==void 0)t.push(r)}if(e.unique!==void 0){let r=D.toNotation(e.unique);if(r!==void 0)t.push(r)}if(e.multiply!==void 0){let r=i.toNotation(e.multiply);if(r!==void 0)t.push(r)}if(e.plus!==void 0){let r=A.toNotation(e.plus);if(r!==void 0)t.push(r)}if(e.minus!==void 0){let r=R.toNotation(e.minus);if(r!==void 0)t.push(r)}if(e.countSuccesses!==void 0){let r=E.toNotation(e.countSuccesses);if(r!==void 0)t.push(r)}if(e.multiplyTotal!==void 0){let r=S.toNotation(e.multiplyTotal);if(r!==void 0)t.push(r)}return t.join("")}function W(e){if(!e)return[];let t=[];if(e.cap!==void 0)t.push(...y.toDescription(e.cap));if(e.drop!==void 0)t.push(...O.toDescription(e.drop));if(e.keep!==void 0)t.push(...b.toDescription(e.keep));if(e.replace!==void 0)t.push(...x.toDescription(e.replace));if(e.reroll!==void 0)t.push(...g.toDescription(e.reroll));if(e.explode!==void 0)t.push(...T.toDescription(e.explode));if(e.compound!==void 0)t.push(...q.toDescription(e.compound));if(e.penetrate!==void 0)t.push(...C.toDescription(e.penetrate));if(e.unique!==void 0)t.push(...D.toDescription(e.unique));if(e.multiply!==void 0)t.push(...i.toDescription(e.multiply));if(e.plus!==void 0)t.push(...A.toDescription(e.plus));if(e.minus!==void 0)t.push(...R.toDescription(e.minus));if(e.countSuccesses!==void 0)t.push(...E.toDescription(e.countSuccesses));if(e.multiplyTotal!==void 0)t.push(...S.toDescription(e.multiplyTotal));return t}function j({sides:e}){if(Array.isArray(e))return{sides:e.length,faces:e};return{sides:e}}function B(e){let{modifiers:t,quantity:r=1,arithmetic:n}=e,{sides:u}=j(e),p=n==="subtract"?"-":"",h=U(t),a=`${p}${r}d${u}${h}`;if(!d(a))throw new P(a,"Generated notation is invalid");return a}function G(e){let{modifiers:t,quantity:r=1,arithmetic:n}=e,{sides:u,faces:p=[]}=j(e),a=`Roll ${r} ${u}-sided ${r===1?"die":"dice"}`,o=`Roll ${r} Dice with the following sides: ${p.join(", ")}`,l=W(t),s=n==="subtract"?"and Subtract the result":"";return[p.length?o:a,...l,s].filter(Boolean)}function J(e){if(!d(e))return{valid:!1,argument:e,error:{message:`Invalid dice notation: "${e}"`,argument:e}};let t=M(e);return{valid:!0,argument:e,options:t,notation:t.map((n)=>B(n)),description:t.map((n)=>G(n)),error:null}}function ae(e){let t=e.replace(/^[+-]/,""),r=J(t);if(r.valid)return r.description[0]?.[0]??e;let n=/^(\d+)[Dd](\d+)/.exec(t);if(!n)return e;let u=parseInt(n[1]??"1",10),p=n[2]??"?";return`Roll ${u} ${p}-sided ${u===1?"die":"dice"}`}function qe(e){let t=J(`1d6${e}`);if(!t.valid)return e;return(t.description[0]??[]).slice(1).join(", ")||e}var Ce=[{type:"multiplyTotal",pattern:/^\*\*\d+/},{type:"multiply",pattern:/^\*\d+/},{type:"compound",pattern:/^!!\d*/},{type:"penetrate",pattern:/^!p\d*/i},{type:"explode",pattern:/^!/},{type:"dropHighest",pattern:/^[Hh]\d*/},{type:"dropLowest",pattern:/^[Ll]\d*/},{type:"dropCondition",pattern:/^[Dd]\{[^}]+\}/},{type:"keepLowest",pattern:/^[Kk][Ll]\d*/},{type:"keepHighest",pattern:/^[Kk]\d*/},{type:"reroll",pattern:/^[Rr]\{[^}]+\}\d*/},{type:"cap",pattern:/^[Cc]\{[^}]+\}/},{type:"replace",pattern:/^[Vv]\{[^}]+\}/},{type:"unique",pattern:/^[Uu](?:\{[^}]+\})?/},{type:"countSuccesses",pattern:/^[Ss]\{\d+(?:,\d+)?\}/},{type:"plus",pattern:/^\+\d+/},{type:"minus",pattern:/^-\d+/}];function De(e,t,r){let n=e[e.length-1];if(n?.type==="unknown")e[e.length-1]={...n,text:n.text+t,end:r+1};else e.push({text:t,type:"unknown",start:r,end:r+1,description:""})}function w(e,t,r){if(t>=e.length)return r;let n=e.slice(t),u=/^[+-]\d+[Dd][1-9]\d*/.exec(n);if(u){let p=u[0];return r.push({text:p,type:"core",start:t,end:t+p.length,description:ae(p)}),w(e,t+p.length,r)}for(let p of Ce){let h=n.match(p.pattern);if(h){let a=h[0];return r.push({text:a,type:p.type,start:t,end:t+a.length,description:qe(a)}),w(e,t+a.length,r)}}return De(r,e[t]??"",t),w(e,t+1,r)}function Ee(e){if(e.length===0)return[];let t=[],r=/^[+-]?\d+[Dd]\d+/.exec(e);if(r){let n=r[0];return t.push({text:n,type:"core",start:0,end:n.length,description:ae(n)}),w(e,n.length,t)}return w(e,0,t)}export{J as validateNotation,D as uniqueSchema,Ee as tokenize,V as suggestNotationFix,g as rerollSchema,x as replaceSchema,A as plusSchema,C as penetrateSchema,N as parseComparisonNotation,j as optionsToSidesFaces,B as optionsToNotation,G as optionsToDescription,M as notationToOptions,Te as notation,S as multiplyTotalSchema,i as multiplySchema,U as modifiersToNotation,W as modifiersToDescription,R as minusSchema,_ as listOfNotations,b as keepSchema,d as isDiceNotation,K as hasConditions,m as formatHumanList,$ as formatComparisonNotation,L as formatComparisonDescription,T as explodeSchema,O as dropSchema,c as defineNotationSchema,E as countSuccessesSchema,f as coreNotationPattern,q as compoundSchema,y as capSchema,P as NotationParseError}; //# debugId=9F5CB065CFBB29A464756E2164756E21