Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.24% covered (success)
95.24%
60 / 63
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
WordCounter
95.24% covered (success)
95.24%
60 / 63
75.00% covered (warning)
75.00%
3 / 4
31
0.00% covered (danger)
0.00%
0 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 countFromFile
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 countFromString
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
10
 countFromResource
87.50% covered (warning)
87.50%
21 / 24
0.00% covered (danger)
0.00%
0 / 1
11.24
 updateCountsFromChunk
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2namespace Apie\CountWords;
3
4use Apie\CountWords\Strategies\HtmlWordCounter;
5use Apie\CountWords\Strategies\JsonWordCounter;
6use Apie\CountWords\Strategies\OfficeDocumentWordCounter;
7use Apie\CountWords\Strategies\PdfFileWordCounter;
8use Apie\CountWords\Strategies\PlaintextWordCounter;
9use Apie\CountWords\Strategies\XmlWordCounter;
10use Apie\CountWords\Strategies\ZipArchiveWordCounter;
11
12class WordCounter
13{
14    private const FILE_STRATEGIES = [
15        JsonWordCounter::class,
16        OfficeDocumentWordCounter::class,
17        PdfFileWordCounter::class,
18        HtmlWordCounter::class,
19        XmlWordCounter::class,
20        PlaintextWordCounter::class,
21        ZipArchiveWordCounter::class,
22    ];
23
24    /**
25     * @codeCoverageIgnore
26     */
27    private function __construct()
28    {
29    }
30
31    /**
32     * @param array<string, int> $counts
33     * @return array<string, int>
34     */
35    public static function countFromFile(string $path, array $counts = [], ?string $mimeType = null): array
36    {
37        $extension = pathinfo($path, PATHINFO_EXTENSION);
38        foreach (self::FILE_STRATEGIES as $fileStrategyClass) {
39            if ($fileStrategyClass::isSupported($extension, $mimeType)) {
40                return $fileStrategyClass::countFromFile($path, $counts);
41            }
42        }
43        return $counts;
44    }
45    
46    /**
47     * @param array<string, int> $counts
48     * @return array<string, int>
49     */
50    public static function countFromString(string $text, array $counts = [], ?string $mimeType = null, ?string $extension = null): array
51    {
52        $originalText = mb_strtolower(trim($text));
53        $text = mb_strtolower($text);
54        $text = preg_replace('/[^\p{L}\p{N}\s.]/u', '', $text);
55        $wordsAndNumbers = preg_split('/[\s]+/', $text);
56
57        foreach (self::FILE_STRATEGIES as $fileStrategyClass) {
58            if ($fileStrategyClass::isSupported($extension, $mimeType)) {
59                return $fileStrategyClass::countFromString($text, $counts);
60            }
61        }
62        if ($mimeType !== null || $extension !== null) {
63            return $counts;
64        }
65            
66        // Iterate through each word/number and count its frequency
67        foreach ($wordsAndNumbers as $item) {
68            // we could not remove '.' beforehand all the time as a floating point would lose the '.' as well.
69            $item = rtrim($item, '.');
70            if (empty($item)) {
71                continue;
72            }
73                
74            if (array_key_exists($item, $counts)) {
75                $counts[$item]++;
76            } else {
77                $counts[$item] = 1;
78            }
79        }
80
81        if (empty($counts) && !empty($originalText)) {
82            $originalText = preg_replace('/[^\P{C}]+/u', '', $originalText);
83            $counts[$originalText] = 1;
84        }
85            
86        return $counts;
87    }
88
89    /**
90     * @param resource $resource
91     * @param array<string, int> $counts
92     * @return array<string, int>
93     */
94    public static function countFromResource($resource, array $counts = [], ?string $mimeType = null, ?string $extension = null): array
95    {
96        if (!is_resource($resource)) {
97            throw new \InvalidArgumentException('The provided argument is not a valid resource.');
98        }
99        foreach (self::FILE_STRATEGIES as $fileStrategyClass) {
100            if ($fileStrategyClass::isSupported($extension, $mimeType)) {
101                return $fileStrategyClass::countFromResource($resource, $counts);
102            }
103        }
104        if ($mimeType !== null || $extension !== null) {
105            return $counts;
106        }
107        $buffer = '';
108        $chunkSize = 4096 * 1024; // Read 4MB at a time
109        rewind($resource);
110        while (!feof($resource)) {
111            $buffer .= fread($resource, $chunkSize);
112            
113            $lastSpacePos = strrpos($buffer, ' ');
114            if ($lastSpacePos !== false) {
115                $chunk = substr($buffer, 0, $lastSpacePos);
116                $buffer = substr($buffer, $lastSpacePos + 1);
117                if (preg_match('/[^\P{C}]+/u', $chunk)) {
118                    return $counts;
119                }
120                $counts = self::updateCountsFromChunk($chunk, $counts);
121            }
122        }
123
124        // Process any remaining words in the buffer
125        if (!empty($buffer)) {
126            if (preg_match('/[^\P{C}]+/u', $buffer)) {
127                return $counts;
128            }
129            $counts = self::updateCountsFromChunk($buffer, $counts);
130        }
131
132        return $counts;
133    }
134
135    /**
136     * @param array<string, int> $counts
137     * @return array<string, int>
138     */
139    private static function updateCountsFromChunk(string $originalText, array $counts): array
140    {
141        $chunk = mb_strtolower($originalText);
142        $chunk = preg_replace('/[^\p{L}\p{N}\s.]/u', '', $chunk);
143        $wordsAndNumbers = preg_split('/[\s]+/', $chunk);
144
145        foreach ($wordsAndNumbers as $item) {
146            $item = rtrim($item, '.');
147            if (empty($item)) {
148                continue;
149            }
150
151            if (array_key_exists($item, $counts)) {
152                $counts[$item]++;
153            } else {
154                $counts[$item] = 1;
155            }
156        }
157
158        if (empty($counts) && !empty($originalText)) {
159            $originalText = preg_replace('/[^\P{C}]+/u', '', $originalText);
160            $counts[$originalText] = 1;
161        }
162
163        return $counts;
164    }
165}