SwiftNetCDF is a library to read and write NetCDF files in Swift with type safety.
- 
SwiftNetCDF requires the NetCDF C client library which can be installed on Mac with brew install netcdfor on Linux withsudo apt install libnetcdf-dev.
- 
Add SwiftNetCDFas a dependency to yourPackage.swift
  dependencies: [
    .package(url: "https://github.com/patrick-zippenfenig/SwiftNetCDF.git", from: "1.0.0")
  ],
  targets: [
    .target(name: "MyApp", dependencies: ["SwiftNetCDF"])
  ]- Build your project:
$ swift build- Write NetCDF files
import SwiftNetCDF
let data = [Int32(0), 3, 4, 6, 12, 45, 89, ...]
var file = try NetCDF.create(path: "test.nc", overwriteExisting: true)
try file.setAttribute("TITLE", "My data set")
let dimensions = [
  try file.createDimension(name: "LAT", length: 10),
  try file.createDimension(name: "LON", length: 5)
]
let variable = try file.createVariable(name: "MyData", type: Int32.self, dimensions: dimensions)
try variable.write(data)- Read NetCDF files
import SwiftNetCDF
guard let file = try NetCDF.open(path: "test.nc", allowUpdate: false) else {
    fatalError("File test.nc does not exist")
}
guard let title: String = try file.getAttribute("TITLE")?.read() else {
    fatalError("TITLE attribute not available or not a String")
}
guard let variable = file.getVariable(name: "MyData") else {
    fatalError("No variable named MyData available")
}
guard let typedVariable = variable.asType(Int32.self) else {
    fatalError("MyData is not a Int32 type")
}
let data2 = try typedVariable.read(offset: [1,1], count: [2,2])- Using groups, unlimited dimensions and compression
import SwiftNetCDF
let file = try NetCDF.create(path: "test.nc", overwriteExisting: true)
// Create new group. Analog the `getGroup(name: )` function can be used for existing groups
let subGroup = try file.createGroup(name: "GROUP1")
let dimLat = try subGroup.createDimension(name: "LAT", length: 10)
let dimLon = try subGroup.createDimension(name: "LON", length: 5, isUnlimited: true)
var lats = try subGroup.createVariable(name: "LATITUDES", type: Float.self, dimensions: [dimLat])
var lons = try subGroup.createVariable(name: "LONGITUDES", type: Float.self, dimensions: [dimLon])
try lats.write((0..<10).map(Float.init))
try lons.write((0..<5).map(Float.init))
// `data` is of type `VariableGeneric<Float>`. Define functions can be accessed via `data.variable`
var data = try subGroup.createVariable(name: "DATA", type: Float.self, dimensions: [dimLat, dimLon])
// Enable compression, shuffle filter and chunking
try data.defineDeflate(enable: true, level: 6, shuffle: true)
try data.defineChunking(chunking: .chunked, chunks: [1, 5])
/// Because the latitude dimension is unlimted, we can write more than the defined size
let array = (0..<1000).map(Float.init)
try data.write(array, offset: [0, 0], count: [10, 100])
/// The check the new dimension count
XCTAssertEqual(data.dimensionsFlat, [10, 100])
// even more data at an offset
try data.write(array, offset: [0, 100], count: [10, 100])
XCTAssertEqual(data.dimensionsFlat, [10, 200])- Discover the structure of a NetCDF file
import SwiftNetCDF
guard let file = try NetCDF.open(path: "test.nc", allowUpdate: false) else {
    fatalError("File test.nc does not exist")
}
/// Recursively print all groups
func printGroup(_ group: Group) {
    print("Group: \(group.name)")
    
    for d in group.getDimensions() {
        print("Dimension: \(d.name) \(d.length) \(d.isUnlimited)")
    }
    
    for v in group.getVariables() {
        print("Variable: \(v.name) \(v.type.asExternalDataType()!)")
        for d in v.dimensions {
            print("Variable dimension: \(d.name) \(d.length) \(d.isUnlimited)")
        }
    }
    
    for a in try! group.getAttributes() {
        print("Attribute: \(a.name) \(a.length) \(a.type.asExternalDataType()!)")
    }
    
    for subgroup in group.getGroups() {
        printGroup(subgroup)
    }
}
// The root entry point of a NetCDF file is also a `Group`
printGroup(file)Output:
Group: /
Group: GROUP1
Dimension: LAT 10 false
Dimension: LON 200 true
Variable: LATITUDES float
Variable dimension: LAT 10 false
Variable: LONGITUDES float
Variable dimension: LON 200 true
Variable: DATA float
Variable dimension: LAT 10 false
Variable dimension: LON 200 true
- Abstract Swift data types to NetCDF external types
- Supported data types: Float,Double,String,Int8,Int16,Int32,Int64,Int,UInt16,UInt32,UInt64andUInt
- Returns nilfor missing files, variables, attributes or data-type mismatch
- Exceptions are thrown for NetCDF library errors
- Uses generics to ensure the correct type is being used
- Thread safe. Access to the netCDF C API is serialised with thread locks
- User defined data tyes not yet implemented
SwiftNetCDF uses a simple data structures to organise access to NetCDF functions. The most important once are listed below.
struct NetCDF {
    static func create(path: String, overwriteExisting: Bool) -> Group
    static func open(path: String, allowUpdate: Bool) -> Group?
    
    /// Opens a NetCDF file from memory in read-only mode
    static func open(memory: UnsafeRawBufferPointer)) -> Group?
}
struct Group {
    let name: String
    
    func getGroup(name: String) -> Group?
    func getGroups() -> [Group]
    func createGroup(name: String) -> Group
    
    func getDimensions() -> [Dimension]
    func createDimension(name: String, length: Int, isUnlimited: Bool = false) -> Dimension
    
    func getVariable(name: String) -> Variable?
    func getVariables() -> [Variable]
    func createVariable<T>(name: String, type: T.Type, dimensions: [Dimension]) -> VariableGeneric<T>
    func getAttribute(_ key: String) -> Attribute?
    func getAttributes() -> [Attribute]
    func setAttribute<T>(_ name: String, _ value: T)
    func setAttribute<T: NetcdfConvertible>(_ name: String, _ value: [T])
}
struct Variable {
    let name: String
    
    var dimensions: [Dimension]
    var dimensionsFlat: [Int]
    
    /// `Nil` in case of type mismatch
    func asType<T>(_ of: T.Type) -> VariableGeneric<T>?
    
    func defineDeflate(enable: Bool, level: Int = 6, shuffle: Bool = false)
    func defineChunking(chunking: VarId.Chunking, chunks: [Int])
    
    // Same get/set attribute functions as a Group
}
struct VariableGeneric<T> {
    func read() -> [T]
    func read(offset: [Int], count: [Int]) -> [T]
    func read(offset: [Int], count: [Int], stride: [Int]) -> [T]
    
    func write(_ data: [T])
    func write(_ data: [T], offset: [Int], count: [Int])
    func write(_ data: [T], offset: [Int], count: [Int], stride: [Int])
    
    // Same get/set attribute functions as a Group
    // Same define functions as Variable
}
struct Dimension {
    let name: String
    let length: Int
    let isUnlimited: Bool
}
struct Attribute {
    let name: String
    let length: Int
    
    func read<T: NetcdfConvertible>() throws -> T?
    func read<T: NetcdfConvertible>() throws -> [T]?
}Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.