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

b1f6c1c4 / ProfessionalAccounting / 328

28 Jun 2025 10:48PM UTC coverage: 48.177% (-3.2%) from 51.346%
328

push

appveyor

b1f6c1c4
fix bugs

5035 of 10451 relevant lines covered (48.18%)

107.82 hits per line

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

92.09
/AccountingServer.Shell/Serializer/DiscountSerializer.cs
1
/* Copyright (C) 2020-2025 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
    {
27✔
54
        if (!expr.StartsWith(TheToken, StringComparison.Ordinal))
27✔
55
            throw new FormatException("格式错误");
1✔
56

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

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

66
        Parsing.Eof(expr);
6✔
67
        return v;
6✔
68
    }
6✔
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
    {
10✔
85
        Parsing.TrimStartComment(ref expr);
10✔
86
        DateTime? date = Client.Today;
10✔
87
        try
88
        {
10✔
89
            date = ParsingF.UniqueTime(ref expr, Client);
10✔
90
        }
1✔
91
        catch (Exception)
9✔
92
        {
9✔
93
            // ignore
94
        }
9✔
95

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

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

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

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

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

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

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

134
        var resLst = new List<VoucherDetail>();
8✔
135
        VoucherDetail vd;
136
        var exprS = new AbbrSerializer { Client = Client };
8✔
137
        while ((vd = exprS.ParseVoucherDetail(ref expr)) != null)
16✔
138
            resLst.AddRange(vd);
8✔
139

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

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

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

155

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

163
        foreach (var item in lst)
40✔
164
        {
34✔
165
            if (!item.UseActualFund)
34✔
166
                continue;
17✔
167

168
            item.Fund -= item.DiscountFund;
17✔
169
            item.DiscountFund = 0D;
17✔
170
        }
17✔
171

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

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

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

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

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

206
        Parsing.TrimStartComment(ref expr);
51✔
207
        var users = new List<string>();
51✔
208
        while (true)
57✔
209
        {
57✔
210
            var user = Parsing.Token(ref expr, false, static t => t.StartsWith("U", StringComparison.Ordinal));
57✔
211
            if (user == null)
57✔
212
                break;
51✔
213

214
            users.Add(user.ParseUserSpec(Client));
6✔
215
        }
6✔
216
        if (users.Count == 0)
51✔
217
            users.Add(Client.User);
47✔
218

219
        var title = Parsing.Title(ref expr);
51✔
220
        if (title == null)
51✔
221
            if (!AlternativeTitle(ref expr, lst, ref title))
34✔
222
                return null;
25✔
223

224
        while (true)
34✔
225
        {
34✔
226
            Parsing.TrimStartComment(ref expr);
34✔
227
            if (Parsing.Optional(ref expr, "+"))
34✔
228
                break;
11✔
229

230
            if (Parsing.Optional(ref expr, ":"))
23✔
231
            {
15✔
232
                expr = $": {expr}";
15✔
233
                break;
15✔
234
            }
235

236
            if (lst.Count > 2)
8✔
237
                throw new ArgumentException("语法错误", nameof(expr));
×
238

239
            Parsing.TrimStartComment(ref expr);
8✔
240
            lst.Add(Parsing.Token(ref expr));
8✔
241
        }
8✔
242

243
        var content = lst.Count >= 1 ? lst[0] : null;
26✔
244
        var remark = lst.Count >= 2 ? lst[1] : null;
26✔
245

246
        if (content == "G()")
26✔
247
        {
×
248
            content = $"G(){guids.Count}";
×
249
            guids.Add(Guid.NewGuid().ToString().ToUpperInvariant());
×
250
        }
×
251

252
        if (remark == "G()")
26✔
253
            remark = Guid.NewGuid().ToString().ToUpperInvariant();
×
254

255

256
        return users.Select(user => new VoucherDetail
28✔
257
            {
258
                User = user,
259
                Currency = currency,
260
                Title = title.Title,
261
                SubTitle = title.SubTitle,
262
                Content = string.IsNullOrEmpty(content) ? null : content,
263
                Remark = string.IsNullOrEmpty(remark) ? null : remark,
264
            });
265
    }
51✔
266

267
    private List<Item> ParseItem(string currency, ref string expr, List<string> guids)
268
    {
25✔
269
        var lst = new List<(VoucherDetail Detail, bool Actual)>();
25✔
270

271
        while (true)
51✔
272
        {
51✔
273
            var actual = Parsing.Optional(ref expr, "!");
51✔
274
            var vds = ParseVoucherDetail(currency, ref expr, guids);
51✔
275
            if (vds == null)
51✔
276
                break;
25✔
277

278
            foreach (var vd in vds)
54✔
279
                lst.Add((vd, actual));
28✔
280
        }
26✔
281

282
        if (ParsingF.Token(ref expr, false, static s => s == ":") == null)
25✔
283
            return null;
10✔
284

285
        var resLst = new List<Item>();
15✔
286

287
        var reg = new Regex(
15✔
288
            @"(?<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]+)?)?");
289
        while (true)
35✔
290
        {
35✔
291
            var res = Parsing.Token(ref expr, false, reg.IsMatch);
35✔
292
            if (res == null)
35✔
293
                break;
15✔
294

295
            var m = reg.Match(res);
20✔
296
            var fund0 = Convert.ToDouble(m.Groups["num"].Value);
20✔
297
            var fundd = 0D;
20✔
298
            if (m.Groups["equals"].Success)
20✔
299
                fundd = fund0 - Convert.ToDouble(m.Groups["equals"].Value[1..]);
2✔
300
            else if (m.Groups["plus"].Success)
18✔
301
            {
5✔
302
                var sreg = new Regex(@"\+\.[0-9]+|\+[0-9]+(?:\.[0-9]+)?");
5✔
303
                foreach (Match sm in sreg.Matches(m.Groups["plus"].Value))
11✔
304
                    fundd += Convert.ToDouble(sm.Value);
6✔
305
                fund0 += fundd;
5✔
306
            }
5✔
307
            else if (m.Groups["minus"].Success)
13✔
308
            {
4✔
309
                var sreg = new Regex(@"-\.[0-9]+|-[0-9]+(?:\.[0-9]+)?");
4✔
310
                foreach (Match sm in sreg.Matches(m.Groups["minus"].Value))
9✔
311
                    fundd -= Convert.ToDouble(sm.Value);
5✔
312
            }
4✔
313

314
            if (m.Groups["times"].Success)
20✔
315
            {
2✔
316
                var mult = Convert.ToDouble(m.Groups["times"].Value[1..]);
2✔
317
                fund0 *= mult;
2✔
318
                fundd *= mult;
2✔
319
            }
2✔
320

321
            resLst.AddRange(
20✔
322
                lst.Select(
323
                    d => new Item
38✔
324
                        {
325
                            User = d.Detail.User,
326
                            Currency = d.Detail.Currency,
327
                            Title = d.Detail.Title,
328
                            SubTitle = d.Detail.SubTitle,
329
                            Content = d.Detail.Content,
330
                            Fund = fund0 / lst.Count,
331
                            DiscountFund = fundd / lst.Count,
332
                            Remark = d.Detail.Remark,
333
                            UseActualFund = d.Actual,
334
                        }));
335
        }
20✔
336

337
        ParsingF.Optional(ref expr, ";");
15✔
338

339
        return resLst;
15✔
340
    }
25✔
341

342
    private static bool AlternativeTitle(ref string expr, ICollection<string> lst, ref ITitle title)
343
        => AbbrSerializer.GetAlternativeTitle(ref expr, lst, ref title);
34✔
344

345
    private sealed class Item : VoucherDetail
346
    {
347
        public double DiscountFund { get; set; }
348

349
        public bool UseActualFund { get; init; }
350
    }
351

352
    private sealed class DetailEqualityComparer : IEqualityComparer<VoucherDetail>
353
    {
354
        public bool Equals(VoucherDetail x, VoucherDetail y)
355
        {
46✔
356
            if (x == null &&
46✔
357
                y == null)
358
                return true;
×
359
            if (x == null ||
46✔
360
                y == null)
361
                return false;
×
362
            if (x.User != y.User)
46✔
363
                return false;
10✔
364
            if (x.Currency != y.Currency)
36✔
365
                return false;
×
366
            if (x.Title != y.Title)
36✔
367
                return false;
21✔
368
            if (x.SubTitle != y.SubTitle)
15✔
369
                return false;
×
370
            if (x.Content != y.Content)
15✔
371
                return false;
×
372
            if (x.Fund.HasValue != y.Fund.HasValue)
15✔
373
                return false;
×
374
            if (x.Fund.HasValue &&
15✔
375
                y.Fund.HasValue)
376
                if (!(x.Fund.Value - y.Fund.Value).IsZero())
×
377
                    return false;
×
378

379
            return x.Remark == y.Remark;
15✔
380
        }
46✔
381

382
        public int GetHashCode(VoucherDetail obj) => obj.Currency?.GetHashCode() | obj.Title?.GetHashCode() |
34✔
383
            obj.SubTitle?.GetHashCode() | obj.Content?.GetHashCode() | obj.Fund?.GetHashCode() |
384
            obj.Remark?.GetHashCode() ?? 0;
385
    }
386
}
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