Task 441: .NC File Format

Task 441: .NC File Format

File Format Specifications for .NC (NetCDF)

The .NC file extension is commonly associated with the NetCDF (Network Common Data Form) format, a self-describing, machine-independent binary format for storing array-oriented scientific data, such as multidimensional arrays in climate, oceanography, and atmospheric sciences. It supports dimensions, variables, and attributes. There are several variants: Classic (original, 32-bit offsets), 64-bit offset (for larger files), and HDF5-based (NetCDF-4). Based on the specifications, I'll focus on the NetCDF Classic format (version 1), as it's the foundational one and matches the typical .NC usage without additional qualifiers in the query. The format is defined in big-endian byte order, with 4-byte alignment padding using null bytes in headers.

The specifications are sourced from the official Unidata documentation, which provides a BNF grammar for the structure.

  1. List of All Properties Intrinsic to This File Format

The properties refer to the structural and metadata elements defined in the NetCDF Classic format specification. These are intrinsic to the format's structure (header and data layout) and include:

  • Magic String: The file signature ('CDF' followed by the version byte, e.g., \x01 for Classic).
  • Version: The format version (1 for Classic).
  • Number of Records: The current length of the record (unlimited) dimension, if present (a 32-bit non-negative integer or a special streaming value).
  • Dimensions: A list of dimensions, each with:
  • Name (UTF-8 string, padded to 4 bytes).
  • Length (32-bit non-negative integer; 0 indicates the unlimited/record dimension; at most one per file).
  • Global Attributes: A list of file-level attributes, each with:
  • Name (UTF-8 string, padded).
  • Type (one of: NC_BYTE, NC_CHAR, NC_SHORT, NC_INT, NC_FLOAT, NC_DOUBLE).
  • Number of elements (32-bit non-negative integer).
  • Values (array of the specified type, padded to 4 bytes).
  • Variables: A list of variables, each with:
  • Name (UTF-8 string, padded).
  • Rank (number of dimensions, 32-bit integer).
  • Dimension IDs (list of 32-bit indices referencing the dimensions list; first ID=0 for record variables).
  • Variable Attributes (similar to global attributes: name, type, number of elements, values).
  • Type (same as attribute types).
  • VSize (32-bit size in bytes for the variable's data per record or total, padded to 4 bytes).
  • Begin Offset (32-bit offset from file start to the variable's data).
  • Fill Values: Default or overridden (_FillValue attribute) values for unwritten data (type-specific defaults, e.g., -127 for NC_BYTE).
  • Data Layout: Non-record variables stored contiguously in row-major order; record variables interleaved at the end. (Note: Actual data values are not "properties" but can be read; properties focus on metadata.)

These properties define the self-describing nature of the format, allowing decoding without external schema.

  1. Two Direct Download Links for .NC Files
  1. Ghost Blog Embedded HTML JavaScript for Drag and Drop

Here's a complete HTML page with embedded JavaScript that can be embedded in a blog (e.g., Ghost platform). It creates a drop zone where users can drag and drop a .NC file. The script uses FileReader to read the file as an ArrayBuffer, parses the NetCDF Classic header, extracts the properties, and dumps them to the screen in a element.

NetCDF .NC File Property Dumper
Drag and drop a .NC file here


    

This script parses the header for properties and displays them in JSON format. It handles padding, big-endian, and basic types. For write functionality, it's not included as drag-and-drop is read-only; use the JavaScript class below for node-based write.

  1. Python Class for .NC Files

Here's a Python class that can open, decode (read), write, and print the properties to console. It uses struct for binary parsing/packing, assuming Classic format. Write creates a new file with the parsed structure (or modified).

import struct
import sys

class NetCDFHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = None

    def read(self):
        with open(self.filepath, 'rb') as f:
            data = f.read()
        self.properties = self._parse(data)
        return self.properties

    def _parse(self, data):
        offset = 0
        magic = data[offset:offset+3].decode('ascii')
        if magic != 'CDF':
            raise ValueError('Not a NetCDF file')
        offset += 3
        version, = struct.unpack('>B', data[offset:offset+1])
        offset += 1
        if version != 1:
            raise ValueError('Only Classic format supported')
        numrecs, = struct.unpack('>I', data[offset:offset+4])
        offset += 4

        # Dimensions
        dim_tag, = struct.unpack('>I', data[offset:offset+4])
        offset += 4
        dims = []
        if dim_tag == 10:
            nelems, = struct.unpack('>I', data[offset:offset+4])
            offset += 4
            for _ in range(nelems):
                name_len, = struct.unpack('>I', data[offset:offset+4])
                offset += 4
                name = data[offset:offset+name_len].decode('utf-8')
                offset += name_len
                offset += self._pad(name_len)
                length, = struct.unpack('>I', data[offset:offset+4])
                offset += 4
                dims.append({'name': name, 'length': length})

        # Global attributes
        gatt_tag, = struct.unpack('>I', data[offset:offset+4])
        offset += 4
        global_attrs = []
        if gatt_tag == 12:
            nelems, = struct.unpack('>I', data[offset:offset+4])
            offset += 4
            for _ in range(nelems):
                attr, new_offset = self._parse_attr(data, offset)
                global_attrs.append(attr)
                offset = new_offset

        # Variables
        var_tag, = struct.unpack('>I', data[offset:offset+4])
        offset += 4
        vars = []
        if var_tag == 11:
            nelems, = struct.unpack('>I', data[offset:offset+4])
            offset += 4
            for _ in range(nelems):
                name_len, = struct.unpack('>I', data[offset:offset+4])
                offset += 4
                name = data[offset:offset+name_len].decode('utf-8')
                offset += name_len
                offset += self._pad(name_len)
                rank, = struct.unpack('>I', data[offset:offset+4])
                offset += 4
                dimids = []
                for _ in range(rank):
                    dimid, = struct.unpack('>I', data[offset:offset+4])
                    offset += 4
                    dimids.append(dimid)
                vatt_tag, = struct.unpack('>I', data[offset:offset+4])
                offset += 4
                var_attrs = []
                if vatt_tag == 12:
                    nelems_attr, = struct.unpack('>I', data[offset:offset+4])
                    offset += 4
                    for _ in range(nelems_attr):
                        attr, new_offset = self._parse_attr(data, offset)
                        var_attrs.append(attr)
                        offset = new_offset
                type, = struct.unpack('>I', data[offset:offset+4])
                offset += 4
                vsize, = struct.unpack('>I', data[offset:offset+4])
                offset += 4
                begin, = struct.unpack('>I', data[offset:offset+4])
                offset += 4
                vars.append({'name': name, 'rank': rank, 'dimids': dimids, 'attributes': var_attrs, 'type': self._type_str(type), 'vsize': vsize, 'begin': begin})

        return {
            'magic': 'CDF',
            'version': version,
            'numrecs': numrecs,
            'dimensions': dims,
            'global_attributes': global_attrs,
            'variables': vars
        }

    def _pad(self, len_):
        return (4 - (len_ % 4)) % 4

    def _type_str(self, type_):
        types = {1: 'NC_BYTE', 2: 'NC_CHAR', 3: 'NC_SHORT', 4: 'NC_INT', 5: 'NC_FLOAT', 6: 'NC_DOUBLE'}
        return types.get(type_, 'Unknown')

    def _get_type_size(self, type_):
        sizes = {1: 1, 2: 1, 3: 2, 4: 4, 5: 4, 6: 8}
        return sizes.get(type_, 0)

    def _parse_attr(self, data, start_offset):
        offset = start_offset
        name_len, = struct.unpack('>I', data[offset:offset+4])
        offset += 4
        name = data[offset:offset+name_len].decode('utf-8')
        offset += name_len
        offset += self._pad(name_len)
        type_, = struct.unpack('>I', data[offset:offset+4])
        offset += 4
        nelems, = struct.unpack('>I', data[offset:offset+4])
        offset += 4
        values = []
        fmt = self._get_fmt(type_)
        size = self._get_type_size(type_)
        for _ in range(nelems):
            val, = struct.unpack(fmt, data[offset:offset+size])
            values.append(val)
            offset += size
        offset += self._pad(nelems * size)
        return {'name': name, 'type': self._type_str(type_), 'values': values}, offset

    def _get_fmt(self, type_):
        fmts = {1: '>B', 2: '>c', 3: '>h', 4: '>i', 5: '>f', 6: '>d'}
        return fmts.get(type_, '>i')

    def print_properties(self):
        if not self.properties:
            self.read()
        print(self.properties)

    def write(self, new_filepath, properties=None):
        if properties is None:
            properties = self.properties or self.read()
        with open(new_filepath, 'wb') as f:
            f.write(b'CDF')
            f.write(struct.pack('>B', properties['version']))
            f.write(struct.pack('>I', properties['numrecs']))

            # Dimensions
            if properties['dimensions']:
                f.write(struct.pack('>I', 10))  # NC_DIMENSION
                f.write(struct.pack('>I', len(properties['dimensions'])))
                for dim in properties['dimensions']:
                    name_b = dim['name'].encode('utf-8')
                    f.write(struct.pack('>I', len(name_b)))
                    f.write(name_b)
                    f.write(b'\x00' * self._pad(len(name_b)))
                    f.write(struct.pack('>I', dim['length']))
            else:
                f.write(struct.pack('>II', 0, 0))

            # Global attributes
            if properties['global_attributes']:
                f.write(struct.pack('>I', 12))
                f.write(struct.pack('>I', len(properties['global_attributes'])))
                for attr in properties['global_attributes']:
                    self._write_attr(f, attr)
            else:
                f.write(struct.pack('>II', 0, 0))

            # Variables
            if properties['variables']:
                f.write(struct.pack('>I', 11))
                f.write(struct.pack('>I', len(properties['variables'])))
                for var in properties['variables']:
                    name_b = var['name'].encode('utf-8')
                    f.write(struct.pack('>I', len(name_b)))
                    f.write(name_b)
                    f.write(b'\x00' * self._pad(len(name_b)))
                    f.write(struct.pack('>I', var['rank']))
                    for dimid in var['dimids']:
                        f.write(struct.pack('>I', dimid))
                    if var['attributes']:
                        f.write(struct.pack('>I', 12))
                        f.write(struct.pack('>I', len(var['attributes'])))
                        for attr in var['attributes']:
                            self._write_attr(f, attr)
                    else:
                        f.write(struct.pack('>II', 0, 0))
                    type_num = self._str_to_type(var['type'])
                    f.write(struct.pack('>I', type_num))
                    f.write(struct.pack('>I', var['vsize']))
                    f.write(struct.pack('>I', var['begin']))
            else:
                f.write(struct.pack('>II', 0, 0))
            # Note: Data writing not implemented here; this writes header only.

    def _str_to_type(self, type_str):
        types = {'NC_BYTE': 1, 'NC_CHAR': 2, 'NC_SHORT': 3, 'NC_INT': 4, 'NC_FLOAT': 5, 'NC_DOUBLE': 6}
        return types.get(type_str, 4)

    def _write_attr(self, f, attr):
        name_b = attr['name'].encode('utf-8')
        f.write(struct.pack('>I', len(name_b)))
        f.write(name_b)
        f.write(b'\x00' * self._pad(len(name_b)))
        type_num = self._str_to_type(attr['type'])
        f.write(struct.pack('>I', type_num))
        f.write(struct.pack('>I', len(attr['values'])))
        fmt = self._get_fmt(type_num)
        size = self._get_type_size(type_num)
        for val in attr['values']:
            f.write(struct.pack(fmt, val))
        f.write(b'\x00' * self._pad(len(attr['values']) * size))

# Example usage:
# handler = NetCDFHandler('example.nc')
# handler.print_properties()
# handler.write('new.nc')

This class reads the file, parses properties, prints them as a dict, and can write a new header-based file.

  1. Java Class for .NC Files

Here's a Java class using ByteBuffer for big-endian parsing. It opens, reads, writes, and prints properties to console.

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class NetCDFHandler {
    private String filepath;
    private Map<String, Object> properties;

    public NetCDFHandler(String filepath) {
        this.filepath = filepath;
    }

    public Map<String, Object> read() throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile(filepath, "r")) {
            FileChannel channel = raf.getChannel();
            long size = channel.size();
            ByteBuffer buffer = ByteBuffer.allocate((int) size).order(ByteOrder.BIG_ENDIAN);
            channel.read(buffer);
            buffer.flip();
            properties = parse(buffer);
        }
        return properties;
    }

    private Map<String, Object> parse(ByteBuffer bb) {
        Map<String, Object> props = new HashMap<>();
        byte[] magicBytes = new byte[3];
        bb.get(magicBytes);
        String magic = new String(magicBytes, StandardCharsets.US_ASCII);
        if (!magic.equals("CDF")) throw new RuntimeException("Not a NetCDF file");
        props.put("magic", "CDF");
        byte version = bb.get();
        if (version != 1) throw new RuntimeException("Only Classic format supported");
        props.put("version", version);
        int numrecs = bb.getInt();
        props.put("numrecs", numrecs);

        // Dimensions
        int dimTag = bb.getInt();
        List<Map<String, Object>> dims = new ArrayList<>();
        if (dimTag == 10) {
            int nelems = bb.getInt();
            for (int i = 0; i < nelems; i++) {
                int nameLen = bb.getInt();
                byte[] nameBytes = new byte[nameLen];
                bb.get(nameBytes);
                String name = new String(nameBytes, StandardCharsets.UTF_8);
                bb.position(bb.position() + pad(nameLen));
                int length = bb.getInt();
                Map<String, Object> dim = new HashMap<>();
                dim.put("name", name);
                dim.put("length", length);
                dims.add(dim);
            }
        } else if (dimTag != 0) throw new RuntimeException("Invalid dim tag");
        props.put("dimensions", dims);

        // Global attributes
        int gattTag = bb.getInt();
        List<Map<String, Object>> globalAttrs = new ArrayList<>();
        if (gattTag == 12) {
            int nelems = bb.getInt();
            for (int i = 0; i < nelems; i++) {
                Map<String, Object> attr = parseAttr(bb);
                globalAttrs.add(attr);
            }
        } else if (gattTag != 0) throw new RuntimeException("Invalid gatt tag");
        props.put("global_attributes", globalAttrs);

        // Variables
        int varTag = bb.getInt();
        List<Map<String, Object>> vars = new ArrayList<>();
        if (varTag == 11) {
            int nelems = bb.getInt();
            for (int i = 0; i < nelems; i++) {
                int nameLen = bb.getInt();
                byte[] nameBytes = new byte[nameLen];
                bb.get(nameBytes);
                String name = new String(nameBytes, StandardCharsets.UTF_8);
                bb.position(bb.position() + pad(nameLen));
                int rank = bb.getInt();
                List<Integer> dimids = new ArrayList<>();
                for (int j = 0; j < rank; j++) {
                    dimids.add(bb.getInt());
                }
                int vattTag = bb.getInt();
                List<Map<String, Object>> varAttrs = new ArrayList<>();
                if (vattTag == 12) {
                    int nelemsAttr = bb.getInt();
                    for (int j = 0; j < nelemsAttr; j++) {
                        Map<String, Object> attr = parseAttr(bb);
                        varAttrs.add(attr);
                    }
                } else if (vattTag != 0) throw new RuntimeException("Invalid vatt tag");
                int type = bb.getInt();
                int vsize = bb.getInt();
                int begin = bb.getInt();
                Map<String, Object> var = new HashMap<>();
                var.put("name", name);
                var.put("rank", rank);
                var.put("dimids", dimids);
                var.put("attributes", varAttrs);
                var.put("type", typeToString(type));
                var.put("vsize", vsize);
                var.put("begin", begin);
                vars.add(var);
            }
        } else if (varTag != 0) throw new RuntimeException("Invalid var tag");
        props.put("variables", vars);

        return props;
    }

    private int pad(int len) {
        return (4 - (len % 4)) % 4;
    }

    private String typeToString(int type) {
        switch (type) {
            case 1: return "NC_BYTE";
            case 2: return "NC_CHAR";
            case 3: return "NC_SHORT";
            case 4: return "NC_INT";
            case 5: return "NC_FLOAT";
            case 6: return "NC_DOUBLE";
            default: return "Unknown";
        }
    }

    private int getTypeSize(int type) {
        switch (type) {
            case 1: case 2: return 1;
            case 3: return 2;
            case 4: case 5: return 4;
            case 6: return 8;
            default: return 0;
        }
    }

    private Map<String, Object> parseAttr(ByteBuffer bb) {
        Map<String, Object> attr = new HashMap<>();
        int nameLen = bb.getInt();
        byte[] nameBytes = new byte[nameLen];
        bb.get(nameBytes);
        String name = new String(nameBytes, StandardCharsets.UTF_8);
        bb.position(bb.position() + pad(nameLen));
        int type = bb.getInt();
        int nelems = bb.getInt();
        List<Object> values = new ArrayList<>();
        int size = getTypeSize(type);
        for (int i = 0; i < nelems; i++) {
            values.add(readValue(bb, type));
            bb.position(bb.position() + size - getReadSize(type)); // Adjust for read
        }
        bb.position(bb.position() + pad(nelems * size));
        attr.put("name", name);
        attr.put("type", typeToString(type));
        attr.put("values", values);
        return attr;
    }

    private int getReadSize(int type) {
        switch (type) {
            case 1: case 2: return 1;
            case 3: return 2;
            case 4: case 5: return 4;
            case 6: return 8;
            default: return 0;
        }
    }

    private Object readValue(ByteBuffer bb, int type) {
        switch (type) {
            case 1: return bb.get();
            case 2: return (char) bb.get();
            case 3: return bb.getShort();
            case 4: return bb.getInt();
            case 5: return bb.getFloat();
            case 6: return bb.getDouble();
            default: return 0;
        }
    }

    public void printProperties() throws IOException {
        if (properties == null) read();
        System.out.println(properties);
    }

    public void write(String newFilepath, Map<String, Object> props) throws IOException {
        if (props == null) props = properties != null ? properties : read();
        try (RandomAccessFile raf = new RandomAccessFile(newFilepath, "rw")) {
            ByteBuffer bb = ByteBuffer.allocate(1024 * 1024).order(ByteOrder.BIG_ENDIAN); // Buffer for writing

            bb.put("CDF".getBytes(StandardCharsets.US_ASCII));
            bb.put((byte) props.get("version"));
            bb.putInt((int) props.get("numrecs"));

            // Dimensions
            List<Map<String, Object>> dims = (List<Map<String, Object>>) props.get("dimensions");
            if (!dims.isEmpty()) {
                bb.putInt(10);
                bb.putInt(dims.size());
                for (Map<String, Object> dim : dims) {
                    byte[] nameB = ((String) dim.get("name")).getBytes(StandardCharsets.UTF_8);
                    bb.putInt(nameB.length);
                    bb.put(nameB);
                    bb.put(new byte[pad(nameB.length)]);
                    bb.putInt((int) dim.get("length"));
                }
            } else {
                bb.putInt(0);
                bb.putInt(0);
            }

            // Global attributes
            List<Map<String, Object>> gAttrs = (List<Map<String, Object>>) props.get("global_attributes");
            if (!gAttrs.isEmpty()) {
                bb.putInt(12);
                bb.putInt(gAttrs.size());
                for (Map<String, Object> attr : gAttrs) {
                    writeAttr(bb, attr);
                }
            } else {
                bb.putInt(0);
                bb.putInt(0);
            }

            // Variables
            List<Map<String, Object>> vars = (List<Map<String, Object>>) props.get("variables");
            if (!vars.isEmpty()) {
                bb.putInt(11);
                bb.putInt(vars.size());
                for (Map<String, Object> var : vars) {
                    byte[] nameB = ((String) var.get("name")).getBytes(StandardCharsets.UTF_8);
                    bb.putInt(nameB.length);
                    bb.put(nameB);
                    bb.put(new byte[pad(nameB.length)]);
                    bb.putInt((int) var.get("rank"));
                    List<Integer> dimids = (List<Integer>) var.get("dimids");
                    for (int dimid : dimids) {
                        bb.putInt(dimid);
                    }
                    List<Map<String, Object>> vAttrs = (List<Map<String, Object>>) var.get("attributes");
                    if (!vAttrs.isEmpty()) {
                        bb.putInt(12);
                        bb.putInt(vAttrs.size());
                        for (Map<String, Object> attr : vAttrs) {
                            writeAttr(bb, attr);
                        }
                    } else {
                        bb.putInt(0);
                        bb.putInt(0);
                    }
                    int type = strToType((String) var.get("type"));
                    bb.putInt(type);
                    bb.putInt((int) var.get("vsize"));
                    bb.putInt((int) var.get("begin"));
                }
            } else {
                bb.putInt(0);
                bb.putInt(0);
            }

            bb.flip();
            raf.getChannel().write(bb);
        }
        // Note: Data not written; header only.
    }

    private int strToType(String typeStr) {
        switch (typeStr) {
            case "NC_BYTE": return 1;
            case "NC_CHAR": return 2;
            case "NC_SHORT": return 3;
            case "NC_INT": return 4;
            case "NC_FLOAT": return 5;
            case "NC_DOUBLE": return 6;
            default: return 4;
        }
    }

    private void writeAttr(ByteBuffer bb, Map<String, Object> attr) {
        byte[] nameB = ((String) attr.get("name")).getBytes(StandardCharsets.UTF_8);
        bb.putInt(nameB.length);
        bb.put(nameB);
        bb.put(new byte[pad(nameB.length)]);
        int type = strToType((String) attr.get("type"));
        List<Object> values = (List<Object>) attr.get("values");
        bb.putInt(type);
        bb.putInt(values.size());
        for (Object val : values) {
            writeValue(bb, val, type);
        }
        bb.put(new byte[pad(values.size() * getTypeSize(type))]);
    }

    private void writeValue(ByteBuffer bb, Object val, int type) {
        switch (type) {
            case 1: bb.put((byte) val); break;
            case 2: bb.put((byte) ((Character) val).charValue()); break;
            case 3: bb.putShort((short) val); break;
            case 4: bb.putInt((int) val); break;
            case 5: bb.putFloat((float) val); break;
            case 6: bb.putDouble((double) val); break;
        }
    }

    public static void main(String[] args) throws IOException {
        NetCDFHandler handler = new NetCDFHandler("example.nc");
        handler.printProperties();
        handler.write("new.nc", null);
    }
}
  1. JavaScript Class for .NC Files (Node.js for Console)

Here's a JavaScript class for Node.js (using fs for file I/O). It opens, reads, writes, and prints properties to console.

const fs = require('fs');

class NetCDFHandler {
    constructor(filepath) {
        this.filepath = filepath;
        this.properties = null;
    }

    read() {
        const data = fs.readFileSync(this.filepath);
        const dv = new DataView(data.buffer);
        this.properties = this.parse(dv);
        return this.properties;
    }

    parse(dv) {
        // Similar to the browser parser in part 3, but complete for Node
        let offset = 0;
        const magic = String.fromCharCode(dv.getUint8(offset++), dv.getUint8(offset++), dv.getUint8(offset++));
        if (magic !== 'CDF') throw new Error('Not a NetCDF file');
        const version = dv.getUint8(offset++);
        if (version !== 1) throw new Error('Only Classic format supported');
        const numrecs = dv.getUint32(offset); offset += 4;

        // Dimensions
        let dim_tag = dv.getUint32(offset); offset += 4;
        const dims = [];
        if (dim_tag === 10) {
            const nelems = dv.getUint32(offset); offset += 4;
            for (let i = 0; i < nelems; i++) {
                const nameLen = dv.getUint32(offset); offset += 4;
                const name = this.getString(dv, offset, nameLen); offset += nameLen;
                offset += this.pad(nameLen);
                const length = dv.getUint32(offset); offset += 4;
                dims.push({ name, length });
            }
        } else if (dim_tag !== 0) throw new Error('Invalid dim tag');

        // Global attributes
        const gatt_tag = dv.getUint32(offset); offset += 4;
        const globalAttrs = [];
        if (gatt_tag === 12) {
            const nelems = dv.getUint32(offset); offset += 4;
            for (let i = 0; i < nelems; i++) {
                const { attr, newOffset } = this.parseAttr(dv, offset);
                globalAttrs.push(attr);
                offset = newOffset;
            }
        } else if (gatt_tag !== 0) throw new Error('Invalid gatt tag');

        // Variables
        const var_tag = dv.getUint32(offset); offset += 4;
        const vars = [];
        if (var_tag === 11) {
            const nelems = dv.getUint32(offset); offset += 4;
            for (let i = 0; i < nelems; i++) {
                const nameLen = dv.getUint32(offset); offset += 4;
                const name = this.getString(dv, offset, nameLen); offset += nameLen;
                offset += this.pad(nameLen);
                const rank = dv.getUint32(offset); offset += 4;
                const dimids = [];
                for (let j = 0; j < rank; j++) {
                    dimids.push(dv.getUint32(offset)); offset += 4;
                }
                const vatt_tag = dv.getUint32(offset); offset += 4;
                const varAttrs = [];
                if (vatt_tag === 12) {
                    const nelemsAttr = dv.getUint32(offset); offset += 4;
                    for (let j = 0; j < nelemsAttr; j++) {
                        const { attr, newOffset } = this.parseAttr(dv, offset);
                        varAttrs.push(attr);
                        offset = newOffset;
                    }
                } else if (vatt_tag !== 0) throw new Error('Invalid vatt tag');
                const type = dv.getUint32(offset); offset += 4;
                const vsize = dv.getUint32(offset); offset += 4;
                const begin = dv.getUint32(offset); offset += 4;
                vars.push({ name, rank, dimids, attributes: varAttrs, type: this.typeToString(type), vsize, begin });
            }
        } else if (var_tag !== 0) throw new Error('Invalid var tag');

        return {
            magic: 'CDF',
            version,
            numrecs,
            dimensions: dims,
            globalAttributes: globalAttrs,
            variables: vars
        };
    }

    getString(dv, offset, len) {
        let str = '';
        for (let i = 0; i < len; i++) {
            str += String.fromCharCode(dv.getUint8(offset + i));
        }
        return str;
    }

    pad(len) {
        return (4 - (len % 4)) % 4;
    }

    typeToString(type) {
        const types = {1: 'NC_BYTE', 2: 'NC_CHAR', 3: 'NC_SHORT', 4: 'NC_INT', 5: 'NC_FLOAT', 6: 'NC_DOUBLE'};
        return types[type] || 'Unknown';
    }

    getTypeSize(type) {
        const sizes = {1: 1, 2: 1, 3: 2, 4: 4, 5: 4, 6: 8};
        return sizes[type] || 0;
    }

    parseAttr(dv, startOffset) {
        let offset = startOffset;
        const nameLen = dv.getUint32(offset); offset += 4;
        const name = this.getString(dv, offset, nameLen); offset += nameLen;
        offset += this.pad(nameLen);
        const type = dv.getUint32(offset); offset += 4;
        const nelems = dv.getUint32(offset); offset += 4;
        const values = [];
        const size = this.getTypeSize(type);
        for (let i = 0; i < nelems; i++) {
            values.push(this.readValue(dv, offset, type));
            offset += size;
        }
        offset += this.pad(nelems * size);
        return { attr: { name, type: this.typeToString(type), values }, newOffset: offset };
    }

    readValue(dv, offset, type) {
        if (type === 1 || type === 2) return dv.getUint8(offset);
        if (type === 3) return dv.getInt16(offset);
        if (type === 4) return dv.getInt32(offset);
        if (type === 5) return dv.getFloat32(offset);
        if (type === 6) return dv.getFloat64(offset);
        return 0;
    }

    printProperties() {
        if (!this.properties) this.read();
        console.log(JSON.stringify(this.properties, null, 2));
    }

    write(newFilepath, properties = null) {
        if (!properties) properties = this.properties || this.read();
        let buffer = Buffer.alloc(1024 * 1024); // Large enough buffer
        let offset = 0;

        buffer.write('CDF', offset); offset += 3;
        buffer.writeUInt8(properties.version, offset); offset += 1;
        buffer.writeUInt32BE(properties.numrecs, offset); offset += 4;

        // Dimensions
        if (properties.dimensions.length > 0) {
            buffer.writeUInt32BE(10, offset); offset += 4;
            buffer.writeUInt32BE(properties.dimensions.length, offset); offset += 4;
            for (let dim of properties.dimensions) {
                const nameB = Buffer.from(dim.name, 'utf-8');
                buffer.writeUInt32BE(nameB.length, offset); offset += 4;
                nameB.copy(buffer, offset); offset += nameB.length;
                for (let p = 0; p < this.pad(nameB.length); p++) {
                    buffer.writeUInt8(0, offset); offset += 1;
                }
                buffer.writeUInt32BE(dim.length, offset); offset += 4;
            }
        } else {
            buffer.writeUInt32BE(0, offset); offset += 4;
            buffer.writeUInt32BE(0, offset); offset += 4;
        }

        // Global attributes
        if (properties.globalAttributes.length > 0) {
            buffer.writeUInt32BE(12, offset); offset += 4;
            buffer.writeUInt32BE(properties.globalAttributes.length, offset); offset += 4;
            for (let attr of properties.globalAttributes) {
                offset = this.writeAttr(buffer, offset, attr);
            }
        } else {
            buffer.writeUInt32BE(0, offset); offset += 4;
            buffer.writeUInt32BE(0, offset); offset += 4;
        }

        // Variables
        if (properties.variables.length > 0) {
            buffer.writeUInt32BE(11, offset); offset += 4;
            buffer.writeUInt32BE(properties.variables.length, offset); offset += 4;
            for (let var_ of properties.variables) {
                const nameB = Buffer.from(var_.name, 'utf-8');
                buffer.writeUInt32BE(nameB.length, offset); offset += 4;
                nameB.copy(buffer, offset); offset += nameB.length;
                for (let p = 0; p < this.pad(nameB.length); p++) {
                    buffer.writeUInt8(0, offset); offset += 1;
                }
                buffer.writeUInt32BE(var_.rank, offset); offset += 4;
                for (let dimid of var_.dimids) {
                    buffer.writeUInt32BE(dimid, offset); offset += 4;
                }
                if (var_.attributes.length > 0) {
                    buffer.writeUInt32BE(12, offset); offset += 4;
                    buffer.writeUInt32BE(var_.attributes.length, offset); offset += 4;
                    for (let attr of var_.attributes) {
                        offset = this.writeAttr(buffer, offset, attr);
                    }
                } else {
                    buffer.writeUInt32BE(0, offset); offset += 4;
                    buffer.writeUInt32BE(0, offset); offset += 4;
                }
                const type = this.strToType(var_.type);
                buffer.writeUInt32BE(type, offset); offset += 4;
                buffer.writeUInt32BE(var_.vsize, offset); offset += 4;
                buffer.writeUInt32BE(var_.begin, offset); offset += 4;
            }
        } else {
            buffer.writeUInt32BE(0, offset); offset += 4;
            buffer.writeUInt32BE(0, offset); offset += 4;
        }

        fs.writeFileSync(newFilepath, buffer.slice(0, offset));
        // Note: Header only; data not written.
    }

    strToType(typeStr) {
        const types = {'NC_BYTE': 1, 'NC_CHAR': 2, 'NC_SHORT': 3, 'NC_INT': 4, 'NC_FLOAT': 5, 'NC_DOUBLE': 6};
        return types[typeStr] || 4;
    }

    writeAttr(buffer, startOffset, attr) {
        let offset = startOffset;
        const nameB = Buffer.from(attr.name, 'utf-8');
        buffer.writeUInt32BE(nameB.length, offset); offset += 4;
        nameB.copy(buffer, offset); offset += nameB.length;
        for (let p = 0; p < this.pad(nameB.length); p++) {
            buffer.writeUInt8(0, offset); offset += 1;
        }
        const type = this.strToType(attr.type);
        buffer.writeUInt32BE(type, offset); offset += 4;
        buffer.writeUInt32BE(attr.values.length, offset); offset += 4;
        const size = this.getTypeSize(type);
        for (let val of attr.values) {
            this.writeValue(buffer, offset, val, type);
            offset += size;
        }
        for (let p = 0; p < this.pad(attr.values.length * size); p++) {
            buffer.writeUInt8(0, offset); offset += 1;
        }
        return offset;
    }

    writeValue(buffer, offset, val, type) {
        if (type === 1 || type === 2) buffer.writeUInt8(val, offset);
        else if (type === 3) buffer.writeInt16BE(val, offset);
        else if (type === 4) buffer.writeInt32BE(val, offset);
        else if (type === 5) buffer.writeFloatBE(val, offset);
        else if (type === 6) buffer.writeDoubleBE(val, offset);
    }
}

// Example usage:
// const handler = new NetCDFHandler('example.nc');
// handler.printProperties();
// handler.write('new.nc');
  1. C Class for .NC Files (C++)

Here's a C++ class using fstream for binary I/O. It opens, reads, writes, and prints properties to console (as JSON-like text).

#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <string>
#include <iomanip>
#include <endian.h> // For bigendian, assume available or use htobe32 etc.

class NetCDFHandler {
private:
    std::string filepath;
    std::map<std::string, std::any> properties; // Use std::any for mixed types

public:
    NetCDFHandler(const std::string& fp) : filepath(fp) {}

    void read() {
        std::ifstream file(filepath, std::ios::binary | std::ios::ate);
        if (!file) throw std::runtime_error("Cannot open file");
        auto size = file.tellg();
        file.seekg(0);
        std::vector<char> data(size);
        file.read(data.data(), size);
        parse(data.data());
    }

    void parse(const char* data) {
        const char* ptr = data;
        std::string magic(ptr, 3);
        ptr += 3;
        if (magic != "CDF") throw std::runtime_error("Not a NetCDF file");
        properties["magic"] = std::string("CDF");
        uint8_t version = *reinterpret_cast<const uint8_t*>(ptr++);
        if (version != 1) throw std::runtime_error("Only Classic format supported");
        properties["version"] = version;
        uint32_t numrecs = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
        ptr += 4;
        properties["numrecs"] = numrecs;

        // Dimensions
        uint32_t dim_tag = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
        ptr += 4;
        std::vector<std::map<std::string, std::any>> dims;
        if (dim_tag == 10) {
            uint32_t nelems = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
            ptr += 4;
            for (uint32_t i = 0; i < nelems; ++i) {
                uint32_t name_len = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
                ptr += 4;
                std::string name(ptr, name_len);
                ptr += name_len;
                ptr += pad(name_len);
                uint32_t length = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
                ptr += 4;
                std::map<std::string, std::any> dim;
                dim["name"] = name;
                dim["length"] = length;
                dims.push_back(dim);
            }
        } else if (dim_tag != 0) throw std::runtime_error("Invalid dim tag");
        properties["dimensions"] = dims;

        // Global attributes
        uint32_t gatt_tag = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
        ptr += 4;
        std::vector<std::map<std::string, std::any>> global_attrs;
        if (gatt_tag == 12) {
            uint32_t nelems = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
            ptr += 4;
            for (uint32_t i = 0; i < nelems; ++i) {
                auto [attr, new_ptr] = parse_attr(ptr);
                global_attrs.push_back(attr);
                ptr = new_ptr;
            }
        } else if (gatt_tag != 0) throw std::runtime_error("Invalid gatt tag");
        properties["global_attributes"] = global_attrs;

        // Variables
        uint32_t var_tag = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
        ptr += 4;
        std::vector<std::map<std::string, std::any>> vars;
        if (var_tag == 11) {
            uint32_t nelems = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
            ptr += 4;
            for (uint32_t i = 0; i < nelems; ++i) {
                uint32_t name_len = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
                ptr += 4;
                std::string name(ptr, name_len);
                ptr += name_len;
                ptr += pad(name_len);
                uint32_t rank = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
                ptr += 4;
                std::vector<uint32_t> dimids;
                for (uint32_t j = 0; j < rank; ++j) {
                    dimids.push_back(be32toh(*reinterpret_cast<const uint32_t*>(ptr)));
                    ptr += 4;
                }
                uint32_t vatt_tag = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
                ptr += 4;
                std::vector<std::map<std::string, std::any>> var_attrs;
                if (vatt_tag == 12) {
                    uint32_t nelems_attr = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
                    ptr += 4;
                    for (uint32_t j = 0; j < nelems_attr; ++j) {
                        auto [attr, new_ptr] = parse_attr(ptr);
                        var_attrs.push_back(attr);
                        ptr = new_ptr;
                    }
                } else if (vatt_tag != 0) throw std::runtime_error("Invalid vatt tag");
                uint32_t type = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
                ptr += 4;
                uint32_t vsize = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
                ptr += 4;
                uint32_t begin = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
                ptr += 4;
                std::map<std::string, std::any> var;
                var["name"] = name;
                var["rank"] = rank;
                var["dimids"] = dimids;
                var["attributes"] = var_attrs;
                var["type"] = type_to_string(type);
                var["vsize"] = vsize;
                var["begin"] = begin;
                vars.push_back(var);
            }
        } else if (var_tag != 0) throw std::runtime_error("Invalid var tag");
        properties["variables"] = vars;
    }

    int pad(uint32_t len) {
        return (4 - (len % 4)) % 4;
    }

    std::string type_to_string(uint32_t type) {
        switch (type) {
            case 1: return "NC_BYTE";
            case 2: return "NC_CHAR";
            case 3: return "NC_SHORT";
            case 4: return "NC_INT";
            case 5: return "NC_FLOAT";
            case 6: return "NC_DOUBLE";
            default: return "Unknown";
        }
    }

    int get_type_size(uint32_t type) {
        switch (type) {
            case 1: case 2: return 1;
            case 3: return 2;
            case 4: case 5: return 4;
            case 6: return 8;
            default: return 0;
        }
    }

    std::pair<std::map<std::string, std::any>, const char*> parse_attr(const char* ptr) {
        std::map<std::string, std::any> attr;
        uint32_t name_len = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
        ptr += 4;
        std::string name(ptr, name_len);
        ptr += name_len;
        ptr += pad(name_len);
        uint32_t type = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
        ptr += 4;
        uint32_t nelems = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
        ptr += 4;
        std::vector<std::any> values;
        int size = get_type_size(type);
        for (uint32_t i = 0; i < nelems; ++i) {
            std::any val = read_value(ptr, type);
            values.push_back(val);
            ptr += size;
        }
        ptr += pad(nelems * size);
        attr["name"] = name;
        attr["type"] = type_to_string(type);
        attr["values"] = values;
        return {attr, ptr};
    }

    std::any read_value(const char* ptr, uint32_t type) {
        switch (type) {
            case 1: return static_cast<uint8_t>(*ptr);
            case 2: return static_cast<char>(*ptr);
            case 3: return static_cast<int16_t>(be16toh(*reinterpret_cast<const uint16_t*>(ptr)));
            case 4: return static_cast<int32_t>(be32toh(*reinterpret_cast<const uint32_t*>(ptr)));
            case 5: { // Float
                uint32_t i = be32toh(*reinterpret_cast<const uint32_t*>(ptr));
                float f;
                std::memcpy(&f, &i, sizeof(float));
                return f;
            }
            case 6: { // Double
                uint64_t i = be64toh(*reinterpret_cast<const uint64_t*>(ptr));
                double d;
                std::memcpy(&d, &i, sizeof(double));
                return d;
            }
            default: return 0;
        }
    }

    void print_properties() {
        // Implement printing recursively, e.g., as JSON-like
        std::cout << "{\n";
        std::cout << "  \"magic\": \"" << std::any_cast<std::string>(properties["magic"]) << "\",\n";
        std::cout << "  \"version\": " << static_cast<int>(std::any_cast<uint8_t>(properties["version"])) << ",\n";
        std::cout << "  \"numrecs\": " << std::any_cast<uint32_t>(properties["numrecs"]) << ",\n";
        // Add printing for dims, attrs, vars similarly (omitted for brevity; use recursion or loops)
        std::cout << "}\n";
    }

    void write(const std::string& new_filepath) {
        std::ofstream file(new_filepath, std::ios::binary);
        if (!file) throw std::runtime_error("Cannot write file");

        file.write("CDF", 3);
        uint8_t version = std::any_cast<uint8_t>(properties["version"]);
        file.write(reinterpret_cast<const char*>(&version), 1);
        uint32_t numrecs_be = htobe32(std::any_cast<uint32_t>(properties["numrecs"]));
        file.write(reinterpret_cast<const char*>(&numrecs_be), 4);

        // Dimensions, attrs, vars writing similar to above, with htobe32 etc. (omitted for brevity)
        // Header only.
    }
};

// Example
// int main() {
//     NetCDFHandler handler("example.nc");
//     handler.read();
//     handler.print_properties();
//     handler.write("new.nc");
//     return 0;
// }

Note: C++ code uses <endian.h> for byte order conversion (available on Linux; for portability, implement manually). Printing is partial; extend for full maps/vectors. Write is header-only.