1use arrow::array::{
21 Array, ArrayRef, BooleanArray, Float32Array, Float64Array, GenericListArray,
22 Int8Array, Int16Array, Int32Array, Int64Array, LargeStringArray, ListBuilder,
23 OffsetSizeTrait, StringArray, StringBuilder, UInt8Array, UInt16Array, UInt32Array,
24 UInt64Array,
25};
26use arrow::datatypes::{DataType, Field};
27
28use datafusion_common::utils::ListCoercion;
29use datafusion_common::{DataFusionError, Result, not_impl_err};
30
31use std::any::Any;
32
33use crate::utils::make_scalar_function;
34use arrow::array::{
35 GenericStringArray, StringArrayType, StringViewArray,
36 builder::{ArrayBuilder, LargeStringBuilder, StringViewBuilder},
37 cast::AsArray,
38};
39use arrow::compute::cast;
40use arrow::datatypes::DataType::{
41 Dictionary, FixedSizeList, LargeList, LargeUtf8, List, Null, Utf8, Utf8View,
42};
43use datafusion_common::cast::{
44 as_fixed_size_list_array, as_large_list_array, as_list_array,
45};
46use datafusion_common::exec_err;
47use datafusion_common::types::logical_string;
48use datafusion_expr::{
49 ArrayFunctionArgument, ArrayFunctionSignature, Coercion, ColumnarValue,
50 Documentation, ScalarUDFImpl, Signature, TypeSignature, TypeSignatureClass,
51 Volatility,
52};
53use datafusion_functions::downcast_arg;
54use datafusion_macros::user_doc;
55use std::sync::Arc;
56
57macro_rules! call_array_function {
58 ($DATATYPE:expr, false) => {
59 match $DATATYPE {
60 DataType::Utf8 => array_function!(StringArray),
61 DataType::Utf8View => array_function!(StringViewArray),
62 DataType::LargeUtf8 => array_function!(LargeStringArray),
63 DataType::Boolean => array_function!(BooleanArray),
64 DataType::Float32 => array_function!(Float32Array),
65 DataType::Float64 => array_function!(Float64Array),
66 DataType::Int8 => array_function!(Int8Array),
67 DataType::Int16 => array_function!(Int16Array),
68 DataType::Int32 => array_function!(Int32Array),
69 DataType::Int64 => array_function!(Int64Array),
70 DataType::UInt8 => array_function!(UInt8Array),
71 DataType::UInt16 => array_function!(UInt16Array),
72 DataType::UInt32 => array_function!(UInt32Array),
73 DataType::UInt64 => array_function!(UInt64Array),
74 dt => not_impl_err!("Unsupported data type in array_to_string: {dt}"),
75 }
76 };
77 ($DATATYPE:expr, $INCLUDE_LIST:expr) => {{
78 match $DATATYPE {
79 DataType::List(_) => array_function!(ListArray),
80 DataType::Utf8 => array_function!(StringArray),
81 DataType::Utf8View => array_function!(StringViewArray),
82 DataType::LargeUtf8 => array_function!(LargeStringArray),
83 DataType::Boolean => array_function!(BooleanArray),
84 DataType::Float32 => array_function!(Float32Array),
85 DataType::Float64 => array_function!(Float64Array),
86 DataType::Int8 => array_function!(Int8Array),
87 DataType::Int16 => array_function!(Int16Array),
88 DataType::Int32 => array_function!(Int32Array),
89 DataType::Int64 => array_function!(Int64Array),
90 DataType::UInt8 => array_function!(UInt8Array),
91 DataType::UInt16 => array_function!(UInt16Array),
92 DataType::UInt32 => array_function!(UInt32Array),
93 DataType::UInt64 => array_function!(UInt64Array),
94 dt => not_impl_err!("Unsupported data type in array_to_string: {dt}"),
95 }
96 }};
97}
98
99macro_rules! to_string {
100 ($ARG:expr, $ARRAY:expr, $DELIMITER:expr, $NULL_STRING:expr, $WITH_NULL_STRING:expr, $ARRAY_TYPE:ident) => {{
101 let arr = downcast_arg!($ARRAY, $ARRAY_TYPE);
102 for x in arr {
103 match x {
104 Some(x) => {
105 $ARG.push_str(&x.to_string());
106 $ARG.push_str($DELIMITER);
107 }
108 None => {
109 if $WITH_NULL_STRING {
110 $ARG.push_str($NULL_STRING);
111 $ARG.push_str($DELIMITER);
112 }
113 }
114 }
115 }
116 Ok($ARG)
117 }};
118}
119
120make_udf_expr_and_func!(
122 ArrayToString,
123 array_to_string,
124 array delimiter, "converts each element to its text representation.", array_to_string_udf );
128
129#[user_doc(
130 doc_section(label = "Array Functions"),
131 description = "Converts each element to its text representation.",
132 syntax_example = "array_to_string(array, delimiter[, null_string])",
133 sql_example = r#"```sql
134> select array_to_string([[1, 2, 3, 4], [5, 6, 7, 8]], ',');
135+----------------------------------------------------+
136| array_to_string(List([1,2,3,4,5,6,7,8]),Utf8(",")) |
137+----------------------------------------------------+
138| 1,2,3,4,5,6,7,8 |
139+----------------------------------------------------+
140```"#,
141 argument(
142 name = "array",
143 description = "Array expression. Can be a constant, column, or function, and any combination of array operators."
144 ),
145 argument(name = "delimiter", description = "Array element separator."),
146 argument(
147 name = "null_string",
148 description = "Optional. String to replace null values in the array. If not provided, nulls will be handled by default behavior."
149 )
150)]
151#[derive(Debug, PartialEq, Eq, Hash)]
152pub struct ArrayToString {
153 signature: Signature,
154 aliases: Vec<String>,
155}
156
157impl Default for ArrayToString {
158 fn default() -> Self {
159 Self::new()
160 }
161}
162
163impl ArrayToString {
164 pub fn new() -> Self {
165 Self {
166 signature: Signature::one_of(
167 vec![
168 TypeSignature::ArraySignature(ArrayFunctionSignature::Array {
169 arguments: vec![
170 ArrayFunctionArgument::Array,
171 ArrayFunctionArgument::String,
172 ArrayFunctionArgument::String,
173 ],
174 array_coercion: Some(ListCoercion::FixedSizedListToList),
175 }),
176 TypeSignature::ArraySignature(ArrayFunctionSignature::Array {
177 arguments: vec![
178 ArrayFunctionArgument::Array,
179 ArrayFunctionArgument::String,
180 ],
181 array_coercion: Some(ListCoercion::FixedSizedListToList),
182 }),
183 ],
184 Volatility::Immutable,
185 ),
186 aliases: vec![
187 String::from("list_to_string"),
188 String::from("array_join"),
189 String::from("list_join"),
190 ],
191 }
192 }
193}
194
195impl ScalarUDFImpl for ArrayToString {
196 fn as_any(&self) -> &dyn Any {
197 self
198 }
199
200 fn name(&self) -> &str {
201 "array_to_string"
202 }
203
204 fn signature(&self) -> &Signature {
205 &self.signature
206 }
207
208 fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
209 Ok(Utf8)
210 }
211
212 fn invoke_with_args(
213 &self,
214 args: datafusion_expr::ScalarFunctionArgs,
215 ) -> Result<ColumnarValue> {
216 make_scalar_function(array_to_string_inner)(&args.args)
217 }
218
219 fn aliases(&self) -> &[String] {
220 &self.aliases
221 }
222
223 fn documentation(&self) -> Option<&Documentation> {
224 self.doc()
225 }
226}
227
228make_udf_expr_and_func!(
229 StringToArray,
230 string_to_array,
231 string delimiter null_string, "splits a `string` based on a `delimiter` and returns an array of parts. Any parts matching the optional `null_string` will be replaced with `NULL`", string_to_array_udf );
235
236#[user_doc(
237 doc_section(label = "Array Functions"),
238 description = "Splits a string into an array of substrings based on a delimiter. Any substrings matching the optional `null_str` argument are replaced with NULL.",
239 syntax_example = "string_to_array(str, delimiter[, null_str])",
240 sql_example = r#"```sql
241> select string_to_array('abc##def', '##');
242+-----------------------------------+
243| string_to_array(Utf8('abc##def')) |
244+-----------------------------------+
245| ['abc', 'def'] |
246+-----------------------------------+
247> select string_to_array('abc def', ' ', 'def');
248+---------------------------------------------+
249| string_to_array(Utf8('abc def'), Utf8(' '), Utf8('def')) |
250+---------------------------------------------+
251| ['abc', NULL] |
252+---------------------------------------------+
253```"#,
254 argument(name = "str", description = "String expression to split."),
255 argument(name = "delimiter", description = "Delimiter string to split on."),
256 argument(
257 name = "null_str",
258 description = "Substring values to be replaced with `NULL`."
259 )
260)]
261#[derive(Debug, PartialEq, Eq, Hash)]
262pub(super) struct StringToArray {
263 signature: Signature,
264 aliases: Vec<String>,
265}
266
267impl StringToArray {
268 pub fn new() -> Self {
269 Self {
270 signature: Signature::one_of(
271 vec![
272 TypeSignature::Coercible(vec![
273 Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
274 Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
275 ]),
276 TypeSignature::Coercible(vec![
277 Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
278 Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
279 Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
280 ]),
281 ],
282 Volatility::Immutable,
283 ),
284 aliases: vec![String::from("string_to_list")],
285 }
286 }
287}
288
289impl ScalarUDFImpl for StringToArray {
290 fn as_any(&self) -> &dyn Any {
291 self
292 }
293
294 fn name(&self) -> &str {
295 "string_to_array"
296 }
297
298 fn signature(&self) -> &Signature {
299 &self.signature
300 }
301
302 fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
303 Ok(List(Arc::new(Field::new_list_field(
304 arg_types[0].clone(),
305 true,
306 ))))
307 }
308
309 fn invoke_with_args(
310 &self,
311 args: datafusion_expr::ScalarFunctionArgs,
312 ) -> Result<ColumnarValue> {
313 let args = &args.args;
314 match args[0].data_type() {
315 Utf8 | Utf8View => make_scalar_function(string_to_array_inner::<i32>)(args),
316 LargeUtf8 => make_scalar_function(string_to_array_inner::<i64>)(args),
317 other => {
318 exec_err!("unsupported type for string_to_array function as {other:?}")
319 }
320 }
321 }
322
323 fn aliases(&self) -> &[String] {
324 &self.aliases
325 }
326
327 fn documentation(&self) -> Option<&Documentation> {
328 self.doc()
329 }
330}
331
332fn array_to_string_inner(args: &[ArrayRef]) -> Result<ArrayRef> {
333 if args.len() < 2 || args.len() > 3 {
334 return exec_err!("array_to_string expects two or three arguments");
335 }
336
337 let arr = &args[0];
338
339 let delimiters: Vec<Option<&str>> = match args[1].data_type() {
340 Utf8 => args[1].as_string::<i32>().iter().collect(),
341 Utf8View => args[1].as_string_view().iter().collect(),
342 LargeUtf8 => args[1].as_string::<i64>().iter().collect(),
343 other => {
344 return exec_err!(
345 "unsupported type for second argument to array_to_string function as {other:?}"
346 );
347 }
348 };
349
350 let mut null_string = String::from("");
351 let mut with_null_string = false;
352 if args.len() == 3 {
353 null_string = match args[2].data_type() {
354 Utf8 => args[2].as_string::<i32>().value(0).to_string(),
355 Utf8View => args[2].as_string_view().value(0).to_string(),
356 LargeUtf8 => args[2].as_string::<i64>().value(0).to_string(),
357 other => {
358 return exec_err!(
359 "unsupported type for second argument to array_to_string function as {other:?}"
360 );
361 }
362 };
363 with_null_string = true;
364 }
365
366 fn compute_array_to_string<'a>(
369 arg: &'a mut String,
370 arr: &ArrayRef,
371 delimiter: String,
372 null_string: String,
373 with_null_string: bool,
374 ) -> Result<&'a mut String> {
375 match arr.data_type() {
376 List(..) => {
377 let list_array = as_list_array(&arr)?;
378 for i in 0..list_array.len() {
379 if !list_array.is_null(i) {
380 compute_array_to_string(
381 arg,
382 &list_array.value(i),
383 delimiter.clone(),
384 null_string.clone(),
385 with_null_string,
386 )?;
387 } else if with_null_string {
388 arg.push_str(&null_string);
389 arg.push_str(&delimiter);
390 }
391 }
392
393 Ok(arg)
394 }
395 FixedSizeList(..) => {
396 let list_array = as_fixed_size_list_array(&arr)?;
397
398 for i in 0..list_array.len() {
399 if !list_array.is_null(i) {
400 compute_array_to_string(
401 arg,
402 &list_array.value(i),
403 delimiter.clone(),
404 null_string.clone(),
405 with_null_string,
406 )?;
407 } else if with_null_string {
408 arg.push_str(&null_string);
409 arg.push_str(&delimiter);
410 }
411 }
412
413 Ok(arg)
414 }
415 LargeList(..) => {
416 let list_array = as_large_list_array(&arr)?;
417 for i in 0..list_array.len() {
418 if !list_array.is_null(i) {
419 compute_array_to_string(
420 arg,
421 &list_array.value(i),
422 delimiter.clone(),
423 null_string.clone(),
424 with_null_string,
425 )?;
426 } else if with_null_string {
427 arg.push_str(&null_string);
428 arg.push_str(&delimiter);
429 }
430 }
431
432 Ok(arg)
433 }
434 Dictionary(_key_type, value_type) => {
435 let values = cast(&arr, value_type.as_ref()).map_err(|e| {
438 DataFusionError::from(e).context(
439 "Casting dictionary to values in compute_array_to_string",
440 )
441 })?;
442 compute_array_to_string(
443 arg,
444 &values,
445 delimiter,
446 null_string,
447 with_null_string,
448 )
449 }
450 Null => Ok(arg),
451 data_type => {
452 macro_rules! array_function {
453 ($ARRAY_TYPE:ident) => {
454 to_string!(
455 arg,
456 arr,
457 &delimiter,
458 &null_string,
459 with_null_string,
460 $ARRAY_TYPE
461 )
462 };
463 }
464 call_array_function!(data_type, false)
465 }
466 }
467 }
468
469 fn generate_string_array<O: OffsetSizeTrait>(
470 list_arr: &GenericListArray<O>,
471 delimiters: &[Option<&str>],
472 null_string: &str,
473 with_null_string: bool,
474 ) -> Result<StringArray> {
475 let mut res: Vec<Option<String>> = Vec::new();
476 for (arr, &delimiter) in list_arr.iter().zip(delimiters.iter()) {
477 if let (Some(arr), Some(delimiter)) = (arr, delimiter) {
478 let mut arg = String::from("");
479 let s = compute_array_to_string(
480 &mut arg,
481 &arr,
482 delimiter.to_string(),
483 null_string.to_string(),
484 with_null_string,
485 )?
486 .clone();
487
488 if let Some(s) = s.strip_suffix(delimiter) {
489 res.push(Some(s.to_string()));
490 } else {
491 res.push(Some(s));
492 }
493 } else {
494 res.push(None);
495 }
496 }
497
498 Ok(StringArray::from(res))
499 }
500
501 let string_arr = match arr.data_type() {
502 List(_) => {
503 let list_array = as_list_array(&arr)?;
504 generate_string_array::<i32>(
505 list_array,
506 &delimiters,
507 &null_string,
508 with_null_string,
509 )?
510 }
511 LargeList(_) => {
512 let list_array = as_large_list_array(&arr)?;
513 generate_string_array::<i64>(
514 list_array,
515 &delimiters,
516 &null_string,
517 with_null_string,
518 )?
519 }
520 _ => return exec_err!("array_to_string expects list as first argument"),
522 };
523
524 Ok(Arc::new(string_arr))
525}
526
527fn string_to_array_inner<T: OffsetSizeTrait>(args: &[ArrayRef]) -> Result<ArrayRef> {
531 if args.len() < 2 || args.len() > 3 {
532 return exec_err!("string_to_array expects two or three arguments");
533 }
534
535 match args[0].data_type() {
536 Utf8 => {
537 let string_array = args[0].as_string::<T>();
538 let builder = StringBuilder::with_capacity(
539 string_array.len(),
540 string_array.get_buffer_memory_size(),
541 );
542 string_to_array_inner_2::<&GenericStringArray<T>, StringBuilder>(
543 args,
544 &string_array,
545 builder,
546 )
547 }
548 Utf8View => {
549 let string_array = args[0].as_string_view();
550 let builder = StringViewBuilder::with_capacity(string_array.len());
551 string_to_array_inner_2::<&StringViewArray, StringViewBuilder>(
552 args,
553 &string_array,
554 builder,
555 )
556 }
557 LargeUtf8 => {
558 let string_array = args[0].as_string::<T>();
559 let builder = LargeStringBuilder::with_capacity(
560 string_array.len(),
561 string_array.get_buffer_memory_size(),
562 );
563 string_to_array_inner_2::<&GenericStringArray<T>, LargeStringBuilder>(
564 args,
565 &string_array,
566 builder,
567 )
568 }
569 other => exec_err!(
570 "unsupported type for first argument to string_to_array function as {other:?}"
571 ),
572 }
573}
574
575fn string_to_array_inner_2<'a, StringArrType, StringBuilderType>(
576 args: &'a [ArrayRef],
577 string_array: &StringArrType,
578 string_builder: StringBuilderType,
579) -> Result<ArrayRef>
580where
581 StringArrType: StringArrayType<'a>,
582 StringBuilderType: StringArrayBuilderType,
583{
584 match args[1].data_type() {
585 Utf8 => {
586 let delimiter_array = args[1].as_string::<i32>();
587 if args.len() == 2 {
588 string_to_array_impl::<
589 StringArrType,
590 &GenericStringArray<i32>,
591 &StringViewArray,
592 StringBuilderType,
593 >(string_array, &delimiter_array, None, string_builder)
594 } else {
595 string_to_array_inner_3::<
596 StringArrType,
597 &GenericStringArray<i32>,
598 StringBuilderType,
599 >(args, string_array, &delimiter_array, string_builder)
600 }
601 }
602 Utf8View => {
603 let delimiter_array = args[1].as_string_view();
604
605 if args.len() == 2 {
606 string_to_array_impl::<
607 StringArrType,
608 &StringViewArray,
609 &StringViewArray,
610 StringBuilderType,
611 >(string_array, &delimiter_array, None, string_builder)
612 } else {
613 string_to_array_inner_3::<
614 StringArrType,
615 &StringViewArray,
616 StringBuilderType,
617 >(args, string_array, &delimiter_array, string_builder)
618 }
619 }
620 LargeUtf8 => {
621 let delimiter_array = args[1].as_string::<i64>();
622 if args.len() == 2 {
623 string_to_array_impl::<
624 StringArrType,
625 &GenericStringArray<i64>,
626 &StringViewArray,
627 StringBuilderType,
628 >(string_array, &delimiter_array, None, string_builder)
629 } else {
630 string_to_array_inner_3::<
631 StringArrType,
632 &GenericStringArray<i64>,
633 StringBuilderType,
634 >(args, string_array, &delimiter_array, string_builder)
635 }
636 }
637 other => exec_err!(
638 "unsupported type for second argument to string_to_array function as {other:?}"
639 ),
640 }
641}
642
643fn string_to_array_inner_3<'a, StringArrType, DelimiterArrType, StringBuilderType>(
644 args: &'a [ArrayRef],
645 string_array: &StringArrType,
646 delimiter_array: &DelimiterArrType,
647 string_builder: StringBuilderType,
648) -> Result<ArrayRef>
649where
650 StringArrType: StringArrayType<'a>,
651 DelimiterArrType: StringArrayType<'a>,
652 StringBuilderType: StringArrayBuilderType,
653{
654 match args[2].data_type() {
655 Utf8 => {
656 let null_type_array = Some(args[2].as_string::<i32>());
657 string_to_array_impl::<
658 StringArrType,
659 DelimiterArrType,
660 &GenericStringArray<i32>,
661 StringBuilderType,
662 >(
663 string_array,
664 delimiter_array,
665 null_type_array,
666 string_builder,
667 )
668 }
669 Utf8View => {
670 let null_type_array = Some(args[2].as_string_view());
671 string_to_array_impl::<
672 StringArrType,
673 DelimiterArrType,
674 &StringViewArray,
675 StringBuilderType,
676 >(
677 string_array,
678 delimiter_array,
679 null_type_array,
680 string_builder,
681 )
682 }
683 LargeUtf8 => {
684 let null_type_array = Some(args[2].as_string::<i64>());
685 string_to_array_impl::<
686 StringArrType,
687 DelimiterArrType,
688 &GenericStringArray<i64>,
689 StringBuilderType,
690 >(
691 string_array,
692 delimiter_array,
693 null_type_array,
694 string_builder,
695 )
696 }
697 other => {
698 exec_err!("unsupported type for string_to_array function as {other:?}")
699 }
700 }
701}
702
703fn string_to_array_impl<
704 'a,
705 StringArrType,
706 DelimiterArrType,
707 NullValueArrType,
708 StringBuilderType,
709>(
710 string_array: &StringArrType,
711 delimiter_array: &DelimiterArrType,
712 null_value_array: Option<NullValueArrType>,
713 string_builder: StringBuilderType,
714) -> Result<ArrayRef>
715where
716 StringArrType: StringArrayType<'a>,
717 DelimiterArrType: StringArrayType<'a>,
718 NullValueArrType: StringArrayType<'a>,
719 StringBuilderType: StringArrayBuilderType,
720{
721 let mut list_builder = ListBuilder::new(string_builder);
722
723 match null_value_array {
724 None => {
725 string_array.iter().zip(delimiter_array.iter()).for_each(
726 |(string, delimiter)| {
727 match (string, delimiter) {
728 (Some(string), Some("")) => {
729 list_builder.values().append_value(string);
730 list_builder.append(true);
731 }
732 (Some(string), Some(delimiter)) => {
733 string.split(delimiter).for_each(|s| {
734 list_builder.values().append_value(s);
735 });
736 list_builder.append(true);
737 }
738 (Some(string), None) => {
739 string.chars().map(|c| c.to_string()).for_each(|c| {
740 list_builder.values().append_value(c.as_str());
741 });
742 list_builder.append(true);
743 }
744 _ => list_builder.append(false), }
746 },
747 )
748 }
749 Some(null_value_array) => string_array
750 .iter()
751 .zip(delimiter_array.iter())
752 .zip(null_value_array.iter())
753 .for_each(|((string, delimiter), null_value)| {
754 match (string, delimiter) {
755 (Some(string), Some("")) => {
756 if Some(string) == null_value {
757 list_builder.values().append_null();
758 } else {
759 list_builder.values().append_value(string);
760 }
761 list_builder.append(true);
762 }
763 (Some(string), Some(delimiter)) => {
764 string.split(delimiter).for_each(|s| {
765 if Some(s) == null_value {
766 list_builder.values().append_null();
767 } else {
768 list_builder.values().append_value(s);
769 }
770 });
771 list_builder.append(true);
772 }
773 (Some(string), None) => {
774 string.chars().map(|c| c.to_string()).for_each(|c| {
775 if Some(c.as_str()) == null_value {
776 list_builder.values().append_null();
777 } else {
778 list_builder.values().append_value(c.as_str());
779 }
780 });
781 list_builder.append(true);
782 }
783 _ => list_builder.append(false), }
785 }),
786 };
787
788 let list_array = list_builder.finish();
789 Ok(Arc::new(list_array) as ArrayRef)
790}
791
792trait StringArrayBuilderType: ArrayBuilder {
793 fn append_value(&mut self, val: &str);
794
795 fn append_null(&mut self);
796}
797
798impl StringArrayBuilderType for StringBuilder {
799 fn append_value(&mut self, val: &str) {
800 StringBuilder::append_value(self, val);
801 }
802
803 fn append_null(&mut self) {
804 StringBuilder::append_null(self);
805 }
806}
807
808impl StringArrayBuilderType for StringViewBuilder {
809 fn append_value(&mut self, val: &str) {
810 StringViewBuilder::append_value(self, val)
811 }
812
813 fn append_null(&mut self) {
814 StringViewBuilder::append_null(self)
815 }
816}
817
818impl StringArrayBuilderType for LargeStringBuilder {
819 fn append_value(&mut self, val: &str) {
820 LargeStringBuilder::append_value(self, val);
821 }
822
823 fn append_null(&mut self) {
824 LargeStringBuilder::append_null(self);
825 }
826}