Bra Size Calculator

<script>
const keyTerms = {
    bcd: 'Bottom Cup Depth',
    hh: 'Horizontal Hemisphere',
    band: 'Band',
    s: 'Petite',
    m: 'Moderate',
    l: 'Substantial',
};

const sizeSet = {
    s: 'A-DD',
    m: 'DD-GG',
    l: 'GG-KK',
};

const units = {
    imperial: 'in',
    metric: 'cm',
}

const sizeChartData = [
    // 1xx
    { id: 101, size: '7.3 / 2.9"', range: 's', metric: { bcd: 7.3, hh: 18.3 }, imperial: { bcd: 2.9, hh: 7.2 } },
    { id: 102, size: '7.9 / 3.1"', range: 's', metric: { bcd: 7.9, hh: 19.4 }, imperial: { bcd: 3.1, hh: 7.6 } },
    { id: 103, size: '8.5 / 3.3"', range: 's', metric: { bcd: 8.5, hh: 20.6 }, imperial: { bcd: 3.3, hh: 8.1 } },
    { id: 104, size: '9.1 / 3.6"', range: 's', metric: { bcd: 9.1, hh: 21.8 }, imperial: { bcd: 3.6, hh: 8.6 } },
    { id: 105, size: '9.7 / 3.8"', range: 's', metric: { bcd: 9.7, hh: 23 }, imperial: { bcd: 3.8, hh: 9.1 } },
    { id: 106, size: '10.3 / 4.1"', range: 's', metric: { bcd: 10.3, hh: 24.2 }, imperial: { bcd: 4.1, hh: 9.5 } },
    { id: 107, size: '10.9 / 4.3"', range: 's', metric: { bcd: 10.9, hh: 25.4 }, imperial: { bcd: 4.3, hh: 10 } },
    { id: 108, size: '11.5 / 4.5"', range: 's', metric: { bcd: 11.5, hh: 26.6 }, imperial: { bcd: 4.5, hh: 10.5 } },
    { id: 109, size: '12.1 / 4.8"', range: 's', metric: { bcd: 12.1, hh: 27.8 }, imperial: { bcd: 4.8, hh: 10.9 } },
    { id: 110, size: '12.7 / 5.0"', range: 's', metric: { bcd: 12.7, hh: 29 }, imperial: { bcd: 5.0, hh: 11.4 } },
    { id: 111, size: '13.3 / 5.2"', range: 's', metric: { bcd: 13.3, hh: 30.2 }, imperial: { bcd: 5.2, hh: 11.9 } },
    { id: 112, size: '13.9 / 5.5"', range: 's', metric: { bcd: 13.9, hh: 31.4 }, imperial: { bcd: 5.5, hh: 12.4 } },
    { id: 113, size: '14.5 / 5.7"', range: 's', metric: { bcd: 14.5, hh: 32.6 }, imperial: { bcd: 5.7, hh: 12.8 } },
    { id: 114, size: '15.1 / 5.9"', range: 's', metric: { bcd: 15.1, hh: 33.8 }, imperial: { bcd: 5.9, hh: 13.3 } },
    { id: 115, size: '15.7 / 6.2"', range: 's', metric: { bcd: 15.7, hh: 35 }, imperial: { bcd: 6.2, hh: 13.8 } },
    { id: 199, metric: { bcd: 15.7, hh: 35.1 }, imperial: { bcd: 6.3, hh: 13.9 } },
    // 2xx
    { id: 201, size: '10.1 / 4.0"', range: 'm', metric: { bcd: 10.1, hh: 22.9 }, imperial: { bcd: 4.0, hh: 9.0 } },
    { id: 202, size: '10.9 / 4.3"', range: 'm', metric: { bcd: 10.9, hh: 24.5 }, imperial: { bcd: 4.3, hh: 9.6 } },
    { id: 203, size: '11.7 / 4.6"', range: 'm', metric: { bcd: 11.7, hh: 25.9 }, imperial: { bcd: 4.6, hh: 10.2 } },
    { id: 204, size: '12.5 / 4.9"', range: 'm', metric: { bcd: 12.5, hh: 27.3 }, imperial: { bcd: 4.9, hh: 10.7 } },
    { id: 205, size: '13.3 / 5.2"', range: 'm', metric: { bcd: 13.3, hh: 28.7 }, imperial: { bcd: 5.2, hh: 11.3 } },
    { id: 206, size: '14.1 / 5.6"', range: 'm', metric: { bcd: 14.1, hh: 30.1 }, imperial: { bcd: 5.6, hh: 11.9 } },
    { id: 207, size: '14.9 / 5.9"', range: 'm', metric: { bcd: 14.9, hh: 31.5 }, imperial: { bcd: 5.9, hh: 12.4 } },
    { id: 208, size: '15.7 / 6.2"', range: 'm', metric: { bcd: 15.7, hh: 32.9 }, imperial: { bcd: 6.2, hh: 13.0 } },
    { id: 209, size: '16.5 / 6.5"', range: 'm', metric: { bcd: 16.5, hh: 34.3 }, imperial: { bcd: 6.5, hh: 13.5 } },
    { id: 210, size: '17.3 / 6.8"', range: 'm', metric: { bcd: 17.3, hh: 35.7 }, imperial: { bcd: 6.8, hh: 14.1 } },
    { id: 211, size: '18.1 / 7.1"', range: 'm', metric: { bcd: 18.1, hh: 37.1 }, imperial: { bcd: 7.1, hh: 14.6 } },
    { id: 212, size: '18.9 / 7.4"', range: 'm', metric: { bcd: 18.9, hh: 38.5 }, imperial: { bcd: 7.4, hh: 15.2 } },
    { id: 213, size: '19.7 / 7.8"', range: 'm', metric: { bcd: 19.7, hh: 39.9 }, imperial: { bcd: 7.8, hh: 15.7 } },
    { id: 214, size: '20.5 / 8.1"', range: 'm', metric: { bcd: 20.5, hh: 41.3 }, imperial: { bcd: 8.1, hh: 16.3 } },
    { id: 215, size: '21.3 / 8.4"', range: 'm', metric: { bcd: 21.3, hh: 42.7 }, imperial: { bcd: 8.4, hh: 16.8 } },
    { id: 299, metric: { bcd: 21.4, hh: 42.8 }, imperial: { bcd: 8.5, hh: 16.9 } },
    // 3xx
    { id: 301, size: '14.1 / 5.6"', range: 'l', metric: { bcd: 14.1, hh: 31.6 }, imperial: { bcd: 5.6, hh: 12.4 } },
    { id: 302, size: '14.7 / 5.8"', range: 'l', metric: { bcd: 14.7, hh: 33.0 }, imperial: { bcd: 5.8, hh: 13.0 } },
    { id: 303, size: '15.3 / 6.0"', range: 'l', metric: { bcd: 15.3, hh: 34.4 }, imperial: { bcd: 6.0, hh: 13.5 } },
    { id: 304, size: '15.9 / 6.3"', range: 'l', metric: { bcd: 15.9, hh: 35.8 }, imperial: { bcd: 6.3, hh: 14.1 } },
    { id: 305, size: '16.5 / 6.5"', range: 'l', metric: { bcd: 16.5, hh: 37.2 }, imperial: { bcd: 6.5, hh: 14.6 } },
    { id: 306, size: '17.1 / 6.7"', range: 'l', metric: { bcd: 17.1, hh: 38.6 }, imperial: { bcd: 6.7, hh: 15.2 } },
    { id: 307, size: '17.7 / 7.0"', range: 'l', metric: { bcd: 17.7, hh: 40.0 }, imperial: { bcd: 7.0, hh: 15.7 } },
    { id: 308, size: '18.3 / 7.2"', range: 'l', metric: { bcd: 18.3, hh: 41.4 }, imperial: { bcd: 7.2, hh: 16.3 } },
    { id: 309, size: '18.9 / 7.4"', range: 'l', metric: { bcd: 18.9, hh: 42.8 }, imperial: { bcd: 7.4, hh: 16.9 } },
    { id: 310, size: '19.5 / 7.7"', range: 'l', metric: { bcd: 19.5, hh: 44.2 }, imperial: { bcd: 7.7, hh: 17.4 } },
    { id: 311, size: '20.1 / 7.9"', range: 'l', metric: { bcd: 20.1, hh: 45.6 }, imperial: { bcd: 7.9, hh: 18.0 } },
    { id: 312, size: '20.7 / 8.1"', range: 'l', metric: { bcd: 20.7, hh: 47.0 }, imperial: { bcd: 8.1, hh: 18.5 } },
    { id: 313, size: '21.3 / 8.4"', range: 'l', metric: { bcd: 21.3, hh: 48.4 }, imperial: { bcd: 8.4, hh: 19.1 } },
    { id: 314, size: '21.9 / 8.6"', range: 'l', metric: { bcd: 21.9, hh: 49.8 }, imperial: { bcd: 8.6, hh: 19.6 } },
    { id: 315, size: '22.5 / 8.9"', range: 'l', metric: { bcd: 22.5, hh: 51.2 }, imperial: { bcd: 8.9, hh: 20.2 } },
    { id: 316, size: '23.1 / 9.1"', range: 'l', metric: { bcd: 23.1, hh: 52.6 }, imperial: { bcd: 9.1, hh: 20.7 } },
    { id: 399, metric: { bcd: 23.2, hh: 52.7 }, imperial: { bcd: 9.2, hh: 20.8 } },
    // 4xx
    { id: 401, size: '7.7 / 3.0"', metric: { bcd: 7.7, hh: 18.7 }, imperial: { bcd: 3.0, hh: 7.4 } },
    { id: 402, size: '8.5 / 3.3"', metric: { bcd: 8.5, hh: 20.1 }, imperial: { bcd: 3.3, hh: 7.9 } },
    { id: 403, size: '9.3 / 3.7"', metric: { bcd: 9.3, hh: 21.5 }, imperial: { bcd: 3.7, hh: 8.5 } },
    { id: 404, size: '10.1 / 4.0"', metric: { bcd: 10.1, hh: 22.9 }, imperial: { bcd: 4.0, hh: 9.0 } },
    { id: 405, size: '10.9 / 4.3"', metric: { bcd: 10.9, hh: 24.3 }, imperial: { bcd: 4.3, hh: 9.6 } },
    { id: 406, size: '11.7 / 4.6"', metric: { bcd: 11.7, hh: 25.7 }, imperial: { bcd: 4.6, hh: 10.1 } },
    { id: 407, size: '12.5 / 4.9"', metric: { bcd: 12.5, hh: 27.1 }, imperial: { bcd: 4.9, hh: 10.7 } },
    { id: 408, size: '13.3 / 5.2"', metric: { bcd: 13.3, hh: 28.5 }, imperial: { bcd: 5.2, hh: 11.2 } },
    { id: 409, size: '14.1 / 5.6"', metric: { bcd: 14.1, hh: 29.9 }, imperial: { bcd: 5.6, hh: 11.8 } },
    { id: 410, size: '14.9 / 5.9"', metric: { bcd: 14.9, hh: 31.3 }, imperial: { bcd: 5.9, hh: 12.3 } },
    { id: 411, size: '15.7 / 6.2"', metric: { bcd: 15.7, hh: 32.7 }, imperial: { bcd: 6.2, hh: 12.9 } },
    { id: 412, size: '16.5 / 6.5"', metric: { bcd: 16.5, hh: 34.1 }, imperial: { bcd: 6.5, hh: 13.4 } },
    { id: 413, size: '17.3 / 6.8"', metric: { bcd: 17.3, hh: 35.5 }, imperial: { bcd: 6.8, hh: 14.0 } },
    { id: 414, size: '18.1 / 7.1"', metric: { bcd: 18.1, hh: 36.9 }, imperial: { bcd: 7.1, hh: 14.5 } },
    { id: 415, size: '18.9 / 7.4"', metric: { bcd: 18.9, hh: 38.3 }, imperial: { bcd: 7.4, hh: 15.1 } },
    { id: 416, size: '19.7 / 7.8"', metric: { bcd: 19.7, hh: 39.7 }, imperial: { bcd: 7.8, hh: 15.6 } },
    { id: 417, size: '20.5 / 8.1"', metric: { bcd: 20.5, hh: 41.1 }, imperial: { bcd: 8.1, hh: 16.2 } },
    { id: 418, size: '21.3 / 8.4"', metric: { bcd: 21.3, hh: 42.5 }, imperial: { bcd: 8.4, hh: 16.7 } },
    { id: 419, size: '22.1 / 8.7"', metric: { bcd: 22.1, hh: 43.9 }, imperial: { bcd: 8.7, hh: 17.3 } },
    { id: 420, size: '22.9 / 9.0"', metric: { bcd: 22.9, hh: 45.3 }, imperial: { bcd: 9.0, hh: 17.8 } },
    { id: 499, metric: { bcd: 23, hh: 45.4 }, imperial: { bcd: 9.1, hh: 17.9 } },
];

const bandChart = [
    { id: 101, size: 28, metric: { min: 66, max: 71 }, imperial: { min: 26, max: 28 } },
    { id: 102, size: 30, metric: { min: 71, max: 76 }, imperial: { min: 28, max: 30 } },
    { id: 103, size: 32, metric: { min: 76, max: 81 }, imperial: { min: 30, max: 32 } },
    { id: 104, size: 34, metric: { min: 81, max: 86 }, imperial: { min: 32, max: 34 } },
    { id: 105, size: 36, metric: { min: 86, max: 91 }, imperial: { min: 34, max: 36 } },
    { id: 106, size: 38, metric: { min: 91, max: 96 }, imperial: { min: 36, max: 38 } },
    { id: 107, size: 40, metric: { min: 96, max: 101 }, imperial: { min: 38, max: 40 } },
    { id: 108, size: 42, metric: { min: 101, max: 106 }, imperial: { min: 40, max: 42 } },
    { id: 109, size: 44, metric: { min: 106, max: 111 }, imperial: { min: 42, max: 44 } },
    { id: 110, size: 46, metric: { min: 111, max: 116 }, imperial: { min: 44, max: 46 } },
    { id: 111, size: 48, metric: { min: 116, max: 121 }, imperial: { min: 46, max: 48 } },
    { id: 112, size: 50, metric: { min: 121, max: 126 }, imperial: { min: 48, max: 50 } },
    { id: 113, size: 52, metric: { min: 126, max: 131 }, imperial: { min: 50, max: 52 } },
    { id: 199, metric: { min: 131, max: 132 }, imperial: { min: 52, max: 53 } },
];

const patterns = [
    {
        id: 'akeru',
        name: 'Akeru',
        sizes: sizeChartData.filter(x => x.id >= 400 && x.id <= 499),
        band: bandChart.filter(x => x.size >= 26 && x.size <= 52),
    },
    {
        id: 'cambia',
        name: 'Cambia',
        sizes: sizeChartData.filter(x => x.id >= 400 && x.id <= 499),
        band: bandChart.filter(x => x.size >= 28 && x.size <= 44),
    },
    {
        id: 'koma',
        name: 'Koma',
        sizes: sizeChartData.filter(x => x.id >= 100 && x.id <= 299),
        band: bandChart.filter(x => x.size >= 28 && x.size <= 44),
    },
    {
        id: 'labellum',
        name: 'Labellum',
        sizes: sizeChartData.filter(x => x.id >= 200 && x.id <= 399),
        band: bandChart.filter(x => x.size >= 29 && x.size <= 42),
    },
    {
        id: 'lamina',
        name: 'Lamina',
        sizes: sizeChartData.filter(x => x.id >= 200 && x.id <= 299),
        band: bandChart.filter(x => x.size >= 29 && x.size <= 42),
    },
    {
        id: 'lanai',
        name: 'Lanai',
        sizes: sizeChartData.filter(x => x.id >= 100 && x.id <= 299),
        band: bandChart.filter(x => x.size >= 28 && x.size <= 44),
    },
    {
        id: 'lotus2',
        name: 'Lotus 2.0',
        sizes: sizeChartData.filter(x => x.id >= 100 && x.id <= 299),
        band: bandChart.filter(x => x.size >= 28 && x.size <= 44),
    },
    {
        id: 'lusamine',
        name: 'Lusamine',
        sizes: sizeChartData.filter(x => x.id >= 100 && x.id <= 399),
        band: bandChart.filter(x => x.size >= 28 && x.size <= 44),
    },
    {
        id: 'momiji',
        name: 'Momiji',
        sizes: sizeChartData.filter(x => x.id >= 200 && x.id <= 399),
        band: bandChart.filter(x => x.size >= 28 && x.size <= 44),
    },
    {
        id: 'mysa',
        name: 'Mysa',
        sizes: sizeChartData.filter(x => x.id >= 100 && x.id <= 399),
        band: bandChart.filter(x => x.size >= 26 && x.size <= 52),
    },
];

var sizeChart = null;

function calculateSize(form) {
    sizeChart = sizeChartData.slice(); //patterns.find(x => x.id === form.pattern.value).sizes;

    const errors = [];

    var system = form['system-imperial'].checked 
               ? form['system-imperial'].value
               : form['system-metric'].checked
               ? form['system-metric'].value
               : null; // form.system.value;
    var bcd = Number(form.bcd.value);
    var hh = Number(form.hh.value);
    var band = Number(form.band.value);

    var checkMinMax = (data, label, unit, value) => {
        const minValue = data[0];
        const maxValue = data.reverse()[0];
        if (value < minValue) errors.push(`${label} minimum value is ${minValue} ${unit}.`);
        if (value > maxValue) errors.push(`${label} maximum value is ${maxValue} ${unit}.`);
    }

    var sizeCheck = (s, f, v) => {
        if (!v || Number.isNaN(v)) {
            errors.push(`Enter a valid ${keyTerms[f]} value.`);
            return;
        }
        const data = getValues(sizeChart, s, f);
        checkMinMax(data, keyTerms[f], units[s], v);
    };

    if (!system) {
        errors.push('Select measurement system.');
    }
    else {
        sizeCheck(system, 'bcd', bcd);
        sizeCheck(system, 'hh', hh);

        if (!band || Number.isNaN(band)) {
            errors.push('Enter a valid Band value.');
        }
        else {
            const data = [
                ...getValues(bandChart, system, 'min'),
                ...getValues(bandChart, system, 'max'),
            ]
            .sort((a,b) => a < b ? -1 : 1);
            checkMinMax(data, 'Band', units[system], band);
        }
    }

    if (processErrors(errors)) return;

    const sizes = [
        ...getSize(1, system, 'hh', hh),
        ...getSize(2, system, 'hh', hh),
        ...getSize(3, system, 'hh', hh),
        ...getSize(4, system, 'hh', hh),
    ];
    processResults(sizes, system, bcd, hh, band);
}

function getValues(arr, s, f) {
    return arr.filter(x => !/[0-9]99/.test(x.id.toString())).map(x => x[s][f]).sort((a,b) => a < b ? -1 : 1);
}

function getSize(set, s, f, value) {
    const n = Number(`${set.toString().substr(0,1)}00`);
    return sizeChart.filter(x => x.id >= n && x.id <= n + 99)
                    .filter((v,i,a) => v[s][f] <= value
                                    && a[i+1] != null
                                    && a[i+1][s][f] > value);
}

function processErrors(errors) {
    const errorsElement = document.getElementById('errors');
    errorsElement.hidden = true;
    if (errors && !!errors.length) {
        let content = '<h3>Error</h3>';
        errors.forEach(x => content += `<p>${x}</p>`);
        errorsElement.innerHTML = content;
        setTimeout(() => errorsElement.hidden = false, 250);
        return true;
    }
    return false;
}

function processResults(sizes, system, bcd, hh, band) {
    const resultsElement = document.getElementById('results'); 
    resultsElement.hidden = true;
    if (!!sizes && sizes.length) {
        let content = '<h3>Results</h3><p>Your measurements match the following size(s):';
        const rangeSets = [];
        content += `<ul>${sizes.map(x => {
            let range = '';
            if (!!x.range && !rangeSets.some(x => x === x.range)) {
                rangeSets.push(x.range);
                range = ` (${keyTerms[x.range]} projection), ${sizeSet[x.range]} recommended`;
            }
            if (x[system].bcd !== bcd) {
                const tbcd = getSize(x.id, system, 'bcd', bcd);
                if (tbcd && !!tbcd.length) {
                    const diff = Math.abs(tbcd[0].id - x.id);
                    if (!!diff)
                        range += `, ${keyTerms['bcd']} adjustments ${diff > 1 ? 'are' : 'may be'} needed.`;
                }
            }
            return `<li>${x.size}${range}</li>`}).join('')}</ul>`;
        if (rangeSets.length > 1) {
            content += '<div>';
            content += '<div>Your measurements are applies to multiple pattern sets, choose the one that closely resembles your projection.</div>';
            content += `<br><span>${rangeSets.map(x => `<img src="projection-${keyTerms[x]}.png" height="150" />`).join('')}</span>`;
        }
        resultsElement.innerHTML = content;
        setTimeout(() => resultsElement.hidden = false, 250);
    }
}
</script>