Skip to content

Commit

Permalink
Adding write support to File_Format.Excel (#3551)
Browse files Browse the repository at this point in the history
Support for writing tables to Excel.

# Important Notes
Has custom support for Error mode as will allow appending a new table in this mode to the file.
  • Loading branch information
jdunkerley authored Jul 4, 2022
1 parent 2b2563a commit 4ca2097
Show file tree
Hide file tree
Showing 32 changed files with 765 additions and 569 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
- [Removed obsolete `from_xls` and `from_xlsx` functions. Added support for
reading column names from first row in `File_Format.Excel`][3523]
- [Added `File_Format.Delimited` support to `Table.write` for new files.][3528]
- [Added `File_Format.Excel` support to `Table.write` for new files.][3551]

[debug-shortcuts]:
https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug
Expand Down Expand Up @@ -227,6 +228,7 @@
[3519]: https://github.com/enso-org/enso/pull/3519
[3523]: https://github.com/enso-org/enso/pull/3523
[3528]: https://github.com/enso-org/enso/pull/3528
[3551]: https://github.com/enso-org/enso/pull/3551

#### Enso Compiler

Expand Down
25 changes: 23 additions & 2 deletions distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso
Original file line number Diff line number Diff line change
Expand Up @@ -524,9 +524,30 @@ type File
Arguments:
- destination: the destination to move the file to.
- replace_existing: specifies if the operation should proceed if the
destination file already exists. Defaults to `True`.
destination file already exists. Defaults to `False`.
copy_to : File -> Boolean -> Nothing ! File_Error
copy_to destination replace_existing=False =
here.handle_java_exceptions self <| case replace_existing of
True ->
copy_options = Array.new_1 StandardCopyOption.REPLACE_EXISTING
self.copy_builtin destination copy_options
False -> self.copy_builtin destination Array.empty

## PRIVATE

Builtin method that copies this file to a new destination.
Recommended to use `File.copy_to` instead which handles potential exceptions.
copy_builtin : File -> Array Any -> Nothing
copy_builtin destination copy_options = @Builtin_Method "File.copy_builtin"

## Moves the file to the specified destination.

Arguments:
- destination: the destination to move the file to.
- replace_existing: specifies if the operation should proceed if the
destination file already exists. Defaults to `False`.
move_to : File -> Boolean -> Nothing ! File_Error
move_to destination replace_existing=True =
move_to destination replace_existing=False =
here.handle_java_exceptions self <| case replace_existing of
True ->
copy_options = Array.new_1 StandardCopyOption.REPLACE_EXISTING
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ write_file_backing_up_old_one file action = Panic.recover [Io_Error, File_Not_Fo
to back-up anymore, but this is not a failure, so it can be
safely ignored.
Panic.catch File_Not_Found handler=(_->Nothing) <|
Panic.rethrow <| file.move_to bak_file
Panic.rethrow <| file.move_to bak_file replace_existing=True
Panic.rethrow <| new_file.move_to file
go 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

example_to_xlsx =
path = Enso_Project.data / example_xlsx_output.xlsx
Examples.inventory_table.write_xlsx path
Examples.inventory_table.write path

> Example
Join multiple tables together. It joins tables on their indices, so we need
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

example_to_xlsx =
path = Enso_Project.data / example_xlsx_output.xlsx
Examples.inventory_table.write_xlsx path
Examples.inventory_table.write path

> Example
Write a table to a CSV file.
Expand Down
2 changes: 1 addition & 1 deletion distribution/lib/Standard/Table/0.0.0-dev/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ component-groups:
- Standard.Table.Data.Column.Column.to_table
- Standard.Base.Output:
exports:
- Standard.Table.Data.Table.Table.write_xlsx
- Standard.Table.Data.Table.Table.write
75 changes: 0 additions & 75 deletions distribution/lib/Standard/Table/0.0.0-dev/src/Data/Table.enso
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import Standard.Base.System.Platform
import Standard.Table.Data.Column
import Standard.Visualization
from Standard.Base.Data.Time.Date as Date_Module import Date
import Standard.Table.Io.Spreadsheet_Write_Mode
import Standard.Table.Io.File_Format
import Standard.Base.System.File
import Standard.Base.System.File.Existing_File_Behavior
Expand Down Expand Up @@ -33,7 +32,6 @@ import Standard.Base.Data.Ordering.Comparator

polyglot java import org.enso.table.data.table.Table as Java_Table
polyglot java import org.enso.table.data.table.Column as Java_Column
polyglot java import org.enso.table.format.xlsx.Writer as Spreadsheet_Writer
polyglot java import org.enso.table.operations.OrderBuilder

## Creates a new table from a vector of `[name, items]` pairs.
Expand Down Expand Up @@ -1030,50 +1028,6 @@ type Table
mask = OrderBuilder.buildReversedMask self.row_count
Table <| self.java_table.applyMask mask

## ALIAS Write Excel File
UNSTABLE

Writes this table into an XLSX spreadsheet.

Arguments:
- file: the XLSX file to write data to. If it exists, the behavior is
specified by the `write_mode` argument. Note that other files may be
created or written to if `max_rows_per_file` is used.
- sheet: the name of the sheet to use for writing the data.
- write_mode: specifies this method's behavior if the specified file and
sheet already exist. Can be one of:
- Spreadsheet_Write_Mode.Create: this is the default value. This
setting will create a new sheet in the file, with a name chosen such
that the clash is avoided.
- Spreadsheet_Write_Mode.Overwrite: will result in removing all
contents of the existing sheet and replacing it with the new data.
- Spreadsheet_Write_Mode.Append: will append this data to the existing
sheet, such that the new data starts after the last row containing
any data.
- include_header: Specifies whether the first line of generated CSV
should contain the column names.
- max_rows_per_file: specifies the maximum number of rows that can be
written to a single file. If this option is set and its value is less
than the number of rows in this table, the behavior of the `file`
argument changes. Instead of writing the contents directly to `file`,
its name is parsed and a numbered series of files with names based
on `file` is written to instead. For example, if `file` points to
`~/my_data/output.xlsx`, `self` contains 250 rows, and
`max_rows_per_file` is set to `100`, 3 different files will be written:
- `~/my_data/output_1.xlsx`, containing rows 0 through 99;
- `~/my_data/output_2.xlsx`, containing rows 100 through 199;
- `~/my_data/output_3.xlsx`, containing rows 200 through 249.

> Example
Write a table to an XLSX file.

import Standard.Examples

example_to_xlsx = Examples.inventory_table.write_xlsx (Enso_Project.data / example_xlsx_output.xlsx)
write_xlsx : File.File -> String -> Spreadsheet_Write_Mode.Speadsheet_Write_Mode -> Boolean -> Nothing | Integer -> Nothing
write_xlsx file sheet='Data' write_mode=Spreadsheet_Write_Mode.Create include_header=True max_rows_per_file=Nothing =
Spreadsheet_Writer.writeXlsx self.java_table file.absolute.path sheet write_mode.to_java include_header max_rows_per_file .write_to_spreadsheet

## ALIAS Write JSON
UNSTABLE

Expand Down Expand Up @@ -1162,35 +1116,6 @@ type Table
to_csv : Text
to_csv = Text.from self (File_Format.Delimited delimiter=",")

## UNSTABLE
ADVANCED

Used to write a value into a spreadsheet cell.

Arguments:
- cell: an instance of `org.apache.poi.ss.usermodel.Cell`, the value of
which should be set by this method.
Any.write_to_spreadsheet cell = cell.setCellValue self.to_text

## UNSTABLE
ADVANCED

Used to write a value into a spreadsheet cell.

Arguments:
- cell: an instance of `org.apache.poi.ss.usermodel.Cell`, the value of
which should be set by this method.
Text.write_to_spreadsheet cell = cell.setCellValue self

## UNSTABLE
ADVANCED

Used to write a value into a spreadsheet cell.

Arguments:
- cell: an instance of `org.apache.poi.ss.usermodel.Cell`, the value of
which should be set by this method.
Date.write_to_spreadsheet cell = cell.setCellValue self.internal_local_date

## UNSTABLE

Expand Down
13 changes: 13 additions & 0 deletions distribution/lib/Standard/Table/0.0.0-dev/src/Error.enso
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,16 @@ type Unsupported_File_Type filename
Unsupported_File_Type.to_display_text : Text
Unsupported_File_Type.to_display_text =
"The "+self.filename+" has a type that is not supported by the Auto format."

## Indicates that the target range contains existing data and the user did not
specify to overwrite.
type Existing_Data message

Existing_Data.to_display_text : Text
Existing_Data.to_display_text = self.message

## Indicates that the specified range is not large enough to fit the data.
type Range_Exceeded message

Range_Exceeded.to_display_text : Text
Range_Exceeded.to_display_text = self.message
85 changes: 69 additions & 16 deletions distribution/lib/Standard/Table/0.0.0-dev/src/Io/Excel.enso
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
from Standard.Base import all
from Standard.Base.Error.Problem_Behavior as Problem_Behavior_Module import Problem_Behavior
import Standard.Base.System.File.Existing_File_Behavior
import Standard.Base.System.File.Option
from Standard.Table.Io.File_Format import Infer

import Standard.Table.Data.Table
from Standard.Table.Error as Error_Module import Invalid_Location, Duplicate_Output_Column_Names, Invalid_Output_Column_Names
from Standard.Table.Error as Error_Module import Invalid_Location, Duplicate_Output_Column_Names, Invalid_Output_Column_Names, Range_Exceeded, Existing_Data
import Standard.Base.Error.Common as Errors

polyglot java import org.enso.table.excel.ExcelRange as Java_Range
polyglot java import org.enso.table.excel.ExcelHeaders
polyglot java import org.enso.table.read.ExcelReader
polyglot java import org.enso.table.write.ExcelWriter
polyglot java import org.enso.table.error.ExistingDataException
polyglot java import org.enso.table.error.RangeExceededException
polyglot java import org.enso.table.error.InvalidLocationException

polyglot java import java.lang.IllegalArgumentException
polyglot java import java.lang.IllegalStateException
polyglot java import java.io.IOException
polyglot java import org.apache.poi.UnsupportedFileFormatException
polyglot java import org.enso.table.util.problems.DuplicateNames
Expand Down Expand Up @@ -103,8 +112,9 @@ type Excel_Range
## Creates a Range from an address.
from_address : Text -> Excel_Range
from_address address =
Panic.catch IllegalArgumentException (Excel_Range (Java_Range.new address)) caught_panic->
Error.throw (Illegal_Argument_Error caught_panic.payload.cause.getMessage caught_panic.payload.cause)
illegal_argument caught_panic = Error.throw (Illegal_Argument_Error caught_panic.payload.cause.getMessage caught_panic.payload.cause)
Panic.catch IllegalArgumentException handler=illegal_argument <|
Excel_Range (Java_Range.new address)

## Create a Range for a single cell.
for_cell : Text -> (Text|Integer) -> Integer -> Excel_Range
Expand Down Expand Up @@ -185,16 +195,31 @@ read_excel file section headers on_problems xls_format=False =
Excel_Range _ -> ExcelReader.readRange stream address.java_range (here.make_java_headers headers) skip_rows row_limit xls_format
Text -> ExcelReader.readRangeByName stream address (here.make_java_headers headers) skip_rows row_limit xls_format

bad_argument caught_panic = Error.throw (Invalid_Location caught_panic.payload.cause.getCause)
handle_bad_argument = Panic.catch IllegalArgumentException handler=bad_argument

bad_format caught_panic = Error.throw (File.Io_Error file caught_panic.payload.cause.getMessage)
handle_bad_format = Panic.catch UnsupportedFileFormatException handler=bad_format

File.handle_java_exceptions file <| handle_bad_argument <| handle_bad_format <|
file.with_input_stream [File.Option.Read] stream->(stream.with_java_stream reader)
here.read_excel_file file reader

write_excel : File -> Table -> Existing_File_Behavior -> (Sheet | Cell_Range) -> (Boolean|Infer) -> Problem_Behavior -> Boolean
write_excel file table on_existing_file section headers _ xls_format=False =
if on_existing_file == Existing_File_Behavior.Append then Errors.unimplemented "Appending to an existing File_Format.Delimited file is not implemented yet." else
workbook = if file.exists.not then ExcelWriter.createWorkbook xls_format else
here.read_excel_file file stream->(ExcelReader.getWorkbook stream xls_format)

replace = (on_existing_file == Existing_File_Behavior.Overwrite) || (on_existing_file == Existing_File_Behavior.Backup)
java_headers = here.make_java_headers headers
if ExcelWriter.getEnsoToTextCallback == Nothing then ExcelWriter.getEnsoToTextCallback (.to_text)
result = here.handle_writer <| case section of
Sheet sheet skip_rows row_limit ->
ExcelWriter.writeTableToSheet workbook sheet replace skip_rows table.java_table row_limit java_headers
Cell_Range address skip_rows row_limit -> case address of
Excel_Range _ -> ExcelWriter.writeTableToRange workbook address.java_range replace skip_rows table.java_table row_limit java_headers
Text -> ExcelWriter.writeTableToRange workbook address replace skip_rows table.java_table row_limit java_headers

if result.is_error then result else
write_stream stream = stream.with_java_stream java_stream->
workbook.write java_stream
on_existing_file.write file write_stream

## PRIVATE
prepare_reader_table : Problem_Behavior -> Any -> Table
prepare_reader_table on_problems result_with_problems =
map_problem java_problem =
if Java.is_instance java_problem DuplicateNames then Duplicate_Output_Column_Names (Vector.Vector java_problem.duplicatedNames) else
Expand All @@ -204,9 +229,37 @@ prepare_reader_table on_problems result_with_problems =
on_problems.attach_problems_after (Table.Table result_with_problems.value) parsing_problems

## PRIVATE
Convert True|False|Infer to the correct HeaderBehavior
make_java_headers : (True|False|Infer) -> ExcelReader.HeaderBehavior
Convert Boolean|Infer to the correct HeaderBehavior
make_java_headers : (Boolean|Infer) -> ExcelHeaders.HeaderBehavior
make_java_headers headers = case headers of
True -> ExcelReader.HeaderBehavior.USE_FIRST_ROW_AS_HEADERS
Infer -> ExcelReader.HeaderBehavior.INFER
False -> ExcelReader.HeaderBehavior.EXCEL_COLUMN_NAMES
True -> ExcelHeaders.HeaderBehavior.USE_FIRST_ROW_AS_HEADERS
Infer -> ExcelHeaders.HeaderBehavior.INFER
False -> ExcelHeaders.HeaderBehavior.EXCEL_COLUMN_NAMES

## PRIVATE
read_excel_file file reader =
bad_format caught_panic = Error.throw (File.Io_Error file caught_panic.payload.cause.getMessage)
handle_bad_format = Panic.catch UnsupportedFileFormatException handler=bad_format

bad_argument caught_panic = Error.throw (Invalid_Location caught_panic.payload.cause.getCause)
handle_bad_argument = Panic.catch InvalidLocationException handler=bad_argument

File.handle_java_exceptions file <| handle_bad_argument <| handle_bad_format <|
file.with_input_stream [File.Option.Read] stream->
stream.with_java_stream reader

## PRIVATE
handle_writer ~writer =
bad_location caught_panic = Error.throw (Invalid_Location caught_panic.payload.cause.getCause)
handle_bad_location = Panic.catch InvalidLocationException handler=bad_location

throw_range_exceeded caught_panic = Error.throw (Range_Exceeded caught_panic.payload.cause.getMessage)
handle_range_exceeded = Panic.catch RangeExceededException handler=throw_range_exceeded

throw_existing_data caught_panic = Error.throw (Existing_Data caught_panic.payload.cause.getMessage)
handle_existing_data = Panic.catch ExistingDataException handler=throw_existing_data

throw_illegal_state caught_panic = Panic.throw (Illegal_State_Error caught_panic.payload.cause.getMessage)
handle_illegal_state = Panic.catch IllegalStateException handler=throw_illegal_state

handle_illegal_state <| handle_bad_location <| handle_range_exceeded <| handle_existing_data <| writer
21 changes: 15 additions & 6 deletions distribution/lib/Standard/Table/0.0.0-dev/src/Io/File_Format.enso
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,22 @@ type Excel
## Implements the `File.read` for this `File_Format`
read : File -> Problem_Behavior -> Any
read file on_problems =
format = if self.xls_format != Infer then self.xls_format else
extension = file.extension
(extension.equals_ignore_case ".xls") || (extension.equals_ignore_case ".xlt")

format = Excel.is_xls_format self.xls_format file
Excel_Module.read_excel file self.section self.headers on_problems format

## Implements the `Table.write` for this `File_Format`.
write_table : File -> Table -> Existing_File_Behavior -> Column_Mapping -> Problem_Behavior -> Nothing
write_table _ _ _ _ _ =
Errors.unimplemented "`Table.write` for the `Excel` format is not implemented yet."
write_table file table on_existing_file _ on_problems =
format = Excel.is_xls_format self.xls_format file
case self.section of
Excel_Module.Sheet_Names -> Error.throw (Illegal_Argument_Error "Sheet_Names cannot be used for `write`.")
Excel_Module.Range_Names -> Error.throw (Illegal_Argument_Error "Range_Names cannot be used for `write`.")
_ -> Excel_Module.write_excel file table on_existing_file self.section self.headers on_problems format

## PRIVATE
Resolve the xls_format setting to a boolean.
is_xls_format : (Boolean|Infer) -> File -> Boolean
is_xls_format xls_format file =
if xls_format != Infer then xls_format else
extension = file.extension
(extension.equals_ignore_case ".xls") || (extension.equals_ignore_case ".xlt")

This file was deleted.

4 changes: 2 additions & 2 deletions distribution/lib/Standard/Table/0.0.0-dev/src/Main.enso
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ from Standard.Base import all

import Standard.Geo.Geo_Json
import Standard.Table.Io.File_Read
import Standard.Table.Io.File_Format
import Standard.Table.Io.Excel
import Standard.Table.Io.Spreadsheet_Write_Mode
import Standard.Table.Data.Table
import Standard.Table.Data.Column
import Standard.Table.Model

from Standard.Table.Io.Excel export Excel_Section, Excel_Range

export Standard.Table.Io.Spreadsheet_Write_Mode
export Standard.Table.Data.Column
export Standard.Table.Model
export Standard.Table.Io.File_Read
export Standard.Table.Io.File_Format

from Standard.Table.Data.Table export new, from_rows, join, concat, No_Such_Column_Error, Table

Expand Down
Loading

0 comments on commit 4ca2097

Please sign in to comment.