defmodule UmyaSpreadsheet.ConditionalFormatting do @moduledoc """ Functions for conditional formatting. """ alias UmyaSpreadsheet.Spreadsheet alias UmyaNative alias UmyaSpreadsheetEx.CustomStructs.CustomColor @doc """ Adds a cell value rule for conditional formatting. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - The cell range to apply formatting to (e.g., "A1:A10") - `operator` - The comparison operator ("equal", "notEqual", "greaterThan", "lessThan", "greaterThanOrEqual", "lessThanOrEqual", "between", "notBetween") - `value1` - The first value for comparison - `value2` - The second value for comparison (required for "between" and "notBetween" operators) - `format_style` - The color to apply when the condition is met (e.g., "#FF0000" for red) ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Highlight cells equal to 50 in red :ok = UmyaSpreadsheet.add_cell_value_rule( spreadsheet, "Sheet1", "A1:A10", "equal", "50", nil, "#FF0000" ) # Highlight cells between 30 and 70 in green :ok = UmyaSpreadsheet.add_cell_value_rule( spreadsheet, "Sheet1", "A1:A10", "between", "30", "70", "#00FF00" ) """ def add_cell_value_rule( %Spreadsheet{reference: ref}, sheet_name, range, operator, value1, value2, format_style ) do case UmyaNative.add_cell_value_rule( ref, sheet_name, range, operator, value1, value2, format_style ) do {:ok, :ok} -> :ok # Handle the true boolean return value {:ok, true} -> :ok # Handle simple ok atoms :ok -> :ok {:error, reason} -> {:error, reason} result -> result end end @doc """ Adds a cell "is" rule for conditional formatting based on text conditions. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - The cell range to apply formatting to (e.g., "A1:A10") - `operator` - The text comparison operator ("beginsWith", "endsWith", "contains", "notContains") - `value1` - The text value for comparison - `value2` - Not used, pass nil - `format_style` - The color to apply when the condition is met (e.g., "#FF0000" for red) ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Highlight cells that begin with "Error" in red :ok = UmyaSpreadsheet.add_cell_is_rule( spreadsheet, "Sheet1", "A1:A10", "beginsWith", "Error", nil, "#FF0000" ) """ def add_cell_is_rule( %Spreadsheet{reference: ref}, sheet_name, range, operator, value1, value2, format_style ) do case UmyaNative.add_cell_is_rule( ref, sheet_name, range, operator, value1, value2, format_style ) do {:ok, :ok} -> :ok result -> result end end @doc """ Adds a top/bottom rule for conditional formatting. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - The cell range to apply formatting to (e.g., "A1:A10") - `rule_type` - The rule type ("top", "bottom") - `rank` - The number of top/bottom items to highlight - `percent` - Whether rank is a percentage (true) or a count (false) - `format_style` - The color to apply when the condition is met (e.g., "#FF0000" for red) ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Highlight top 3 values in green :ok = UmyaSpreadsheet.add_top_bottom_rule( spreadsheet, "Sheet1", "A1:A10", "top", 3, false, "#00FF00" ) # Highlight bottom 10% of values in red :ok = UmyaSpreadsheet.add_top_bottom_rule( spreadsheet, "Sheet1", "A1:A10", "bottom", 10, true, "#FF0000" ) """ def add_top_bottom_rule( %Spreadsheet{reference: ref}, sheet_name, range, rule_type, rank, percent, format_style ) do case UmyaNative.add_top_bottom_rule( ref, sheet_name, range, rule_type, rank, percent, format_style ) do {:ok, :ok} -> :ok result -> result end end @doc """ Adds a data bar conditional formatting rule to a range of cells. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - The cell range to apply formatting to (e.g., "A1:A10") - `min_value` - Tuple of {type, value} for minimum, or nil for automatic - `max_value` - Tuple of {type, value} for maximum, or nil for automatic - `color` - The color to use for the data bars (e.g., "#638EC6") ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Add data bars with automatic min/max :ok = UmyaSpreadsheet.add_data_bar( spreadsheet, "Sheet1", "A1:A10", nil, nil, "#638EC6" ) # Add data bars with fixed min/max :ok = UmyaSpreadsheet.add_data_bar( spreadsheet, "Sheet1", "B1:B10", {"num", "0"}, {"num", "100"}, "#FF0000" ) """ def add_data_bar(%Spreadsheet{reference: ref}, sheet_name, range, min_value, max_value, color) do case UmyaNative.add_data_bar(ref, sheet_name, range, min_value, max_value, color) do {:ok, :ok} -> :ok result -> result end end @doc """ Adds a text rule conditional formatting rule. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - The cell range to apply formatting to (e.g., "A1:A10") - `operator` - The text operator ("contains", "notContains", "beginsWith", "endsWith") - `text` - The text to search for - `format_style` - The color to apply when the condition is met (e.g., "#FF0000" for red) ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Highlight cells containing "Error" in red :ok = UmyaSpreadsheet.add_text_rule( spreadsheet, "Sheet1", "A1:A10", "contains", "Error", "#FF0000" ) # Highlight cells beginning with "Warning" in orange :ok = UmyaSpreadsheet.add_text_rule( spreadsheet, "Sheet1", "A1:A10", "beginsWith", "Warning", "#FFA500" ) """ def add_text_rule(%Spreadsheet{reference: ref}, sheet_name, range, operator, text, format_style) do case UmyaNative.add_text_rule(ref, sheet_name, range, operator, text, format_style) do {:ok, :ok} -> :ok result -> result end end @doc """ Adds a color scale conditional formatting rule to a range of cells (two-color version). ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - The cell range to apply formatting to (e.g., "A1:A10") - `min_type` - The minimum value type-value tuple or nil for automatic - `max_type` - The maximum value type-value tuple or nil for automatic - `color` - The color to use for the data bars (e.g., "#638EC6") ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Add data bars with automatic min/max :ok = UmyaSpreadsheet.add_data_bar( spreadsheet, "Sheet1", "A1:A10", nil, nil, "#638EC6" ) # Add data bars with fixed min/max :ok = UmyaSpreadsheet.add_data_bar( spreadsheet, "Sheet1", "B1:B10", {"num", "0"}, {"num", "100"}, "#FF0000" ) """ def add_color_scale( %Spreadsheet{reference: ref}, sheet_name, range, min_type, min_value, min_color, max_type, max_value, max_color ) do # Validate range parameter if range == "" do {:error, "Range cannot be empty"} else # Process parameters to ensure they're in the right format min_type_str = process_type(min_type, "min") max_type_str = process_type(max_type, "max") min_value_str = process_value(min_value) max_value_str = process_value(max_value) min_color_map = convert_color(min_color) max_color_map = convert_color(max_color) # Call the NIF with explicit nil values for mid parameters try do case UmyaNative.add_color_scale( ref, sheet_name, range, min_type_str, min_value_str, min_color_map, # mid_type nil, # mid_value nil, # mid_color nil, max_type_str, max_value_str, max_color_map ) do {:ok, :ok} -> :ok {:ok, true} -> :ok :ok -> :ok {:error, reason} -> {:error, reason} other -> IO.puts("Unexpected response from add_color_scale: #{inspect(other)}") :ok end rescue e -> IO.puts("Error in add_color_scale: #{inspect(e, pretty: true)}") IO.puts("Min color: #{inspect(min_color_map, pretty: true)}") IO.puts("Max color: #{inspect(max_color_map, pretty: true)}") {:error, "Function call failed: #{Exception.message(e)}"} end end end @doc """ Adds a color scale conditional formatting rule to a range of cells (three-color version). ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - The cell range to apply formatting to (e.g., "A1:A10") - `min_type` - The minimum value type string or nil for automatic - `min_value` - The minimum value or nil for automatic - `min_color` - The color or color map for minimum values (e.g., "#FF0000" for red) - `mid_type` - The midpoint value type string or nil for automatic - `mid_value` - The midpoint value or nil for automatic - `mid_color` - The color or color map for midpoint values (e.g., "#FFFF00" for yellow) - `max_type` - The maximum value type string or nil for automatic - `max_value` - The maximum value or nil for automatic - `max_color` - The color or color map for maximum values (e.g., "#00FF00" for green) ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Add a three-color scale (red-yellow-green) :ok = UmyaSpreadsheet.add_color_scale( spreadsheet, "Sheet1", "A1:A10", "min", nil, %{argb: "FFFF0000"}, "percentile", "50", %{argb: "FFFFFF00"}, "max", nil, %{argb: "FF00FF00"} ) """ def add_color_scale( %Spreadsheet{reference: ref}, sheet_name, range, min_type, min_value, min_color, mid_type, mid_value, mid_color, max_type, max_value, max_color ) do # Validate range parameter if range == "" do {:error, "Range cannot be empty"} else # Process parameters to ensure they're in the right format min_type_str = process_type(min_type, "min") mid_type_str = process_type(mid_type, "percentile") max_type_str = process_type(max_type, "max") min_value_str = process_value(min_value) mid_value_str = process_value(mid_value) max_value_str = process_value(max_value) min_color_map = convert_color(min_color) mid_color_map = convert_color(mid_color) max_color_map = convert_color(max_color) # Call the NIF with explicit error handling try do case UmyaNative.add_color_scale( ref, sheet_name, range, min_type_str, min_value_str, min_color_map, mid_type_str, mid_value_str, mid_color_map, max_type_str, max_value_str, max_color_map ) do {:ok, :ok} -> :ok {:ok, true} -> :ok :ok -> :ok {:error, reason} -> {:error, reason} other -> IO.puts("Unexpected response from add_color_scale (3-color): #{inspect(other)}") :ok end rescue e -> IO.puts("Error in add_color_scale (3-color): #{inspect(e, pretty: true)}") IO.puts("Min color: #{inspect(min_color_map, pretty: true)}") IO.puts("Mid color: #{inspect(mid_color_map, pretty: true)}") IO.puts("Max color: #{inspect(max_color_map, pretty: true)}") {:error, "Function call failed: #{Exception.message(e)}"} end end end # New helper functions to simplify parameter processing defp process_type(nil, default), do: default defp process_type({type, _}, _) when is_binary(type), do: type defp process_type(type, _) when is_binary(type), do: type defp process_type(_, default), do: default defp process_value(nil), do: nil defp process_value({_, value}) when is_binary(value), do: if(value == "", do: nil, else: value) defp process_value(value) when is_binary(value), do: if(value == "", do: nil, else: value) defp process_value(_), do: nil # Helper function to convert various color formats to a CustomColor struct with argb field alias UmyaSpreadsheetEx.CustomStructs.CustomColor defp convert_color(nil), do: nil # Already a proper CustomColor struct defp convert_color(%CustomColor{} = color), do: color # Map with argb field defp convert_color(%{argb: argb}) when is_binary(argb), do: %CustomColor{argb: argb} defp convert_color(hex_color) when is_binary(hex_color) do # Use the CustomColor.from_hex function to create a proper struct CustomColor.from_hex(hex_color) end # Default to white for unknown formats defp convert_color(_), do: %CustomColor{argb: "FFFFFFFF"} @doc """ Adds an icon set rule for conditional formatting. Icon sets display different icons based on the values in the cells, providing a visual representation of data trends. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - The cell range to apply formatting to (e.g., "A1:A10") - `icon_style` - The style of icons to use (currently supports basic icon sets) - `thresholds` - A list of threshold tuples {type, value} defining the icon boundaries ## Threshold Types - `{"min", ""}` - Use the minimum value in the range - `{"max", ""}` - Use the maximum value in the range - `{"number", "X"}` - Use a specific number - `{"percent", "X"}` - Use X percent of the range - `{"percentile", "X"}` - Use the Xth percentile - `{"formula", "X"}` - Use a formula result ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Simple 3-icon set with percentile thresholds :ok = UmyaSpreadsheet.add_icon_set( spreadsheet, "Sheet1", "A1:A10", "3_traffic_lights", [ {"percentile", "33"}, {"percentile", "67"} ] ) # 5-icon set with custom number thresholds :ok = UmyaSpreadsheet.add_icon_set( spreadsheet, "Sheet1", "B1:B10", "5_arrows", [ {"number", "20"}, {"number", "40"}, {"number", "60"}, {"number", "80"} ] ) """ def add_icon_set(%Spreadsheet{reference: ref}, sheet_name, range, icon_style, thresholds) do case UmyaNative.add_icon_set(ref, sheet_name, range, icon_style, thresholds) do {:ok, :ok} -> :ok # Handle the true boolean return value {:ok, true} -> :ok # Handle simple ok atoms :ok -> :ok {:error, reason} -> {:error, reason} result -> result end end @doc """ Adds an above or below average rule for conditional formatting. This rule highlights cells that are above or below the average value of the selected range. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - The cell range to apply formatting to (e.g., "A1:A10") - `rule_type` - The type of rule: "above", "below", "above_equal", "below_equal" - `std_dev` - Optional standard deviation for more advanced rules (nil for basic average) - `format_style` - The color to apply when the condition is met (e.g., "#FF0000" for red) ## Rule Types - `"above"` - Highlight cells above the average - `"below"` - Highlight cells below the average - `"above_equal"` - Highlight cells above or equal to the average - `"below_equal"` - Highlight cells below or equal to the average ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Highlight cells above average in green :ok = UmyaSpreadsheet.add_above_below_average_rule( spreadsheet, "Sheet1", "A1:A10", "above", nil, "#00FF00" ) # Highlight cells below average in red :ok = UmyaSpreadsheet.add_above_below_average_rule( spreadsheet, "Sheet1", "B1:B10", "below", nil, "#FF0000" ) # Highlight cells above average plus one standard deviation in blue :ok = UmyaSpreadsheet.add_above_below_average_rule( spreadsheet, "Sheet1", "C1:C10", "above", 1, "#0000FF" ) """ def add_above_below_average_rule( %Spreadsheet{reference: ref}, sheet_name, range, rule_type, std_dev, format_style ) do case UmyaNative.add_above_below_average_rule( ref, sheet_name, range, rule_type, std_dev, format_style ) do {:ok, :ok} -> :ok # Handle the true boolean return value {:ok, true} -> :ok # Handle simple ok atoms :ok -> :ok {:error, reason} -> {:error, reason} result -> result end end @doc """ Gets all conditional formatting rules for a sheet or specific range. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - Optional. The cell range to get rules for. If nil, returns all rules for the sheet. ## Returns A list of maps, each representing a conditional formatting rule with the following keys: - `:range` - The cell range the rule applies to - `:rule_type` - The type of rule (":cell_is", ":color_scale", ":data_bar", ":icon_set", etc.) - Other rule-specific fields depending on the rule type ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Get all conditional formatting rules in Sheet1 rules = UmyaSpreadsheet.get_conditional_formatting_rules( spreadsheet, "Sheet1" ) # Get conditional formatting rules for a specific range range_rules = UmyaSpreadsheet.get_conditional_formatting_rules( spreadsheet, "Sheet1", "A1:A10" ) """ def get_conditional_formatting_rules(%Spreadsheet{reference: ref}, sheet_name, range \\ nil) do case UmyaNative.get_conditional_formatting_rules(ref, sheet_name, range) do {:ok, rules} -> rules {:error, reason} -> {:error, reason} result -> result end end @doc """ Gets all cell value rules for a sheet or specific range. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - Optional. The cell range to get rules for. If nil, returns all rules for the sheet. ## Returns A list of maps, each representing a cell value rule with the following keys: - `:range` - The cell range the rule applies to - `:operator` - The comparison operator (e.g., ":equal", ":greater_than") - `:formula` - The formula or value to compare against - `:format_style` - The color or style to apply when the condition is met ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Get all cell value rules in Sheet1 rules = UmyaSpreadsheet.get_cell_value_rules( spreadsheet, "Sheet1" ) """ def get_cell_value_rules(%Spreadsheet{reference: ref}, sheet_name, range \\ nil) do case UmyaNative.get_cell_value_rules(ref, sheet_name, range) do {:ok, rules} -> rules {:error, reason} -> {:error, reason} result -> result end end @doc """ Gets all color scale rules for a sheet or specific range. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - Optional. The cell range to get rules for. If nil, returns all rules for the sheet. ## Returns A list of maps, each representing a color scale rule with the following keys: - `:range` - The cell range the rule applies to - `:min_type` - The type for minimum value (e.g., ":min", ":number", ":percent") - `:min_value` - The minimum value (if applicable) - `:min_color` - The color for minimum values - `:mid_type` - Optional. The type for midpoint value - `:mid_value` - Optional. The midpoint value - `:mid_color` - Optional. The color for midpoint values - `:max_type` - The type for maximum value - `:max_value` - The maximum value (if applicable) - `:max_color` - The color for maximum values ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Get all color scale rules in Sheet1 rules = UmyaSpreadsheet.get_color_scales( spreadsheet, "Sheet1" ) """ def get_color_scales(%Spreadsheet{reference: ref}, sheet_name, range \\ nil) do case UmyaNative.get_color_scales(ref, sheet_name, range) do result when is_list(result) -> {:ok, result} result -> result end end @doc """ Gets all data bar rules for a sheet or specific range. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - Optional. The cell range to get rules for. If nil, returns all rules for the sheet. ## Returns A list of maps, each representing a data bar rule with the following keys: - `:range` - The cell range the rule applies to - `:min_value` - Optional tuple of {type, value} for minimum - `:max_value` - Optional tuple of {type, value} for maximum - `:color` - The color of the data bars ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Get all data bar rules in Sheet1 rules = UmyaSpreadsheet.get_data_bars( spreadsheet, "Sheet1" ) """ def get_data_bars(%Spreadsheet{reference: ref}, sheet_name, range \\ nil) do case UmyaNative.get_data_bars(ref, sheet_name, range) do result when is_list(result) -> {:ok, result} result -> result end end @doc """ Gets all icon set rules for a sheet or specific range. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - Optional. The cell range to get rules for. If nil, returns all rules for the sheet. ## Returns A list of maps, each representing an icon set rule with the following keys: - `:range` - The cell range the rule applies to - `:icon_style` - The style of icons to use - `:thresholds` - A list of threshold tuples {type, value} defining the icon boundaries ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Get all icon set rules in Sheet1 rules = UmyaSpreadsheet.get_icon_sets( spreadsheet, "Sheet1" ) """ def get_icon_sets(%Spreadsheet{reference: ref}, sheet_name, range \\ nil) do case UmyaNative.get_icon_sets(ref, sheet_name, range) do result when is_list(result) -> {:ok, result} result -> result end end @doc """ Gets all top/bottom rules for a sheet or specific range. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - Optional. The cell range to get rules for. If nil, returns all rules for the sheet. ## Returns A list of maps, each representing a top/bottom rule with the following keys: - `:range` - The cell range the rule applies to - `:rule_type_value` - "top" or "bottom" - `:rank` - The number of top/bottom items to highlight - `:percent` - Whether rank is a percentage (true) or a count (false) - `:format_style` - The color to apply when the condition is met ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Get all top/bottom rules in Sheet1 rules = UmyaSpreadsheet.get_top_bottom_rules( spreadsheet, "Sheet1" ) """ def get_top_bottom_rules(%Spreadsheet{reference: ref}, sheet_name, range \\ nil) do case UmyaNative.get_top_bottom_rules(ref, sheet_name, range) do {:ok, rules} -> rules {:error, reason} -> {:error, reason} result -> result end end @doc """ Gets all above/below average rules for a sheet or specific range. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - Optional. The cell range to get rules for. If nil, returns all rules for the sheet. ## Returns A list of maps, each representing an above/below average rule with the following keys: - `:range` - The cell range the rule applies to - `:rule_type_value` - "above", "below", "above_equal", or "below_equal" - `:std_dev` - Optional standard deviation value - `:format_style` - The color to apply when the condition is met ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Get all above/below average rules in Sheet1 rules = UmyaSpreadsheet.get_above_below_average_rules( spreadsheet, "Sheet1" ) """ def get_above_below_average_rules(%Spreadsheet{reference: ref}, sheet_name, range \\ nil) do case UmyaNative.get_above_below_average_rules(ref, sheet_name, range) do {:ok, rules} -> rules {:error, reason} -> {:error, reason} result -> result end end @doc """ Gets all text rules for a sheet or specific range. ## Parameters - `spreadsheet` - The spreadsheet struct - `sheet_name` - The name of the sheet - `range` - Optional. The cell range to get rules for. If nil, returns all rules for the sheet. ## Returns A list of maps, each representing a text rule with the following keys: - `:range` - The cell range the rule applies to - `:operator` - The text operator ("contains", "notContains", "beginsWith", "endsWith") - `:text` - The text to search for - `:format_style` - The color to apply when the condition is met ## Examples {:ok, spreadsheet} = UmyaSpreadsheet.read_file("input.xlsx") # Get all text rules in Sheet1 rules = UmyaSpreadsheet.get_text_rules( spreadsheet, "Sheet1" ) """ def get_text_rules(%Spreadsheet{reference: ref}, sheet_name, range \\ nil) do case UmyaNative.get_text_rules(ref, sheet_name, range) do {:ok, rules} -> rules {:error, reason} -> {:error, reason} result -> result end end end