1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * This file is part of the LibreOffice project.
4  *
5  * This Source Code Form is subject to the terms of the Mozilla Public
6  * License, v. 2.0. If a copy of the MPL was not distributed with this
7  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8  *
9  */
10 
11 #include <memory>
12 #include <sfx2/dispatch.hxx>
13 #include <svl/zforlist.hxx>
14 #include <svl/undo.hxx>
15 
16 #include <formulacell.hxx>
17 #include <rangelst.hxx>
18 #include <scitems.hxx>
19 #include <docsh.hxx>
20 #include <document.hxx>
21 #include <uiitems.hxx>
22 #include <reffact.hxx>
23 #include <docfunc.hxx>
24 #include <TableFillingAndNavigationTools.hxx>
25 #include <AnalysisOfVarianceDialog.hxx>
26 #include <scresid.hxx>
27 #include <strings.hrc>
28 
29 namespace
30 {
31 
32 struct StatisticCalculation {
33     const char* aLabelId;
34     const char* aFormula;
35     const char* aResultRangeName;
36 };
37 
38 static StatisticCalculation const lclBasicStatistics[] =
39 {
40     { STR_ANOVA_LABEL_GROUPS, nullptr,             nullptr       },
41     { STRID_CALC_COUNT,       "=COUNT(%RANGE%)",   "COUNT_RANGE" },
42     { STRID_CALC_SUM,         "=SUM(%RANGE%)",     "SUM_RANGE"   },
43     { STRID_CALC_MEAN,        "=AVERAGE(%RANGE%)", "MEAN_RANGE"  },
44     { STRID_CALC_VARIANCE,    "=VAR(%RANGE%)",     "VAR_RANGE"   },
45     { nullptr,                nullptr,             nullptr       }
46 };
47 
48 static const char* lclAnovaLabels[] =
49 {
50     STR_ANOVA_LABEL_SOURCE_OF_VARIATION,
51     STR_ANOVA_LABEL_SS,
52     STR_ANOVA_LABEL_DF,
53     STR_ANOVA_LABEL_MS,
54     STR_ANOVA_LABEL_F,
55     STR_ANOVA_LABEL_P_VALUE,
56     STR_ANOVA_LABEL_F_CRITICAL,
57     nullptr
58 };
59 
60 static const char strWildcardRange[] = "%RANGE%";
61 
62 OUString lclCreateMultiParameterFormula(
63             ScRangeList&        aRangeList, const OUString& aFormulaTemplate,
64             const OUString&     aWildcard,  const ScDocument*     pDocument,
65             const ScAddress::Details& aAddressDetails)
66 {
67     OUStringBuffer aResult;
68     for (size_t i = 0; i < aRangeList.size(); i++)
69     {
70         OUString aRangeString(aRangeList[i].Format(ScRefFlags::RANGE_ABS, pDocument, aAddressDetails));
71         OUString aFormulaString = aFormulaTemplate.replaceAll(aWildcard, aRangeString);
72         aResult.append(aFormulaString);
73         if(i != aRangeList.size() - 1) // Not Last
74             aResult.append(";");
75     }
76     return aResult.makeStringAndClear();
77 }
78 
79 void lclMakeSubRangesList(ScRangeList& rRangeList, const ScRange& rInputRange, ScStatisticsInputOutputDialog::GroupedBy aGroupedBy)
80 {
81     std::unique_ptr<DataRangeIterator> pIterator;
82     if (aGroupedBy == ScStatisticsInputOutputDialog::BY_COLUMN)
83         pIterator.reset(new DataRangeByColumnIterator(rInputRange));
84     else
85         pIterator.reset(new DataRangeByRowIterator(rInputRange));
86 
87     for( ; pIterator->hasNext(); pIterator->next() )
88     {
89         ScRange aRange = pIterator->get();
90         rRangeList.push_back(aRange);
91     }
92 }
93 
94 }
95 
96 ScAnalysisOfVarianceDialog::ScAnalysisOfVarianceDialog(
97                     SfxBindings* pSfxBindings, SfxChildWindow* pChildWindow,
98                     vcl::Window* pParent, ScViewData* pViewData ) :
99     ScStatisticsInputOutputDialog(
100             pSfxBindings, pChildWindow, pParent, pViewData,
101             "AnalysisOfVarianceDialog", "modules/scalc/ui/analysisofvariancedialog.ui" ),
102     meFactor(SINGLE_FACTOR)
103 {
104     get(mpAlphaField,         "alpha-spin");
105     get(mpSingleFactorRadio,  "radio-single-factor");
106     get(mpTwoFactorRadio,     "radio-two-factor");
107     get(mpRowsPerSampleField, "rows-per-sample-spin");
108 
109     mpSingleFactorRadio->SetToggleHdl( LINK( this, ScAnalysisOfVarianceDialog, FactorChanged ) );
110     mpTwoFactorRadio->SetToggleHdl( LINK( this, ScAnalysisOfVarianceDialog, FactorChanged ) );
111 
112     mpSingleFactorRadio->Check();
113     mpTwoFactorRadio->Check(false);
114 
115     FactorChanged();
116 }
117 
118 ScAnalysisOfVarianceDialog::~ScAnalysisOfVarianceDialog()
119 {
120     disposeOnce();
121 }
122 
123 void ScAnalysisOfVarianceDialog::dispose()
124 {
125     mpAlphaField.clear();
126     mpSingleFactorRadio.clear();
127     mpTwoFactorRadio.clear();
128     mpRowsPerSampleField.clear();
129     ScStatisticsInputOutputDialog::dispose();
130 }
131 
132 bool ScAnalysisOfVarianceDialog::Close()
133 {
134     return DoClose( ScAnalysisOfVarianceDialogWrapper::GetChildWindowId() );
135 }
136 
137 const char* ScAnalysisOfVarianceDialog::GetUndoNameId()
138 {
139     return STR_ANALYSIS_OF_VARIANCE_UNDO_NAME;
140 }
141 
142 IMPL_LINK_NOARG( ScAnalysisOfVarianceDialog, FactorChanged, RadioButton&, void )
143 {
144     FactorChanged();
145 }
146 
147 void ScAnalysisOfVarianceDialog::FactorChanged()
148 {
149     if (mpSingleFactorRadio->IsChecked())
150     {
151         mpGroupByRowsRadio->Enable();
152         mpGroupByColumnsRadio->Enable();
153         mpRowsPerSampleField->Enable(false);
154         meFactor = SINGLE_FACTOR;
155     }
156     else if (mpTwoFactorRadio->IsChecked())
157     {
158         mpGroupByRowsRadio->Enable(false);
159         mpGroupByColumnsRadio->Enable(false);
160         mpRowsPerSampleField->Enable(false); // Rows per sample not yet implemented
161         meFactor = TWO_FACTOR;
162     }
163 }
164 
165 void ScAnalysisOfVarianceDialog::RowColumn(ScRangeList& rRangeList, AddressWalkerWriter& aOutput, FormulaTemplate& aTemplate,
166                                            const OUString& sFormula, GroupedBy aGroupedBy, ScRange* pResultRange)
167 {
168     if (pResultRange != nullptr)
169         pResultRange->aStart = aOutput.current();
170     if (!sFormula.isEmpty())
171     {
172         for (size_t i = 0; i < rRangeList.size(); i++)
173         {
174             ScRange const & rRange = rRangeList[i];
175             aTemplate.setTemplate(sFormula);
176             aTemplate.applyRange(strWildcardRange, rRange);
177             aOutput.writeFormula(aTemplate.getTemplate());
178             if (pResultRange != nullptr)
179                 pResultRange->aEnd = aOutput.current();
180             aOutput.nextRow();
181         }
182     }
183     else
184     {
185         const char* pLabelId = (aGroupedBy == BY_COLUMN) ? STR_COLUMN_LABEL_TEMPLATE : STR_ROW_LABEL_TEMPLATE;
186         OUString aLabelTemplate(ScResId(pLabelId));
187 
188         for (size_t i = 0; i < rRangeList.size(); i++)
189         {
190             aTemplate.setTemplate(aLabelTemplate);
191             aTemplate.applyNumber("%NUMBER%", i + 1);
192             aOutput.writeString(aTemplate.getTemplate());
193             if (pResultRange != nullptr)
194                 pResultRange->aEnd = aOutput.current();
195             aOutput.nextRow();
196         }
197     }
198 }
199 
200 void ScAnalysisOfVarianceDialog::AnovaSingleFactor(AddressWalkerWriter& output, FormulaTemplate& aTemplate)
201 {
202     output.writeBoldString(ScResId(STR_ANOVA_SINGLE_FACTOR_LABEL));
203     output.newLine();
204 
205     double aAlphaValue = mpAlphaField->GetValue() / 100.0;
206     output.writeString(ScResId(STR_LABEL_ALPHA));
207     output.nextColumn();
208     output.writeValue(aAlphaValue);
209     aTemplate.autoReplaceAddress("%ALPHA%", output.current());
210     output.newLine();
211     output.newLine();
212 
213     // Write labels
214     for(sal_Int32 i = 0; lclBasicStatistics[i].aLabelId; i++)
215     {
216         output.writeString(ScResId(lclBasicStatistics[i].aLabelId));
217         output.nextColumn();
218     }
219     output.newLine();
220 
221     // Collect aRangeList
222     ScRangeList aRangeList;
223     lclMakeSubRangesList(aRangeList, mInputRange, mGroupedBy);
224 
225     output.push();
226 
227     // Write values
228     for(sal_Int32 i = 0; lclBasicStatistics[i].aLabelId; i++)
229     {
230         output.resetRow();
231         ScRange aResultRange;
232         OUString sFormula = OUString::createFromAscii(lclBasicStatistics[i].aFormula);
233         RowColumn(aRangeList, output, aTemplate, sFormula, mGroupedBy, &aResultRange);
234         output.nextColumn();
235         if (lclBasicStatistics[i].aResultRangeName != nullptr)
236         {
237             OUString sResultRangeName = OUString::createFromAscii(lclBasicStatistics[i].aResultRangeName);
238             aTemplate.autoReplaceRange("%" + sResultRangeName + "%", aResultRange);
239         }
240     }
241 
242     output.nextRow(); // Blank row
243 
244     // Write ANOVA labels
245     output.resetColumn();
246     for(sal_Int32 i = 0; lclAnovaLabels[i]; i++)
247     {
248         output.writeString(ScResId(lclAnovaLabels[i]));
249         output.nextColumn();
250     }
251     output.nextRow();
252 
253     aTemplate.autoReplaceRange("%FIRST_COLUMN%", aRangeList[0]);
254 
255     // Between Groups
256     {
257         // Label
258         output.resetColumn();
259         output.writeString(ScResId(STR_ANOVA_LABEL_BETWEEN_GROUPS));
260         output.nextColumn();
261 
262         // Sum of Squares
263 
264         aTemplate.setTemplate("=SUMPRODUCT(%SUM_RANGE%;%MEAN_RANGE%)-SUM(%SUM_RANGE%)^2/SUM(%COUNT_RANGE%)");
265         aTemplate.autoReplaceAddress("%BETWEEN_SS%", output.current());
266         output.writeFormula(aTemplate.getTemplate());
267         output.nextColumn();
268 
269         // Degree of freedom
270         aTemplate.setTemplate("=COUNT(%SUM_RANGE%)-1");
271         aTemplate.autoReplaceAddress("%BETWEEN_DF%", output.current());
272         output.writeFormula(aTemplate.getTemplate());
273         output.nextColumn();
274 
275         // MS
276         aTemplate.setTemplate("=%BETWEEN_SS% / %BETWEEN_DF%");
277         aTemplate.autoReplaceAddress("%BETWEEN_MS%", output.current());
278         output.writeFormula(aTemplate.getTemplate());
279         output.nextColumn();
280 
281         // F
282         aTemplate.setTemplate("=%BETWEEN_MS% / %WITHIN_MS%");
283         aTemplate.applyAddress("%WITHIN_MS%",  output.current(-1, 1));
284         aTemplate.autoReplaceAddress("%F_VAL%", output.current());
285         output.writeFormula(aTemplate.getTemplate());
286         output.nextColumn();
287 
288         // P-value
289         aTemplate.setTemplate("=FDIST(%F_VAL%; %BETWEEN_DF%; %WITHIN_DF%");
290         aTemplate.applyAddress("%WITHIN_DF%",   output.current(-3, 1));
291         output.writeFormula(aTemplate.getTemplate());
292         output.nextColumn();
293 
294         // F critical
295         aTemplate.setTemplate("=FINV(%ALPHA%; %BETWEEN_DF%; %WITHIN_DF%");
296         aTemplate.applyAddress("%WITHIN_DF%",  output.current(-4, 1));
297         output.writeFormula(aTemplate.getTemplate());
298     }
299     output.nextRow();
300 
301     // Within Groups
302     {
303         // Label
304         output.resetColumn();
305         output.writeString(ScResId(STR_ANOVA_LABEL_WITHIN_GROUPS));
306         output.nextColumn();
307 
308         // Sum of Squares
309         OUString aSSPart = lclCreateMultiParameterFormula(aRangeList, "DEVSQ(%RANGE%)", strWildcardRange, mDocument, mAddressDetails);
310         aTemplate.setTemplate("=SUM(%RANGE%)");
311         aTemplate.applyString(strWildcardRange, aSSPart);
312         aTemplate.autoReplaceAddress("%WITHIN_SS%", output.current());
313         output.writeFormula(aTemplate.getTemplate());
314         output.nextColumn();
315 
316         // Degree of freedom
317         aTemplate.setTemplate("=SUM(%COUNT_RANGE%)-COUNT(%COUNT_RANGE%)");
318         aTemplate.autoReplaceAddress("%WITHIN_DF%", output.current());
319         output.writeFormula(aTemplate.getTemplate());
320         output.nextColumn();
321 
322         // MS
323         aTemplate.setTemplate("=%WITHIN_SS% / %WITHIN_DF%");
324         output.writeFormula(aTemplate.getTemplate());
325     }
326     output.nextRow();
327 
328     // Total
329     {
330         // Label
331         output.resetColumn();
332         output.writeString(ScResId(STR_ANOVA_LABEL_TOTAL));
333         output.nextColumn();
334 
335         // Sum of Squares
336         aTemplate.setTemplate("=DEVSQ(%RANGE_LIST%)");
337         aTemplate.applyRangeList("%RANGE_LIST%", aRangeList, ';');
338         output.writeFormula(aTemplate.getTemplate());
339         output.nextColumn();
340 
341         // Degree of freedom
342         aTemplate.setTemplate("=SUM(%COUNT_RANGE%) - 1");
343         output.writeFormula(aTemplate.getTemplate());
344     }
345     output.nextRow();
346 }
347 
348 void ScAnalysisOfVarianceDialog::AnovaTwoFactor(AddressWalkerWriter& output, FormulaTemplate& aTemplate)
349 {
350     output.writeBoldString(ScResId(STR_ANOVA_TWO_FACTOR_LABEL));
351     output.newLine();
352 
353     double aAlphaValue = mpAlphaField->GetValue() / 100.0;
354     output.writeString("Alpha");
355     output.nextColumn();
356     output.writeValue(aAlphaValue);
357     aTemplate.autoReplaceAddress("%ALPHA%", output.current());
358     output.newLine();
359     output.newLine();
360 
361     // Write labels
362     for(sal_Int32 i = 0; lclBasicStatistics[i].aLabelId; i++)
363     {
364         output.writeString(ScResId(lclBasicStatistics[i].aLabelId));
365         output.nextColumn();
366     }
367     output.newLine();
368 
369     ScRangeList aColumnRangeList;
370     ScRangeList aRowRangeList;
371 
372     lclMakeSubRangesList(aColumnRangeList, mInputRange, BY_COLUMN);
373     lclMakeSubRangesList(aRowRangeList, mInputRange, BY_ROW);
374 
375     // Write ColumnX values
376     output.push();
377     for(sal_Int32 i = 0; lclBasicStatistics[i].aLabelId; i++)
378     {
379         output.resetRow();
380         ScRange aResultRange;
381         OUString sFormula = OUString::createFromAscii(lclBasicStatistics[i].aFormula);
382         RowColumn(aColumnRangeList, output, aTemplate, sFormula, BY_COLUMN, &aResultRange);
383         if (lclBasicStatistics[i].aResultRangeName != nullptr)
384         {
385             OUString sResultRangeName = OUString::createFromAscii(lclBasicStatistics[i].aResultRangeName);
386             aTemplate.autoReplaceRange("%" + sResultRangeName + "_COLUMN%", aResultRange);
387         }
388         output.nextColumn();
389     }
390     output.newLine();
391 
392     // Write RowX values
393     output.push();
394     for(sal_Int32 i = 0; lclBasicStatistics[i].aLabelId; i++)
395     {
396         output.resetRow();
397         ScRange aResultRange;
398         OUString sFormula = OUString::createFromAscii(lclBasicStatistics[i].aFormula);
399         RowColumn(aRowRangeList, output, aTemplate, sFormula, BY_ROW, &aResultRange);
400 
401         if (lclBasicStatistics[i].aResultRangeName != nullptr)
402         {
403             OUString sResultRangeName = OUString::createFromAscii(lclBasicStatistics[i].aResultRangeName);
404             aTemplate.autoReplaceRange("%" + sResultRangeName + "_ROW%", aResultRange);
405         }
406         output.nextColumn();
407     }
408     output.newLine();
409 
410     // Write ANOVA labels
411     for(sal_Int32 i = 0; lclAnovaLabels[i]; i++)
412     {
413         output.writeString(ScResId(lclAnovaLabels[i]));
414         output.nextColumn();
415     }
416     output.nextRow();
417 
418     // Setup auto-replace strings
419     aTemplate.autoReplaceRange(strWildcardRange, mInputRange);
420     aTemplate.autoReplaceRange("%FIRST_COLUMN%", aColumnRangeList[0]);
421     aTemplate.autoReplaceRange("%FIRST_ROW%",    aRowRangeList[0]);
422 
423     // Rows
424     {
425         // Label
426         output.resetColumn();
427         output.writeString("Rows");
428         output.nextColumn();
429 
430         // Sum of Squares
431         aTemplate.setTemplate("=SUMPRODUCT(%SUM_RANGE_ROW%;%MEAN_RANGE_ROW%) - SUM(%RANGE%)^2 / COUNT(%RANGE%)");
432         aTemplate.autoReplaceAddress("%ROW_SS%", output.current());
433         output.writeFormula(aTemplate.getTemplate());
434         output.nextColumn();
435 
436         // Degree of freedom
437         aTemplate.setTemplate("=MAX(%COUNT_RANGE_COLUMN%) - 1");
438         aTemplate.autoReplaceAddress("%ROW_DF%", output.current());
439         output.writeFormula(aTemplate.getTemplate());
440         output.nextColumn();
441 
442         // MS
443         aTemplate.setTemplate("=%ROW_SS% / %ROW_DF%");
444         aTemplate.autoReplaceAddress("%MS_ROW%", output.current());
445         output.writeFormula(aTemplate.getTemplate());
446         output.nextColumn();
447 
448         // F
449         aTemplate.setTemplate("=%MS_ROW% / %MS_ERROR%");
450         aTemplate.applyAddress("%MS_ERROR%", output.current(-1, 2));
451         aTemplate.autoReplaceAddress("%F_ROW%", output.current());
452         output.writeFormula(aTemplate.getTemplate());
453         output.nextColumn();
454 
455         // P-value
456         aTemplate.setTemplate("=FDIST(%F_ROW%; %ROW_DF%; %ERROR_DF%");
457         aTemplate.applyAddress("%ERROR_DF%",   output.current(-3, 2));
458         output.writeFormula(aTemplate.getTemplate());
459         output.nextColumn();
460 
461         // F critical
462         aTemplate.setTemplate("=FINV(%ALPHA%; %ROW_DF%; %ERROR_DF%");
463         aTemplate.applyAddress("%ERROR_DF%",  output.current(-4, 2));
464         output.writeFormula(aTemplate.getTemplate());
465         output.nextColumn();
466     }
467     output.nextRow();
468 
469     // Columns
470     {
471         // Label
472         output.resetColumn();
473         output.writeString("Columns");
474         output.nextColumn();
475 
476         // Sum of Squares
477         aTemplate.setTemplate("=SUMPRODUCT(%SUM_RANGE_COLUMN%;%MEAN_RANGE_COLUMN%) - SUM(%RANGE%)^2 / COUNT(%RANGE%)");
478         aTemplate.autoReplaceAddress("%COLUMN_SS%", output.current());
479         output.writeFormula(aTemplate.getTemplate());
480         output.nextColumn();
481 
482         // Degree of freedom
483         aTemplate.setTemplate("=MAX(%COUNT_RANGE_ROW%) - 1");
484         aTemplate.autoReplaceAddress("%COLUMN_DF%", output.current());
485         output.writeFormula(aTemplate.getTemplate());
486         output.nextColumn();
487 
488         // MS
489         aTemplate.setTemplate("=%COLUMN_SS% / %COLUMN_DF%");
490         aTemplate.autoReplaceAddress("%MS_COLUMN%", output.current());
491         output.writeFormula(aTemplate.getTemplate());
492         output.nextColumn();
493 
494         // F
495         aTemplate.setTemplate("=%MS_COLUMN% / %MS_ERROR%");
496         aTemplate.applyAddress("%MS_ERROR%", output.current(-1, 1));
497         aTemplate.autoReplaceAddress("%F_COLUMN%", output.current());
498         output.writeFormula(aTemplate.getTemplate());
499         output.nextColumn();
500 
501          // P-value
502         aTemplate.setTemplate("=FDIST(%F_COLUMN%; %COLUMN_DF%; %ERROR_DF%");
503         aTemplate.applyAddress("%ERROR_DF%",   output.current(-3, 1));
504         output.writeFormula(aTemplate.getTemplate());
505         output.nextColumn();
506 
507         // F critical
508         aTemplate.setTemplate("=FINV(%ALPHA%; %COLUMN_DF%; %ERROR_DF%");
509         aTemplate.applyAddress("%ERROR_DF%",  output.current(-4, 1));
510         output.writeFormula(aTemplate.getTemplate());
511         output.nextColumn();
512     }
513     output.nextRow();
514 
515     // Error
516     {
517         // Label
518         output.resetColumn();
519         output.writeString("Error");
520         output.nextColumn();
521 
522         // Sum of Squares
523         aTemplate.setTemplate("=SUMSQ(%RANGE%)+SUM(%RANGE%)^2/COUNT(%RANGE%) - (SUMPRODUCT(%SUM_RANGE_ROW%;%MEAN_RANGE_ROW%) + SUMPRODUCT(%SUM_RANGE_COLUMN%;%MEAN_RANGE_COLUMN%))");
524         aTemplate.autoReplaceAddress("%ERROR_SS%", output.current());
525         output.writeFormula(aTemplate.getTemplate());
526         output.nextColumn();
527 
528         // Degree of freedom
529         aTemplate.setTemplate("=%TOTAL_DF% - %ROW_DF% - %COLUMN_DF%");
530         aTemplate.applyAddress("%TOTAL_DF%", output.current(0,1));
531         aTemplate.autoReplaceAddress("%ERROR_DF%", output.current());
532         output.writeFormula(aTemplate.getTemplate());
533         output.nextColumn();
534 
535         // MS
536         aTemplate.setTemplate("=%ERROR_SS% / %ERROR_DF%");
537         output.writeFormula(aTemplate.getTemplate());
538     }
539     output.nextRow();
540 
541      // Total
542     {
543         // Label
544         output.resetColumn();
545         output.writeString("Total");
546         output.nextColumn();
547 
548         // Sum of Squares
549         aTemplate.setTemplate("=SUM(%ROW_SS%;%COLUMN_SS%;%ERROR_SS%)");
550         output.writeFormula(aTemplate.getTemplate());
551         output.nextColumn();
552 
553         // Degree of freedom
554         aTemplate.setTemplate("=COUNT(%RANGE%)-1");
555         output.writeFormula(aTemplate.getTemplate());
556         output.nextColumn();
557     }
558 }
559 
560 ScRange ScAnalysisOfVarianceDialog::ApplyOutput(ScDocShell* pDocShell)
561 {
562     AddressWalkerWriter output(mOutputAddress, pDocShell, mDocument,
563         formula::FormulaGrammar::mergeToGrammar(formula::FormulaGrammar::GRAM_ENGLISH, mAddressDetails.eConv));
564     FormulaTemplate aTemplate(mDocument);
565 
566     if (meFactor == SINGLE_FACTOR)
567     {
568         AnovaSingleFactor(output, aTemplate);
569     }
570     else if (meFactor == TWO_FACTOR)
571     {
572         AnovaTwoFactor(output, aTemplate);
573     }
574 
575     return ScRange(output.mMinimumAddress, output.mMaximumAddress);
576 }
577 
578 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
579