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

b1f6c1c4 / ProfessionalAccounting / 307

25 Sep 2023 04:48AM UTC coverage: 56.975% (-1.3%) from 58.254%
307

push

appveyor

b1f6c1c4
raw subtotal more controllable

5947 of 10438 relevant lines covered (56.97%)

125.9 hits per line

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

93.37
/AccountingServer.Shell/Serializer/DiscountSerializer.cs
1
/* Copyright (C) 2020-2023 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 currency = Parsing.Token(ref expr, false, static s => s.StartsWith("@", StringComparison.Ordinal))?[1..]
9✔
97
                .ToUpperInvariant()
98
            ?? BaseCurrency.Now;
99

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

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

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

122
        if (!d.HasValue && !t.HasValue)
9✔
123
            throw new ApplicationException("不定项过多");
2✔
124

125
        var resLst = new List<VoucherDetail>();
7✔
126
        VoucherDetail vd;
127
        var exprS = new AbbrSerializer { Client = Client };
7✔
128
        while ((vd = exprS.ParseVoucherDetail(ref expr)) != null)
14✔
129
            resLst.Add(vd);
7✔
130

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

134
        if (!d.HasValue || !t.HasValue)
7✔
135
        {
4✔
136
            if (!actualVals.HasValue)
4✔
137
                throw new ApplicationException("不定项过多");
2✔
138

139
            var sum = lst.Sum(static item => item.Fund - item.DiscountFund);
12✔
140
            if (!d.HasValue)
2✔
141
                d = sum + t + actualVals;
1✔
142
            else // if (!t.HasValue)
143
                t = -(sum - d + actualVals);
1✔
144
        }
2✔
145

146

147
        var total = lst.Sum(static it => it.Fund!.Value);
30✔
148
        foreach (var item in lst)
35✔
149
        {
30✔
150
            item.DiscountFund += d!.Value / total * item.Fund!.Value;
30✔
151
            item.Fund += t.Value / total * item.Fund;
30✔
152
        }
30✔
153

154
        foreach (var item in lst)
35✔
155
        {
30✔
156
            if (!item.UseActualFund)
30✔
157
                continue;
15✔
158

159
            item.Fund -= item.DiscountFund;
15✔
160
            item.DiscountFund = 0D;
15✔
161
        }
15✔
162

163
        foreach (var grp in lst.GroupBy(static it => new()
30✔
164
                         {
165
                             User = it.User,
166
                             Currency = it.Currency,
167
                             Title = it.Title,
168
                             SubTitle = it.SubTitle,
169
                             Content = it.Content,
170
                             Remark = it.Remark,
171
                         },
172
                     new DetailEqualityComparer()))
173
        {
15✔
174
            grp.Key.Fund = grp.Sum(static it => it.Fund!.Value);
30✔
175
            resLst.Add(grp.Key);
15✔
176
        }
15✔
177

178
        var totalDs = new Dictionary<string, double>();
5✔
179
        foreach (var it in lst)
35✔
180
            if (totalDs.ContainsKey(it.User))
30✔
181
                totalDs[it.User] += it.DiscountFund;
24✔
182
            else
183
                totalDs[it.User] = it.DiscountFund;
6✔
184

185
        foreach (var kvp in totalDs)
11✔
186
            if (!kvp.Value.IsZero())
6✔
187
                resLst.Add(
6✔
188
                    new() { User = kvp.Key, Currency = currency, Title = 6603, Fund = -kvp.Value });
189

190
        return new() { Type = VoucherType.Ordinary, Date = date, Details = resLst };
5✔
191
    }
5✔
192

193
    private VoucherDetail ParseVoucherDetail(string currency, ref string expr)
194
    {
47✔
195
        var lst = new List<string>();
47✔
196

197
        Parsing.TrimStartComment(ref expr);
47✔
198
        var user = Parsing.Token(ref expr, false, static t => t.StartsWith("U", StringComparison.Ordinal))
47✔
199
            .ParseUserSpec(Client);
200
        var title = Parsing.Title(ref expr);
47✔
201
        if (title == null)
47✔
202
            if (!AlternativeTitle(ref expr, lst, ref title))
32✔
203
                return null;
23✔
204

205
        while (true)
32✔
206
        {
32✔
207
            Parsing.TrimStartComment(ref expr);
32✔
208
            if (Parsing.Optional(ref expr, "+"))
32✔
209
                break;
10✔
210

211
            if (Parsing.Optional(ref expr, ":"))
22✔
212
            {
14✔
213
                expr = $": {expr}";
14✔
214
                break;
14✔
215
            }
216

217
            if (lst.Count > 2)
8✔
218
                throw new ArgumentException("语法错误", nameof(expr));
×
219

220
            Parsing.TrimStartComment(ref expr);
8✔
221
            lst.Add(Parsing.Token(ref expr));
8✔
222
        }
8✔
223

224
        var content = lst.Count >= 1 ? lst[0] : null;
24✔
225
        var remark = lst.Count >= 2 ? lst[1] : null;
24✔
226

227
        if (content == "G()")
24✔
228
            content = Guid.NewGuid().ToString().ToUpperInvariant();
×
229

230
        if (remark == "G()")
24✔
231
            remark = Guid.NewGuid().ToString().ToUpperInvariant();
×
232

233

234
        return new()
24✔
235
            {
236
                User = user,
237
                Currency = currency,
238
                Title = title.Title,
239
                SubTitle = title.SubTitle,
240
                Content = string.IsNullOrEmpty(content) ? null : content,
241
                Remark = string.IsNullOrEmpty(remark) ? null : remark,
242
            };
243
    }
47✔
244

245
    private List<Item> ParseItem(string currency, ref string expr)
246
    {
23✔
247
        var lst = new List<(VoucherDetail Detail, bool Actual)>();
23✔
248

249
        while (true)
47✔
250
        {
47✔
251
            var actual = Parsing.Optional(ref expr, "!");
47✔
252
            var vd = ParseVoucherDetail(currency, ref expr);
47✔
253
            if (vd == null)
47✔
254
                break;
23✔
255

256
            lst.Add((vd, actual));
24✔
257
        }
24✔
258

259
        if (ParsingF.Token(ref expr, false, static s => s == ":") == null)
23✔
260
            return null;
9✔
261

262
        var resLst = new List<Item>();
14✔
263

264
        var reg = new Regex(
14✔
265
            @"(?<num>[0-9]+(?:\.[0-9]+)?)(?:(?<equals>=[0-9]+(?:\.[0-9]+)?)|(?<plus>(?:\+[0-9]+(?:\.[0-9]+)?)+)|(?<minus>(?:-[0-9]+(?:\.[0-9]+)?)+))?(?<times>\*[0-9]+(?:\.[0-9]+)?)?");
266
        while (true)
33✔
267
        {
33✔
268
            var res = Parsing.Token(ref expr, false, reg.IsMatch);
33✔
269
            if (res == null)
33✔
270
                break;
14✔
271

272
            var m = reg.Match(res);
19✔
273
            var fund0 = Convert.ToDouble(m.Groups["num"].Value);
19✔
274
            var fundd = 0D;
19✔
275
            if (m.Groups["equals"].Success)
19✔
276
                fundd = fund0 - Convert.ToDouble(m.Groups["equals"].Value[1..]);
2✔
277
            else if (m.Groups["plus"].Success)
17✔
278
            {
5✔
279
                var sreg = new Regex(@"\+[0-9]+(?:\.[0-9]+)?");
5✔
280
                foreach (Match sm in sreg.Matches(m.Groups["plus"].Value))
11✔
281
                    fundd += Convert.ToDouble(sm.Value);
6✔
282
                fund0 += fundd;
5✔
283
            }
5✔
284
            else if (m.Groups["minus"].Success)
12✔
285
            {
4✔
286
                var sreg = new Regex(@"-[0-9]+(?:\.[0-9]+)?");
4✔
287
                foreach (Match sm in sreg.Matches(m.Groups["minus"].Value))
9✔
288
                    fundd -= Convert.ToDouble(sm.Value);
5✔
289
            }
4✔
290

291
            if (m.Groups["times"].Success)
19✔
292
            {
2✔
293
                var mult = Convert.ToDouble(m.Groups["times"].Value[1..]);
2✔
294
                fund0 *= mult;
2✔
295
                fundd *= mult;
2✔
296
            }
2✔
297

298
            resLst.AddRange(
19✔
299
                lst.Select(
300
                    d => new Item
34✔
301
                        {
302
                            User = d.Detail.User,
303
                            Currency = d.Detail.Currency,
304
                            Title = d.Detail.Title,
305
                            SubTitle = d.Detail.SubTitle,
306
                            Content = d.Detail.Content,
307
                            Fund = fund0 / lst.Count,
308
                            DiscountFund = fundd / lst.Count,
309
                            Remark = d.Detail.Remark,
310
                            UseActualFund = d.Actual,
311
                        }));
312
        }
19✔
313

314
        ParsingF.Optional(ref expr, ";");
14✔
315

316
        return resLst;
14✔
317
    }
23✔
318

319
    private static bool AlternativeTitle(ref string expr, ICollection<string> lst, ref ITitle title)
320
        => AbbrSerializer.GetAlternativeTitle(ref expr, lst, ref title);
32✔
321

322
    private sealed class Item : VoucherDetail
323
    {
324
        public double DiscountFund { get; set; }
325

326
        public bool UseActualFund { get; init; }
327
    }
328

329
    private sealed class DetailEqualityComparer : IEqualityComparer<VoucherDetail>
330
    {
331
        public bool Equals(VoucherDetail x, VoucherDetail y)
332
        {
40✔
333
            if (x == null &&
40✔
334
                y == null)
335
                return true;
×
336
            if (x == null ||
40✔
337
                y == null)
338
                return false;
×
339
            if (x.User != y.User)
40✔
340
                return false;
4✔
341
            if (x.Currency != y.Currency)
36✔
342
                return false;
×
343
            if (x.Title != y.Title)
36✔
344
                return false;
21✔
345
            if (x.SubTitle != y.SubTitle)
15✔
346
                return false;
×
347
            if (x.Content != y.Content)
15✔
348
                return false;
×
349
            if (x.Fund.HasValue != y.Fund.HasValue)
15✔
350
                return false;
×
351
            if (x.Fund.HasValue &&
15✔
352
                y.Fund.HasValue)
353
                if (!(x.Fund.Value - y.Fund.Value).IsZero())
×
354
                    return false;
×
355

356
            return x.Remark == y.Remark;
15✔
357
        }
40✔
358

359
        public int GetHashCode(VoucherDetail obj) => obj.Currency?.GetHashCode() | obj.Title?.GetHashCode() |
30✔
360
            obj.SubTitle?.GetHashCode() | obj.Content?.GetHashCode() | obj.Fund?.GetHashCode() |
361
            obj.Remark?.GetHashCode() ?? 0;
362
    }
363
}
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