petuomin/marc-record-js

View on GitHub
lib/MarcRecord.js

Summary

Maintainability
F
3 days
Test Coverage

/*
{
  leader: 'string',
  fields: [
    // For controlfields:
    // format allows multiple controlfields with same tag
    {
      tag: 'string',
      value: 'string'
    }

    // For data fields
    {
      tag: 'string',
      ind1: 'char',
      ind2: 'char',
      subfields: [
        {
          code: 'char'
          value: 'string'
        }
      ]
    }
  ]
}
*/

(function(root, factory) {
  'use strict';
  
  /* istanbul ignore next: umd wrapper */
  if (typeof define === 'function' && define.amd) { // jshint ignore:line
    define([], factory); // jshint ignore:line
  } else if(typeof exports === 'object') {
    module.exports = factory();  // jshint ignore:line
  } else {
    root.MARCRecord = factory();
  }
}(this, function() {
  'use strict';

  function Record(rec) {

    var CONTROL_FIELD_LIST = ['FMT', '001', '002', '003', '004', '005', '006', '007', '008', '009'];

    if (rec !== undefined) {
      this.leader = rec.leader;
      this.fields = JSON.parse(JSON.stringify(rec.fields));
    } else {
      this.leader = '';
      this.fields = [];
    }

    this.get = function(query) {
      return this.fields.filter(function(field) {
        return field.tag.match(query);
      });
    };

    this.setLeader = function(newLeader) {
      this.leader = newLeader;
    };

    this.appendControlField = function(fieldDef) {
      this.insertControlField(fieldDef, this.fields.length);
    };

    this.insertControlField = function(fieldDef, index) {
      if (isArray(fieldDef)) {
        if (fieldDef.length != 2) {
          throw new Error('Control field array must contain exactly 2 elements: [tag, value]');
        }
        this.insertControlField({
          tag: fieldDef[0],
          value: fieldDef[1]
        }, index);

      } else {
        if (typeof fieldDef.tag !== 'string') {
          throw new Error('Tag must be string type, not ' + typeof fieldDef.tag);
        }
        if (index === undefined) {
          index = this.findPosition(fieldDef.tag);
        }
        this.fields.splice(index, 0, fieldDef);
      }
    };

    this.appendField = function(fieldDef) {
      this.insertField(fieldDef, this.fields.length);
    };

    this.insertField = function(fieldDef, index) {
      
      if (isArray(fieldDef)) {
        if (fieldDef.length < 5) {
          throw new Error('Data field array must contain at least 5 elements: [tag, i1, i2, subcode1, subvalue1]');
        }
        var tag = fieldDef[0];
        var ind1 = fieldDef[1];
        var ind2 = fieldDef[2];
        var subfields = [];

        var subfieldArray = fieldDef.slice(3);

        if (subfieldArray.length % 2 !== 0) {
          throw new Error('Each subfield must have a code and value');
        }
        while (subfieldArray.length > 0) {
          var subfield = subfieldArray.splice(0,2);
          subfields.push({
            code: subfield[0],
            value: subfield[1]
          });
        }

        this.insertField({
          tag: tag,
          ind1: ind1,
          ind2: ind2,
          subfields: subfields
        }, index);

      } else {
        if (typeof fieldDef.tag !== 'string') {
          throw new Error('Tag must be string type, not ' + typeof fieldDef.tag);
        }

        validateIndicator(fieldDef.ind1);
        validateIndicator(fieldDef.ind2);

        if (index === undefined) {
          index = this.findPosition(fieldDef.tag);
        }

        this.fields.splice(index, 0, fieldDef);
      }
    };

    this.findPosition = function(tag) {
      var i;
      for (i=0;i<this.fields.length;i++) {
        if (this.fields[i].tag > tag) {
          return i;
        }
      }
      return this.fields.length;
    };

    this.getControlfields = function() {
      return this.fields.filter(function(field) {
        return CONTROL_FIELD_LIST.indexOf(field.tag) !== -1;
      });
    };

    this.getDatafields = function() {
      return this.fields.filter(function(field) {
        return CONTROL_FIELD_LIST.indexOf(field.tag) === -1;
      });
    };

    this.getFields = function() {
      var args = Array.prototype.slice.call(arguments);
      var tag = args[0];
      if (args.length == 2) {

        return this.fields.filter(function(field) {
          return field.tag === tag && field.value == args[1];
        });

      } else {
        var queryParams = args.slice(1).reduce(pairsReducer, [])
          .filter(function(pair) {
            return pair.length == 2;
          })
          .map(function(pair) {
            return {code: pair[0], value: pair[1]};
          });

        return this.fields.filter(function(field) {
          return field.tag === tag;
        }).filter(function(field) {
          return queryParams.every(function(expectedSubfield) {
            return field.subfields.some(function(subfield) {
              return subfield.code === expectedSubfield.code && subfield.value === expectedSubfield.value;
            });
          });
        });

      }
          
  
    };

    this.containsFieldWithValue = function() {
      var args = Array.prototype.slice.call(arguments);
      if (args.length < 2) {
        throw new Error('At least 2 arguments are required');
      }
      var tag = args[0];
      if (args.length == 2) {
        if (args[1].constructor === Array) {
          return querySubfields(this.getFields(tag), args[1]);
        } else {
          var expectedValue = args[1];
          return this.getFields(tag).some(function(field) {
            return field.value === expectedValue;
          });
        }
      } else {
        var expectedValues = args.slice(1).reduce(pairsReducer, [])
          .filter(function(pair) {
            return pair.length == 2;
          })
          .map(function(pair) {
            return {code: pair[0], value: pair[1]};
          });

        return querySubfields(this.getFields(tag), expectedValues);
      }

      // Some field has every expected subfield (and maybe some others too)
      function querySubfields(fields, subfieldQueryArray) {
        return fields.some(function(field) {
          return subfieldQueryArray.every(function(expectedSubfield) {
            return field.subfields.some(function(subfield) {
              return subfield.code === expectedSubfield.code && subfield.value === expectedSubfield.value;
            });
          });
        });
      }
    };

    function pairsReducer(acc, value, i) {
      if (i%2 === 0) {
        acc.push([]);
      }
      acc[acc.length-1].push(value);
      return acc;
    }

    this.isDeleted = function() {
      if (this.containsFieldWithValue('STA','a','DELETED')) {
        return true;
      }
      if (this.containsFieldWithValue('DEL','a','Y')) {
        return true;
      }
      if (this.leader && this.leader.length >= 6) {
        return this.leader.substr(5,1) === 'd';
      }
      
      return false;
    };

    this.equalsTo = function(record) {
      return Record.isEqual(this, record);
    };

    function validateIndicator(indicator) {
      if (indicator === undefined || indicator === null) {
        return true;
      }

      if (indicator.length > 1) {
        throw new Error('Indicators must be at most 1 characters long.');
      }

      return true;
    }

    this.toString = function() {
      var controlFieldLines = [];
      var dataFieldLines = [];
      controlFieldLines.push(['LDR', this.leader]);
      this.fields.forEach(function(field) {
        if (isControlField(field)) {
          controlFieldLines.push([field.tag, field.value]);
        } else {
          var subfieldDataArray = field.subfields.map(function(subfield) {
            return subfield.code + subfield.value;
          });

          dataFieldLines.push([field.tag, field.ind1, field.ind2, subfieldDataArray]);
        }
      });

      var lines = [
        controlFieldLines.map(controlFieldStringFormatter).join('\n'),
        dataFieldLines.map(dataFieldStringFormatter).join('\n')
      ];

      return lines.join('\n');

    };

    this.toJsonObject = function() {
      return {
        leader: this.leader,
        fields: JSON.parse(JSON.stringify(this.fields))
      };
    };

    function controlFieldStringFormatter(controlFieldArray) {
      return controlFieldArray[0] + '    ' + controlFieldArray[1];
    }

    function dataFieldStringFormatter(dataFieldArray) {
      var subfieldMarker = '‡';

      var indicatorsString = '';
      indicatorsString += indicatorStringFormatter(dataFieldArray[1]);
      indicatorsString += indicatorStringFormatter(dataFieldArray[2]);

      return dataFieldArray[0] + ' ' +indicatorsString+' ' + subfieldMarker + dataFieldArray[3].join(subfieldMarker);
    }

    function indicatorStringFormatter(indicator) {
      if (indicator === undefined || indicator === null) {
        return ' ';
      }
      if (indicator.length === 0) {
        return ' ';
      }
      return indicator;

    }

    function isControlField(field) {
      return (field.tag !== undefined && field.value !== undefined);
    }

    function isArray(value) {
      return Object.prototype.toString.call( value ) === '[object Array]';
    }
  }

  Record.fromString = function(stringRep) {

    var rec = new Record();

    var fields = stringRep.split('\n');

    fields.forEach(function(fieldLine) {

      var tag = fieldLine.substr(0,3);
      var ind1 = fieldLine.substr(4,1);
      var ind2 = fieldLine.substr(5,1);
      var data = fieldLine.substr(7);

      if (tag == 'LDR') {
        rec.setLeader(data);
      } else {

        if (data.substr(0,1) !== '‡') {
          //controlfield
          rec.appendControlField([tag, data]);

        } else {
          var subfieldArray = data.substr(1).split('‡').map(function(codeAndValue) { 
            return [codeAndValue.substr(0,1), codeAndValue.substr(1)]; 
          });

          var flattened = Array.prototype.concat.apply([], subfieldArray);

          rec.appendField([tag,ind1,ind2].concat(flattened));

        }
      }
    });

    return rec;

  };

  Record.clone = function(record) {
    return new Record(record);
  };

  Record.isEqual = function(record1, record2) {
    return record1.leader === record2.leader && fieldsEqual(record1.fields, record2.fields);

    function fieldsEqual(fields1, fields2) {
      
      if (fields1.length !== fields2.length) return false;
      return fields1.every(function(field, index) {

        if (!compareAttributes(['tag', 'value', 'ind1', 'ind2'])(fields1[index], fields2[index])) {
          return false;
        }
        
        var bothHaveSubfields = fields1[index].subfields && fields2[index].subfields;

        if (bothHaveSubfields && fields1[index].subfields.length !== fields2[index].subfields.length) return false;

        if (fields1[index].subfields) {

          return fields1[index].subfields.every(function(subfield, subfieldIndex) {
            var field1Subfield = fields1[index].subfields[subfieldIndex];
            var field2Subfield = fields2[index].subfields[subfieldIndex];
            return subfieldsEqual(field1Subfield, field2Subfield);
          });
        } else {
          return true;
        }
      });
    }

    function compareAttributes(attributeKeyList) {
      return function(obj1, obj2) {
        return attributeKeyList.every(function(key) {
          return obj1[key] === obj2[key];
        });
      };
    }

    function subfieldsEqual(sub1, sub2) {
      return sub1.code === sub2.code && sub1.value === sub2.value;
    }
  };

  return Record;
}));