001package org.xbib.standardnumber;
002
003import java.util.regex.Matcher;
004import java.util.regex.Pattern;
005
006/**
007 * ISO 10957 International Standard Music Number (ISMN)
008 *
009 * Z39.50 BIB-1 Use Attribute 1092
010 *
011 * The International Standard Music Number (ISMN) is a thirteen-character alphanumeric identifier
012 * for printed music developed by ISO. The original proposal for an ISMN was made by the
013 * UK Branch of IAML (International Association of Music Libraries, Archives and Documentation
014 * Centres).
015 *
016 * The original format comprised four elements: a distinguishing prefix M, a publisher ID,
017 * an item ID and a check digit, typically looking like M-2306-7118-7.
018 *
019 * From 1 January 2008 the ISMN was defined as a thirteen digit identifier beginning 979-0 where
020 * the zero replaced M in the old-style number. The resulting number is identical with its
021 * EAN-13 number as encoded in the item's barcode.
022 *
023 * @see <a href="http://www.ismn-international.org/download/Web_ISMN%20Manual_2008-3.pdf">ISMN Manual 2008</a>
024 */
025public class ISMN extends AbstractStandardNumber implements Comparable<ISMN>, StandardNumber {
026
027    private static final Pattern PATTERN = Pattern.compile("[\\p{Digit}M\\-]{0,17}");
028
029    private String value;
030
031    private boolean createWithChecksum;
032
033    @Override
034    public String type() {
035        return "ismn";
036    }
037
038    @Override
039    public int compareTo(ISMN ismn) {
040        return ismn != null ? normalizedValue().compareTo(ismn.normalizedValue()) : -1;
041    }
042
043    @Override
044    public ISMN set(CharSequence value) {
045        this.value = value != null ? value.toString() : null;
046        return this;
047    }
048
049    @Override
050    public ISMN createChecksum(boolean createWithChecksum) {
051        this.createWithChecksum = createWithChecksum;
052        return this;
053    }
054
055    @Override
056    public ISMN normalize() {
057        Matcher m = PATTERN.matcher(value);
058        if (m.find()) {
059            this.value = value.substring(m.start(), m.end());
060            this.value = (value.startsWith("979") ? "" : "979") + dehyphenate(value.replace('M', '0'));
061        }
062        return this;
063    }
064
065    @Override
066    public boolean isValid() {
067        return value != null && !value.isEmpty() && check();
068    }
069
070    @Override
071    public ISMN verify() throws NumberFormatException {
072        if (value == null || value.isEmpty()) {
073            throw new NumberFormatException("invalid");
074        }
075        if (!check()) {
076            throw new NumberFormatException("invalid createChecksum");
077        }
078        return this;
079    }
080
081    @Override
082    public String normalizedValue() {
083        return value;
084    }
085
086    @Override
087    public String format() {
088        return value;
089    }
090
091    @Override
092    public ISMN reset() {
093        this.value = null;
094        this.createWithChecksum = false;
095        return this;
096    }
097
098    public GTIN toGTIN() throws NumberFormatException {
099        return new GTIN().set(value).normalize().verify();
100    }
101
102    private boolean check() {
103        int l = createWithChecksum ? value.length() : value.length() - 1;
104        int checksum = 0;
105        int weight;
106        int val;
107        for (int i = 0; i < l; i++) {
108            val = value.charAt(i) - '0';
109            weight = i % 2 == 0 ? 1 : 3;
110            checksum += val * weight;
111        }
112        int chk = 10 - checksum % 10;
113        if (createWithChecksum) {
114            char ch = (char)('0' + chk);
115            value = value + ch;
116        }
117        return chk == value.charAt(l) - '0';
118    }
119
120    private String dehyphenate(String isbn) {
121        StringBuilder sb = new StringBuilder(isbn);
122        int i = sb.indexOf("-");
123        while (i >= 0) {
124            sb.deleteCharAt(i);
125            i = sb.indexOf("-");
126        }
127        return sb.toString();
128    }
129}