5164
|
1 // Template sparse classes |
|
2 /* |
|
3 |
|
4 Copyright (C) 2004 David Bateman |
|
5 Copyright (C) 1998-2004 Andy Adler |
|
6 |
|
7 Octave is free software; you can redistribute it and/or modify it |
|
8 under the terms of the GNU General Public License as published by the |
|
9 Free Software Foundation; either version 2, or (at your option) any |
|
10 later version. |
|
11 |
|
12 Octave is distributed in the hope that it will be useful, but WITHOUT |
|
13 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
14 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
15 for more details. |
|
16 |
|
17 You should have received a copy of the GNU General Public License |
5307
|
18 along with this program; see the file COPYING. If not, write to the |
|
19 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
|
20 Boston, MA 02110-1301, USA. |
5164
|
21 |
|
22 */ |
|
23 |
|
24 #if !defined (octave_Sparse_h) |
|
25 #define octave_Sparse_h 1 |
|
26 |
|
27 #include <cassert> |
|
28 #include <cstddef> |
|
29 |
|
30 #include <iostream> |
|
31 |
|
32 #include "Array.h" |
|
33 #include "Array2.h" |
|
34 #include "dim-vector.h" |
|
35 #include "lo-utils.h" |
|
36 |
|
37 class idx_vector; |
|
38 |
|
39 // Two dimensional sparse class. Handles the reference counting for |
|
40 // all the derived classes. |
|
41 |
|
42 template <class T> |
|
43 class |
|
44 Sparse |
|
45 { |
|
46 protected: |
|
47 //-------------------------------------------------------------------- |
|
48 // The real representation of all Sparse arrays. |
|
49 //-------------------------------------------------------------------- |
|
50 |
|
51 class SparseRep |
|
52 { |
|
53 public: |
|
54 |
|
55 T *d; |
5275
|
56 octave_idx_type *r; |
|
57 octave_idx_type *c; |
5604
|
58 octave_idx_type nzmx; |
5275
|
59 octave_idx_type nrows; |
|
60 octave_idx_type ncols; |
5164
|
61 int count; |
|
62 |
5604
|
63 SparseRep (void) : d (0), r (0), c (new octave_idx_type [1]), nzmx (0), nrows (0), |
5164
|
64 ncols (0), count (1) { c[0] = 0; } |
|
65 |
5604
|
66 SparseRep (octave_idx_type n) : d (0), r (0), c (new octave_idx_type [n+1]), nzmx (0), nrows (n), |
5164
|
67 ncols (n), count (1) |
|
68 { |
5275
|
69 for (octave_idx_type i = 0; i < n + 1; i++) |
5164
|
70 c[i] = 0; |
|
71 } |
|
72 |
5604
|
73 SparseRep (octave_idx_type nr, octave_idx_type nc) : d (0), r (0), c (new octave_idx_type [nc+1]), nzmx (0), |
5164
|
74 nrows (nr), ncols (nc), count (1) |
|
75 { |
5275
|
76 for (octave_idx_type i = 0; i < nc + 1; i++) |
5164
|
77 c[i] = 0; |
|
78 } |
|
79 |
5275
|
80 SparseRep (octave_idx_type nr, octave_idx_type nc, octave_idx_type nz) : d (new T [nz]), |
5604
|
81 r (new octave_idx_type [nz]), c (new octave_idx_type [nc+1]), nzmx (nz), nrows (nr), |
5164
|
82 ncols (nc), count (1) |
|
83 { |
5275
|
84 for (octave_idx_type i = 0; i < nc + 1; i++) |
5164
|
85 c[i] = 0; |
|
86 } |
|
87 |
|
88 SparseRep (const SparseRep& a) |
5604
|
89 : d (new T [a.nzmx]), r (new octave_idx_type [a.nzmx]), c (new octave_idx_type [a.ncols + 1]), |
|
90 nzmx (a.nzmx), nrows (a.nrows), ncols (a.ncols), count (1) |
5164
|
91 { |
5604
|
92 for (octave_idx_type i = 0; i < nzmx; i++) |
5164
|
93 { |
|
94 d[i] = a.d[i]; |
|
95 r[i] = a.r[i]; |
|
96 } |
5275
|
97 for (octave_idx_type i = 0; i < ncols + 1; i++) |
5164
|
98 c[i] = a.c[i]; |
|
99 } |
|
100 |
|
101 ~SparseRep (void) { delete [] d; delete [] r; delete [] c; } |
|
102 |
5604
|
103 octave_idx_type length (void) const { return nzmx; } |
5164
|
104 |
5604
|
105 octave_idx_type nnz (void) const { return c [ncols]; } |
5164
|
106 |
5275
|
107 T& elem (octave_idx_type _r, octave_idx_type _c); |
5164
|
108 |
5275
|
109 T celem (octave_idx_type _r, octave_idx_type _c) const; |
5164
|
110 |
5275
|
111 T& data (octave_idx_type i) { return d[i]; } |
5164
|
112 |
5275
|
113 T cdata (octave_idx_type i) const { return d[i]; } |
5164
|
114 |
5275
|
115 octave_idx_type& ridx (octave_idx_type i) { return r[i]; } |
5164
|
116 |
5275
|
117 octave_idx_type cridx (octave_idx_type i) const { return r[i]; } |
5164
|
118 |
5275
|
119 octave_idx_type& cidx (octave_idx_type i) { return c[i]; } |
5164
|
120 |
5275
|
121 octave_idx_type ccidx (octave_idx_type i) const { return c[i]; } |
5164
|
122 |
|
123 void maybe_compress (bool remove_zeros); |
|
124 |
5275
|
125 void change_length (octave_idx_type nz); |
5164
|
126 |
|
127 private: |
|
128 |
|
129 // No assignment! |
|
130 |
|
131 SparseRep& operator = (const SparseRep& a); |
|
132 }; |
|
133 |
|
134 //-------------------------------------------------------------------- |
|
135 |
|
136 void make_unique (void) |
|
137 { |
|
138 if (rep->count > 1) |
|
139 { |
|
140 --rep->count; |
|
141 rep = new SparseRep (*rep); |
|
142 } |
|
143 } |
|
144 |
|
145 public: |
|
146 |
|
147 // !!! WARNING !!! -- these should be protected, not public. You |
|
148 // should not access these data members directly! |
|
149 |
|
150 typename Sparse<T>::SparseRep *rep; |
|
151 |
|
152 dim_vector dimensions; |
|
153 |
|
154 protected: |
|
155 idx_vector *idx; |
5275
|
156 octave_idx_type idx_count; |
5164
|
157 |
|
158 private: |
|
159 |
|
160 typename Sparse<T>::SparseRep *nil_rep (void) const |
|
161 { |
|
162 static typename Sparse<T>::SparseRep *nr |
|
163 = new typename Sparse<T>::SparseRep (); |
|
164 |
|
165 nr->count++; |
|
166 |
|
167 return nr; |
|
168 } |
|
169 |
|
170 public: |
|
171 |
|
172 Sparse (void) |
|
173 : rep (nil_rep ()), dimensions (dim_vector(0,0)), |
|
174 idx (0), idx_count (0) { } |
|
175 |
5275
|
176 explicit Sparse (octave_idx_type n) |
5164
|
177 : rep (new typename Sparse<T>::SparseRep (n)), |
|
178 dimensions (dim_vector (n, n)), idx (0), idx_count (0) { } |
|
179 |
5275
|
180 explicit Sparse (octave_idx_type nr, octave_idx_type nc) |
5164
|
181 : rep (new typename Sparse<T>::SparseRep (nr, nc)), |
|
182 dimensions (dim_vector (nr, nc)), idx (0), idx_count (0) { } |
|
183 |
5275
|
184 explicit Sparse (octave_idx_type nr, octave_idx_type nc, T val); |
5164
|
185 |
5275
|
186 Sparse (const dim_vector& dv, octave_idx_type nz) |
5164
|
187 : rep (new typename Sparse<T>::SparseRep (dv(0), dv(1), nz)), |
|
188 dimensions (dv), idx (0), idx_count (0) { } |
|
189 |
5275
|
190 Sparse (octave_idx_type nr, octave_idx_type nc, octave_idx_type nz) |
5164
|
191 : rep (new typename Sparse<T>::SparseRep (nr, nc, nz)), |
|
192 dimensions (dim_vector (nr, nc)), idx (0), idx_count (0) { } |
|
193 |
|
194 // Type conversion case. |
|
195 template <class U> Sparse (const Sparse<U>& a); |
|
196 |
|
197 // No type conversion case. |
|
198 Sparse (const Sparse<T>& a) |
|
199 : rep (a.rep), dimensions (a.dimensions), idx (0), idx_count (0) |
|
200 { |
|
201 rep->count++; |
|
202 } |
|
203 |
|
204 public: |
|
205 |
|
206 Sparse (const dim_vector& dv); |
|
207 |
|
208 Sparse (const Sparse<T>& a, const dim_vector& dv); |
|
209 |
5275
|
210 Sparse (const Array<T>& a, const Array<octave_idx_type>& r, const Array<octave_idx_type>& c, |
|
211 octave_idx_type nr, octave_idx_type nc, bool sum_terms); |
5164
|
212 |
|
213 Sparse (const Array<T>& a, const Array<double>& r, const Array<double>& c, |
5275
|
214 octave_idx_type nr, octave_idx_type nc, bool sum_terms); |
5164
|
215 |
|
216 // Sparsify a normal matrix |
|
217 Sparse (const Array2<T>& a); |
|
218 Sparse (const Array<T>& a); |
|
219 |
|
220 virtual ~Sparse (void); |
|
221 |
|
222 Sparse<T>& operator = (const Sparse<T>& a) |
|
223 { |
|
224 if (this != &a) |
|
225 { |
|
226 if (--rep->count <= 0) |
|
227 delete rep; |
|
228 |
|
229 rep = a.rep; |
|
230 rep->count++; |
|
231 |
|
232 dimensions = a.dimensions; |
|
233 } |
|
234 |
|
235 idx_count = 0; |
|
236 idx = 0; |
|
237 |
|
238 return *this; |
|
239 } |
|
240 |
5604
|
241 // Note that nzmax and capacity are the amount of storage for |
|
242 // non-zero elements, while nnz is the actual number of non-zero |
|
243 // terms. |
|
244 octave_idx_type nzmax (void) const { return rep->length (); } |
|
245 octave_idx_type capacity (void) const { return nzmax (); } |
|
246 octave_idx_type nnz (void) const { return rep->nnz (); } |
5164
|
247 |
|
248 // Paranoid number of elements test for case of dims = (-1,-1) |
5275
|
249 octave_idx_type numel (void) const |
5164
|
250 { |
|
251 if (dim1() < 0 || dim2() < 0) |
|
252 return 0; |
|
253 else |
|
254 return dimensions.numel (); |
|
255 } |
|
256 |
5275
|
257 octave_idx_type nelem (void) const { return capacity (); } |
|
258 octave_idx_type length (void) const { return numel (); } |
5164
|
259 |
5275
|
260 octave_idx_type dim1 (void) const { return dimensions(0); } |
|
261 octave_idx_type dim2 (void) const { return dimensions(1); } |
5164
|
262 |
5275
|
263 octave_idx_type rows (void) const { return dim1 (); } |
|
264 octave_idx_type cols (void) const { return dim2 (); } |
|
265 octave_idx_type columns (void) const { return dim2 (); } |
5164
|
266 |
5275
|
267 octave_idx_type get_row_index (octave_idx_type k) { return ridx (k); } |
|
268 octave_idx_type get_col_index (octave_idx_type k) |
5164
|
269 { |
5275
|
270 octave_idx_type ret = 0; |
5164
|
271 while (cidx(ret+1) < k) |
|
272 ret++; |
|
273 return ret; |
|
274 } |
5275
|
275 size_t byte_size (void) const { return (cols () + 1) * sizeof (octave_idx_type) + |
|
276 capacity () * (sizeof (T) + sizeof (octave_idx_type)); } |
5164
|
277 |
|
278 dim_vector dims (void) const { return dimensions; } |
|
279 |
|
280 Sparse<T> squeeze (void) const { return *this; } |
|
281 |
5275
|
282 octave_idx_type compute_index (const Array<octave_idx_type>& ra_idx) const; |
5164
|
283 |
5275
|
284 T range_error (const char *fcn, octave_idx_type n) const; |
|
285 T& range_error (const char *fcn, octave_idx_type n); |
5164
|
286 |
5275
|
287 T range_error (const char *fcn, octave_idx_type i, octave_idx_type j) const; |
|
288 T& range_error (const char *fcn, octave_idx_type i, octave_idx_type j); |
5164
|
289 |
5275
|
290 T range_error (const char *fcn, const Array<octave_idx_type>& ra_idx) const; |
|
291 T& range_error (const char *fcn, const Array<octave_idx_type>& ra_idx); |
5164
|
292 |
|
293 // No checking, even for multiple references, ever. |
|
294 |
5275
|
295 T& xelem (octave_idx_type n) |
5164
|
296 { |
5275
|
297 octave_idx_type i = n % rows (), j = n / rows(); |
5164
|
298 return xelem (i, j); |
|
299 } |
|
300 |
5275
|
301 T xelem (octave_idx_type n) const |
5164
|
302 { |
5275
|
303 octave_idx_type i = n % rows (), j = n / rows(); |
5164
|
304 return xelem (i, j); |
|
305 } |
|
306 |
5275
|
307 T& xelem (octave_idx_type i, octave_idx_type j) { return rep->elem (i, j); } |
|
308 T xelem (octave_idx_type i, octave_idx_type j) const { return rep->celem (i, j); } |
5164
|
309 |
5275
|
310 T& xelem (const Array<octave_idx_type>& ra_idx) |
5164
|
311 { return xelem (compute_index (ra_idx)); } |
|
312 |
5275
|
313 T xelem (const Array<octave_idx_type>& ra_idx) const |
5164
|
314 { return xelem (compute_index (ra_idx)); } |
|
315 |
5775
|
316 // FIXME -- would be nice to fix this so that we don't |
5164
|
317 // unnecessarily force a copy, but that is not so easy, and I see no |
|
318 // clean way to do it. |
|
319 |
5275
|
320 T& checkelem (octave_idx_type n) |
5164
|
321 { |
|
322 if (n < 0 || n >= numel ()) |
|
323 return range_error ("T& Sparse<T>::checkelem", n); |
|
324 else |
|
325 { |
|
326 make_unique (); |
|
327 return xelem (n); |
|
328 } |
|
329 } |
|
330 |
5275
|
331 T& checkelem (octave_idx_type i, octave_idx_type j) |
5164
|
332 { |
|
333 if (i < 0 || j < 0 || i >= dim1 () || j >= dim2 ()) |
|
334 return range_error ("T& Sparse<T>::checkelem", i, j); |
|
335 else |
|
336 { |
|
337 make_unique (); |
|
338 return xelem (i, j); |
|
339 } |
|
340 } |
|
341 |
5275
|
342 T& checkelem (const Array<octave_idx_type>& ra_idx) |
5164
|
343 { |
5275
|
344 octave_idx_type i = compute_index (ra_idx); |
5164
|
345 |
|
346 if (i < 0) |
|
347 return range_error ("T& Sparse<T>::checkelem", ra_idx); |
|
348 else |
|
349 return elem (i); |
|
350 } |
|
351 |
5275
|
352 T& elem (octave_idx_type n) |
5164
|
353 { |
|
354 make_unique (); |
|
355 return xelem (n); |
|
356 } |
|
357 |
5275
|
358 T& elem (octave_idx_type i, octave_idx_type j) |
5164
|
359 { |
|
360 make_unique (); |
|
361 return xelem (i, j); |
|
362 } |
|
363 |
5275
|
364 T& elem (const Array<octave_idx_type>& ra_idx) |
5164
|
365 { return Sparse<T>::elem (compute_index (ra_idx)); } |
|
366 |
|
367 #if defined (BOUNDS_CHECKING) |
5275
|
368 T& operator () (octave_idx_type n) { return checkelem (n); } |
|
369 T& operator () (octave_idx_type i, octave_idx_type j) { return checkelem (i, j); } |
|
370 T& operator () (const Array<octave_idx_type>& ra_idx) { return checkelem (ra_idx); } |
5164
|
371 #else |
5275
|
372 T& operator () (octave_idx_type n) { return elem (n); } |
|
373 T& operator () (octave_idx_type i, octave_idx_type j) { return elem (i, j); } |
|
374 T& operator () (const Array<octave_idx_type>& ra_idx) { return elem (ra_idx); } |
5164
|
375 #endif |
|
376 |
5275
|
377 T checkelem (octave_idx_type n) const |
5164
|
378 { |
|
379 if (n < 0 || n >= numel ()) |
|
380 return range_error ("T Sparse<T>::checkelem", n); |
|
381 else |
|
382 return xelem (n); |
|
383 } |
|
384 |
5275
|
385 T checkelem (octave_idx_type i, octave_idx_type j) const |
5164
|
386 { |
|
387 if (i < 0 || j < 0 || i >= dim1 () || j >= dim2 ()) |
|
388 return range_error ("T Sparse<T>::checkelem", i, j); |
|
389 else |
|
390 return xelem (i, j); |
|
391 } |
|
392 |
5275
|
393 T checkelem (const Array<octave_idx_type>& ra_idx) const |
5164
|
394 { |
5275
|
395 octave_idx_type i = compute_index (ra_idx); |
5164
|
396 |
|
397 if (i < 0) |
|
398 return range_error ("T Sparse<T>::checkelem", ra_idx); |
|
399 else |
|
400 return Sparse<T>::elem (i); |
|
401 } |
|
402 |
5275
|
403 T elem (octave_idx_type n) const { return xelem (n); } |
5164
|
404 |
5275
|
405 T elem (octave_idx_type i, octave_idx_type j) const { return xelem (i, j); } |
5164
|
406 |
5275
|
407 T elem (const Array<octave_idx_type>& ra_idx) const |
5164
|
408 { return Sparse<T>::elem (compute_index (ra_idx)); } |
|
409 |
|
410 #if defined (BOUNDS_CHECKING) |
5275
|
411 T operator () (octave_idx_type n) const { return checkelem (n); } |
|
412 T operator () (octave_idx_type i, octave_idx_type j) const { return checkelem (i, j); } |
|
413 T operator () (const Array<octave_idx_type>& ra_idx) const { return checkelem (ra_idx); } |
5164
|
414 #else |
5275
|
415 T operator () (octave_idx_type n) const { return elem (n); } |
|
416 T operator () (octave_idx_type i, octave_idx_type j) const { return elem (i, j); } |
|
417 T operator () (const Array<octave_idx_type>& ra_idx) const { return elem (ra_idx); } |
5164
|
418 #endif |
|
419 |
|
420 Sparse<T> maybe_compress (bool remove_zeros = false) |
|
421 { rep->maybe_compress (remove_zeros); return (*this); } |
|
422 |
|
423 Sparse<T> reshape (const dim_vector& new_dims) const; |
|
424 |
|
425 // !!! WARNING !!! -- the following resize_no_fill functions are |
|
426 // public because template friends don't work properly with versions |
|
427 // of gcc earlier than 3.3. You should use these functions only in |
|
428 // classes that are derived from Sparse<T>. |
|
429 |
|
430 // protected: |
|
431 |
5275
|
432 void resize_no_fill (octave_idx_type r, octave_idx_type c); |
5164
|
433 |
|
434 void resize_no_fill (const dim_vector& dv); |
|
435 |
|
436 public: |
5275
|
437 Sparse<T> permute (const Array<octave_idx_type>& vec, bool inv = false) const; |
5164
|
438 |
5275
|
439 Sparse<T> ipermute (const Array<octave_idx_type>& vec) const |
5164
|
440 { return permute (vec, true); } |
|
441 |
5275
|
442 void resize (octave_idx_type r, octave_idx_type c) { resize_no_fill (r, c); } |
5164
|
443 |
|
444 void resize (const dim_vector& dv) { resize_no_fill (dv); } |
|
445 |
5275
|
446 void change_capacity (octave_idx_type nz) { rep->change_length (nz); } |
5164
|
447 |
5275
|
448 Sparse<T>& insert (const Sparse<T>& a, octave_idx_type r, octave_idx_type c); |
|
449 Sparse<T>& insert (const Sparse<T>& a, const Array<octave_idx_type>& idx); |
5164
|
450 |
|
451 bool is_square (void) const { return (dim1 () == dim2 ()); } |
|
452 |
|
453 bool is_empty (void) const { return (rows () < 1 && cols () < 1); } |
|
454 |
|
455 Sparse<T> transpose (void) const; |
|
456 |
|
457 T* data (void) { make_unique (); return rep->d; } |
5275
|
458 T& data (octave_idx_type i) { make_unique (); return rep->data (i); } |
5164
|
459 T* xdata (void) { return rep->d; } |
5275
|
460 T& xdata (octave_idx_type i) { return rep->data (i); } |
5164
|
461 |
5275
|
462 T data (octave_idx_type i) const { return rep->data (i); } |
5164
|
463 T* data (void) const { return rep->d; } |
|
464 |
5275
|
465 octave_idx_type* ridx (void) { make_unique (); return rep->r; } |
|
466 octave_idx_type& ridx (octave_idx_type i) { make_unique (); return rep->ridx (i); } |
|
467 octave_idx_type* xridx (void) { return rep->r; } |
|
468 octave_idx_type& xridx (octave_idx_type i) { return rep->ridx (i); } |
5164
|
469 |
5275
|
470 octave_idx_type ridx (octave_idx_type i) const { return rep->cridx (i); } |
|
471 octave_idx_type* ridx (void) const { return rep->r; } |
5164
|
472 |
5275
|
473 octave_idx_type* cidx (void) { make_unique (); return rep->c; } |
|
474 octave_idx_type& cidx (octave_idx_type i) { make_unique (); return rep->cidx (i); } |
|
475 octave_idx_type* xcidx (void) { return rep->c; } |
|
476 octave_idx_type& xcidx (octave_idx_type i) { return rep->cidx (i); } |
5164
|
477 |
5275
|
478 octave_idx_type cidx (octave_idx_type i) const { return rep->ccidx (i); } |
|
479 octave_idx_type* cidx (void) const { return rep->c; } |
5164
|
480 |
5275
|
481 octave_idx_type ndims (void) const { return dimensions.length (); } |
5164
|
482 |
|
483 void clear_index (void); |
|
484 |
|
485 void set_index (const idx_vector& i); |
|
486 |
5275
|
487 octave_idx_type index_count (void) const { return idx_count; } |
5164
|
488 |
|
489 idx_vector *get_idx (void) const { return idx; } |
|
490 |
|
491 void maybe_delete_elements (idx_vector& i); |
|
492 |
|
493 void maybe_delete_elements (idx_vector& i, idx_vector& j); |
|
494 |
|
495 void maybe_delete_elements (Array<idx_vector>& ra_idx); |
|
496 |
|
497 Sparse<T> value (void); |
|
498 |
|
499 Sparse<T> index (idx_vector& i, int resize_ok = 0) const; |
|
500 |
|
501 Sparse<T> index (idx_vector& i, idx_vector& j, int resize_ok = 0) const; |
|
502 |
|
503 Sparse<T> index (Array<idx_vector>& ra_idx, int resize_ok = 0) const; |
|
504 |
|
505 void print_info (std::ostream& os, const std::string& prefix) const; |
|
506 |
|
507 }; |
|
508 |
|
509 // NOTE: these functions should be friends of the Sparse<T> class and |
|
510 // Sparse<T>::dimensions should be protected, not public, but we can't |
|
511 // do that because of bugs in gcc prior to 3.3. |
|
512 |
|
513 template <class LT, class RT> |
|
514 /* friend */ int |
|
515 assign (Sparse<LT>& lhs, const Sparse<RT>& rhs); |
|
516 |
|
517 template <class LT, class RT> |
|
518 /* friend */ int |
|
519 assign1 (Sparse<LT>& lhs, const Sparse<RT>& rhs); |
|
520 |
|
521 #define INSTANTIATE_SPARSE_ASSIGN(LT, RT) \ |
|
522 template int assign (Sparse<LT>&, const Sparse<RT>&); \ |
|
523 template int assign1 (Sparse<LT>&, const Sparse<RT>&); |
|
524 |
|
525 #define INSTANTIATE_SPARSE(T) \ |
|
526 template class Sparse<T>; |
|
527 |
|
528 #define INSTANTIATE_SPARSE_AND_ASSIGN(T) \ |
|
529 INSTANTIATE_SPARSE (T); \ |
|
530 INSTANTIATE_SPARSE_ASSIGN (T, T) |
|
531 |
|
532 #endif |
|
533 |
|
534 /* |
|
535 ;;; Local Variables: *** |
|
536 ;;; mode: C++ *** |
|
537 ;;; End: *** |
|
538 */ |