Skip to content

Commit ea14349

Browse files
feat(importexport): add xlsx import/export module
1 parent 430228d commit ea14349

1 file changed

Lines changed: 161 additions & 0 deletions

File tree

src/importexport/xlsx.jl

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# import DataFrames: DataFrame
2+
# using DataFrames
3+
4+
# ---------------------------------------------------------------------------
5+
# Stringification helpers (keeps uncertainties visible in Excel)
6+
# ---------------------------------------------------------------------------
7+
stringify(x) = string(x) # fallback (rarely reached)
8+
stringify(::Missing) = ""
9+
stringify(x::Real) = @sprintf("%.12g", float(x))
10+
stringify(x::Measurements.Measurement) =
11+
@sprintf("%.12g ± %.6g", Measurements.value(x), Measurements.uncertainty(x))
12+
13+
function df_to_strings(df::DataFrame)
14+
DataFrame((name => stringify.(df[!, name]) for name in names(df))...; copycols = false)
15+
end
16+
17+
# helper to fetch the units dict from df.metadata
18+
_get_units(df::DataFrame) =
19+
try
20+
DataFrames.metadata(df, "units", style = :note)
21+
catch
22+
try
23+
DataFrames.metadata(df, "units")
24+
catch
25+
nothing
26+
end
27+
end
28+
29+
# ---------------------------------------------------------------------------
30+
# XLSX sheet writer: reuses/renames Sheet1 for the first write to avoid blanks
31+
# ---------------------------------------------------------------------------
32+
function _write_sheet!(xf, sheetname::String, df::DataFrame; use_first_sheet::Bool)
33+
units = _get_units(df)
34+
df_str = df_to_strings(df)
35+
36+
ws = nothing
37+
if use_first_sheet
38+
ws = try
39+
xf["Sheet1"] # reuse default first sheet
40+
catch
41+
nothing
42+
end
43+
ws = ws === nothing ? XLSX.addsheet!(xf, sheetname) : ws
44+
# If rename! exists, great; if not, we still write so Sheet1 isn't blank.
45+
try
46+
XLSX.rename!(ws, sheetname)
47+
catch
48+
end
49+
else
50+
ws = XLSX.addsheet!(xf, sheetname)
51+
end
52+
53+
# Start row for writing
54+
start_row = 1
55+
56+
# Optional UNITS block (Column | Unit) from DataFrame metadata
57+
if units isa AbstractDict
58+
for name in names(df)
59+
ws[start_row, 1] = String(name)
60+
u = get(units, name, get(units, Symbol(name), ""))
61+
ws[start_row, 2] = String(u)
62+
start_row += 1
63+
end
64+
start_row += 1 # spacer line
65+
end
66+
67+
# IMPORTANT: anchor_cell must be a CellRef, not a String
68+
XLSX.writetable!(
69+
ws,
70+
Tables.columntable(df_str);
71+
anchor_cell = XLSX.CellRef(start_row, 1),
72+
)
73+
return nothing
74+
end
75+
76+
77+
# ---------------------------------------------------------------------------
78+
# Main export
79+
# ---------------------------------------------------------------------------
80+
function export_data(
81+
::Val{:xlsx},
82+
line_params::LineParameters;
83+
file_name::Union{String, Nothing} = nothing,
84+
cable_system::Union{LineCableSystem, Nothing} = nothing,
85+
)::Union{String, Nothing}
86+
87+
# ---- Resolve final file_name (exactly as requested) --------------------
88+
if isnothing(file_name)
89+
if isnothing(cable_system)
90+
file_name = joinpath(@__DIR__, "ZY_export.xlsx")
91+
else
92+
file_name = joinpath(@__DIR__, "$(cable_system.system_id)_ZY_export.xlsx")
93+
end
94+
else
95+
requested = isabspath(file_name) ? file_name : joinpath(@__DIR__, file_name)
96+
if isnothing(cable_system)
97+
file_name = requested
98+
else
99+
dir = dirname(requested)
100+
base = basename(requested)
101+
file_name = joinpath(dir, "$(cable_system.system_id)_$base")
102+
end
103+
end
104+
105+
# ---- Build the DataFrames once (uses LP.f internally) ------------------
106+
df_z, df_y = DataFrame(line_params) # each is Matrix{DataFrame}
107+
108+
# Shapes
109+
nzx, nzy = size(df_z)
110+
nyx, nyy = size(df_y)
111+
112+
# Diagonal-only logic (modal parameters)
113+
Z_isdiag = isdiag_approx(line_params.Z[:, :, 1])
114+
Y_isdiag = isdiag_approx(line_params.Y[:, :, 1])
115+
116+
if Z_isdiag
117+
@warn "Z appears modal/diagonal (isdiag_approx=true). Exporting ONLY diagonal elements Z[i,i]; off-diagonals are intentionally omitted."
118+
end
119+
if Y_isdiag
120+
@warn "Y appears modal/diagonal (isdiag_approx=true). Exporting ONLY diagonal elements Y[i,i]; off-diagonals are intentionally omitted."
121+
end
122+
123+
# ---- Write XLSX --------------------------------------------------------
124+
try
125+
first_sheet = true
126+
XLSX.openxlsx(file_name, mode = "w") do xf
127+
# Z sheets
128+
if Z_isdiag
129+
for i in 1:min(nzx, nzy)
130+
_write_sheet!(xf, "Z($i,$i)", df_z[i, i]; use_first_sheet = first_sheet)
131+
first_sheet = false
132+
end
133+
else
134+
for i in 1:nzx, j in 1:nzy
135+
_write_sheet!(xf, "Z($i,$j)", df_z[i, j]; use_first_sheet = first_sheet)
136+
first_sheet = false
137+
end
138+
end
139+
140+
# Y sheets
141+
if Y_isdiag
142+
for i in 1:min(nyx, nyy)
143+
_write_sheet!(xf, "Y($i,$i)", df_y[i, i]; use_first_sheet = first_sheet)
144+
first_sheet = false
145+
end
146+
else
147+
for i in 1:nyx, j in 1:nyy
148+
_write_sheet!(xf, "Y($i,$j)", df_y[i, j]; use_first_sheet = first_sheet)
149+
first_sheet = false
150+
end
151+
end
152+
end
153+
154+
return file_name
155+
catch err
156+
# If anything explodes (e.g., filesystem perms), return nothing.
157+
# Let the caller decide whether to rethrow.
158+
@error "Failed to export XLSX: $(err)"
159+
return nothing
160+
end
161+
end

0 commit comments

Comments
 (0)