You are viewing a plain text version of this content. The canonical link for it is here.
Posted to github@arrow.apache.org by "tobixdev (via GitHub)" <gi...@apache.org> on 2025/11/12 22:56:04 UTC

[PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

tobixdev opened a new pull request, #8829:
URL: https://github.com/apache/arrow-rs/pull/8829

   # Which issue does this PR close?
   
   - Closes #8821.
   
   # Rationale for this change
   
   Allows users that require custom pretty-printing logic for batches to supply this implementation.
   
   # What changes are included in this PR?
   
   Changes to existing code:
   
   - Make fields in `FormatOptions` public. This is necessary as the custom `ArrayFormatter` must also have access to the formatting options. (see `<NULL>` in the test)
   - Deprecate `types_info()` method as the field is now public
   - Allow directly creating `ArrayFormatter` with a `DisplayIndex` implementation
   - Make `FormatError`, `FormatResult`, and `DisplayIndex` public. I do have some second thoughts about `DisplayIndex` not having any concept of length even though its taking an index as input. However, it may be fine for now.
   
   New code:
   
   - `ArrayFormatterFactory`: Allows creating `ArrayFormatters` with custom behavior
   - `pretty_format_batches_with_options_and_formatters` pretty printing with custom formatters
   - Similar thing for format column
   
   # Are these changes tested?
   
   Yes, existing tests cover the default formatting path.
   
   Three new tests:
   - Format record batch with custom type (append € sign)
   - Format column with custom formatter (append (32-Bit) for `Int32`)
   - Allow overriding the custom types with a custom schema (AFAIK this is not possible with the current API but might make sense).
   - Added a sanity check that the number of fields in a custom schema must match the number of columns in the record batch.
   
   # Are there any user-facing changes?
   
   Yes, multiple things become public, `types_info()` becomes deprecated, and there are new APIs for custom pretty printing of batches.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "tobixdev (via GitHub)" <gi...@apache.org>.
tobixdev commented on code in PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#discussion_r2520065872


##########
arrow-cast/src/pretty.rs:
##########
@@ -22,14 +22,99 @@
 //! [`RecordBatch`]: arrow_array::RecordBatch
 //! [`Array`]: arrow_array::Array
 
-use std::fmt::Display;
-
 use comfy_table::{Cell, Table};
+use std::fmt::{Display, Write};
+use std::sync::Arc;
 
-use arrow_array::{Array, ArrayRef, RecordBatch};
-use arrow_schema::{ArrowError, SchemaRef};
+use arrow_array::cast::AsArray;
+use arrow_array::{Array, ArrayRef, Int32Array, RecordBatch, array};
+use arrow_schema::{ArrowError, Field, SchemaRef};
 
-use crate::display::{ArrayFormatter, FormatOptions};
+use crate::display::{ArrayFormatter, DisplayIndex, FormatOptions};
+
+/// Allows creating a new [`ArrayFormatter`] for a given [`Array`] and an optional [`Field`].
+///
+/// # Example
+///
+/// The example below shows how to create a custom formatter for a custom type `my_money`.
+///
+/// ```rust
+/// use std::fmt::Write;
+/// use arrow_array::{Array, Int32Array, cast::AsArray};
+/// use arrow_cast::display::{ArrayFormatter, DisplayIndex, FormatOptions, FormatResult};
+/// use arrow_cast::pretty::{pretty_format_batches_with_options_and_formatters, ArrayFormatterFactory};
+/// use arrow_schema::{ArrowError, Field};
+///
+/// /// A custom formatter factory that can create a formatter for the special type `my_money`.
+/// ///
+/// /// This struct could have access to some kind of extension type registry that can lookup the
+/// /// correct formatter for an extension type on-demand.
+/// struct MyFormatters {}
+///
+/// impl ArrayFormatterFactory for MyFormatters {

Review Comment:
   From the DataFusion perspective, this would be the trait that we would implenent. It looks up any existing extension types in the registry and uses the custom printing implementation.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "alamb (via GitHub)" <gi...@apache.org>.
alamb commented on PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#issuecomment-3544038414

   I'll try and review this tomorrow too. I am hoping it can make the next release in the next day or two
   - #8464 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "alamb (via GitHub)" <gi...@apache.org>.
alamb merged PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "martin-g (via GitHub)" <gi...@apache.org>.
martin-g commented on code in PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#discussion_r2527643605


##########
arrow-cast/src/pretty.rs:
##########
@@ -208,10 +206,24 @@ fn create_table(
     }
 
     for batch in results {
+        let schema = schema_opt.as_ref().unwrap_or(batch.schema_ref());
+
+        // Could be a custom schema that was provided.
+        if batch.columns().len() != schema.fields().len() {
+            return Err(ArrowError::InvalidArgumentError("Expected the same number of columns in a record batch as the number of fields in the schema".to_owned()));

Review Comment:
   Consider adding the actual lengths in the error message for easier debugging.



##########
arrow-cast/src/display.rs:
##########
@@ -82,6 +91,43 @@ impl Default for FormatOptions<'_> {
     }
 }
 
+impl PartialEq for FormatOptions<'_> {
+    fn eq(&self, other: &Self) -> bool {
+        self.safe == other.safe
+            && self.null == other.null
+            && self.date_format == other.date_format
+            && self.datetime_format == other.datetime_format
+            && self.timestamp_format == other.timestamp_format
+            && self.timestamp_tz_format == other.timestamp_tz_format
+            && self.time_format == other.time_format
+            && self.duration_format == other.duration_format
+            && match (self.formatter_factory, other.formatter_factory) {
+                (Some(f1), Some(f2)) => std::ptr::eq(f1, f2),
+                (None, None) => true,
+                _ => false,
+            }
+    }
+}
+
+impl Eq for FormatOptions<'_> {}
+
+impl Hash for FormatOptions<'_> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.safe.hash(state);
+        self.null.hash(state);
+        self.date_format.hash(state);
+        self.datetime_format.hash(state);
+        self.timestamp_format.hash(state);
+        self.timestamp_tz_format.hash(state);
+        self.time_format.hash(state);
+        self.duration_format.hash(state);
+        self.types_info.hash(state);

Review Comment:
   The Hash/Equality contract is broken - the `types_info` is used for the Hash, but not for the PartialEq impl.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "tobixdev (via GitHub)" <gi...@apache.org>.
tobixdev commented on code in PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#discussion_r2520072165


##########
arrow-cast/src/pretty.rs:
##########
@@ -1283,4 +1428,247 @@ mod tests {
         let actual: Vec<&str> = iso.lines().collect();
         assert_eq!(expected_iso, actual, "Actual result:\n{iso}");
     }
+
+    //
+    // Custom Formatting
+    //
+
+    /// The factory that will create the [`ArrayFormatter`]s.
+    struct TestFormatters {}
+
+    impl ArrayFormatterFactory for TestFormatters {
+        fn create_display_index<'formatter>(
+            &self,
+            array: &'formatter dyn Array,
+            options: &'formatter FormatOptions<'formatter>,
+            field: Option<&'formatter Field>,
+        ) -> Result<Option<ArrayFormatter<'formatter>>, ArrowError> {
+            if field
+                .map(|f| f.extension_type_name() == Some("my_money"))
+                .unwrap_or(false)
+            {
+                // We assume that my_money always is an Int32.
+                let array = array.as_primitive();
+                let display_index = Box::new(MyMoneyFormatter { array, options });
+                return Ok(Some(ArrayFormatter::new(display_index, options.safe)));
+            }
+
+            if array.data_type() == &DataType::Int32 {
+                // We assume that my_money always is an Int32.
+                let array = array.as_primitive();
+                let display_index = Box::new(MyInt32Formatter { array, options });
+                return Ok(Some(ArrayFormatter::new(display_index, options.safe)));
+            }
+
+            Ok(None)
+        }
+    }
+
+    /// The actual formatter
+    struct MyMoneyFormatter<'a> {
+        array: &'a Int32Array,
+        options: &'a FormatOptions<'a>,
+    }
+
+    impl<'a> DisplayIndex for MyMoneyFormatter<'a> {
+        fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
+            match self.array.is_valid(idx) {
+                true => write!(f, "{} €", self.array.value(idx))?,
+                false => write!(f, "{}", self.options.null)?,
+            }
+
+            Ok(())
+        }
+    }
+
+    /// The actual formatter
+    struct MyInt32Formatter<'a> {
+        array: &'a Int32Array,
+        options: &'a FormatOptions<'a>,
+    }
+
+    impl<'a> DisplayIndex for MyInt32Formatter<'a> {
+        fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
+            match self.array.is_valid(idx) {
+                true => write!(f, "{} (32-Bit)", self.array.value(idx))?,
+                false => write!(f, "{}", self.options.null)?,
+            }
+
+            Ok(())
+        }
+    }
+
+    #[test]
+    fn test_format_batches_with_custom_formatters() {
+        // define a schema.
+        let options = FormatOptions::new().with_null("<NULL>");
+        let money_metadata = HashMap::from([(
+            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
+            "my_money".to_owned(),
+        )]);
+        let schema = Arc::new(Schema::new(vec![
+            Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
+        ]));
+
+        // define data.
+        let batch = RecordBatch::try_new(
+            schema,
+            vec![Arc::new(array::Int32Array::from(vec![
+                Some(1),
+                None,
+                Some(10),
+                Some(100),
+            ]))],
+        )
+        .unwrap();
+
+        let mut buf = String::new();
+        write!(
+            &mut buf,
+            "{}",
+            pretty_format_batches_with_options_and_formatters(
+                &[batch],
+                &options,
+                Some(&TestFormatters {})
+            )
+            .unwrap()
+        )
+        .unwrap();
+
+        let s = [
+            "+--------+",
+            "| income |",
+            "+--------+",
+            "| 1 €    |",
+            "| <NULL> |",
+            "| 10 €   |",
+            "| 100 €  |",
+            "+--------+",
+        ];
+        let expected = s.join("\n");
+        assert_eq!(expected, buf);
+    }
+
+    #[test]
+    fn test_format_batches_with_custom_formatters_custom_schema_overrules_batch_schema() {
+        // define a schema.
+        let options = FormatOptions::new();
+        let money_metadata = HashMap::from([(
+            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
+            "my_money".to_owned(),
+        )]);
+        let schema = Arc::new(Schema::new(vec![
+            Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
+        ]));
+
+        // define data.
+        let batch = RecordBatch::try_new(
+            schema,
+            vec![Arc::new(array::Int32Array::from(vec![
+                Some(1),
+                None,
+                Some(10),
+                Some(100),
+            ]))],
+        )
+        .unwrap();
+
+        let mut buf = String::new();
+        write!(
+            &mut buf,
+            "{}",
+            create_table(
+                // No metadata compared to test_format_batches_with_custom_formatters
+                Some(Arc::new(Schema::new(vec![Field::new(
+                    "income",
+                    DataType::Int32,
+                    true
+                ),]))),
+                &[batch],
+                &options,
+                Some(&TestFormatters {})
+            )
+            .unwrap()
+        )
+        .unwrap();
+
+        // No € formatting as in test_format_batches_with_custom_formatters
+        let s = [
+            "+--------------+",

Review Comment:
   Overriding pretty-printing result based on the `DataType` (appen 32-Bit to `Int32`).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "alamb (via GitHub)" <gi...@apache.org>.
alamb commented on PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#issuecomment-3549607950

   > The only question I have is whether it's worth a test to make sure this works when printing nested lists and structs of extension types (I'm not sure exactly how the internals work to know if this takes a code path not covered here).
   
   this is a great idea
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "alamb (via GitHub)" <gi...@apache.org>.
alamb commented on code in PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#discussion_r2539713099


##########
arrow-cast/src/display.rs:
##########
@@ -169,10 +217,161 @@ impl<'a> FormatOptions<'a> {
         Self { types_info, ..self }
     }
 
-    /// Returns true if type info should be included in visual representation of batches
+    /// Overrides the [`ArrayFormatterFactory`] used to instantiate custom [`ArrayFormatter`]s.
+    pub const fn with_formatter_factory(
+        self,
+        formatter_factory: &'a dyn ArrayFormatterFactory,
+    ) -> Self {
+        Self {
+            formatter_factory: Some(formatter_factory),
+            ..self
+        }
+    }
+
+    /// Removes the [`ArrayFormatterFactory`] used to instantiate custom [`ArrayFormatter`]s. This
+    /// will cause pretty-printers to use the default [`ArrayFormatter`]s.
+    pub const fn without_formatter_factory(self) -> Self {
+        Self {
+            formatter_factory: None,
+            ..self
+        }
+    }
+
+    /// Returns whether formatting errors should be written to the output instead of being converted

Review Comment:
   👍  thank you -- these are nice additions



##########
arrow-cast/src/display.rs:
##########
@@ -53,7 +54,12 @@ pub enum DurationFormat {
 /// By default nulls are formatted as `""` and temporal types formatted
 /// according to RFC3339
 ///
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+/// # Equality

Review Comment:
   👍 



##########
arrow-cast/src/pretty.rs:
##########
@@ -1283,4 +1428,247 @@ mod tests {
         let actual: Vec<&str> = iso.lines().collect();
         assert_eq!(expected_iso, actual, "Actual result:\n{iso}");
     }
+
+    //
+    // Custom Formatting
+    //
+
+    /// The factory that will create the [`ArrayFormatter`]s.
+    struct TestFormatters {}
+
+    impl ArrayFormatterFactory for TestFormatters {
+        fn create_display_index<'formatter>(
+            &self,
+            array: &'formatter dyn Array,
+            options: &'formatter FormatOptions<'formatter>,
+            field: Option<&'formatter Field>,
+        ) -> Result<Option<ArrayFormatter<'formatter>>, ArrowError> {
+            if field
+                .map(|f| f.extension_type_name() == Some("my_money"))
+                .unwrap_or(false)
+            {
+                // We assume that my_money always is an Int32.
+                let array = array.as_primitive();
+                let display_index = Box::new(MyMoneyFormatter { array, options });
+                return Ok(Some(ArrayFormatter::new(display_index, options.safe)));
+            }
+
+            if array.data_type() == &DataType::Int32 {
+                // We assume that my_money always is an Int32.
+                let array = array.as_primitive();
+                let display_index = Box::new(MyInt32Formatter { array, options });
+                return Ok(Some(ArrayFormatter::new(display_index, options.safe)));
+            }
+
+            Ok(None)
+        }
+    }
+
+    /// The actual formatter
+    struct MyMoneyFormatter<'a> {
+        array: &'a Int32Array,
+        options: &'a FormatOptions<'a>,
+    }
+
+    impl<'a> DisplayIndex for MyMoneyFormatter<'a> {
+        fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
+            match self.array.is_valid(idx) {
+                true => write!(f, "{} €", self.array.value(idx))?,
+                false => write!(f, "{}", self.options.null)?,
+            }
+
+            Ok(())
+        }
+    }
+
+    /// The actual formatter
+    struct MyInt32Formatter<'a> {
+        array: &'a Int32Array,
+        options: &'a FormatOptions<'a>,
+    }
+
+    impl<'a> DisplayIndex for MyInt32Formatter<'a> {
+        fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
+            match self.array.is_valid(idx) {
+                true => write!(f, "{} (32-Bit)", self.array.value(idx))?,
+                false => write!(f, "{}", self.options.null)?,
+            }
+
+            Ok(())
+        }
+    }
+
+    #[test]
+    fn test_format_batches_with_custom_formatters() {
+        // define a schema.
+        let options = FormatOptions::new().with_null("<NULL>");
+        let money_metadata = HashMap::from([(
+            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
+            "my_money".to_owned(),
+        )]);
+        let schema = Arc::new(Schema::new(vec![
+            Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
+        ]));
+
+        // define data.
+        let batch = RecordBatch::try_new(
+            schema,
+            vec![Arc::new(array::Int32Array::from(vec![
+                Some(1),
+                None,
+                Some(10),
+                Some(100),
+            ]))],
+        )
+        .unwrap();
+
+        let mut buf = String::new();
+        write!(
+            &mut buf,
+            "{}",
+            pretty_format_batches_with_options_and_formatters(
+                &[batch],
+                &options,
+                Some(&TestFormatters {})
+            )
+            .unwrap()
+        )
+        .unwrap();
+
+        let s = [
+            "+--------+",
+            "| income |",
+            "+--------+",
+            "| 1 €    |",
+            "| <NULL> |",
+            "| 10 €   |",
+            "| 100 €  |",
+            "+--------+",
+        ];
+        let expected = s.join("\n");
+        assert_eq!(expected, buf);
+    }
+
+    #[test]
+    fn test_format_batches_with_custom_formatters_custom_schema_overrules_batch_schema() {
+        // define a schema.
+        let options = FormatOptions::new();
+        let money_metadata = HashMap::from([(
+            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
+            "my_money".to_owned(),
+        )]);
+        let schema = Arc::new(Schema::new(vec![
+            Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
+        ]));
+
+        // define data.
+        let batch = RecordBatch::try_new(
+            schema,
+            vec![Arc::new(array::Int32Array::from(vec![
+                Some(1),
+                None,
+                Some(10),
+                Some(100),
+            ]))],
+        )
+        .unwrap();
+
+        let mut buf = String::new();
+        write!(
+            &mut buf,
+            "{}",
+            create_table(
+                // No metadata compared to test_format_batches_with_custom_formatters
+                Some(Arc::new(Schema::new(vec![Field::new(
+                    "income",
+                    DataType::Int32,
+                    true
+                ),]))),
+                &[batch],
+                &options,
+                Some(&TestFormatters {})
+            )
+            .unwrap()
+        )
+        .unwrap();
+
+        // No € formatting as in test_format_batches_with_custom_formatters
+        let s = [
+            "+--------------+",

Review Comment:
   this makes sense



##########
arrow-cast/src/display.rs:
##########
@@ -352,7 +361,8 @@ impl From<ArrowError> for FormatError {
 }
 
 /// [`Display`] but accepting an index
-trait DisplayIndex {
+pub trait DisplayIndex {
+    /// Write the value of the underlying array at `idx` to `f`.
     fn write(&self, idx: usize, f: &mut dyn Write) -> FormatResult;

Review Comment:
   I recommend waiting until we have a usecase before we expand the API surface



##########
arrow-cast/src/display.rs:
##########
@@ -169,10 +217,161 @@ impl<'a> FormatOptions<'a> {
         Self { types_info, ..self }
     }
 
-    /// Returns true if type info should be included in visual representation of batches
+    /// Overrides the [`ArrayFormatterFactory`] used to instantiate custom [`ArrayFormatter`]s.
+    pub const fn with_formatter_factory(
+        self,
+        formatter_factory: &'a dyn ArrayFormatterFactory,
+    ) -> Self {

Review Comment:
   I think it is more common in the arrow APIs to pass the Option instead of having both a with and without method
   
   
   ```suggestion
       /// Overrides the [`ArrayFormatterFactory`] used to instantiate custom [`ArrayFormatter`]s.
       pub const fn with_formatter_factory(
           self,
           formatter_factory: Option<&'a dyn ArrayFormatterFactory>,
       ) -> Self {
   ```
   
   Then you can remove the other method
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "alamb (via GitHub)" <gi...@apache.org>.
alamb commented on code in PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#discussion_r2542135700


##########
arrow-cast/src/pretty.rs:
##########
@@ -1283,4 +1302,473 @@ mod tests {
         let actual: Vec<&str> = iso.lines().collect();
         assert_eq!(expected_iso, actual, "Actual result:\n{iso}");
     }
+
+    //
+    // Custom Formatting
+    //
+
+    /// The factory that will create the [`ArrayFormatter`]s.
+    #[derive(Debug)]
+    struct TestFormatters {}
+
+    impl ArrayFormatterFactory for TestFormatters {
+        fn create_array_formatter<'formatter>(
+            &self,
+            array: &'formatter dyn Array,
+            options: &FormatOptions<'formatter>,
+            field: Option<&'formatter Field>,
+        ) -> Result<Option<ArrayFormatter<'formatter>>, ArrowError> {
+            if field
+                .map(|f| f.extension_type_name() == Some("my_money"))
+                .unwrap_or(false)
+            {
+                // We assume that my_money always is an Int32.
+                let array = array.as_primitive();
+                let display_index = Box::new(MyMoneyFormatter {
+                    array,
+                    options: options.clone(),
+                });
+                return Ok(Some(ArrayFormatter::new(display_index, options.safe())));
+            }
+
+            if array.data_type() == &DataType::Int32 {
+                let array = array.as_primitive();
+                let display_index = Box::new(MyInt32Formatter {
+                    array,
+                    options: options.clone(),
+                });
+                return Ok(Some(ArrayFormatter::new(display_index, options.safe())));
+            }
+
+            Ok(None)
+        }
+    }
+
+    /// A format that will append a "€" sign to the end of the Int32 values.
+    struct MyMoneyFormatter<'a> {
+        array: &'a Int32Array,
+        options: FormatOptions<'a>,
+    }
+
+    impl<'a> DisplayIndex for MyMoneyFormatter<'a> {
+        fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
+            match self.array.is_valid(idx) {
+                true => write!(f, "{} €", self.array.value(idx))?,
+                false => write!(f, "{}", self.options.null())?,
+            }
+
+            Ok(())
+        }
+    }
+
+    /// The actual formatter
+    struct MyInt32Formatter<'a> {
+        array: &'a Int32Array,
+        options: FormatOptions<'a>,
+    }
+
+    impl<'a> DisplayIndex for MyInt32Formatter<'a> {
+        fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
+            match self.array.is_valid(idx) {
+                true => write!(f, "{} (32-Bit)", self.array.value(idx))?,
+                false => write!(f, "{}", self.options.null())?,
+            }
+
+            Ok(())
+        }
+    }
+
+    #[test]
+    fn test_format_batches_with_custom_formatters() {
+        // define a schema.
+        let options = FormatOptions::new()
+            .with_null("<NULL>")
+            .with_formatter_factory(Some(&TestFormatters {}));
+        let money_metadata = HashMap::from([(
+            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
+            "my_money".to_owned(),
+        )]);
+        let schema = Arc::new(Schema::new(vec![
+            Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
+        ]));
+
+        // define data.
+        let batch = RecordBatch::try_new(
+            schema,
+            vec![Arc::new(array::Int32Array::from(vec![
+                Some(1),
+                None,
+                Some(10),
+                Some(100),
+            ]))],
+        )
+        .unwrap();
+
+        let mut buf = String::new();
+        write!(
+            &mut buf,
+            "{}",
+            pretty_format_batches_with_options(&[batch], &options).unwrap()
+        )
+        .unwrap();
+
+        let s = [
+            "+--------+",
+            "| income |",
+            "+--------+",
+            "| 1 €    |",
+            "| <NULL> |",
+            "| 10 €   |",
+            "| 100 €  |",
+            "+--------+",
+        ];
+        let expected = s.join("\n");
+        assert_eq!(expected, buf);
+    }
+
+    #[test]
+    fn test_format_batches_with_custom_formatters_multi_nested_list() {
+        // define a schema.
+        let options = FormatOptions::new()
+            .with_null("<NULL>")
+            .with_formatter_factory(Some(&TestFormatters {}));
+        let money_metadata = HashMap::from([(
+            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
+            "my_money".to_owned(),
+        )]);
+        let nested_field = Arc::new(
+            Field::new_list_field(DataType::Int32, true).with_metadata(money_metadata.clone()),
+        );
+
+        // Create nested data
+        let inner_list = ListBuilder::new(Int32Builder::new()).with_field(nested_field);
+        let mut outer_list = FixedSizeListBuilder::new(inner_list, 2);
+        outer_list.values().append_value([Some(1)]);
+        outer_list.values().append_null();
+        outer_list.append(true);
+        outer_list.values().append_value([Some(2), Some(8)]);
+        outer_list
+            .values()
+            .append_value([Some(50), Some(25), Some(25)]);
+        outer_list.append(true);
+        let outer_list = outer_list.finish();
+
+        let schema = Arc::new(Schema::new(vec![Field::new(
+            "income",
+            outer_list.data_type().clone(),
+            true,
+        )]));
+
+        // define data.
+        let batch = RecordBatch::try_new(schema, vec![Arc::new(outer_list)]).unwrap();
+
+        let mut buf = String::new();
+        write!(
+            &mut buf,
+            "{}",
+            pretty_format_batches_with_options(&[batch], &options).unwrap()
+        )
+        .unwrap();
+
+        let s = [
+            "+----------------------------------+",
+            "| income                           |",
+            "+----------------------------------+",
+            "| [[1 €], <NULL>]                  |",
+            "| [[2 €, 8 €], [50 €, 25 €, 25 €]] |",
+            "+----------------------------------+",
+        ];
+        let expected = s.join("\n");
+        assert_eq!(expected, buf);
+    }
+
+    #[test]
+    fn test_format_batches_with_custom_formatters_nested_struct() {
+        // define a schema.
+        let options = FormatOptions::new()
+            .with_null("<NULL>")
+            .with_formatter_factory(Some(&TestFormatters {}));
+        let money_metadata = HashMap::from([(
+            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
+            "my_money".to_owned(),
+        )]);
+        let fields = Fields::from(vec![
+            Field::new("name", DataType::Utf8, true),
+            Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
+        ]);
+
+        let schema = Arc::new(Schema::new(vec![Field::new(
+            "income",
+            DataType::Struct(fields.clone()),
+            true,
+        )]));
+
+        // Create nested data
+        let mut nested_data = StructBuilder::new(
+            fields,
+            vec![
+                Box::new(StringBuilder::new()),
+                Box::new(Int32Builder::new()),
+            ],
+        );
+        nested_data
+            .field_builder::<StringBuilder>(0)
+            .unwrap()
+            .extend([Some("Gimli"), Some("Legolas"), Some("Aragorn")]);

Review Comment:
   ⚔️ 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "alamb (via GitHub)" <gi...@apache.org>.
alamb commented on PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#issuecomment-3553202493

   I found an issue while testing:
   - https://github.com/apache/arrow-rs/issues/8875


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "tobixdev (via GitHub)" <gi...@apache.org>.
tobixdev commented on code in PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#discussion_r2527840698


##########
arrow-cast/src/display.rs:
##########
@@ -82,6 +91,43 @@ impl Default for FormatOptions<'_> {
     }
 }
 
+impl PartialEq for FormatOptions<'_> {
+    fn eq(&self, other: &Self) -> bool {
+        self.safe == other.safe
+            && self.null == other.null
+            && self.date_format == other.date_format
+            && self.datetime_format == other.datetime_format
+            && self.timestamp_format == other.timestamp_format
+            && self.timestamp_tz_format == other.timestamp_tz_format
+            && self.time_format == other.time_format
+            && self.duration_format == other.duration_format
+            && match (self.formatter_factory, other.formatter_factory) {
+                (Some(f1), Some(f2)) => std::ptr::eq(f1, f2),
+                (None, None) => true,
+                _ => false,
+            }
+    }
+}
+
+impl Eq for FormatOptions<'_> {}
+
+impl Hash for FormatOptions<'_> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.safe.hash(state);
+        self.null.hash(state);
+        self.date_format.hash(state);
+        self.datetime_format.hash(state);
+        self.timestamp_format.hash(state);
+        self.timestamp_tz_format.hash(state);
+        self.time_format.hash(state);
+        self.duration_format.hash(state);
+        self.types_info.hash(state);

Review Comment:
   Thank you for catching that!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "tobixdev (via GitHub)" <gi...@apache.org>.
tobixdev commented on code in PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#discussion_r2520071255


##########
arrow-cast/src/pretty.rs:
##########
@@ -1283,4 +1428,247 @@ mod tests {
         let actual: Vec<&str> = iso.lines().collect();
         assert_eq!(expected_iso, actual, "Actual result:\n{iso}");
     }
+
+    //
+    // Custom Formatting
+    //
+
+    /// The factory that will create the [`ArrayFormatter`]s.
+    struct TestFormatters {}
+
+    impl ArrayFormatterFactory for TestFormatters {
+        fn create_display_index<'formatter>(
+            &self,
+            array: &'formatter dyn Array,
+            options: &'formatter FormatOptions<'formatter>,
+            field: Option<&'formatter Field>,
+        ) -> Result<Option<ArrayFormatter<'formatter>>, ArrowError> {
+            if field
+                .map(|f| f.extension_type_name() == Some("my_money"))
+                .unwrap_or(false)
+            {
+                // We assume that my_money always is an Int32.
+                let array = array.as_primitive();
+                let display_index = Box::new(MyMoneyFormatter { array, options });
+                return Ok(Some(ArrayFormatter::new(display_index, options.safe)));
+            }
+
+            if array.data_type() == &DataType::Int32 {
+                // We assume that my_money always is an Int32.
+                let array = array.as_primitive();
+                let display_index = Box::new(MyInt32Formatter { array, options });
+                return Ok(Some(ArrayFormatter::new(display_index, options.safe)));
+            }
+
+            Ok(None)
+        }
+    }
+
+    /// The actual formatter
+    struct MyMoneyFormatter<'a> {
+        array: &'a Int32Array,
+        options: &'a FormatOptions<'a>,
+    }
+
+    impl<'a> DisplayIndex for MyMoneyFormatter<'a> {
+        fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
+            match self.array.is_valid(idx) {
+                true => write!(f, "{} €", self.array.value(idx))?,
+                false => write!(f, "{}", self.options.null)?,
+            }
+
+            Ok(())
+        }
+    }
+
+    /// The actual formatter
+    struct MyInt32Formatter<'a> {
+        array: &'a Int32Array,
+        options: &'a FormatOptions<'a>,
+    }
+
+    impl<'a> DisplayIndex for MyInt32Formatter<'a> {
+        fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
+            match self.array.is_valid(idx) {
+                true => write!(f, "{} (32-Bit)", self.array.value(idx))?,
+                false => write!(f, "{}", self.options.null)?,
+            }
+
+            Ok(())
+        }
+    }
+
+    #[test]
+    fn test_format_batches_with_custom_formatters() {
+        // define a schema.
+        let options = FormatOptions::new().with_null("<NULL>");
+        let money_metadata = HashMap::from([(
+            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
+            "my_money".to_owned(),
+        )]);
+        let schema = Arc::new(Schema::new(vec![
+            Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
+        ]));
+
+        // define data.
+        let batch = RecordBatch::try_new(
+            schema,
+            vec![Arc::new(array::Int32Array::from(vec![
+                Some(1),
+                None,
+                Some(10),
+                Some(100),
+            ]))],
+        )
+        .unwrap();
+
+        let mut buf = String::new();
+        write!(
+            &mut buf,
+            "{}",
+            pretty_format_batches_with_options_and_formatters(
+                &[batch],
+                &options,
+                Some(&TestFormatters {})
+            )
+            .unwrap()
+        )
+        .unwrap();
+
+        let s = [
+            "+--------+",

Review Comment:
   Pretty-printing result with a custom extension type



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "tobixdev (via GitHub)" <gi...@apache.org>.
tobixdev commented on PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#issuecomment-3552236489

   > The only question I have is whether it's worth a test to make sure this works when printing nested lists and structs of extension types (I'm not sure exactly how the internals work to know if this takes a code path not covered here).
   
   @paleolimbot Great call! This wasn't working as the nested data types did not consider the factory in options.
   
   https://github.com/apache/arrow-rs/pull/8829/commits/d494979c6bd0950db01bae2a50ca769975016cdd
   
   However, this is a again bigger change that will require a further review. Here are the changes:
   - Add `entries_fields` to map array for obtaining the (key, value) fields.
   - `make_array_formatter` utility function that either chooses the formatter from the factory or the default
   - Rename `make_formatter` -> `make_default_display_index` for distinguishing against the formatter
   - Rewrite `DisplayIndexState` for nested data structures to extract the field
   - Replace `Box<dyn DisplayIndex + 'a>` with `ArrayFormatter<'a>` in the nested formatters.
   - Replace `values.write(idx, f)?;` with `write!(f, "{}", values.value(idx))?` (this should consider the safe property of the formatters I think)
   - Tests (except for RunArray where I couldn't figure out how to create one with a custom field).


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "kylebarron (via GitHub)" <gi...@apache.org>.
kylebarron commented on PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#issuecomment-3543661535

   I'm just getting back from vacation and haven't read through this PR yet, but I'm excited for this because I think it unblocks https://github.com/kylebarron/arro3/issues/432, where the default `ArrayFormatter` was slow to print very wide cells (i.e. a nested list with many elements)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "tobixdev (via GitHub)" <gi...@apache.org>.
tobixdev commented on PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#issuecomment-3531638186

   @alamb Thanks for the review and the great suggestions! I think that these changes will make the API better.
   
   One thing that resulted from moving the `ArrayFormatterFactory` into `FormatOptions` is that we need some concept of equality / hashing the factories. For now, I've chosen to compare the pointers of the factories. If you think that an equality implementation based on the content of the formatter factory (which I guess will often be a `HashMap` that maps extension types to an `Arc<MyExtType>`), we can iterate further.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "tobixdev (via GitHub)" <gi...@apache.org>.
tobixdev commented on code in PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#discussion_r2520069266


##########
arrow-cast/src/pretty.rs:
##########
@@ -22,14 +22,99 @@
 //! [`RecordBatch`]: arrow_array::RecordBatch
 //! [`Array`]: arrow_array::Array
 
-use std::fmt::Display;
-
 use comfy_table::{Cell, Table};
+use std::fmt::{Display, Write};
+use std::sync::Arc;
 
-use arrow_array::{Array, ArrayRef, RecordBatch};
-use arrow_schema::{ArrowError, SchemaRef};
+use arrow_array::cast::AsArray;
+use arrow_array::{Array, ArrayRef, Int32Array, RecordBatch, array};
+use arrow_schema::{ArrowError, Field, SchemaRef};
 
-use crate::display::{ArrayFormatter, FormatOptions};
+use crate::display::{ArrayFormatter, DisplayIndex, FormatOptions};
+
+/// Allows creating a new [`ArrayFormatter`] for a given [`Array`] and an optional [`Field`].
+///
+/// # Example
+///
+/// The example below shows how to create a custom formatter for a custom type `my_money`.
+///
+/// ```rust
+/// use std::fmt::Write;
+/// use arrow_array::{Array, Int32Array, cast::AsArray};
+/// use arrow_cast::display::{ArrayFormatter, DisplayIndex, FormatOptions, FormatResult};
+/// use arrow_cast::pretty::{pretty_format_batches_with_options_and_formatters, ArrayFormatterFactory};
+/// use arrow_schema::{ArrowError, Field};
+///
+/// /// A custom formatter factory that can create a formatter for the special type `my_money`.
+/// ///
+/// /// This struct could have access to some kind of extension type registry that can lookup the
+/// /// correct formatter for an extension type on-demand.
+/// struct MyFormatters {}
+///
+/// impl ArrayFormatterFactory for MyFormatters {
+///     fn create_display_index<'formatter>(
+///         &self,
+///         array: &'formatter dyn Array,
+///         options: &'formatter FormatOptions<'formatter>,
+///         field: Option<&'formatter Field>,
+///     ) -> Result<Option<ArrayFormatter<'formatter>>, ArrowError> {
+///         // check if this is the money type
+///         if field
+///             .map(|f| f.extension_type_name() == Some("my_money"))
+///             .unwrap_or(false)
+///         {
+///             // We assume that my_money always is an Int32.
+///             let array = array.as_primitive();
+///             let display_index = Box::new(MyMoneyFormatter { array, options });
+///             return Ok(Some(ArrayFormatter::new(display_index, options.safe)));
+///         }
+///
+///         Ok(None) // None indicates that the default formatter should be used.
+///     }
+/// }
+///
+/// /// A formatter for the type `my_money` that wraps a specific array and has access to the
+/// /// formatting options.
+/// struct MyMoneyFormatter<'a> {

Review Comment:
   From the DataFusion perspective, this would be a struct that users implement such that they can pretty-print their custom types.
   
   We would also do something similar for canonical data types (e.g., formatting UUIDs).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "tobixdev (via GitHub)" <gi...@apache.org>.
tobixdev commented on code in PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#discussion_r2520073223


##########
arrow-cast/src/display.rs:
##########
@@ -352,7 +361,8 @@ impl From<ArrowError> for FormatError {
 }
 
 /// [`Display`] but accepting an index
-trait DisplayIndex {
+pub trait DisplayIndex {
+    /// Write the value of the underlying array at `idx` to `f`.
     fn write(&self, idx: usize, f: &mut dyn Write) -> FormatResult;

Review Comment:
   Maybe we should add a `len` method to make the trait more complete (?).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] Allow Users to Provide Custom `ArrayFormatter`s when Pretty-Printing Record Batches [arrow-rs]

Posted by "alamb (via GitHub)" <gi...@apache.org>.
alamb commented on code in PR #8829:
URL: https://github.com/apache/arrow-rs/pull/8829#discussion_r2524529377


##########
arrow-cast/src/display.rs:
##########
@@ -57,23 +57,23 @@ pub enum DurationFormat {
 pub struct FormatOptions<'a> {
     /// If set to `true` any formatting errors will be written to the output
     /// instead of being converted into a [`std::fmt::Error`]
-    safe: bool,
+    pub safe: bool,

Review Comment:
   > Make fields in FormatOptions public. This is necessary as the custom ArrayFormatter must also have access to the formatting options. (see <NULL> in the test)
   
   Rather than making these public, what about adding accessors for them? I think that would make it easier to change the underlying implementation in the future without causing breaking API changes
   
   I think by making all these fields  `pub` it means people can construct format options explicitly like
   
   ```rust
   let options = FormatOptions {
     safe: false,
     null: "", 
   ...
   };
   ```
   
   So adding any new field to the struct will be a breaking API change
   
   If we keep them private fields, then we can add new fields without breaking existing peopel



##########
arrow-cast/src/pretty.rs:
##########
@@ -131,7 +214,27 @@ pub fn pretty_format_batches_with_options(
     results: &[RecordBatch],
     options: &FormatOptions,
 ) -> Result<impl Display + use<>, ArrowError> {
-    create_table(None, results, options)
+    create_table(None, results, options, None)
+}
+
+/// Create a visual representation of [`RecordBatch`]es with formatting options.
+///
+/// # Arguments
+/// * `results` - A slice of record batches to display
+/// * `options` - [`FormatOptions`] that control the resulting display
+/// * `formatters` - A slice of [`ArrayFormatter`]s that control the formatting of each column. If
+///   a formatter is [`None`], the default formatter will be used. Must be exactly as long as the
+///   number of fields in the record batches.
+///
+/// # Example
+///
+/// For an example, see [`ArrayFormatterFactory`].
+pub fn pretty_format_batches_with_options_and_formatters(
+    results: &[RecordBatch],
+    options: &FormatOptions,
+    formatters: Option<&dyn ArrayFormatterFactory>,

Review Comment:
   Rather than needing a parameter, did you consider adding a new field to `FormatOptions`?
   
   ```rust
   struct FormatOptions {
   ...
     formatter_factory: Option<&dyn ArrayFormatterFactory>,
   }
   ```
   
   That would reduce the new APIs and make it easier for existing code to use custom formatters (just need to update options)



##########
arrow-cast/src/pretty.rs:
##########
@@ -22,15 +22,98 @@
 //! [`RecordBatch`]: arrow_array::RecordBatch
 //! [`Array`]: arrow_array::Array
 
-use std::fmt::Display;
-
 use comfy_table::{Cell, Table};
+use std::fmt::{Display};
 
 use arrow_array::{Array, ArrayRef, RecordBatch};
-use arrow_schema::{ArrowError, SchemaRef};
+use arrow_schema::{ArrowError, Field, SchemaRef};
 
 use crate::display::{ArrayFormatter, FormatOptions};
 
+/// Allows creating a new [`ArrayFormatter`] for a given [`Array`] and an optional [`Field`].
+///
+/// # Example
+///
+/// The example below shows how to create a custom formatter for a custom type `my_money`.
+///
+/// ```rust
+/// use std::fmt::Write;
+/// use arrow_array::{Array, Int32Array, cast::AsArray};
+/// use arrow_cast::display::{ArrayFormatter, DisplayIndex, FormatOptions, FormatResult};
+/// use arrow_cast::pretty::{pretty_format_batches_with_options_and_formatters, ArrayFormatterFactory};
+/// use arrow_schema::{ArrowError, Field};
+///
+/// /// A custom formatter factory that can create a formatter for the special type `my_money`.
+/// ///
+/// /// This struct could have access to some kind of extension type registry that can lookup the
+/// /// correct formatter for an extension type on-demand.
+/// struct MyFormatters {}
+///
+/// impl ArrayFormatterFactory for MyFormatters {
+///     fn create_display_index<'formatter>(
+///         &self,
+///         array: &'formatter dyn Array,
+///         options: &'formatter FormatOptions<'formatter>,
+///         field: Option<&'formatter Field>,
+///     ) -> Result<Option<ArrayFormatter<'formatter>>, ArrowError> {
+///         // check if this is the money type
+///         if field
+///             .map(|f| f.extension_type_name() == Some("my_money"))
+///             .unwrap_or(false)
+///         {
+///             // We assume that my_money always is an Int32.
+///             let array = array.as_primitive();
+///             let display_index = Box::new(MyMoneyFormatter { array, options });
+///             return Ok(Some(ArrayFormatter::new(display_index, options.safe)));
+///         }
+///
+///         Ok(None) // None indicates that the default formatter should be used.
+///     }
+/// }
+///
+/// /// A formatter for the type `my_money` that wraps a specific array and has access to the

Review Comment:
   This is great -- thank you



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org