Task 254: .GDT File Format

Task 254: .GDT File Format

1. List of All Properties Intrinsic to the .GDT File Format

The .GDT file format is the XML-based data file format used by gretl (GNU Regression, Econometrics, and Time-series Library), an open-source econometric analysis software. It follows a strict structure defined by a Document Type Definition (DTD) version 1.5, which ensures hierarchical storage of dataset metadata, variables, observations, and optional panel or string data. The format is text-based (not binary, which uses .gdtb), human-readable, and designed for portability across platforms.

Intrinsic properties refer to the core structural elements, attributes, and content rules enforced by the DTD. These define the file's schema, validation rules, and data organization. Below is a comprehensive list, grouped by the root element (gretldata) and its children:

Root Element: gretldata (Required)

  • Purpose: Encapsulates the entire dataset.
  • Attributes (all optional unless noted):
  • version: String (NMTOKEN) – GDT schema version (e.g., "1.5").
  • name: String (NMTOKEN) – Dataset name.
  • frequency: String (CDATA) – Data frequency (e.g., annual, quarterly).
  • n: String (CDATA) – Number of observations.
  • startobs: String (CDATA) – Starting observation label (e.g., time series start).
  • endobs: String (CDATA) – Ending observation label.
  • type: String (CDATA) – Required; dataset type (e.g., "time-series", "cross-section").
  • binary: String (CDATA) – Flag for binary mode (typically "no" for .gdt).
  • rseed: String (CDATA) – Random seed for reproducibility.
  • mapfile: String (CDATA) – Path to a mapping file.
  • mpi-transfer: String (CDATA) – Flag for MPI (Message Passing Interface) transfer.
  • Child Elements (in order, optional unless noted):
  • description (0 or 1).
  • variables (1 or more, but effectively required via structure).
  • observations (0 or 1).
  • string-tables (0 or 1).
  • panel-info (0 or 1).

Element: description (Optional)

  • Purpose: Human-readable description of the dataset.
  • Content: Parsed character data (#PCDATA) – Free-text description.
  • Attributes:
  • source: String (CDATA) – Origin/source of the data.

Element: variables (Required)

  • Purpose: Defines the dataset's variables (columns).
  • Attributes:
  • count: String (CDATA) – Required; number of variables.
  • Child Elements: 0 or more variable elements.

Element: variable (Per Variable, 0 or More)

  • Purpose: Describes a single variable.
  • Content: Parsed character data (#PCDATA) – Variable name or identifier.
  • Attributes (all optional):
  • name: String (NMTOKEN) – Variable identifier.
  • label: String (CDATA) – Descriptive label.
  • displayname: String (CDATA) – Name for display/UI.
  • parent: String (CDATA) – Parent variable (for derived vars).
  • transform: String (CDATA) – Transformation applied (e.g., log).
  • lag: String (CDATA) – Lag order.
  • compact-method: String (CDATA) – Compression method.
  • discrete: String (CDATA) – Flag if discrete/categorical.
  • coded: String (CDATA) – Flag if coded values.
  • role: String (CDATA) – Role (e.g., dependent, independent).
  • value: String (CDATA) – Default or initial value.

Element: observations (Optional)

  • Purpose: Defines rows/observations in the dataset.
  • Attributes:
  • count: String (CDATA) – Required; number of observations.
  • missval: String (CDATA) – Missing value representation (e.g., "NA").
  • labels: String (CDATA) – Required; flag for observation labels ("yes" or "no").
  • panel-info: String (CDATA) – Reference to panel structure.
  • Child Elements: 1 or more obs elements.

Element: obs (Per Observation, 1 or More)

  • Purpose: Single observation row.
  • Content: Parsed character data (#PCDATA) – Values for all variables in this row, space- or tab-separated.
  • Attributes (all optional):
  • label: String (CDATA) – Observation label (e.g., date).
  • unit: String (NMTOKEN) – Unit identifier (e.g., for panel data).
  • period: String (NMTOKEN) – Time period.
  • id: String (NMTOKEN) – Unique ID.

Element: string-tables (Optional)

  • Purpose: Stores string value tables for categorical/discrete variables.
  • Attributes:
  • count: String (CDATA) – Required; number of string tables.
  • Child Elements: 1 or more valstrings elements.

Element: valstrings (Per String Table, 1 or More)

  • Purpose: Defines a set of string values for a variable.
  • Content: Parsed character data (#PCDATA) – Space- or comma-separated strings.
  • Attributes:
  • owner: String (CDATA) – Required; owning variable name.
  • count: String (CDATA) – Required; number of strings.

Element: panel-info (Optional, Empty)

  • Purpose: Metadata for panel (longitudinal) data.
  • Content: Empty (no #PCDATA).
  • Attributes (all optional):
  • group-names: String (CDATA) – Names of groups.
  • time-frequency: String (CDATA) – Time dimension frequency.
  • time-start: String (CDATA) – Starting time.
  • skip-padding: String (CDATA) – Flag to skip padding observations.

These properties ensure the format's self-describing nature, supporting time-series, cross-sectional, and panel data with metadata for econometric analysis. Files start with <?xml version="1.0" encoding="UTF-8"?> and reference the DTD (e.g., <!DOCTYPE gretldata SYSTEM "gretldata.dtd">).

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .GDT Parsing

This is a self-contained HTML snippet with embedded JavaScript for drag-and-drop file upload. Paste it into a Ghost blog post (use the HTML card or code block). It parses the .GDT XML, extracts all intrinsic properties from the list above, and dumps them to a <pre> block on screen. It validates basic XML structure and handles errors.

Drag and drop a .GDT file here to parse its properties



4. Python Class for .GDT Handling

This GDTParser class uses xml.etree.ElementTree (standard library) to read/parse .gdt files, extract/print all properties, and write (serialize) back to XML (with optional modifications, e.g., updating a attribute).

import xml.etree.ElementTree as ET
from xml.dom import minidom

class GDTParser:
    def __init__(self, filename=None):
        self.root = None
        if filename:
            self.load(filename)

    def load(self, filename):
        """Read and parse .gdt file."""
        try:
            self.root = ET.parse(filename).getroot()
            if self.root.tag != 'gretldata':
                raise ValueError('Not a valid .GDT file')
            self.print_properties()
        except ET.ParseError as e:
            print(f'XML Parse Error: {e}')

    def print_properties(self):
        """Print all intrinsic properties to console."""
        print('=== .GDT File Properties ===')
        print('\nRoot Attributes:')
        print(f'- version: {self.root.get("version", "N/A")}')
        print(f'- name: {self.root.get("name", "N/A")}')
        print(f'- frequency: {self.root.get("frequency", "N/A")}')
        print(f'- n: {self.root.get("n", "N/A")}')
        print(f'- startobs: {self.root.get("startobs", "N/A")}')
        print(f'- endobs: {self.root.get("endobs", "N/A")}')
        print(f'- type: {self.root.get("type", "N/A")}')
        print(f'- binary: {self.root.get("binary", "N/A")}')
        print(f'- rseed: {self.root.get("rseed", "N/A")}')
        print(f'- mapfile: {self.root.get("mapfile", "N/A")}')
        print(f'- mpi-transfer: {self.root.get("mpi-transfer", "N/A")}')

        desc = self.root.find('description')
        if desc is not None:
            print('\nDescription:')
            print(f'- Text: {desc.text.strip() if desc.text else "N/A"}')
            print(f'- Source: {desc.get("source", "N/A")}')

        vars_elem = self.root.find('variables')
        if vars_elem is not None:
            print(f'\nVariables (count: {vars_elem.get("count", "N/A")}):')
            for v in self.root.findall('variables/variable'):
                print(f'  - Name: {v.text.strip() if v.text else v.get("name", "N/A")}')
                print(f'    Label: {v.get("label", "N/A")}')
                print(f'    Displayname: {v.get("displayname", "N/A")}')
                print(f'    Parent: {v.get("parent", "N/A")}')
                print(f'    Transform: {v.get("transform", "N/A")}')
                print(f'    Lag: {v.get("lag", "N/A")}')
                print(f'    Compact-method: {v.get("compact-method", "N/A")}')
                print(f'    Discrete: {v.get("discrete", "N/A")}')
                print(f'    Coded: {v.get("coded", "N/A")}')
                print(f'    Role: {v.get("role", "N/A")}')
                print(f'    Value: {v.get("value", "N/A")}\n')

        obs_elem = self.root.find('observations')
        if obs_elem is not None:
            print(f'\nObservations (count: {obs_elem.get("count", "N/A")}):')
            print(f'- Missval: {obs_elem.get("missval", "N/A")}')
            print(f'- Labels: {obs_elem.get("labels", "N/A")}')
            print(f'- Panel-info ref: {obs_elem.get("panel-info", "N/A")}')
            for o in self.root.findall('observations/obs'):
                print(f'  - Values: {o.text.strip() if o.text else "N/A"}')
                print(f'    Label: {o.get("label", "N/A")}')
                print(f'    Unit: {o.get("unit", "N/A")}')
                print(f'    Period: {o.get("period", "N/A")}')
                print(f'    ID: {o.get("id", "N/A")}\n')

        str_tables = self.root.find('string-tables')
        if str_tables is not None:
            print(f'\nString Tables (count: {str_tables.get("count", "N/A")}):')
            for s in self.root.findall('string-tables/valstrings'):
                print(f'  - Owner: {s.get("owner", "N/A")}')
                print(f'    Count: {s.get("count", "N/A")}')
                print(f'    Strings: {s.text.strip() if s.text else "N/A"}\n')

        panel = self.root.find('panel-info')
        if panel is not None:
            print('\nPanel Info:')
            print(f'- Group-names: {panel.get("group-names", "N/A")}')
            print(f'- Time-frequency: {panel.get("time-frequency", "N/A")}')
            print(f'- Time-start: {panel.get("time-start", "N/A")}')
            print(f'- Skip-padding: {panel.get("skip-padding", "N/A")}')

    def write(self, filename, pretty_print=True):
        """Write the parsed XML back to file (optionally pretty-printed)."""
        if self.root is None:
            raise ValueError('No data loaded')
        rough_string = ET.tostring(self.root, 'unicode')
        if pretty_print:
            reparsed = minidom.parseString(rough_string)
            pretty_string = reparsed.toprettyxml(indent='  ')
            # Remove extra newlines
            pretty_string = '\n'.join([line for line in pretty_string.split('\n') if line.strip()])
        else:
            pretty_string = rough_string
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(pretty_string)
        print(f'Written to {filename}')

# Example usage:
# parser = GDTParser('sample.gdt')  # Loads and prints
# parser.root.set('name', 'Modified Dataset')  # Example modification
# parser.write('modified.gdt')

5. Java Class for .GDT Handling

This GDTParser class uses standard Java XML APIs (javax.xml.parsers, org.w3c.dom) to read/parse .gdt files, extract/print all properties, and write (serialize) back to XML. Compile with javac GDTParser.java and run with java GDTParser sample.gdt.

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.IOException;

public class GDTParser {
    private Document doc;

    public GDTParser(String filename) {
        load(filename);
    }

    private void load(String filename) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            doc = builder.parse(new File(filename));
            if (!doc.getDocumentElement().getTagName().equals("gretldata")) {
                throw new RuntimeException("Not a valid .GDT file");
            }
            printProperties();
        } catch (SAXException | IOException | ParserConfigurationException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }

    private void printProperties() {
        Element root = doc.getDocumentElement();
        System.out.println("=== .GDT File Properties ===");
        System.out.println("\nRoot Attributes:");
        System.out.println("- version: " + getAttr(root, "version"));
        System.out.println("- name: " + getAttr(root, "name"));
        System.out.println("- frequency: " + getAttr(root, "frequency"));
        System.out.println("- n: " + getAttr(root, "n"));
        System.out.println("- startobs: " + getAttr(root, "startobs"));
        System.out.println("- endobs: " + getAttr(root, "endobs"));
        System.out.println("- type: " + getAttr(root, "type"));
        System.out.println("- binary: " + getAttr(root, "binary"));
        System.out.println("- rseed: " + getAttr(root, "rseed"));
        System.out.println("- mapfile: " + getAttr(root, "mapfile"));
        System.out.println("- mpi-transfer: " + getAttr(root, "mpi-transfer"));

        NodeList descList = doc.getElementsByTagName("description");
        if (descList.getLength() > 0) {
            Element desc = (Element) descList.item(0);
            System.out.println("\nDescription:");
            System.out.println("- Text: " + (desc.getTextContent().trim().isEmpty() ? "N/A" : desc.getTextContent().trim()));
            System.out.println("- Source: " + getAttr(desc, "source"));
        }

        NodeList varsList = doc.getElementsByTagName("variables");
        if (varsList.getLength() > 0) {
            Element vars = (Element) varsList.item(0);
            System.out.println("\nVariables (count: " + getAttr(vars, "count") + "):");
            NodeList varNodes = doc.getElementsByTagName("variable");
            for (int i = 0; i < varNodes.getLength(); i++) {
                Element v = (Element) varNodes.item(i);
                System.out.println("  - Name: " + (v.getTextContent().trim().isEmpty() ? getAttr(v, "name") : v.getTextContent().trim()));
                System.out.println("    Label: " + getAttr(v, "label"));
                System.out.println("    Displayname: " + getAttr(v, "displayname"));
                System.out.println("    Parent: " + getAttr(v, "parent"));
                System.out.println("    Transform: " + getAttr(v, "transform"));
                System.out.println("    Lag: " + getAttr(v, "lag"));
                System.out.println("    Compact-method: " + getAttr(v, "compact-method"));
                System.out.println("    Discrete: " + getAttr(v, "discrete"));
                System.out.println("    Coded: " + getAttr(v, "coded"));
                System.out.println("    Role: " + getAttr(v, "role"));
                System.out.println("    Value: " + getAttr(v, "value"));
                System.out.println();
            }
        }

        NodeList obsList = doc.getElementsByTagName("observations");
        if (obsList.getLength() > 0) {
            Element obsElem = (Element) obsList.item(0);
            System.out.println("\nObservations (count: " + getAttr(obsElem, "count") + "):");
            System.out.println("- Missval: " + getAttr(obsElem, "missval"));
            System.out.println("- Labels: " + getAttr(obsElem, "labels"));
            System.out.println("- Panel-info ref: " + getAttr(obsElem, "panel-info"));
            NodeList obsNodes = doc.getElementsByTagName("obs");
            for (int i = 0; i < obsNodes.getLength(); i++) {
                Element o = (Element) obsNodes.item(i);
                System.out.println("  - Values: " + (o.getTextContent().trim().isEmpty() ? "N/A" : o.getTextContent().trim()));
                System.out.println("    Label: " + getAttr(o, "label"));
                System.out.println("    Unit: " + getAttr(o, "unit"));
                System.out.println("    Period: " + getAttr(o, "period"));
                System.out.println("    ID: " + getAttr(o, "id"));
                System.out.println();
            }
        }

        NodeList strTables = doc.getElementsByTagName("string-tables");
        if (strTables.getLength() > 0) {
            Element strElem = (Element) strTables.item(0);
            System.out.println("\nString Tables (count: " + getAttr(strElem, "count") + "):");
            NodeList strNodes = doc.getElementsByTagName("valstrings");
            for (int i = 0; i < strNodes.getLength(); i++) {
                Element s = (Element) strNodes.item(i);
                System.out.println("  - Owner: " + getAttr(s, "owner"));
                System.out.println("    Count: " + getAttr(s, "count"));
                System.out.println("    Strings: " + (s.getTextContent().trim().isEmpty() ? "N/A" : s.getTextContent().trim()));
                System.out.println();
            }
        }

        NodeList panelList = doc.getElementsByTagName("panel-info");
        if (panelList.getLength() > 0) {
            Element panel = (Element) panelList.item(0);
            System.out.println("\nPanel Info:");
            System.out.println("- Group-names: " + getAttr(panel, "group-names"));
            System.out.println("- Time-frequency: " + getAttr(panel, "time-frequency"));
            System.out.println("- Time-start: " + getAttr(panel, "time-start"));
            System.out.println("- Skip-padding: " + getAttr(panel, "skip-padding"));
        }
    }

    private String getAttr(Element elem, String attr) {
        String val = elem.getAttribute(attr);
        return val.isEmpty() ? "N/A" : val;
    }

    public void write(String filename) {
        try {
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(javax.xml.transform.OutputKeys.INDENT, "yes");
            DOMSource source = new DOMSource(doc);
            StreamResult result = new StreamResult(new File(filename));
            transformer.transform(source, result);
            System.out.println("Written to " + filename);
        } catch (TransformerException e) {
            System.err.println("Write Error: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        if (args.length > 0) {
            GDTParser parser = new GDTParser(args[0]);
            // Example: parser.doc.getDocumentElement().setAttribute("name", "Modified");
            // parser.write("modified.gdt");
        }
    }
}

6. JavaScript Class for .GDT Handling

This GDTParser class works in Node.js (use fs and xmldom – install via npm i xmldom) or browser (adapt for DOMParser). It reads/parses .gdt, prints properties to console, and writes (serializes) to file/string. For browser, replace fs with Blob/download.

const fs = require('fs'); // Node.js only
const { DOMParser, XMLSerializer } = require('xmldom'); // npm i xmldom

class GDTParser {
  constructor(filename = null) {
    this.doc = null;
    if (filename) {
      this.load(filename);
    }
  }

  load(filename) {
    try {
      const xmlString = fs.readFileSync(filename, 'utf8');
      const parser = new DOMParser();
      this.doc = parser.parseFromString(xmlString, 'text/xml');
      const root = this.doc.documentElement;
      if (root.tagName !== 'gretldata') {
        throw new Error('Not a valid .GDT file');
      }
      this.printProperties();
    } catch (e) {
      console.error('Error:', e.message);
    }
  }

  printProperties() {
    const root = this.doc.documentElement;
    console.log('=== .GDT File Properties ===');
    console.log('\nRoot Attributes:');
    console.log(`- version: ${this.getAttr(root, 'version')}`);
    console.log(`- name: ${this.getAttr(root, 'name')}`);
    console.log(`- frequency: ${this.getAttr(root, 'frequency')}`);
    console.log(`- n: ${this.getAttr(root, 'n')}`);
    console.log(`- startobs: ${this.getAttr(root, 'startobs')}`);
    console.log(`- endobs: ${this.getAttr(root, 'endobs')}`);
    console.log(`- type: ${this.getAttr(root, 'type')}`);
    console.log(`- binary: ${this.getAttr(root, 'binary')}`);
    console.log(`- rseed: ${this.getAttr(root, 'rseed')}`);
    console.log(`- mapfile: ${this.getAttr(root, 'mapfile')}`);
    console.log(`- mpi-transfer: ${this.getAttr(root, 'mpi-transfer')}`);

    const desc = this.doc.getElementsByTagName('description')[0];
    if (desc) {
      console.log('\nDescription:');
      console.log(`- Text: ${desc.textContent.trim() || 'N/A'}`);
      console.log(`- Source: ${this.getAttr(desc, 'source')}`);
    }

    const vars = this.doc.getElementsByTagName('variables')[0];
    if (vars) {
      console.log(`\nVariables (count: ${this.getAttr(vars, 'count')}):`);
      const varList = this.doc.getElementsByTagName('variable');
      for (let i = 0; i < varList.length; i++) {
        const v = varList[i];
        console.log(`  - Name: ${v.textContent.trim() || this.getAttr(v, 'name')}`);
        console.log(`    Label: ${this.getAttr(v, 'label')}`);
        console.log(`    Displayname: ${this.getAttr(v, 'displayname')}`);
        console.log(`    Parent: ${this.getAttr(v, 'parent')}`);
        console.log(`    Transform: ${this.getAttr(v, 'transform')}`);
        console.log(`    Lag: ${this.getAttr(v, 'lag')}`);
        console.log(`    Compact-method: ${this.getAttr(v, 'compact-method')}`);
        console.log(`    Discrete: ${this.getAttr(v, 'discrete')}`);
        console.log(`    Coded: ${this.getAttr(v, 'coded')}`);
        console.log(`    Role: ${this.getAttr(v, 'role')}`);
        console.log(`    Value: ${this.getAttr(v, 'value')}\n`);
      }
    }

    const obsElem = this.doc.getElementsByTagName('observations')[0];
    if (obsElem) {
      console.log(`\nObservations (count: ${this.getAttr(obsElem, 'count')}):`);
      console.log(`- Missval: ${this.getAttr(obsElem, 'missval')}`);
      console.log(`- Labels: ${this.getAttr(obsElem, 'labels')}`);
      console.log(`- Panel-info ref: ${this.getAttr(obsElem, 'panel-info')}`);
      const obsList = this.doc.getElementsByTagName('obs');
      for (let i = 0; i < obsList.length; i++) {
        const o = obsList[i];
        console.log(`  - Values: ${o.textContent.trim() || 'N/A'}`);
        console.log(`    Label: ${this.getAttr(o, 'label')}`);
        console.log(`    Unit: ${this.getAttr(o, 'unit')}`);
        console.log(`    Period: ${this.getAttr(o, 'period')}`);
        console.log(`    ID: ${this.getAttr(o, 'id')}\n`);
      }
    }

    const strTables = this.doc.getElementsByTagName('string-tables')[0];
    if (strTables) {
      console.log(`\nString Tables (count: ${this.getAttr(strTables, 'count')}):`);
      const strList = this.doc.getElementsByTagName('valstrings');
      for (let i = 0; i < strList.length; i++) {
        const s = strList[i];
        console.log(`  - Owner: ${this.getAttr(s, 'owner')}`);
        console.log(`    Count: ${this.getAttr(s, 'count')}`);
        console.log(`    Strings: ${s.textContent.trim() || 'N/A'}\n`);
      }
    }

    const panel = this.doc.getElementsByTagName('panel-info')[0];
    if (panel) {
      console.log('\nPanel Info:');
      console.log(`- Group-names: ${this.getAttr(panel, 'group-names')}`);
      console.log(`- Time-frequency: ${this.getAttr(panel, 'time-frequency')}`);
      console.log(`- Time-start: ${this.getAttr(panel, 'time-start')}`);
      console.log(`- Skip-padding: ${this.getAttr(panel, 'skip-padding')}`);
    }
  }

  getAttr(elem, attr) {
    return elem.getAttribute(attr) || 'N/A';
  }

  write(filename) {
    if (!this.doc) throw new Error('No data loaded');
    const serializer = new XMLSerializer();
    const xmlString = serializer.serializeToString(this.doc);
    fs.writeFileSync(filename, xmlString, 'utf8');
    console.log(`Written to ${filename}`);
  }
}

// Example usage (Node.js):
// const parser = new GDTParser('sample.gdt');
// parser.doc.documentElement.setAttribute('name', 'Modified Dataset');
// parser.write('modified.gdt');

7. C Class (Struct) for .GDT Handling

This uses libxml2 (common XML library; compile with gcc -o gdt_parser gdt_parser.c -lxml2 and link pthread if needed). The GDTParser struct reads/parses .gdt, prints properties to stdout, and writes back. Assumes libxml2 installed.

#include <stdio.h>
#include <stdlib.h>
#include <libxml/parser.h>
#include <libxml/tree.h>

typedef struct {
    xmlDocPtr doc;
} GDTParser;

static void print_attr(xmlNodePtr node, const char* attr_name, const char* label) {
    xmlChar* attr_val = xmlGetProp(node, (const xmlChar*)attr_name);
    printf("    %s: %s\n", label, attr_val ? (char*)attr_val : "N/A");
    if (attr_val) xmlFree(attr_val);
}

void print_properties(GDTParser* parser) {
    xmlNodePtr root = xmlDocGetRootElement(parser->doc);
    if (!root || xmlStrcmp(root->name, (const xmlChar*)"gretldata")) {
        fprintf(stderr, "Not a valid .GDT file\n");
        return;
    }

    printf("=== .GDT File Properties ===\n");
    printf("\nRoot Attributes:\n");
    print_attr(root, "version", "- version");
    print_attr(root, "name", "- name");
    print_attr(root, "frequency", "- frequency");
    print_attr(root, "n", "- n");
    print_attr(root, "startobs", "- startobs");
    print_attr(root, "endobs", "- endobs");
    print_attr(root, "type", "- type");
    print_attr(root, "binary", "- binary");
    print_attr(root, "rseed", "- rseed");
    print_attr(root, "mapfile", "- mapfile");
    print_attr(root, "mpi-transfer", "- mpi-transfer");

    xmlNodePtr desc = NULL;
    for (xmlNodePtr child = root->children; child; child = child->next) {
        if (xmlStrcmp(child->name, (const xmlChar*)"description") == 0) {
            desc = child;
            break;
        }
    }
    if (desc) {
        xmlChar* text = xmlNodeGetContent(desc);
        printf("\nDescription:\n");
        printf("- Text: %s\n", text ? (char*)text : "N/A");
        print_attr(desc, "source", "- Source");
        if (text) xmlFree(text);
    }

    xmlNodePtr vars = NULL;
    for (xmlNodePtr child = root->children; child; child = child->next) {
        if (xmlStrcmp(child->name, (const xmlChar*)"variables") == 0) {
            vars = child;
            break;
        }
    }
    if (vars) {
        print_attr(vars, "count", "\nVariables (count:");
        printf("):\n");
        for (xmlNodePtr v = vars->children; v; v = v->next) {
            if (xmlStrcmp(v->name, (const xmlChar*)"variable") == 0) {
                xmlChar* name = xmlNodeGetContent(v);
                printf("  - Name: %s\n", name ? (char*)name : "N/A");
                print_attr(v, "label", "    Label");
                print_attr(v, "displayname", "    Displayname");
                print_attr(v, "parent", "    Parent");
                print_attr(v, "transform", "    Transform");
                print_attr(v, "lag", "    Lag");
                print_attr(v, "compact-method", "    Compact-method");
                print_attr(v, "discrete", "    Discrete");
                print_attr(v, "coded", "    Coded");
                print_attr(v, "role", "    Role");
                print_attr(v, "value", "    Value");
                printf("\n");
                if (name) xmlFree(name);
            }
        }
    }

    xmlNodePtr obs_elem = NULL;
    for (xmlNodePtr child = root->children; child; child = child->next) {
        if (xmlStrcmp(child->name, (const xmlChar*)"observations") == 0) {
            obs_elem = child;
            break;
        }
    }
    if (obs_elem) {
        print_attr(obs_elem, "count", "\nObservations (count:");
        printf("):\n");
        print_attr(obs_elem, "missval", "- Missval");
        print_attr(obs_elem, "labels", "- Labels");
        print_attr(obs_elem, "panel-info", "- Panel-info ref");
        for (xmlNodePtr o = obs_elem->children; o; o = o->next) {
            if (xmlStrcmp(o->name, (const xmlChar*)"obs") == 0) {
                xmlChar* values = xmlNodeGetContent(o);
                printf("  - Values: %s\n", values ? (char*)values : "N/A");
                print_attr(o, "label", "    Label");
                print_attr(o, "unit", "    Unit");
                print_attr(o, "period", "    Period");
                print_attr(o, "id", "    ID");
                printf("\n");
                if (values) xmlFree(values);
            }
        }
    }

    xmlNodePtr str_tables = NULL;
    for (xmlNodePtr child = root->children; child; child = child->next) {
        if (xmlStrcmp(child->name, (const xmlChar*)"string-tables") == 0) {
            str_tables = child;
            break;
        }
    }
    if (str_tables) {
        print_attr(str_tables, "count", "\nString Tables (count:");
        printf("):\n");
        for (xmlNodePtr s = str_tables->children; s; s = s->next) {
            if (xmlStrcmp(s->name, (const xmlChar*)"valstrings") == 0) {
                print_attr(s, "owner", "  - Owner");
                print_attr(s, "count", "    Count");
                xmlChar* strings = xmlNodeGetContent(s);
                printf("    Strings: %s\n", strings ? (char*)strings : "N/A");
                printf("\n");
                if (strings) xmlFree(strings);
            }
        }
    }

    xmlNodePtr panel = NULL;
    for (xmlNodePtr child = root->children; child; child = child->next) {
        if (xmlStrcmp(child->name, (const xmlChar*)"panel-info") == 0) {
            panel = child;
            break;
        }
    }
    if (panel) {
        printf("\nPanel Info:\n");
        print_attr(panel, "group-names", "- Group-names");
        print_attr(panel, "time-frequency", "- Time-frequency");
        print_attr(panel, "time-start", "- Time-start");
        print_attr(panel, "skip-padding", "- Skip-padding");
    }
}

void load(GDTParser* parser, const char* filename) {
    parser->doc = xmlReadFile(filename, NULL, 0);
    if (!parser->doc) {
        fprintf(stderr, "Failed to parse %s\n", filename);
        return;
    }
    print_properties(parser);
}

void write(GDTParser* parser, const char* filename) {
    if (!parser->doc) return;
    int success = xmlSaveFormatFileEnc(filename, parser->doc, "UTF-8", 1);
    if (success != -1) {
        printf("Written to %s\n", filename);
    } else {
        fprintf(stderr, "Write failed\n");
    }
}

int main(int argc, char** argv) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <file.gdt>\n", argv[0]);
        return 1;
    }
    GDTParser parser = {0};
    load(&parser, argv[1]);
    // Example modification: xmlSetProp(xmlDocGetRootElement(parser.doc), (xmlChar*)"name", (xmlChar*)"Modified");
    // write(&parser, "modified.gdt");
    xmlFreeDoc(parser.doc);
    xmlCleanupParser();
    return 0;
}