• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

b1f6c1c4 / ProfessionalAccounting / 321

25 Oct 2024 04:08PM UTC coverage: 56.293% (+0.5%) from 55.799%
321

push

appveyor

b1f6c1c4
fix critical error in discount

6324 of 11234 relevant lines covered (56.29%)

126.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.71
/AccountingServer.Shell/Serializer/DiscountSerializer.cs
1
/* Copyright (C) 2020-2024 b1f6c1c4
2
 *
3
 * This file is part of ProfessionalAccounting.
4
 *
5
 * ProfessionalAccounting is free software: you can redistribute it and/or
6
 * modify it under the terms of the GNU Affero General Public License as
7
 * published by the Free Software Foundation, version 3.
8
 *
9
 * ProfessionalAccounting is distributed in the hope that it will be useful, but
10
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License
12
 * for more details.
13
 *
14
 * You should have received a copy of the GNU Affero General Public License
15
 * along with ProfessionalAccounting.  If not, see
16
 * <https://www.gnu.org/licenses/>.
17
 */
18

19
using System;
20
using System.Collections.Generic;
21
using System.Linq;
22
using System.Text.RegularExpressions;
23
using AccountingServer.BLL;
24
using AccountingServer.BLL.Util;
25
using AccountingServer.Entities;
26
using AccountingServer.Entities.Util;
27
using AccountingServer.Shell.Util;
28
using static AccountingServer.BLL.Parsing.Facade;
29
using static AccountingServer.BLL.Parsing.FacadeF;
30

31
namespace AccountingServer.Shell.Serializer;
32

33
public class DiscountSerializer : IClientDependable, IEntitySerializer
34
{
35
    private const string TheToken = "new Voucher {";
36

37
    public Client Client { private get; set; }
38

39
    /// <inheritdoc />
40
    public string PresentVoucher(Voucher voucher) => throw new NotImplementedException();
15✔
41

42
    /// <inheritdoc />
43
    public string PresentVoucher(Voucher voucher, string inject) => throw new NotImplementedException();
×
44

45
    /// <inheritdoc />
46
    public string PresentVoucherDetail(VoucherDetail detail) => throw new NotImplementedException();
1✔
47

48
    /// <inheritdoc />
49
    public string PresentVoucherDetail(VoucherDetailR detail) => throw new NotImplementedException();
3✔
50

51
    /// <inheritdoc />
52
    public Voucher ParseVoucher(string expr)
53
    {
26✔
54
        if (!expr.StartsWith(TheToken, StringComparison.Ordinal))
26✔
55
            throw new FormatException("格式错误");
1✔
56

57
        expr = expr[TheToken.Length..];
25✔
58
        if (ParsingF.Token(ref expr, false, static s => s == "!") == null)
24✔
59
            throw new NotImplementedException();
16✔
60

61
        var v = GetVoucher(ref expr);
9✔
62
        Parsing.TrimStartComment(ref expr);
5✔
63
        if (Parsing.Token(ref expr, false) != "}")
5✔
64
            throw new FormatException("格式错误" + expr);
×
65

66
        Parsing.Eof(expr);
5✔
67
        return v;
5✔
68
    }
5✔
69

70
    /// <inheritdoc />
71
    public VoucherDetail ParseVoucherDetail(string expr) => throw new NotImplementedException();
1✔
72

73
    public string PresentAsset(Asset asset) => throw new NotImplementedException();
1✔
74
    public Asset ParseAsset(string str) => throw new NotImplementedException();
1✔
75
    public string PresentAmort(Amortization amort) => throw new NotImplementedException();
1✔
76
    public Amortization ParseAmort(string str) => throw new NotImplementedException();
1✔
77

78
    /// <summary>
79
    ///     解析记账凭证表达式
80
    /// </summary>
81
    /// <param name="expr">表达式</param>
82
    /// <returns>记账凭证</returns>
83
    private Voucher GetVoucher(ref string expr)
84
    {
9✔
85
        Parsing.TrimStartComment(ref expr);
9✔
86
        DateTime? date = Client.Today;
9✔
87
        try
88
        {
9✔
89
            date = ParsingF.UniqueTime(ref expr, Client);
9✔
90
        }
1✔
91
        catch (Exception)
8✔
92
        {
8✔
93
            // ignore
94
        }
8✔
95

96
        var vremark = Parsing.Quoted(ref expr, '%');
9✔
97

98
        var currency = Parsing.Token(ref expr, false, static s => s.StartsWith("@", StringComparison.Ordinal))?[1..]
9✔
99
                .ToUpperInvariant()
100
            ?? BaseCurrency.Now;
101

102
        var lst = new List<Item>();
9✔
103
        var guids = new List<string>();
9✔
104
        List<Item> ds;
105
        while ((ds = ParseItem(currency, ref expr, guids))?.Any() == true)
23✔
106
            lst.AddRange(ds);
14✔
107

108
        var d = (double?)0D;
9✔
109
        var t = (double?)0D;
9✔
110
        var reg = new Regex(@"(?<dt>[dt])(?<num>\.[0-9]+|[0-9]+(?:\.[0-9]+)?|null)");
9✔
111
        while (true)
28✔
112
        {
28✔
113
            var res = Parsing.Token(ref expr, false, reg.IsMatch);
28✔
114
            if (res == null)
28✔
115
                break;
9✔
116

117
            var m = reg.Match(res);
19✔
118
            var num = m.Groups["num"].Value == "null" ? (double?)null : Convert.ToDouble(m.Groups["num"].Value);
19✔
119
            if (m.Groups["dt"].Value == "d")
19✔
120
                d += num;
8✔
121
            else // if (m.Groups["dt"].Value == "t")
122
                t += num;
11✔
123
        }
19✔
124

125
        if (!d.HasValue && !t.HasValue)
9✔
126
            throw new ApplicationException("不定项过多");
2✔
127

128
        guids.Sort();
7✔
129
        foreach (var it in lst)
39✔
130
            if (it.Content?.StartsWith("G()", StringComparison.InvariantCulture) == true)
32✔
131
                it.Content = guids[Convert.ToInt32(it.Content[3..])];
×
132

133
        var resLst = new List<VoucherDetail>();
7✔
134
        VoucherDetail vd;
135
        var exprS = new AbbrSerializer { Client = Client };
7✔
136
        while ((vd = exprS.ParseVoucherDetail(ref expr)) != null)
14✔
137
            resLst.Add(vd);
7✔
138

139
        // Don't use Enumerable.Sum, it ignores null values
140
        var actualVals = resLst.Aggregate((double?)0D, static (s, dd) => s + dd.Fund);
7✔
141

142
        if (!d.HasValue || !t.HasValue)
7✔
143
        {
4✔
144
            if (!actualVals.HasValue)
4✔
145
                throw new ApplicationException("不定项过多");
2✔
146

147
            var sum = lst.Sum(static item => item.Fund - item.DiscountFund);
12✔
148
            if (!d.HasValue)
2✔
149
                d = sum + t + actualVals;
1✔
150
            else // if (!t.HasValue)
151
                t = -(sum - d + actualVals);
1✔
152
        }
2✔
153

154

155
        var total = lst.Sum(static it => it.Fund!.Value);
30✔
156
        foreach (var item in lst)
35✔
157
        {
30✔
158
            item.DiscountFund += d!.Value / total * item.Fund!.Value;
30✔
159
            item.Fund += t.Value / total * item.Fund;
30✔
160
        }
30✔
161

162
        foreach (var item in lst)
35✔
163
        {
30✔
164
            if (!item.UseActualFund)
30✔
165
                continue;
15✔
166

167
            item.Fund -= item.DiscountFund;
15✔
168
            item.DiscountFund = 0D;
15✔
169
        }
15✔
170

171
        foreach (var grp in lst.GroupBy(static it => new()
30✔
172
                         {
173
                             User = it.User,
174
                             Currency = it.Currency,
175
                             Title = it.Title,
176
                             SubTitle = it.SubTitle,
177
                             Content = it.Content,
178
                             Remark = it.Remark,
179
                         },
180
                     new DetailEqualityComparer()))
181
        {
15✔
182
            grp.Key.Fund = grp.Sum(static it => it.Fund!.Value);
30✔
183
            resLst.Add(grp.Key);
15✔
184
        }
15✔
185

186
        var totalDs = new Dictionary<string, double>();
5✔
187
        foreach (var it in lst)
35✔
188
            if (totalDs.ContainsKey(it.User))
30✔
189
                totalDs[it.User] += it.DiscountFund;
24✔
190
            else
191
                totalDs[it.User] = it.DiscountFund;
6✔
192

193
        foreach (var kvp in totalDs)
11✔
194
            if (!kvp.Value.IsZero())
6✔
195
                resLst.Add(
6✔
196
                    new() { User = kvp.Key, Currency = currency, Title = 6603, Fund = -kvp.Value });
197

198
        return new() { Type = VoucherType.Ordinary, Date = date, Remark = vremark, Details = resLst };
5✔
199
    }
5✔
200

201
    private VoucherDetail ParseVoucherDetail(string currency, ref string expr, List<string> guids)
202
    {
47✔
203
        var lst = new List<string>();
47✔
204

205
        Parsing.TrimStartComment(ref expr);
47✔
206
        var user = Parsing.Token(ref expr, false, static t => t.StartsWith("U", StringComparison.Ordinal))
47✔
207
            .ParseUserSpec(Client);
208
        var title = Parsing.Title(ref expr);
47✔
209
        if (title == null)
47✔
210
            if (!AlternativeTitle(ref expr, lst, ref title))
32✔
211
                return null;
23✔
212

213
        while (true)
32✔
214
        {
32✔
215
            Parsing.TrimStartComment(ref expr);
32✔
216
            if (Parsing.Optional(ref expr, "+"))
32✔
217
                break;
10✔
218

219
            if (Parsing.Optional(ref expr, ":"))
22✔
220
            {
14✔
221
                expr = $": {expr}";
14✔
222
                break;
14✔
223
            }
224

225
            if (lst.Count > 2)
8✔
226
                throw new ArgumentException("语法错误", nameof(expr));
×
227

228
            Parsing.TrimStartComment(ref expr);
8✔
229
            lst.Add(Parsing.Token(ref expr));
8✔
230
        }
8✔
231

232
        var content = lst.Count >= 1 ? lst[0] : null;
24✔
233
        var remark = lst.Count >= 2 ? lst[1] : null;
24✔
234

235
        if (content == "G()")
24✔
236
        {
×
237
            content = $"G(){guids.Count}";
×
238
            guids.Add(Guid.NewGuid().ToString().ToUpperInvariant());
×
239
        }
×
240

241
        if (remark == "G()")
24✔
242
            remark = Guid.NewGuid().ToString().ToUpperInvariant();
×
243

244

245
        return new()
24✔
246
            {
247
                User = user,
248
                Currency = currency,
249
                Title = title.Title,
250
                SubTitle = title.SubTitle,
251
                Content = string.IsNullOrEmpty(content) ? null : content,
252
                Remark = string.IsNullOrEmpty(remark) ? null : remark,
253
            };
254
    }
47✔
255

256
    private List<Item> ParseItem(string currency, ref string expr, List<string> guids)
257
    {
23✔
258
        var lst = new List<(VoucherDetail Detail, bool Actual)>();
23✔
259

260
        while (true)
47✔
261
        {
47✔
262
            var actual = Parsing.Optional(ref expr, "!");
47✔
263
            var vd = ParseVoucherDetail(currency, ref expr, guids);
47✔
264
            if (vd == null)
47✔
265
                break;
23✔
266

267
            lst.Add((vd, actual));
24✔
268
        }
24✔
269

270
        if (ParsingF.Token(ref expr, false, static s => s == ":") == null)
23✔
271
            return null;
9✔
272

273
        var resLst = new List<Item>();
14✔
274

275
        var reg = new Regex(
14✔
276
            @"(?<num>\.[0-9]+|[0-9]+(?:\.[0-9]+)?)(?:(?<equals>=\.[0-9]+|=[0-9]+(?:\.[0-9]+)?)|(?<plus>(?:\+\.[0-9]+|\+[0-9]+(?:\.[0-9]+)?)+)|(?<minus>(?:-\.[0-9]+|-[0-9]+(?:\.[0-9]+)?)+))?(?<times>\*\.[0-9]+|\*[0-9]+(?:\.[0-9]+)?)?");
277
        while (true)
33✔
278
        {
33✔
279
            var res = Parsing.Token(ref expr, false, reg.IsMatch);
33✔
280
            if (res == null)
33✔
281
                break;
14✔
282

283
            var m = reg.Match(res);
19✔
284
            var fund0 = Convert.ToDouble(m.Groups["num"].Value);
19✔
285
            var fundd = 0D;
19✔
286
            if (m.Groups["equals"].Success)
19✔
287
                fundd = fund0 - Convert.ToDouble(m.Groups["equals"].Value[1..]);
2✔
288
            else if (m.Groups["plus"].Success)
17✔
289
            {
5✔
290
                var sreg = new Regex(@"\+\.[0-9]+|\+[0-9]+(?:\.[0-9]+)?");
5✔
291
                foreach (Match sm in sreg.Matches(m.Groups["plus"].Value))
11✔
292
                    fundd += Convert.ToDouble(sm.Value);
6✔
293
                fund0 += fundd;
5✔
294
            }
5✔
295
            else if (m.Groups["minus"].Success)
12✔
296
            {
4✔
297
                var sreg = new Regex(@"-\.[0-9]+|-[0-9]+(?:\.[0-9]+)?");
4✔
298
                foreach (Match sm in sreg.Matches(m.Groups["minus"].Value))
9✔
299
                    fundd -= Convert.ToDouble(sm.Value);
5✔
300
            }
4✔
301

302
            if (m.Groups["times"].Success)
19✔
303
            {
2✔
304
                var mult = Convert.ToDouble(m.Groups["times"].Value[1..]);
2✔
305
                fund0 *= mult;
2✔
306
                fundd *= mult;
2✔
307
            }
2✔
308

309
            resLst.AddRange(
19✔
310
                lst.Select(
311
                    d => new Item
34✔
312
                        {
313
                            User = d.Detail.User,
314
                            Currency = d.Detail.Currency,
315
                            Title = d.Detail.Title,
316
                            SubTitle = d.Detail.SubTitle,
317
                            Content = d.Detail.Content,
318
                            Fund = fund0 / lst.Count,
319
                            DiscountFund = fundd / lst.Count,
320
                            Remark = d.Detail.Remark,
321
                            UseActualFund = d.Actual,
322
                        }));
323
        }
19✔
324

325
        ParsingF.Optional(ref expr, ";");
14✔
326

327
        return resLst;
14✔
328
    }
23✔
329

330
    private static bool AlternativeTitle(ref string expr, ICollection<string> lst, ref ITitle title)
331
        => AbbrSerializer.GetAlternativeTitle(ref expr, lst, ref title);
32✔
332

333
    private sealed class Item : VoucherDetail
334
    {
335
        public double DiscountFund { get; set; }
336

337
        public bool UseActualFund { get; init; }
338
    }
339

340
    private sealed class DetailEqualityComparer : IEqualityComparer<VoucherDetail>
341
    {
342
        public bool Equals(VoucherDetail x, VoucherDetail y)
343
        {
40✔
344
            if (x == null &&
40✔
345
                y == null)
346
                return true;
×
347
            if (x == null ||
40✔
348
                y == null)
349
                return false;
×
350
            if (x.User != y.User)
40✔
351
                return false;
4✔
352
            if (x.Currency != y.Currency)
36✔
353
                return false;
×
354
            if (x.Title != y.Title)
36✔
355
                return false;
21✔
356
            if (x.SubTitle != y.SubTitle)
15✔
357
                return false;
×
358
            if (x.Content != y.Content)
15✔
359
                return false;
×
360
            if (x.Fund.HasValue != y.Fund.HasValue)
15✔
361
                return false;
×
362
            if (x.Fund.HasValue &&
15✔
363
                y.Fund.HasValue)
364
                if (!(x.Fund.Value - y.Fund.Value).IsZero())
×
365
                    return false;
×
366

367
            return x.Remark == y.Remark;
15✔
368
        }
40✔
369

370
        public int GetHashCode(VoucherDetail obj) => obj.Currency?.GetHashCode() | obj.Title?.GetHashCode() |
30✔
371
            obj.SubTitle?.GetHashCode() | obj.Content?.GetHashCode() | obj.Fund?.GetHashCode() |
372
            obj.Remark?.GetHashCode() ?? 0;
373
    }
374
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc