Task 325: .JCM File Format

Task 325: .JCM File Format

The .JCM file format is a variant extension used for JCAMP-DX, an IUPAC standard text-based format for exchanging spectroscopic data, particularly NMR data in this case (versions 4.24 for core structure and 5.01 for NMR-specific extensions). It consists of ASCII text with labeled data records (LDRs) for headers and metadata, followed by tabular spectral data that may be compressed using ASDF (ASCII Squeezed Difference Form). Files are structured as one or more blocks, each starting with ##TITLE= and ending with ##END=.

The properties intrinsic to the .JCM (JCAMP-DX) file format are the labeled data records (LDRs), which define metadata, parameters, notes, and data. These are key-value pairs starting with ## (or ##. for vendor-specific), with values in TEXT, STRING, or AFFN (ASCII Free Format Numeric) formats. The full list of required and optional LDRs (including NMR-specific from version 5.01) is:

  • TITLE (required): Descriptive title of the spectrum or block.
  • JCAMP-DX (required): Version number (e.g., 4.24 or 5.01).
  • DATA TYPE (required): Type of data (e.g., NMR SPECTRUM, FID, LINK for compound files).
  • XUNITS (required): Abscissa units (e.g., HZ, PPM).
  • YUNITS (required): Ordinate units (e.g., ARBITRARY UNITS, POWER, MAGNITUDE for transformed NMR data).
  • FIRSTX (required): First abscissa value.
  • LASTX (required): Last abscissa value.
  • NPOINTS (required): Number of data points.
  • FIRSTY (required): First ordinate value.
  • XFACTOR (required): Scaling factor for X values.
  • YFACTOR (required): Scaling factor for Y values.
  • ORIGIN (required): Contributor of the data.
  • OWNER (required): Data owner, including copyright.
  • END (required): End of block marker.
  • RESOLUTION (optional): Spectral resolution.
  • DELTAX (optional): Spacing between points.
  • MAXX (optional): Maximum X value.
  • MINX (optional): Minimum X value.
  • MAXY (optional): Maximum Y value.
  • MINY (optional): Minimum Y value.
  • XYDATA (optional): Tabular spectral data (may be ASDF-compressed).
  • XYPOINTS (optional): X,Y pairs for irregular data.
  • PEAK TABLE (optional): Peak positions and intensities.
  • PEAK ASSIGNMENTS (optional): Assigned peaks with descriptions.
  • CLASS (optional): Spectrum classification (e.g., Coblentz class).
  • DATE (optional): Measurement date.
  • TIME (optional): Measurement time.
  • LONG DATE (optional): Extended date/time with Y2K compliance and timezone.
  • SAMPLE DESCRIPTION (optional): Sample details, including concentration for NMR.
  • CAS NAME (optional): CAS chemical name.
  • MOLFORM (optional): Molecular formula.
  • SPECTROMETER/DATA SYSTEM (optional): Instrument details.
  • SAMPLING PROCEDURE (optional): Sampling method (legacy; use PULSE SEQUENCE for NMR).
  • AUDIT TRAIL (optional): GLP audit log.
  • PULSE SEQUENCE (NMR optional): Pulse sequence details.
  • SOLVENT NAME (NMR optional): Solvent description, including pH.
  • SHIFT REFERENCE (NMR optional): Chemical shift reference (internal/external, compound, point, value).
  • MAS FREQUENCY (NMR optional): Magic angle spinning frequency in Hz.
  • SOLVENT REFERENCE (NMR optional, legacy): Solvent reference shift.
  • OBSERVE FREQUENCY (NMR optional): Observation frequency for PPM scaling.

Two direct download links for .JCM files (test samples for JCAMP-DX NMR data):

Here is the embedded HTML/JavaScript code for a Ghost blog post that allows drag-and-drop of a .JCM file and dumps all properties to the screen:

Drag and drop a .JCM file here to dump its properties.
  1. Here is a Python class for handling .JCM files:
import os

class JCMFile:
    def __init__(self, filename=None):
        self.properties = {}
        if filename:
            self.read(filename)

    def read(self, filename):
        with open(filename, 'r') as f:
            lines = f.readlines()
        current_key = None
        multi_line_value = ''
        for line in lines:
            line = line.strip()
            if line.startswith('##'):
                if current_key:
                    self.properties[current_key] = multi_line_value.strip()
                parts = line.split('=', 1)
                if len(parts) == 2:
                    key = parts[0].replace('##.', '').replace('##', '').strip()
                    multi_line_value = parts[1].strip()
                    current_key = key
                else:
                    current_key = None
            elif current_key:
                multi_line_value += '\n' + line
        if current_key:
            self.properties[current_key] = multi_line_value.strip()

    def decode(self):
        # Basic decode: properties are already parsed. For ASDF data decoding (e.g., XYDATA), add custom logic here if needed.
        pass  # Placeholder; extend for ASDF decompression if required beyond printing raw properties.

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key}: {value}")

    def write(self, filename):
        with open(filename, 'w') as f:
            for key, value in self.properties.items():
                f.write(f"##{key}={value}\n")

# Example usage:
# jcm = JCMFile('example.jcm')
# jcm.print_properties()
# jcm.write('output.jcm')
  1. Here is a Java class for handling .JCM files:
import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class JCMFile {
    private Map<String, String> properties = new HashMap<>();

    public JCMFile(String filename) throws IOException {
        read(filename);
    }

    public JCMFile() {}

    public void read(String filename) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
            String line;
            String currentKey = null;
            StringBuilder multiLineValue = new StringBuilder();
            while ((line = br.readLine()) != null) {
                line = line.trim();
                if (line.startsWith("##")) {
                    if (currentKey != null) {
                        properties.put(currentKey, multiLineValue.toString().trim());
                    }
                    String[] parts = line.split("=", 2);
                    if (parts.length == 2) {
                        String key = parts[0].replaceAll("^##[\\.]?", "").trim();
                        multiLineValue = new StringBuilder(parts[1].trim());
                        currentKey = key;
                    } else {
                        currentKey = null;
                    }
                } else if (currentKey != null) {
                    multiLineValue.append("\n").append(line);
                }
            }
            if (currentKey != null) {
                properties.put(currentKey, multiLineValue.toString().trim());
            }
        }
    }

    public void decode() {
        // Basic decode: properties are parsed. Extend for ASDF decompression if needed.
    }

    public void printProperties() {
        for (Map.Entry<String, String> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

    public void write(String filename) throws IOException {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename))) {
            for (Map.Entry<String, String> entry : properties.entrySet()) {
                bw.write("##" + entry.getKey() + "=" + entry.getValue() + "\n");
            }
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     JCMFile jcm = new JCMFile("example.jcm");
    //     jcm.printProperties();
    //     jcm.write("output.jcm");
    // }
}
  1. Here is a JavaScript class for handling .JCM files (node.js compatible for file I/O):
const fs = require('fs');

class JCMFile {
  constructor(filename) {
    this.properties = {};
    if (filename) {
      this.read(filename);
    }
  }

  read(filename) {
    const text = fs.readFileSync(filename, 'utf8');
    const lines = text.split('\n');
    let currentKey = null;
    let multiLineValue = '';
    for (let line of lines) {
      line = line.trim();
      if (line.startsWith('##')) {
        if (currentKey) {
          this.properties[currentKey] = multiLineValue.trim();
        }
        const parts = line.split('=', 2);
        if (parts.length === 2) {
          const key = parts[0].replace(/^##[\.]?/, '').trim();
          multiLineValue = parts[1].trim();
          currentKey = key;
        } else {
          currentKey = null;
        }
      } else if (currentKey) {
        multiLineValue += '\n' + line;
      }
    }
    if (currentKey) {
      this.properties[currentKey] = multiLineValue.trim();
    }
  }

  decode() {
    // Basic decode: properties are parsed. Extend for ASDF decompression if needed.
  }

  printProperties() {
    for (const [key, value] of Object.entries(this.properties)) {
      console.log(`${key}: ${value}`);
    }
  }

  write(filename) {
    let content = '';
    for (const [key, value] of Object.entries(this.properties)) {
      content += `##${key}=${value}\n`;
    }
    fs.writeFileSync(filename, content);
  }
}

// Example usage:
// const jcm = new JCMFile('example.jcm');
// jcm.printProperties();
// jcm.write('output.jcm');
  1. Here is a C implementation (using struct as a "class" equivalent) for handling .JCM files:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_PROPS 100
#define MAX_KEY_LEN 64
#define MAX_VALUE_LEN 4096

typedef struct {
    char keys[MAX_PROPS][MAX_KEY_LEN];
    char values[MAX_PROPS][MAX_VALUE_LEN];
    int count;
} JCMFile;

void jcm_init(JCMFile *jcm) {
    jcm->count = 0;
}

void jcm_read(JCMFile *jcm, const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (!fp) {
        perror("Failed to open file");
        return;
    }
    char line[8192];
    char *current_key = NULL;
    char multi_line_value[MAX_VALUE_LEN] = "";
    int value_len = 0;
    while (fgets(line, sizeof(line), fp)) {
        char *trimmed = strtok(line, "\r\n");
        if (!trimmed) continue;
        if (strncmp(trimmed, "##", 2) == 0) {
            if (current_key) {
                multi_line_value[value_len] = '\0';
                strcpy(jcm->values[jcm->count], multi_line_value);
                strcpy(jcm->keys[jcm->count++], current_key);
            }
            char *eq = strchr(trimmed, '=');
            if (eq) {
                *eq = '\0';
                char *key = trimmed + 2;
                if (*key == '.') key++;
                while (*key == ' ') key++;
                current_key = key;
                char *value = eq + 1;
                while (*value == ' ') value++;
                strcpy(multi_line_value, value);
                value_len = strlen(value);
            } else {
                current_key = NULL;
            }
        } else if (current_key) {
            multi_line_value[value_len++] = '\n';
            strcpy(multi_line_value + value_len, trimmed);
            value_len += strlen(trimmed);
        }
    }
    if (current_key) {
        multi_line_value[value_len] = '\0';
        strcpy(jcm->values[jcm->count], multi_line_value);
        strcpy(jcm->keys[jcm->count++], current_key);
    }
    fclose(fp);
}

void jcm_decode(JCMFile *jcm) {
    // Basic decode: properties are parsed. Extend for ASDF decompression if needed.
}

void jcm_print_properties(JCMFile *jcm) {
    for (int i = 0; i < jcm->count; i++) {
        printf("%s: %s\n", jcm->keys[i], jcm->values[i]);
    }
}

void jcm_write(JCMFile *jcm, const char *filename) {
    FILE *fp = fopen(filename, "w");
    if (!fp) {
        perror("Failed to open file");
        return;
    }
    for (int i = 0; i < jcm->count; i++) {
        fprintf(fp, "##%s=%s\n", jcm->keys[i], jcm->values[i]);
    }
    fclose(fp);
}

// Example usage:
// int main() {
//     JCMFile jcm;
//     jcm_init(&jcm);
//     jcm_read(&jcm, "example.jcm");
//     jcm_print_properties(&jcm);
//     jcm_write(&jcm, "output.jcm");
//     return 0;
// }