32
32
import numpy as np
33
33
import numpy ._core .umath as umath
34
34
import numpy ._core .numerictypes as ntypes
35
- from numpy ._core import multiarray as mu
35
+ from numpy ._core import multiarray as mu , arrayprint
36
36
from numpy import ndarray , amax , amin , iscomplexobj , bool_ , _NoValue , angle
37
37
from numpy import array as narray , expand_dims , iinfo , finfo
38
38
from numpy ._core .numeric import normalize_axis_tuple
@@ -4082,8 +4082,207 @@ def _insert_masked_print(self):
4082
4082
res = self .filled (self .fill_value )
4083
4083
return res
4084
4084
4085
+ def _flatten (self , x ):
4086
+ # prevent infinite recursion
4087
+ if isinstance (x , np .matrix ):
4088
+ x = np .asarray (x )
4089
+
4090
+ # handle structured scalar (np.void)
4091
+ if isinstance (x , np .void ):
4092
+ return [item for name in x .dtype .names for item in self ._flatten (x [name ])]
4093
+
4094
+ # handle 0d structured arrays (e.g., array(..., dtype=[(...)]) )
4095
+ if isinstance (x , np .ndarray ) and x .ndim == 0 and x .dtype .names :
4096
+ return self ._flatten (x .item ())
4097
+
4098
+ # handle masked scalars and arrays
4099
+ if isinstance (x , (MaskedArray , MaskedConstant )):
4100
+ if hasattr (x , 'ndim' ) and x .ndim == 0 :
4101
+ return [x .item ()]
4102
+ else :
4103
+ return [item for sub in x for item in self ._flatten (sub )]
4104
+
4105
+ # handle lists and tuples
4106
+ if isinstance (x , (list , tuple )):
4107
+ return [item for sub in x for item in self ._flatten (sub )]
4108
+
4109
+ # handle non-scalar ndarrays
4110
+ if isinstance (x , np .ndarray ) and x .ndim > 0 :
4111
+ return [item for sub in x for item in self ._flatten (sub )]
4112
+
4113
+ # base case: scalar value
4114
+ return [x ]
4115
+
4116
+ def _replacer (self , match , data_iter , exp_format ):
4117
+ """
4118
+ Replace matched tokens in a string with corresponding data from an iterator.
4119
+
4120
+ Parameters
4121
+ ----------
4122
+ match : re.Match
4123
+ The regular expression match object containing the token to be replaced.
4124
+ data_iter : iterator
4125
+ An iterator providing the data to replace the matched tokens.
4126
+ Returns
4127
+ -------
4128
+ str
4129
+ The string with the matched token replaced by the appropriate data.
4130
+ """
4131
+ token = match .group (0 )
4132
+ # handle ellipsis in legacy printing
4133
+ if token == '...' :
4134
+ value = next (data_iter ) # consume from iterator to keep alignment
4135
+ return '...'
4136
+ # compute decimal precision from token
4137
+ if '.' in token :
4138
+ decimal_places = len (token .split ("." )[1 ])
4139
+ else :
4140
+ decimal_places = 0
4141
+
4142
+ width = len (token )
4143
+ float_format = f"{{:>{ width } .{ decimal_places } f}}"
4144
+ value = next (data_iter )
4145
+ # handle masked values
4146
+ if (
4147
+ value is masked or
4148
+ isinstance (value , _MaskedPrintOption ) or
4149
+ is_masked (value )
4150
+ ):
4151
+ return '--' .center (width )
4152
+
4153
+ opts = np .get_printoptions ()
4154
+ suppress = opts .get ('suppress' )
4155
+ precision = opts .get ('precision' )
4156
+
4157
+ if not suppress and isinstance (value , float ) and exp_format :
4158
+ return arrayprint .format_float_scientific (
4159
+ value ,
4160
+ precision = precision ,
4161
+ min_digits = builtins .min (decimal_places , precision ),
4162
+ sign = False ,
4163
+ )
4164
+
4165
+ # trim trailing .0 in floats if template does so
4166
+ if token .endswith ('.' ) and str (value ).endswith ('.0' ):
4167
+ return str (value )[:- 1 ].rjust (width )
4168
+
4169
+ return float_format .format (float (value ))
4170
+
4171
+ def _list_to_string (self , template , data ):
4172
+ """
4173
+ Convert a data structure into a formatted string based on a template.
4174
+
4175
+ Parameters
4176
+ ----------
4177
+ template : str
4178
+ The string template that dictates the formatting.
4179
+ data : array-like
4180
+ The data to be formatted into the string.
4181
+
4182
+ Returns
4183
+ -------
4184
+ str
4185
+ The formatted string representation of the data.
4186
+ """
4187
+ # handle scalar object arrays
4188
+ if (isinstance (data , np .ndarray )
4189
+ and data .dtype == object
4190
+ and data .shape == ()):
4191
+ return str (data .item ())
4192
+
4193
+ # apply truncation before flattening, if legacy and 1D MaskedArray
4194
+ legacy = np .get_printoptions ().get ('legacy' ) == '1.13'
4195
+ if legacy and data .ndim == 1 and data .size > 10 :
4196
+ head = data [:3 ].tolist ()
4197
+ tail = data [- 3 :].tolist ()
4198
+ data = head + ['...' ] + tail # insert ellipsis marker in the data
4199
+
4200
+ flat_data = self ._flatten (data )
4201
+ data_iter = iter (flat_data )
4202
+ # match numbers, masked token, or ellipsis
4203
+ pattern = (
4204
+ r"-?\d+\.\d*(?:[eE][+-]?\d+)?|"
4205
+ r"-?\d+(?:[eE][+-]?\d+)?|--|\.\.\."
4206
+ )
4207
+
4208
+ opts = np .get_printoptions ()
4209
+ suppress = opts .get ('suppress' )
4210
+ precision = opts .get ('precision' )
4211
+ legacy = opts .get ('legacy' )
4212
+ floatmode = opts .get ('floatmode' )
4213
+
4214
+ # flatten the original masked array to extract all scalar elements
4215
+ flat_data = self ._flatten (self .data )
4216
+
4217
+ # collect all float elements to analyze formatting needs
4218
+ float_values = [x for x in flat_data if isinstance (x , float )]
4219
+
4220
+ # convert to a NumPy array (empty if no float values)
4221
+ if float_values :
4222
+ float_array = np .array (float_values , dtype = float )
4223
+ else :
4224
+ float_array = np .array ([], dtype = float )
4225
+
4226
+ exp_format = False
4227
+ if not suppress :
4228
+ # normalize legacy printoption to an integer (e.g., "1.13" → 113)
4229
+ if isinstance (legacy , str ):
4230
+ legacy_val = int (legacy .replace ('.' , '' ))
4231
+ else :
4232
+ legacy_val = legacy
4233
+
4234
+ # determine whether exponential format should be used
4235
+ exp_format = arrayprint .FloatingFormat (
4236
+ float_array , precision , floatmode , suppress , legacy = legacy_val
4237
+ ).exp_format
4238
+
4239
+ # prepare replacement function for regex substitution
4240
+ replacer_fn = lambda match : self ._replacer (
4241
+ match , data_iter , exp_format
4242
+ )
4243
+
4244
+ # substitute matched tokens with formatted values
4245
+ return re .sub (pattern , replacer_fn , template )
4246
+
4247
+ def _masked_array_to_string (self , masked_data ):
4248
+ """
4249
+ Process a masked array and return its string representation.
4250
+
4251
+ Parameters
4252
+ ----------
4253
+ masked_data : numpy.ma.MaskedArray
4254
+ The masked array to process.
4255
+
4256
+ Returns
4257
+ -------
4258
+ str
4259
+ The string representation of the masked array.
4260
+ """
4261
+ # get formatted string using array2string
4262
+ formatted_str = np .array2string (self .data )
4263
+
4264
+ # if legacy mode is enabled, normalize whitespace
4265
+ legacy = np .get_printoptions ().get ('legacy' ) == '1.13'
4266
+ if legacy :
4267
+ formatted_str = re .sub (r"\s{2,}" , " " , formatted_str )
4268
+ formatted_str = re .sub (r"(?<=\[)\s+" , "" , formatted_str )
4269
+
4270
+ return self ._list_to_string (formatted_str , masked_data )
4271
+
4085
4272
def __str__ (self ):
4086
- return str (self ._insert_masked_print ())
4273
+ # handle 0-dimensional unmasked arrays by returning the scalar value
4274
+ if self .shape == () and not self .mask :
4275
+ return str (self .item ())
4276
+
4277
+ masked_data = self ._insert_masked_print ()
4278
+ # if the masked data has a custom __str__, use it
4279
+ if (type (masked_data ) is not np .ndarray
4280
+ and hasattr (masked_data , '__str__' )
4281
+ and type (masked_data ).__str__ is not np .ndarray .__str__ ):
4282
+ return masked_data .__str__ ()
4283
+
4284
+ # process the array using our method
4285
+ return self ._masked_array_to_string (masked_data )
4087
4286
4088
4287
def __repr__ (self ):
4089
4288
"""
0 commit comments