specpress
Version:
Export PDF and/or DOCX files from a subset of Markdown, ASN.1 and JSON files
454 lines • 18.6 kB
JavaScript
// BC_ID.ts — Band Combination ID parsing
import { BWC_ID } from "./BWC_ID.js";
import { BandNumber, RAT } from "./BandNumber.js";
//////////////////////////////
// Exceptions
/** Thrown when a BC-ID string cannot be parsed or is structurally invalid. */
export class InvalidBcIdException extends Error {
constructor(message) { super(message); this.name = "InvalidBcIdException"; }
}
//////////////////////////////
// Enums
/**
* The type of band combination.
* - SC — single carrier (one band, one carrier)
* - CA — carrier aggregation (NR-only, multiple carriers or bands)
* - NRDC — NR dual connectivity
* - ENDC — EN-DC (E-UTRA anchor + NR secondary)
* - NEDC — NE-DC (NR anchor + E-UTRA secondary)
*/
export var BcType;
(function (BcType) {
BcType["SC"] = "SC";
BcType["CA"] = "CA";
BcType["NRDC"] = "NR-DC";
BcType["ENDC"] = "EN-DC";
BcType["NEDC"] = "NE-DC";
})(BcType || (BcType = {}));
//////////////////////////////
// Helper functions
/**
* Splits a single band entry string (e.g. `"n78C"`, `"3A"`, `"(n)40CA"`) into
* its RAT/BcType, BandNumber, and BWC-ID components.
*
* @param aString — a single band entry from a BC-ID (e.g. `"n1A"`, `"7C"`, `"(n)3AA"`).
* @returns a tuple of [RAT or BcType, BandNumber, BWC_ID or raw string].
* @throws InvalidBcIdException if the string cannot be parsed.
*/
export function SplitBandEntry(aString) {
if (aString === null || aString === undefined) {
throw new Error("aString must not be None.");
}
if (aString === "") {
throw new Error("aString must not be empty.");
}
let s = aString.trim();
let rat;
let bandNumber;
let bwcid;
if (s.includes("(n)")) {
if (s.startsWith("(n)")) {
rat = BcType.ENDC;
}
else {
rat = BcType.NEDC;
}
s = s.replaceAll("(n)", "");
}
else if (s.startsWith("n")) {
rat = RAT.NR;
s = s.substring(1);
}
else if (s[0] >= "0" && s[0] <= "9") {
rat = RAT.EUTRA;
}
else {
throw new Error(`The given string must start with 'n' or with a band number or contain '(n)' but was: '${s}'`);
}
let bandNumberStr = "";
let i = 0;
for (; i < s.length; i++) {
const c = s[i];
if (c >= "0" && c <= "9") {
bandNumberStr += c;
}
else {
break;
}
}
if (rat === BcType.NEDC || rat === BcType.ENDC) {
bwcid = s.substring(i).trim();
bandNumber = new BandNumber(parseInt(bandNumberStr, 10), RAT.NR);
}
else {
bwcid = new BWC_ID(s.substring(i));
if (!bwcid.isValid) {
throw new InvalidBcIdException(`Could not determine a valid BWC from the given string '${s}'.`);
}
bandNumber = new BandNumber(parseInt(bandNumberStr, 10), rat);
}
return [rat, bandNumber, bwcid];
}
/**
* Splits a BC-ID string into its RAT-level components.
*
* For inter-RAT DC identifiers containing `(n)`, the string is split around
* the `(n)` marker. For NR-only or EUTRA-only identifiers, the string is
* split on `_` (which separates the NR and EUTRA parts in DC identifiers).
*
* @param aString — the BC-ID value string (without prefix).
* @returns an array of RAT-level component strings.
*/
export function GetRatComponents(aString) {
const components = [];
if (aString === null || aString === undefined || aString === "") {
return components;
}
const nCount = (aString.match(/\(n\)/g) || []).length;
if (nCount > 1) {
throw new Error(`Expecting at most one occurrence of '(n)' but found ${nCount} in '${aString}'`);
}
if (nCount === 1) {
const pos = aString.indexOf("(n)");
const lowerEdge = aString.lastIndexOf("-", pos);
let upperEdge = aString.indexOf("-", pos);
if (upperEdge < 0)
upperEdge = aString.length;
if (lowerEdge > 0) {
components.push(aString.substring(0, lowerEdge));
}
components.push(aString.substring(lowerEdge + 1, upperEdge));
if (upperEdge < aString.length - 1) {
components.push(aString.substring(upperEdge + 1));
}
}
else {
const underscoreCount = (aString.match(/_/g) || []).length;
if (underscoreCount > 1) {
throw new Error(`Expecting at most one occurrence of '_'-separator but found ${underscoreCount} in '${aString}'`);
}
components.push(...aString.split("_"));
}
return components;
}
//////////////////////////////
// BC_ID class
/**
* Represents a 3GPP Band Combination Identifier (BC-ID).
*
* A BC-ID encodes one or more bands with their BWC-IDs, and optionally a
* prefix (`CA_` or `DC_`) indicating carrier aggregation or dual connectivity.
*
* Examples:
* - `"CA_n1A-n78C"` — inter-band CA with bands n1 (single carrier) and n78 (contiguous pair)
* - `"DC_1A-8A_n77(2A)"` — EN-DC with EUTRA bands 1+8 and NR band n77 (two non-contiguous carriers)
* - `"n78A"` — single-carrier NR configuration
*
* The class parses the identifier, determines the BcType (SC, CA, NR-DC, EN-DC, NE-DC),
* extracts per-band BWC-IDs, and provides comparison operators for sorting.
*/
export class BC_ID {
str;
/** The prefix: `"CA_"`, `"DC_"`, or `""`. */
prefix;
/** If this BC-ID uses SUL naming, the original SUL name; otherwise null. */
sulName;
/** Total number of band entries in this BC-ID. */
nrofBandEntries = 0;
/** Map from band number string (e.g. `"n78"`) to its BWC-ID or raw DC string. */
bwcIdsPerBand = new Map();
/** True if any band has a BWC-ID other than `"A"` (i.e. intra-band CA). */
intraBand = false;
/** The determined combination type (SC, CA, NR-DC, EN-DC, NE-DC). */
bctype = null;
bandNumberObjects = new Map();
/**
* Parses a BC-ID string or copies an existing BC_ID.
*
* @param aValue — the BC-ID string (e.g. `"CA_n1A-n78C"`, `"DC_1A_n77A"`)
* or an existing BC_ID to copy.
* @throws InvalidBcIdException if the string cannot be parsed.
*/
constructor(aValue) {
if (aValue === null || aValue === undefined) {
throw new InvalidBcIdException("A BCID shall not be 'None'.");
}
let strValue = aValue instanceof BC_ID ? aValue.str : String(aValue);
strValue = strValue.replaceAll(" ", "");
let prefix = "";
if (strValue.toUpperCase().startsWith("CA_")) {
prefix = "CA_";
}
else if (strValue.toUpperCase().startsWith("DC_")) {
prefix = "DC_";
}
if (prefix !== "") {
strValue = strValue.substring(3);
}
let tempSUL = null;
if (strValue.includes("_SUL_")) {
tempSUL = strValue;
strValue = strValue.replace("_SUL_", "_");
}
this.str = strValue;
this.prefix = prefix.toUpperCase();
this.sulName = tempSUL;
// If copy-constructing from another BC_ID, copy its parsed state
if (aValue instanceof BC_ID) {
this.prefix = aValue.prefix;
this.sulName = aValue.sulName;
this.nrofBandEntries = aValue.nrofBandEntries;
this.bwcIdsPerBand = new Map(aValue.bwcIdsPerBand);
this.bandNumberObjects = new Map(aValue.bandNumberObjects);
this.intraBand = aValue.intraBand;
this.bctype = aValue.bctype;
return;
}
// Parse
for (const oneRAT of GetRatComponents(this.str)) {
let prevBandNumber = 0;
let bandEntries;
if (oneRAT.startsWith("n")) {
bandEntries = oneRAT.substring(1).split("-n").map(e => "n" + e);
}
else if (oneRAT[0] >= "0" && oneRAT[0] <= "9") {
bandEntries = oneRAT.split("-");
}
else if (oneRAT.includes("(n)")) {
bandEntries = [oneRAT];
}
else {
throw new InvalidBcIdException(`BC-ID '${this.str}' looks like inter-RAT DC but could not determine type of this component: '${oneRAT}'`);
}
for (const oneBandEntry of bandEntries) {
this.nrofBandEntries++;
const [rat, bandNumber, bwcid] = SplitBandEntry(oneBandEntry);
if (this.bctype === null) {
this.bctype = rat;
}
else if ((this.bctype === BcType.NEDC && rat === BcType.ENDC) ||
(this.bctype === BcType.ENDC && rat === BcType.NEDC)) {
throw new InvalidBcIdException(`The given BC-ID contains both NE-DC and EN-DC components: '${this.str}'`);
}
else if (this.bctype === BcType.NEDC && rat === RAT.NR) {
throw new InvalidBcIdException(`The given NE-DC BC-ID contains an NR component AFTER EUTRA components: '${this.str}'`);
}
else if (this.bctype === BcType.ENDC && rat === RAT.EUTRA) {
throw new InvalidBcIdException(`The given EN-DC BC-ID contains an EUTRA component AFTER NR components: '${this.str}'`);
}
else if (this.bctype === RAT.NR && (rat === RAT.EUTRA || rat === BcType.NEDC)) {
this.bctype = BcType.NEDC;
}
else if (this.bctype === RAT.EUTRA && (rat === RAT.NR || rat === BcType.ENDC)) {
this.bctype = BcType.ENDC;
}
const bandKey = bandNumber.toString();
if (rat === RAT.NR) {
if (this.bwcIdsPerBand.has(bandKey)) {
throw new InvalidBcIdException(`The given BC-ID contains more than one entry for band number ${bandNumber}: '${this.str}'`);
}
}
if (bandNumber.asInt() < prevBandNumber) {
throw new InvalidBcIdException(`In the given BC-ID (${this.str}) the band in the component '${oneBandEntry}' occurs in the wrong order: 'n${prevBandNumber}' >= 'n${bandNumber}'.`);
}
this.bwcIdsPerBand.set(bandKey, bwcid);
this.bandNumberObjects.set(bandKey, bandNumber);
prevBandNumber = bandNumber.asInt();
const bwcStr = bwcid instanceof BWC_ID ? bwcid.toString() : bwcid;
if (bwcStr !== "A") {
this.intraBand = true;
}
}
}
if (this.nrofBandEntries === 0) {
throw new InvalidBcIdException(`The given BC-ID did not contain any band entries: '${this.str}'`);
}
else if ((this.bctype === BcType.ENDC || this.bctype === BcType.NEDC) && this.prefix === "CA_") {
throw new InvalidBcIdException(`In BC-ID '${this.str}' the prefix ${this.prefix} is incompatible with determined bcType ${this.bctype}`);
}
else if ((this.bctype === BcType.CA || this.bctype === BcType.SC) && this.prefix === "DC_") {
throw new InvalidBcIdException(`In BC-ID '${this.str}' the prefix ${this.prefix} is incompatible with determined bcType ${this.bctype}`);
}
else if (this.nrofBandEntries === 1 && !this.intraBand) {
this.bctype = BcType.SC;
}
else if (this.bctype === RAT.NR) {
if (this.prefix === "DC_") {
this.bctype = BcType.NRDC;
}
else {
this.bctype = BcType.CA;
if (this.prefix === "") {
this.prefix = "CA_";
}
}
}
}
/** Returns the BC-ID value string (without prefix). */
valueOf() { return this.str; }
/** Returns the full BC-ID string including prefix (e.g. `"CA_n1A-n78C"`). */
toString() {
if (this.sulName !== null)
return this.prefix + this.sulName;
return this.prefix + this.str;
}
/** Returns a human-readable description including intra/inter-band and BcType. */
describe() {
const intra = this.intraBand ? "intra" : "";
const plus = this.intraBand && this.nrofBandEntries > 1 ? "+" : "";
const inter = this.nrofBandEntries > 1 ? "inter" : "";
return `${this}: ${intra}${plus}${inter}-band ${this.bctype}`;
}
/** Returns the BandNumber objects in this BC-ID, sorted by numeric value. */
getBandNumbers() {
return [...this.bandNumberObjects.values()].sort((a, b) => a.asInt() - b.asInt());
}
/** Returns the number of distinct bands in this BC-ID. */
nrofBands() { return this.bwcIdsPerBand.size; }
/**
* Returns the total number of component carriers across all bands.
*
* Sums the actual carrier count from each band's BWC-ID using getNrofCarriers().
* E.g., "CA_n1A-n3A" returns 2 (1+1), "CA_n3B" returns 2 (contiguous pair),
* "CA_n25(2A)-n41(A-C)" returns 5 (2*1 + 1 + 2).
*
* For DC configurations with EUTRA bands, the EUTRA carriers are not parsed
* (BWC-ID is stored as a raw string), so the count uses a heuristic.
*/
getNrofCarriers() {
let total = 0;
for (const [bandKey, bwcid] of this.bwcIdsPerBand.entries()) {
if (bwcid instanceof BWC_ID) {
// Determine frequency range from band number
const bandNumber = this.bandNumberObjects.get(bandKey);
const frequencyRange = bandNumber && bandNumber.isFr2() ? 2 : 1;
total += bwcid.getNrofCarriers(frequencyRange);
}
else {
// For DC strings (EUTRA), heuristic: count uppercase letters
total += (bwcid.match(/[A-Z]/g) || []).length;
}
}
return total;
}
/** Returns true if all carriers belong to a single band. */
isIntraBand() { return this.nrofBands() === 1; }
/** Returns the map from band number string to BWC-ID (or raw DC string). */
getBwcIDsPerBand() { return this.bwcIdsPerBand; }
/** Returns true if this is a single-carrier configuration (one band, BWC = 'A'). */
isSingleCarrier() {
if (this.bwcIdsPerBand.size !== 1)
return false;
const val = [...this.bwcIdsPerBand.values()][0];
const s = val instanceof BWC_ID ? val.toString() : val;
return s === "A";
}
/** Returns true if the prefix is `"DC_"`. */
isDualConnectivity() { return this.prefix === "DC_"; }
/** Returns true if all bands are in FR1. */
isFr1() {
for (const oneBandNumber of this.getBandNumbers()) {
if (oneBandNumber.isFr2())
return false;
}
return true;
}
/** Returns true if all bands are in FR2. */
isFr2() {
for (const oneBandNumber of this.getBandNumbers()) {
if (oneBandNumber.isFr1())
return false;
}
return true;
}
/** Returns true if at least one band is in FR2. */
hasFr2() {
for (const oneBandNumber of this.getBandNumbers()) {
if (oneBandNumber.isFr2())
return true;
}
return false;
}
/** Returns true if this is a pure NR combination (no EUTRA bands, no EN-DC/NE-DC). */
isNR() {
if (this.bctype === BcType.ENDC || this.bctype === BcType.NEDC)
return false;
for (const oneBandNumber of this.getBandNumbers()) {
if (oneBandNumber.rat === RAT.EUTRA)
return false;
}
return true;
}
/** Determines the specification this BC-ID belongs to. */
getSpecification() {
if (this.isNR() && this.isFr1())
return "38.101-1";
if (this.isNR() && this.isFr2())
return "38.101-2";
return "38.101-3";
}
/** Returns the prefix string (`"CA_"`, `"DC_"`, or `""`). */
getPrefix() { return this.prefix ?? ""; }
/** Returns true if this BC-ID uses SUL naming. */
isSUL() { return this.sulName !== null; }
/** Returns true if this BC-ID has the same value string as `other`. */
equals(other) {
const otherStr = other instanceof BC_ID ? other.str : other;
return this.str === otherStr;
}
/**
* Ordering comparison for sorting BC-IDs.
*
* Compares first by number of bands (fewer first), then by band number
* lists lexicographically, then by BWC-IDs per band.
*/
lessThan(value) {
const other = value instanceof BC_ID ? value : new BC_ID(value);
const selfBands = this.getBandNumbers();
const otherBands = other.getBandNumbers();
if (selfBands.length < otherBands.length)
return true;
if (selfBands.length === otherBands.length) {
// Compare band number lists
const cmpBands = compareBandLists(selfBands, otherBands);
if (cmpBands < 0)
return true;
if (cmpBands === 0) {
// Compare BWC_IDs per band
for (const oneBandNumber of selfBands) {
const key = oneBandNumber.toString();
const selfBwc = this.bwcIdsPerBand.get(key);
const otherBwc = other.bwcIdsPerBand.get(key);
const selfBwcObj = selfBwc instanceof BWC_ID ? selfBwc : new BWC_ID(selfBwc);
const otherBwcObj = otherBwc instanceof BWC_ID ? otherBwc : new BWC_ID(otherBwc);
if (selfBwcObj.lessThan(otherBwcObj))
return true;
}
}
}
return false;
}
/** Returns true if this BC-ID is strictly greater than `value`. */
greaterThan(value) {
return !this.equals(value) && !this.lessThan(value);
}
/** Returns true if this BC-ID is less than or equal to `value`. */
lessOrEqual(value) {
return this.equals(value) || this.lessThan(value);
}
/** Returns true if this BC-ID is greater than or equal to `value`. */
greaterOrEqual(value) {
return !this.lessThan(value);
}
}
/** Compares two sorted BandNumber arrays lexicographically by numeric value. */
function compareBandLists(a, b) {
for (let i = 0; i < Math.min(a.length, b.length); i++) {
if (a[i].asInt() < b[i].asInt())
return -1;
if (a[i].asInt() > b[i].asInt())
return 1;
}
return a.length - b.length;
}
//# sourceMappingURL=BC_ID.js.map